import { RRule, rrulestr } from "rrule";
import type { RecurringRule, Transaction } from "@/db/schema";

export type ProjectedTransaction = {
  id: string;
  bankAccountId: string;
  categoryId: string | null;
  recurringRuleId: string;
  date: string;
  amountMinor: number;
  description: string;
  status: "projected";
  source: "recurring";
};

function toISODate(d: Date): string {
  const y = d.getUTCFullYear();
  const m = String(d.getUTCMonth() + 1).padStart(2, "0");
  const day = String(d.getUTCDate()).padStart(2, "0");
  return `${y}-${m}-${day}`;
}

function parseISODate(s: string): Date {
  const [y, m, d] = s.split("-").map(Number);
  return new Date(Date.UTC(y, (m ?? 1) - 1, d ?? 1));
}

export function rruleFromRule(rule: Pick<RecurringRule, "rrule" | "startDate" | "endDate">): RRule {
  const dtstart = parseISODate(rule.startDate);
  const until = rule.endDate ? parseISODate(rule.endDate) : undefined;

  const trimmed = rule.rrule.trim();
  if (trimmed.startsWith("RRULE:") || trimmed.startsWith("DTSTART")) {
    const r = rrulestr(trimmed) as RRule;
    return new RRule({ ...r.options, dtstart, until: until ?? r.options.until });
  }

  const opts = RRule.parseString(trimmed);
  return new RRule({ ...opts, dtstart, until: until ?? opts.until });
}

export function generateOccurrences(
  rule: Pick<RecurringRule, "rrule" | "startDate" | "endDate">,
  from: Date,
  to: Date,
): Date[] {
  const r = rruleFromRule(rule);
  return r.between(from, to, true);
}

export function projectTransactionsFromRule(
  rule: RecurringRule,
  from: Date,
  to: Date,
  postedKeyset: Set<string> = new Set(),
): ProjectedTransaction[] {
  if (!rule.isActive) return [];
  const dates = generateOccurrences(rule, from, to);
  return dates
    .map((d) => {
      const dateStr = toISODate(d);
      const dedupKey = `${rule.id}|${dateStr}`;
      if (postedKeyset.has(dedupKey)) return null;
      return {
        id: `proj_${rule.id}_${dateStr}`,
        bankAccountId: rule.bankAccountId,
        categoryId: rule.categoryId,
        recurringRuleId: rule.id,
        date: dateStr,
        amountMinor: rule.amountMinor,
        description: rule.description,
        status: "projected" as const,
        source: "recurring" as const,
      } satisfies ProjectedTransaction;
    })
    .filter((x): x is ProjectedTransaction => x !== null);
}

export type LedgerRow = {
  id: string;
  bankAccountId: string;
  categoryId: string | null;
  recurringRuleId: string | null;
  date: string;
  amountMinor: number;
  description: string;
  status: "posted" | "projected" | "cancelled";
  source: string;
  runningBalanceMinor: number;
};

export function buildLedger(
  posted: Transaction[],
  rules: RecurringRule[],
  from: Date,
  to: Date,
  openingBalanceByAccount: Record<string, number>,
): LedgerRow[] {
  const postedKeyset = new Set(
    posted
      .filter((t) => t.recurringRuleId)
      .map((t) => `${t.recurringRuleId}|${t.date}`),
  );

  const projected = rules.flatMap((r) =>
    projectTransactionsFromRule(r, from, to, postedKeyset),
  );

  const all = [
    ...posted.map((p) => ({
      id: p.id,
      bankAccountId: p.bankAccountId,
      categoryId: p.categoryId,
      recurringRuleId: p.recurringRuleId,
      date: p.date,
      amountMinor: p.amountMinor,
      description: p.description,
      status: p.status,
      source: p.source,
    })),
    ...projected.map((p) => ({
      id: p.id,
      bankAccountId: p.bankAccountId,
      categoryId: p.categoryId,
      recurringRuleId: p.recurringRuleId,
      date: p.date,
      amountMinor: p.amountMinor,
      description: p.description,
      status: p.status as "projected",
      source: p.source as string,
    })),
  ].sort((a, b) => {
    if (a.date < b.date) return -1;
    if (a.date > b.date) return 1;
    if (a.status === "posted" && b.status !== "posted") return -1;
    if (a.status !== "posted" && b.status === "posted") return 1;
    return 0;
  });

  const balances: Record<string, number> = { ...openingBalanceByAccount };
  return all.map((row) => {
    const prev = balances[row.bankAccountId] ?? 0;
    const next = row.status === "cancelled" ? prev : prev + row.amountMinor;
    balances[row.bankAccountId] = next;
    return { ...row, runningBalanceMinor: next };
  });
}

export const RRULE_PRESETS: Array<{ label: string; rrule: string }> = [
  { label: "Daily", rrule: "FREQ=DAILY" },
  { label: "Weekly (Mon)", rrule: "FREQ=WEEKLY;BYDAY=MO" },
  { label: "Weekly (Fri)", rrule: "FREQ=WEEKLY;BYDAY=FR" },
  { label: "Every 2 weeks", rrule: "FREQ=WEEKLY;INTERVAL=2" },
  { label: "Monthly (same day)", rrule: "FREQ=MONTHLY" },
  { label: "Monthly (1st)", rrule: "FREQ=MONTHLY;BYMONTHDAY=1" },
  { label: "Monthly (last day)", rrule: "FREQ=MONTHLY;BYMONTHDAY=-1" },
  { label: "Yearly", rrule: "FREQ=YEARLY" },
];
