// Reference implementation of the Financial Reports reconciliation matrix (§9).
// Pure functions over a minimal ledger model so they can be unit-tested on
// synthetic ledgers (§10) and reused by the in-app Reconciliation report.

export type AccountType = "asset" | "liability" | "equity" | "income" | "expense";

export interface RecAccount {
  id: string;
  type: AccountType;
  name?: string;
  /** Cash / cash-equivalent account (bank or undeposited funds). */
  is_cash?: boolean;
}

export interface RecLine {
  account_id: string;
  date: string;          // ISO yyyy-mm-dd
  debit: number;
  credit: number;
}

export interface ReconcileInput {
  accounts: RecAccount[];
  /** Cumulative GL lines dated <= asOf. */
  lines: RecLine[];
  /** Flow-report window start (period_start). Lines dated < this are "beginning". */
  periodStart: string;
  /** Sum of OPEN customer invoice balances as of asOf (§1.5). */
  openInvoices: number;
  /** Sum of OPEN vendor bill balances as of asOf (§1.5). */
  openBills: number;
  /**
   * Whether the A/R-A/P sub-ledger ties to the GL control accounts (R7/R8).
   * Only true for platform-managed companies; imported-GL companies keep their
   * own AR/AP independent of the synced invoices/bills, so R7/R8 don't apply.
   * Defaults to true.
   */
  arApApplicable?: boolean;
  /**
   * Net income for the period as produced by the app's P&L builder. R3 cross-checks
   * it against the raw-GL period net income computed here, catching a P&L
   * classification/grouping bug. Omitted → R3 is skipped.
   */
  reportPLNetIncome?: number;
  /**
   * Ending equity from the Movement-of-Equity builder and total equity from the
   * Balance Sheet builder. R5 cross-checks that the two statements agree.
   * Both omitted → R5 is skipped.
   */
  sceEndingEquity?: number;
  bsTotalEquity?: number;
}

export interface RecCheck { id: string; label: string; residual: number; pass: boolean }

const TOL = 0.005;
const near = (n: number) => Math.abs(n) < TOL;

/** R1, R2, R4, R7, R8 residuals. Residual 0 == pass; never plug a residual (§10). */
export function reconcile(input: ReconcileInput): RecCheck[] {
  const acctById = new Map(input.accounts.map((a) => [a.id, a]));

  let dr = 0, cr = 0;
  let assets = 0, liab = 0, equity = 0, income = 0, expense = 0;
  let arControl = 0, apControl = 0;
  const beginRaw = new Map<string, number>();
  const endRaw = new Map<string, number>();

  for (const l of input.lines) {
    const debit = Number(l.debit || 0), credit = Number(l.credit || 0);
    const raw = debit - credit;
    dr += debit; cr += credit;
    const a = acctById.get(l.account_id);
    if (!a) continue;
    const name = String(a.name || "").toLowerCase();
    switch (a.type) {
      case "asset": assets += raw; if (name.includes("receivable")) arControl += raw; break;
      case "liability": liab += -raw; if (name.includes("payable")) apControl += -raw; break;
      case "equity": equity += -raw; break;
      case "income": income += -raw; break;
      case "expense": expense += raw; break;
    }
    endRaw.set(l.account_id, (endRaw.get(l.account_id) ?? 0) + raw);
    if (l.date < input.periodStart) beginRaw.set(l.account_id, (beginRaw.get(l.account_id) ?? 0) + raw);
  }

  const netIncomeCum = income - expense;

  // R4: indirect CFO+CFI+CFF (= net income + all non-cash movements) == ΔCash.
  let deltaCash = 0, periodNI = 0, nonCashImpact = 0;
  const ids = new Set<string>([...endRaw.keys(), ...beginRaw.keys()]);
  for (const id of ids) {
    const a = acctById.get(id); if (!a) continue;
    const delta = (endRaw.get(id) ?? 0) - (beginRaw.get(id) ?? 0);
    if (a.is_cash) { deltaCash += delta; continue; }
    if (a.type === "income") periodNI += -delta;
    else if (a.type === "expense") periodNI -= delta;
    else nonCashImpact += -delta;
  }

  const checks: Omit<RecCheck, "pass">[] = [
    { id: "R1", label: "Trial Balance — total debits = total credits", residual: dr - cr },
    { id: "R2", label: "Balance Sheet — Assets = Liabilities + Equity (incl. net income)", residual: assets - (liab + equity + netIncomeCum) },
    { id: "R4", label: "Cash Flow — CFO+CFI+CFF = change in cash", residual: (periodNI + nonCashImpact) - deltaCash },
  ];
  // R3: the P&L builder's net income must equal the raw-GL period net income.
  if (input.reportPLNetIncome !== undefined) {
    checks.push({ id: "R3", label: "P&L net income = period net income from the GL", residual: input.reportPLNetIncome - periodNI });
  }
  // R5: the Movement-of-Equity ending equity must equal the Balance Sheet total equity.
  if (input.sceEndingEquity !== undefined && input.bsTotalEquity !== undefined) {
    checks.push({ id: "R5", label: "Movement of Equity ending = Balance Sheet total equity", residual: input.sceEndingEquity - input.bsTotalEquity });
  }
  // R7/R8 (sub-ledger vs GL control) only apply to platform-managed companies.
  if (input.arApApplicable !== false) {
    checks.push(
      { id: "R7", label: "A/R = open invoice balances (§1.5)", residual: arControl - input.openInvoices },
      { id: "R8", label: "A/P = open bill balances (§1.5)", residual: apControl - input.openBills },
    );
  }
  checks.sort((a, b) => Number(a.id.slice(1)) - Number(b.id.slice(1)));
  return checks.map((c) => ({ ...c, pass: near(c.residual) }));
}
