import { Link } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import { Fragment, useEffect, useMemo, useState } from "react";
import { accounting } from "@/lib/accountingClient";
import { supabase } from "@/integrations/supabase/client";
import { useCompanyId } from "./lib/useCompanyId";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/ui/table";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip as RTooltip, Legend, ResponsiveContainer } from "recharts";
import { FileText, Download, FileDown, Eye } from "lucide-react";
import { toast } from "sonner";
import { money, fmtDate } from "./lib/format";
import jsPDF from "jspdf";
import autoTable from "jspdf-autotable";
import {
  renderReportPdf, fmtAmount,
  type StructuredReport, type StructuredRow,
} from "./lib/reportPdf";
import { reconcile, type RecAccount, type RecLine } from "./lib/reconcile";
import {
  computePnL, computeMargins, toMinor, fromMinor, PnlValidationError,
  type PnlAccount, type PnlClassification, type Posting as PnlPosting, type PnlResult,
} from "./lib/pnl";
import { Lock } from "lucide-react";
import { TrialBalanceReport } from "./components/TrialBalanceReport";
import { GeneralLedgerReport } from "./components/GeneralLedgerReport";
import { ReserveFundReport } from "./components/ReserveFundReport";
import { ReportSheet } from "./components/ReportSheet";
import { loadBrandedLogo, drawBrandedHeader, drawBrandedFooter, type BrandedLogo } from "./lib/reportHeader";
import { generateBudgetVsActualPdf } from "@/lib/budgetVsActualPdf";

type ReportId =
  | "pnl" | "balance-sheet" | "cash-flow" | "movement-of-equity" | "budget-vs-actuals"
  | "trial-balance" | "general-ledger"
  | "invoice-summary" | "customer-balances" | "ar-aging" | "homeowner-summary" | "delinquency"
  | "expense-summary" | "vendor-balances" | "ap-aging" | "reconciliation"
  | "reserve-fund";

const APP_NAME = "Cozy Books";
const FINANCIAL: ReportId[] = ["pnl", "balance-sheet", "cash-flow", "movement-of-equity"];

const GROUPS = [
  { name: "Business Overview", reports: [
    { id: "pnl" as ReportId, name: "Profit & Loss" },
    { id: "balance-sheet" as ReportId, name: "Balance Sheet" },
    { id: "cash-flow" as ReportId, name: "Cash Flow Statement" },
    { id: "movement-of-equity" as ReportId, name: "Movement of Equity" },
    { id: "trial-balance" as ReportId, name: "Trial Balance" },
    { id: "general-ledger" as ReportId, name: "General Ledger" },
    { id: "budget-vs-actuals" as ReportId, name: "Budget vs Actuals" },
  ]},
  { name: "Receivables", reports: [
    { id: "ar-aging" as ReportId, name: "AR Aging Details" },
    { id: "homeowner-summary" as ReportId, name: "Homeowner Balance Summary" },
    { id: "customer-balances" as ReportId, name: "Invoice Summary by Customer" },
    { id: "invoice-summary" as ReportId, name: "Invoice Summary" },
    { id: "delinquency" as ReportId, name: "Delinquency Report" },
  ]},
  { name: "Payables", reports: [
    { id: "ap-aging" as ReportId, name: "AP Aging Details" },
    { id: "expense-summary" as ReportId, name: "Expense Summary" },
    { id: "vendor-balances" as ReportId, name: "Vendor Balance Summary" },
  ]},
  { name: "Reserves", reports: [
    { id: "reserve-fund" as ReportId, name: "Reserve Fund Schedule" },
  ]},
  { name: "Audit", reports: [
    { id: "reconciliation" as ReportId, name: "Reconciliation Checks" },
  ]},
];

const TZ_ET = "America/New_York";
function etDateStr(d = new Date()) { return d.toLocaleDateString("en-CA", { timeZone: TZ_ET }); } // YYYY-MM-DD in ET
function startOfYear() { return `${new Date().toLocaleDateString("en-US", { timeZone: TZ_ET, year: "numeric" })}-01-01`; }
function today() { return etDateStr(); }
function shiftBack(from: string, to: string) {
  const d1 = new Date(from), d2 = new Date(to);
  const span = d2.getTime() - d1.getTime();
  const pTo = new Date(d1.getTime() - 86400000);
  const pFrom = new Date(pTo.getTime() - span);
  return { from: pFrom.toISOString().slice(0, 10), to: pTo.toISOString().slice(0, 10) };
}

function useReportData(cid: string, from: string, to: string) {

  return useQuery({
    queryKey: ["reports-data", cid, from, to],
    enabled: !!cid,
    queryFn: async () => {
      const ytdStart = new Date(new Date().getFullYear(), 0, 1).toISOString().slice(0,10);
      const [inv, bills, accs, exp, custs, vends, ob, ytdInv, ytdExp, ytdBills, allBills, glRes, glCumRes, allInvRes, companyRes] = await Promise.all([
        accounting.from("invoices").select("number,total,paid_amount,status,issue_date,customers(name)").eq("company_id", cid).gte("issue_date", from).lte("issue_date", to),
        accounting.from("bills").select("number,total,paid_amount,status,issue_date,due_date,vendors(name)").eq("company_id", cid).gte("issue_date", from).lte("issue_date", to),
        accounting.from("accounts").select("id,name,code,type,subtype,balance,is_bank").eq("company_id", cid),
        accounting.from("expenses").select("date,category,amount,vendor_name,vendors(name)").eq("company_id", cid).gte("date", from).lte("date", to),
        accounting.from("customers").select("id,name,balance,email,phone,property_address,lot_number").eq("company_id", cid).order("name"),
        accounting.from("vendors").select("id,name").eq("company_id", cid),
        accounting.from("opening_balances").select("account_id,debit,credit").eq("company_id", cid),
        accounting.from("invoices").select("total,status,issue_date").eq("company_id", cid).gte("issue_date", ytdStart).lte("issue_date", to),
        accounting.from("expenses").select("amount,date").eq("company_id", cid).gte("date", ytdStart).lte("date", to),
        // YTD bills (accrual — for net income in Balance Sheet / Movement of Equity)
        accounting.from("bills").select("total,status,issue_date").eq("company_id", cid).gte("issue_date", ytdStart).lte("issue_date", to),
        // All bills (not date-filtered) for AP aging
        accounting.from("bills").select("id,vendor_id,total,paid_amount,status,due_date,issue_date,vendors(id,name)").eq("company_id", cid),
        // General-ledger lines in period — P&L is built from these, grouped by account
        accounting.from("journal_entry_lines")
          .select("debit,credit,accounts!inner(id,name,code,type,parent_account_id),journal_entries!inner(company_id,date)")
          .eq("journal_entries.company_id", cid)
          .gte("journal_entries.date", from)
          .lte("journal_entries.date", to),
        // Cumulative GL through `to` — Balance Sheet is built from these (as-of balances)
        accounting.from("journal_entry_lines")
          .select("debit,credit,account_id,accounts!inner(type),journal_entries!inner(company_id,date)")
          .eq("journal_entries.company_id", cid)
          .lte("journal_entries.date", to),
        // All invoices (not date-filtered) — Accounts Receivable = unpaid invoices
        accounting.from("invoices").select("total,paid_amount,status").eq("company_id", cid),
        // Whether the platform manages this company's GL (A/R-A/P sub-ledgers tie to the GL).
        // Imported-GL companies (gl_auto_post=false) keep their own AR/AP, so the sub-ledger
        // vs GL control reconciliation (R7/R8) does not apply to them.
        accounting.from("companies").select("gl_auto_post").eq("id", cid).maybeSingle(),
      ]);
      return {
        invoices: inv.data ?? [], bills: bills.data ?? [], accounts: accs.data ?? [],
        expenses: exp.data ?? [], customers: custs.data ?? [], vendors: vends.data ?? [],
        openingBalances: ob.data ?? [],
        ytdInvoices: ytdInv.data ?? [], ytdExpenses: ytdExp.data ?? [], ytdBills: ytdBills.data ?? [],
        allBills: allBills.data ?? [],
        glLines: glRes.data ?? [],
        glCumulative: glCumRes.data ?? [],
        allInvoices: allInvRes.data ?? [],
        glManaged: companyRes.data ? companyRes.data.gl_auto_post !== false : true,
        from, asOf: to,
      };
    },
  });
}

// ── Period preset helpers ────────────────────────────────────────────────────

function etNow() {
  // Current date/time as a Date object normalised to ET by parsing the ET date string
  const etStr = new Date().toLocaleDateString("en-US", { timeZone: TZ_ET, year: "numeric", month: "2-digit", day: "2-digit" });
  return new Date(etStr); // local Date at ET midnight
}
function startOfMonth(d = etNow()) {
  return new Date(d.getFullYear(), d.getMonth(), 1).toLocaleDateString("en-CA");
}
function endOfMonth(d = etNow()) {
  return new Date(d.getFullYear(), d.getMonth() + 1, 0).toLocaleDateString("en-CA");
}
function startOfLastMonth() {
  const d = etNow(); d.setMonth(d.getMonth() - 1);
  return startOfMonth(d);
}
function endOfLastMonth() {
  const d = etNow(); d.setMonth(d.getMonth() - 1);
  return endOfMonth(d);
}
function startOfLastYear() {
  return `${etNow().getFullYear() - 1}-01-01`;
}
function endOfLastYear() {
  return `${etNow().getFullYear() - 1}-12-31`;
}

type Preset = "this-month" | "last-month" | "this-quarter" | "ytd" | "last-year" | "custom";
type CompareMode = "none" | "prior-period" | "prior-year" | "custom";

function presetDates(p: Preset): { from: string; to: string } {
  const now = new Date();
  const q = Math.floor(now.getMonth() / 3);
  switch (p) {
    case "this-month":    return { from: startOfMonth(), to: endOfMonth() };
    case "last-month":    return { from: startOfLastMonth(), to: endOfLastMonth() };
    case "this-quarter":  return { from: new Date(now.getFullYear(), q * 3, 1).toISOString().slice(0, 10), to: today() };
    case "ytd":           return { from: startOfYear(), to: today() };
    case "last-year":     return { from: startOfLastYear(), to: endOfLastYear() };
    default:              return { from: startOfYear(), to: today() };
  }
}

function compareDates(mode: CompareMode, from: string, to: string, customFrom: string, customTo: string) {
  if (mode === "none") return null;
  if (mode === "prior-period") return shiftBack(from, to);
  if (mode === "prior-year") {
    const py = (d: string) => `${parseInt(d.slice(0, 4)) - 1}${d.slice(4)}`;
    return { from: py(from), to: py(to) };
  }
  if (mode === "custom") return { from: customFrom, to: customTo };
  return null;
}

export default function AccountingReportsPage({ association }: { association?: { id: string; name?: string } | null } = {}) {
  const { companyId, associationName, associationId } = useCompanyId(association);
  const cid = companyId ?? "";
  const cur = "USD";
  const [active, setActive] = useState<ReportId>("pnl");
  // Drill-down: clicking an account amount in a financial report opens the
  // General Ledger focused on that account (its transaction list for the COA).
  const [drillAccountId, setDrillAccountId] = useState<string | null>(null);
  const drillToAccount = (accountId: string) => { setDrillAccountId(accountId); setActive("general-ledger"); };

  // Period
  const [preset, setPreset] = useState<Preset>("ytd");
  const [from, setFrom] = useState(startOfYear());
  const [to, setTo] = useState(today());

  const applyPreset = (p: Preset) => {
    setPreset(p);
    if (p !== "custom") {
      const d = presetDates(p);
      setFrom(d.from);
      setTo(d.to);
    }
  };

  // Comparison
  const [compareMode, setCompareMode] = useState<CompareMode>("none");
  const [compareFrom, setCompareFrom] = useState(startOfLastYear());
  const [compareTo, setCompareTo] = useState(endOfLastYear());
  const showCompare = compareMode !== "none";
  const comparePeriod = compareDates(compareMode, from, to, compareFrom, compareTo);

  // Toggles
  const [showCodes, setShowCodes] = useState(false);
  const [showZero, setShowZero] = useState(false);

  const { data: companyMeta } = useQuery({
    queryKey: ["company-fy", cid],
    enabled: !!cid,
    queryFn: async () => (await accounting.from("companies").select("fiscal_year_start").eq("id", cid).maybeSingle()).data,
  });
  const fiscalYearStart = (companyMeta as any)?.fiscal_year_start || "01-01";

  // Association logo for branded reports (ACM fallback handled downstream).
  const { data: assocMeta } = useQuery({
    queryKey: ["assoc-logo", associationId],
    enabled: !!associationId,
    queryFn: async () => (await supabase.from("associations").select("logo_url").eq("id", associationId!).maybeSingle()).data,
  });
  const logoUrl = (assocMeta as any)?.logo_url || null;
  // Preloaded logo dataURL for synchronous PDF header drawing.
  const { data: brandedLogo } = useQuery({
    queryKey: ["branded-logo", logoUrl],
    queryFn: async () => await loadBrandedLogo(logoUrl),
  });

  const { data } = useReportData(cid, from, to);
  const { data: prevData } = useReportData(
    showCompare && comparePeriod ? cid : "",
    comparePeriod?.from ?? from,
    comparePeriod?.to ?? to
  );

  // AR open invoices — used by AR aging, homeowner summary, delinquency
  const arReports: ReportId[] = ["customer-balances", "ar-aging", "homeowner-summary", "delinquency"];
  const { data: arOpen = [] } = useQuery({
    queryKey: ["ar-aging", cid],
    enabled: !!cid && arReports.includes(active),
    queryFn: async () => {
      const { data } = await accounting
        .from("invoices")
        .select("id,customer_id,total,paid_amount,due_date,issue_date,status,number,customers(id,name)")
        .eq("company_id", cid);
      return data ?? [];
    },
  });

  const isFinancial = FINANCIAL.includes(active);
  const activeMeta = GROUPS.flatMap(g => g.reports).find(r => r.id === active)!;
  const rangeLabel = active === "balance-sheet"
    ? `As of ${fmtDate(to)}`
    : `${fmtDate(from)} – ${fmtDate(to)}`;

  const structured = useMemo<StructuredReport | null>(() => {
    if (!data || !isFinancial) return null;
    return buildFinancial(active, data, prevData, showCompare);
  }, [active, data, prevData, showCompare, isFinancial]);

  const flat = useMemo(() => (data && !isFinancial) ? buildFlat(active, data, cur) : null, [active, data, cur, isFinancial]);

  // Build exportable flat data for custom-rendered reports (aging, delinquency, etc.)
  const exportFlat = useMemo((): Flat | null => {
    if (structured || flat) return null; // already have a better source
    const m = (n: number) => money(n, cur);
    const now = new Date();

    if (active === "ar-aging" || active === "customer-balances") {
      type AR = { name: string; current: number; d30: number; d60: number; d90: number; d90p: number; total: number };
      const byC = new Map<string, AR>();
      for (const inv of arOpen as any[]) {
        const open = Number(inv.total ?? 0) - Number(inv.paid_amount ?? 0);
        if (open <= 0) continue;
        const cid2 = inv.customer_id; if (!cid2) continue;
        const name = inv.customers?.name ?? "—";
        const days = Math.floor((now.getTime() - new Date(inv.due_date ?? inv.issue_date).getTime()) / 86400000);
        const r = byC.get(cid2) ?? { name, current: 0, d30: 0, d60: 0, d90: 0, d90p: 0, total: 0 };
        if (days <= 0) r.current += open; else if (days <= 30) r.d30 += open; else if (days <= 60) r.d60 += open; else if (days <= 90) r.d90 += open; else r.d90p += open;
        r.total += open; byC.set(cid2, r);
      }
      const list = [...byC.values()].sort((a, b) => b.total - a.total);
      const tot = list.reduce((s, r) => ({ current: s.current + r.current, d30: s.d30 + r.d30, d60: s.d60 + r.d60, d90: s.d90 + r.d90, d90p: s.d90p + r.d90p, total: s.total + r.total }), { current: 0, d30: 0, d60: 0, d90: 0, d90p: 0, total: 0 });
      return {
        title: activeMeta.name, columns: ["Homeowner", "Current", "1-30 days", "31-60 days", "61-90 days", "90+ days", "Total"],
        rows: [...list.map(r => [r.name, m(r.current), m(r.d30), m(r.d60), m(r.d90), m(r.d90p), m(r.total)]),
               ["TOTAL", m(tot.current), m(tot.d30), m(tot.d60), m(tot.d90), m(tot.d90p), m(tot.total)]],
        boldRows: [list.length],
      };
    }

    if (active === "ap-aging") {
      type AP = { name: string; current: number; d30: number; d60: number; d90: number; d90p: number; total: number };
      const byV = new Map<string, AP>();
      for (const b of ((data as any)?.allBills ?? []) as any[]) {
        const open = Number(b.total ?? 0) - Number(b.paid_amount ?? 0);
        if (open <= 0) continue;
        const vid = b.vendor_id ?? b.id; const name = b.vendors?.name ?? "Unknown";
        const days = Math.floor((now.getTime() - new Date(b.due_date ?? b.issue_date ?? Date.now()).getTime()) / 86400000);
        const r = byV.get(vid) ?? { name, current: 0, d30: 0, d60: 0, d90: 0, d90p: 0, total: 0 };
        if (days <= 0) r.current += open; else if (days <= 30) r.d30 += open; else if (days <= 60) r.d60 += open; else if (days <= 90) r.d90 += open; else r.d90p += open;
        r.total += open; byV.set(vid, r);
      }
      const list = [...byV.values()].sort((a, b) => b.total - a.total);
      return {
        title: "AP Aging Details", columns: ["Vendor", "Current", "1-30 days", "31-60 days", "61-90 days", "90+ days", "Total"],
        rows: list.map(r => [r.name, m(r.current), m(r.d30), m(r.d60), m(r.d90), m(r.d90p), m(r.total)]),
      };
    }

    if (active === "homeowner-summary") {
      const customers = (data as any)?.customers ?? [];
      const openByC = new Map<string, { open: number; count: number }>();
      for (const inv of arOpen as any[]) {
        const open = Number(inv.total ?? 0) - Number(inv.paid_amount ?? 0);
        if (open > 0) { const e = openByC.get(inv.customer_id) ?? { open: 0, count: 0 }; e.open += open; e.count++; openByC.set(inv.customer_id, e); }
      }
      const rows = (customers as any[])
        .map((c: any) => [c.name, c.property_address ?? "—", c.lot_number ? `Lot ${c.lot_number}` : "—", String(openByC.get(c.id)?.count ?? 0), m(openByC.get(c.id)?.open ?? 0)])
        .sort((a, b) => parseFloat(String(b[4]).replace(/[^0-9.]/g, "")) - parseFloat(String(a[4]).replace(/[^0-9.]/g, "")));
      return { title: "Homeowner Balance Summary", columns: ["Homeowner", "Property", "Lot", "Open Invoices", "Outstanding"] , rows };
    }

    if (active === "delinquency") {
      const customers = (data as any)?.customers ?? [];
      const overdue = new Map<string, { name: string; property: string; email: string; phone: string; amount: number; oldest: number }>();
      for (const inv of arOpen as any[]) {
        const open = Number(inv.total ?? 0) - Number(inv.paid_amount ?? 0);
        if (open <= 0) continue;
        const due = new Date(inv.due_date ?? inv.issue_date);
        const days = Math.floor((now.getTime() - due.getTime()) / 86400000);
        if (days <= 0) continue;
        const cust = (customers as any[]).find((c: any) => c.id === inv.customer_id);
        const r = overdue.get(inv.customer_id) ?? { name: cust?.name ?? "—", property: cust?.property_address ?? "—", email: cust?.email ?? "—", phone: cust?.phone ?? "—", amount: 0, oldest: 0 };
        r.amount += open; r.oldest = Math.max(r.oldest, days); overdue.set(inv.customer_id, r);
      }
      const list = [...overdue.values()].sort((a, b) => b.amount - a.amount);
      return {
        title: "Delinquency Report", columns: ["Homeowner", "Property", "Email", "Phone", "Days Overdue", "Amount Overdue"],
        rows: list.map(r => [r.name, r.property, r.email, r.phone, String(r.oldest), m(r.amount)]),
      };
    }

    return null;
  }, [active, arOpen, data, flat, structured, cur, activeMeta.name]);

  // Reports whose export is handled internally (own PDF/CSV buttons inside the component)
  const hasOwnExport = active === "trial-balance" || active === "general-ledger" || active === "budget-vs-actuals";
  const anyExportable = !!(structured || flat || exportFlat);

  const doExportPDF = async () => {
    const fileBase = `${activeMeta.name.replace(/\s+/g, "-").toLowerCase()}-${from}-to-${to}`;
    const src = flat ?? exportFlat;
    const logo = brandedLogo ?? (await loadBrandedLogo(logoUrl));
    if (structured) {
      const doc = renderReportPdf(
        structured,
        { companyName: associationName ?? "Company", appName: APP_NAME, rangeLabel, currency: cur, showCodes, showCompare, showZero, logo },
      );
      doc.save(`${fileBase}.pdf`);
    } else if (src) {
      const doc = new jsPDF({ unit: "pt", format: "letter", orientation: src.columns.length > 6 ? "landscape" : "portrait" });
      const startY = drawBrandedHeader(doc, {
        logo, title: src.title,
        metaLines: [{ label: "Properties:", value: associationName ?? "" }, { label: "Period:", value: rangeLabel }],
      });
      const lastCol = src.columns.length - 1;
      autoTable(doc, {
        head: [src.columns],
        body: src.rows.map(r => r.map(String)),
        startY,
        margin: { left: 40, right: 40 },
        styles: { font: "helvetica", fontSize: 8, textColor: [33, 37, 41], lineColor: [222, 226, 230], lineWidth: 0.1 },
        headStyles: { fillColor: [237, 239, 242], textColor: [33, 37, 41], fontStyle: "bold", lineColor: [196, 200, 205], lineWidth: 0.2 },
        alternateRowStyles: { fillColor: [247, 248, 250] },
        columnStyles: { [lastCol]: { halign: "right" } },
        didParseCell: ({ row, cell }) => { if (src.boldRows?.includes(row.index)) cell.styles.fontStyle = "bold"; },
      });
      drawBrandedFooter(doc);
      doc.save(`${fileBase}.pdf`);
    } else {
      toast.error("No data to export for this report");
    }
  };

  const doExportCSV = () => {
    const fileBase = `${activeMeta.name.replace(/\s+/g, "-").toLowerCase()}-${from}-to-${to}`;
    let lines: string[] = [];
    if (structured) {
      lines.push(["Account", "Amount", showCompare ? "Previous" : ""].filter(Boolean).join(","));
      for (const r of structured.rows) {
        if (r.kind === "spacer") continue;
        if (r.kind === "sub" && !showZero && (r.amount ?? 0) === 0) continue;
        const label = (r.kind === "sub" ? "  " : "") + (showCodes && r.code ? `${r.code} ${r.label}` : r.label);
        const cells = [`"${label}"`, fmtAmount(r.amount)];
        if (showCompare) cells.push(fmtAmount(r.compare));
        lines.push(cells.join(","));
      }
    } else {
      const src = flat ?? exportFlat;
      if (src) {
        lines.push(src.columns.map(c => `"${c}"`).join(","));
        for (const r of src.rows) lines.push(r.map(c => `"${String(c).replace(/"/g, '""')}"`).join(","));
      }
    }
    if (!lines.length) { toast.error("No data to export"); return; }
    const blob = new Blob([lines.join("\n")], { type: "text/csv" });
    const a = document.createElement("a");
    a.href = URL.createObjectURL(blob); a.download = `${fileBase}.csv`; a.click();
  };

  // Keep old names for any remaining references
  const exportPDF = doExportPDF;
  const exportCSV = doExportCSV;

  return (
    <div className="flex gap-6">
      <aside className="w-60 shrink-0">
        <Card>
          <CardContent className="p-3 space-y-4">
            {GROUPS.map((g) => (
              <div key={g.name}>
                <div className="px-2 pb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">{g.name}</div>
                <ul className="space-y-0.5">
                  {g.reports.map((r) => (
                    <li key={r.id}>
                      <button
                        onClick={() => setActive(r.id)}
                        className={`flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors ${active === r.id ? "bg-primary text-primary-foreground" : "hover:bg-muted"}`}
                      >
                        <FileText className="h-3.5 w-3.5 shrink-0" />
                        <span className="truncate">{r.name}</span>
                      </button>
                    </li>
                  ))}
                </ul>
              </div>
            ))}
          </CardContent>
        </Card>
      </aside>

      <div className="min-w-0 flex-1 space-y-4">
        <div className="space-y-3">
          <div className="flex flex-wrap items-end justify-between gap-3">
            <div>
              <h1 className="text-2xl font-semibold">{activeMeta.name}</h1>
              <p className="text-sm text-muted-foreground">{rangeLabel}</p>
            </div>
            <div className="flex gap-2">
              {hasOwnExport ? (
                <span className="text-xs text-muted-foreground self-center">Export available inside the report ↓</span>
              ) : (
                <>
                  <Button variant="outline" onClick={exportCSV} disabled={!anyExportable}><Download className="mr-1 h-4 w-4" /> CSV</Button>
                  <Button onClick={exportPDF} disabled={!anyExportable}><FileDown className="mr-1 h-4 w-4" /> PDF</Button>
                </>
              )}
            </div>
          </div>

          {/* Period presets */}
          <Card>
            <CardContent className="py-3 px-4 space-y-3">
              <div className="flex flex-wrap items-center gap-2">
                <span className="text-xs font-medium text-muted-foreground w-14">Period</span>
                {([
                  { v: "this-month", l: "This Month" },
                  { v: "last-month", l: "Last Month" },
                  { v: "this-quarter", l: "This Quarter" },
                  { v: "ytd", l: "YTD" },
                  { v: "last-year", l: "Last Year" },
                  { v: "custom", l: "Custom" },
                ] as { v: Preset; l: string }[]).map(({ v, l }) => (
                  <button key={v} onClick={() => applyPreset(v)}
                    className={`rounded-md px-3 py-1 text-xs font-medium border transition-colors ${preset === v ? "bg-primary text-primary-foreground border-primary" : "bg-background border-border text-muted-foreground hover:text-foreground hover:border-foreground/30"}`}>
                    {l}
                  </button>
                ))}
                {preset === "custom" && (
                  <div className="flex items-center gap-2 ml-2">
                    <Input type="date" value={from} onChange={e => setFrom(e.target.value)} className="h-7 w-36 text-xs" />
                    <span className="text-muted-foreground text-xs">to</span>
                    <Input type="date" value={to} onChange={e => setTo(e.target.value)} className="h-7 w-36 text-xs" />
                  </div>
                )}
              </div>

              {/* Comparison period — only for financial reports */}
              {isFinancial && (
                <div className="flex flex-wrap items-center gap-2 border-t pt-3">
                  <span className="text-xs font-medium text-muted-foreground w-14">Compare</span>
                  {([
                    { v: "none", l: "None" },
                    { v: "prior-period", l: "Prior Period" },
                    { v: "prior-year", l: "Prior Year" },
                    { v: "custom", l: "Custom" },
                  ] as { v: CompareMode; l: string }[]).map(({ v, l }) => (
                    <button key={v} onClick={() => setCompareMode(v)}
                      className={`rounded-md px-3 py-1 text-xs font-medium border transition-colors ${compareMode === v ? "bg-primary text-primary-foreground border-primary" : "bg-background border-border text-muted-foreground hover:text-foreground hover:border-foreground/30"}`}>
                      {l}
                    </button>
                  ))}
                  {compareMode === "custom" && (
                    <div className="flex items-center gap-2 ml-2">
                      <Input type="date" value={compareFrom} onChange={e => setCompareFrom(e.target.value)} className="h-7 w-36 text-xs" />
                      <span className="text-muted-foreground text-xs">to</span>
                      <Input type="date" value={compareTo} onChange={e => setCompareTo(e.target.value)} className="h-7 w-36 text-xs" />
                    </div>
                  )}
                </div>
              )}

              {/* Display toggles */}
              <div className="flex flex-wrap gap-4 border-t pt-3">
                <Toggle id="t-codes" checked={showCodes} onChange={setShowCodes} label="Account codes" />
                <Toggle id="t-zero" checked={showZero} onChange={setShowZero} label="Zero-balance accounts" />
              </div>
            </CardContent>
          </Card>
        </div>
        {active === "budget-vs-actuals" && (
          <BudgetVsActuals companyId={cid} from={from} to={to} currency={cur} companyName={associationName ?? "Company"} rangeLabel={rangeLabel} logoUrl={logoUrl} />
        )}
        {active === "trial-balance" && (
          <TrialBalanceReport companyId={cid} companyName={associationName ?? ""} logoUrl={logoUrl} />
        )}
        {active === "general-ledger" && (
          <GeneralLedgerReport companyId={cid} companyName={associationName ?? ""} logoUrl={logoUrl} initialAccountId={drillAccountId} />
        )}
        {active === "reserve-fund" && (
          <ReserveFundReport companyId={cid} companyName={associationName ?? ""} fiscalYearStart={fiscalYearStart} logoUrl={logoUrl} />
        )}
        {active === "reconciliation" && (
          <ReportSheet title="Reconciliation Checks" companyName={associationName ?? "Company"} period={rangeLabel} logoUrl={logoUrl}>
            <ReconciliationReport d={data} currency={cur} />
          </ReportSheet>
        )}
        {isFinancial && (
          !data ? (
            <Card><CardContent className="p-6"><div className="py-8 text-center text-sm text-muted-foreground">Loading…</div></CardContent></Card>
          ) : structured ? (
            <ReportSheet title={activeMeta.name} subtitle="Accrual basis" companyName={associationName ?? "Company"} period={rangeLabel} logoUrl={logoUrl}>
              <StructuredTable report={structured} showCodes={showCodes} showCompare={showCompare} showZero={showZero} currency={cur} onDrill={drillToAccount} />
            </ReportSheet>
          ) : (
            <Card><CardContent className="p-6"><div className="py-12 text-center text-sm text-muted-foreground">No data for this report in the selected range.</div></CardContent></Card>
          )
        )}
        {!isFinancial && active !== "budget-vs-actuals" && active !== "trial-balance" && active !== "general-ledger" && active !== "reserve-fund" && active !== "reconciliation" && (
        <ReportSheet title={activeMeta.name} companyName={associationName ?? "Company"} period={rangeLabel} logoUrl={logoUrl}>
            {!data ? (
              <div className="text-sm text-muted-foreground">Loading…</div>
            ) : structured ? (
              <StructuredTable report={structured} showCodes={showCodes} showCompare={showCompare} showZero={showZero} currency={cur} />
            ) : active === "customer-balances" ? (
              <ARAgingTable rows={arOpen as any[]} currency={cur} />
            ) : active === "ar-aging" ? (
              <ARAgingTable rows={arOpen as any[]} currency={cur} detailed />
            ) : active === "ap-aging" ? (
              <APAgingTable rows={(data as any)?.allBills ?? []} currency={cur} />
            ) : active === "homeowner-summary" ? (
              <HomeownerSummaryTable customers={(data as any)?.customers ?? []} invoices={arOpen as any[]} currency={cur} />
            ) : active === "delinquency" ? (
              <DelinquencyTable customers={(data as any)?.customers ?? []} invoices={arOpen as any[]} currency={cur} />
            ) : flat && flat.rows.length > 0 ? (
              <Table>
                <TableHeader>
                  <TableRow>{flat.columns.map((c, i) => (
                    <TableHead key={c} className={i === flat.columns.length - 1 ? "text-right" : ""}>{c}</TableHead>
                  ))}</TableRow>
                </TableHeader>
                <TableBody>
                  {flat.rows.map((row, i) => (
                    <TableRow key={i} className={flat.boldRows?.includes(i) ? "font-semibold bg-muted/30" : ""}>
                      {row.map((cell, j) => (
                        <TableCell key={j} className={j === row.length - 1 ? "text-right" : ""}>{cell}</TableCell>
                      ))}
                    </TableRow>
                  ))}
                </TableBody>
              </Table>
            ) : (
              <div className="py-12 text-center text-sm text-muted-foreground">No data for this report in the selected range.</div>
            )}
        </ReportSheet>
        )}
      </div>

    </div>
  );
}

function Toggle({ id, checked, onChange, label, disabled }: { id: string; checked: boolean; onChange: (v: boolean) => void; label: string; disabled?: boolean }) {
  return (
    <div className="flex items-center gap-2">
      <Switch id={id} checked={checked} onCheckedChange={onChange} disabled={disabled} />
      <Label htmlFor={id} className={disabled ? "text-muted-foreground" : ""}>{label}</Label>
    </div>
  );
}

function StructuredTable({ report, showCodes, showCompare, showZero, currency, onDrill }: {
  report: StructuredReport; showCodes: boolean; showCompare: boolean; showZero: boolean; currency: string;
  onDrill?: (accountId: string, label: string) => void;
}) {
  let alt = false;
  const span = showCompare ? 5 : 2;
  const pctStr = (amount?: number, compare?: number) => {
    if (amount === undefined || compare === undefined || Math.abs(compare) < 0.005) return "—";
    return `${(((amount - compare) / Math.abs(compare)) * 100).toFixed(1)}%`;
  };
  return (
    <table className="w-full text-sm">
      <thead>
        <tr className="border-b text-xs uppercase tracking-wide text-muted-foreground">
          <th className="py-2 text-left font-semibold">Account</th>
          <th className="py-2 text-right font-semibold">Amount</th>
          {showCompare && <th className="py-2 text-right font-semibold">Comparative</th>}
          {showCompare && <th className="py-2 text-right font-semibold">Change</th>}
          {showCompare && <th className="py-2 text-right font-semibold">Change %</th>}
        </tr>
      </thead>
      <tbody>
        {report.rows.map((r, i) => {
          if (r.kind === "spacer") { alt = false; return <tr key={i}><td colSpan={span} className="h-3" /></tr>; }
          if (r.kind === "sub" && !showZero && (r.amount ?? 0) === 0) return null;
          if (r.kind === "section") {
            alt = false;
            return (
              <tr key={i} className="bg-[hsl(174_47%_94%)]">
                <td colSpan={span} className="py-2 px-2 font-semibold text-[hsl(174_70%_25%)] text-sm">{r.label}</td>
              </tr>
            );
          }
          if (r.kind === "group") {
            alt = false;
            return (
              <tr key={i}>
                <td colSpan={span} className="pt-2.5 pb-1 pl-4 font-semibold text-sm">{r.label}</td>
              </tr>
            );
          }
          const bold = r.kind === "total" || r.kind === "grand";
          const shaded = r.kind === "sub" && alt;
          if (r.kind === "sub") alt = !alt;
          const delta = (r.amount !== undefined && r.compare !== undefined) ? r.amount - r.compare : undefined;
          const drillable = r.kind === "sub" && !!r.accountId && !!onDrill;
          return (
            <tr key={i}
              onClick={drillable ? () => onDrill!(r.accountId!, r.label) : undefined}
              title={drillable ? "View transactions for this account" : undefined}
              className={[
              shaded ? "bg-muted/40" : "",
              bold ? "border-t font-semibold" : "",
              drillable ? "cursor-pointer hover:bg-primary/5" : "",
            ].join(" ")}>
              <td className={r.kind === "sub" ? "pl-6 py-1.5 border-l-2 border-muted ml-2" : "py-1.5 px-2"}>
                {showCodes && r.code && <span className="text-xs text-muted-foreground mr-2 font-mono">{r.code}</span>}
                <span className={drillable ? "text-primary underline-offset-2 hover:underline" : ""}>{r.label}</span>
              </td>
              <AmountCell n={r.amount} bold={bold} doubleUnderline={r.kind === "grand"} />
              {showCompare && <AmountCell n={r.compare} />}
              {showCompare && <AmountCell n={delta} bold={bold} />}
              {showCompare && <td className={`py-1.5 px-2 text-right tabular-nums text-muted-foreground ${bold ? "font-semibold" : ""}`}>{r.amount !== undefined ? pctStr(r.amount, r.compare) : ""}</td>}
            </tr>
          );
        })}
      </tbody>
      {report.balanced !== undefined && (
        <tfoot>
          <tr>
            <td colSpan={span} className="pt-3">
              <div className={`rounded-md px-4 py-2 text-sm font-semibold text-white ${report.balanced ? "bg-primary" : "bg-red-600"}`}>
                {report.balanced ? "Balance Sheet is balanced ✓" : `Balance Sheet is OUT OF BALANCE by ${money(report.outOfBalanceAmount ?? 0, currency)} (Assets − Liabilities − Equity)`}
              </div>
            </td>
          </tr>
        </tfoot>
      )}
      {report.cashHighlight && (
        <tfoot>
          <tr><td colSpan={span} className="pt-3">
            <div className="flex items-center justify-between rounded-md border border-primary bg-[hsl(174_47%_94%)] px-4 py-2.5 text-sm font-semibold text-[hsl(174_70%_25%)]">
              <span>{report.cashHighlight.label}</span>
              <span className={report.cashHighlight.amount < 0 ? "text-red-600" : ""}>{fmtAmount(report.cashHighlight.amount)}</span>
            </div>
          </td></tr>
        </tfoot>
      )}
    </table>
  );
}

function AmountCell({ n, bold, doubleUnderline }: { n?: number; bold?: boolean; doubleUnderline?: boolean }) {
  if (n === undefined) return <td className="py-1.5 px-2" />;
  const cls = [
    "py-1.5 px-2 text-right tabular-nums",
    n < 0 ? "text-red-600" : "",
    bold ? "font-semibold" : "",
    doubleUnderline ? "border-b-4 border-double border-primary" : "",
  ].join(" ");
  return <td className={cls}>{fmtAmount(n)}</td>;
}

function PreviewSheet({ report, companyName, rangeLabel, showCodes, showCompare, showZero }: {
  report: StructuredReport | null;
  companyName: string; rangeLabel: string; showCodes: boolean; showCompare: boolean; showZero: boolean;
}) {
  if (!report) return <div className="text-center text-muted-foreground py-12">No data.</div>;
  return (
    <div className="mx-auto bg-white shadow-md" style={{ width: 816, minHeight: 1056, fontFamily: "Georgia, 'Times New Roman', serif" }}>
      <div className="px-12 pt-10 pb-6">
        <div className="flex items-start gap-4">
          <div className="h-11 w-11 rounded bg-primary flex items-center justify-center text-white font-bold text-lg">
            {(companyName[0] ?? "?").toUpperCase()}
          </div>
          <div className="text-xl font-bold text-gray-900 mt-1">{companyName}</div>
        </div>
        <h1 className="text-2xl font-bold text-center mt-4">{report.title}</h1>
        <div className="text-center text-sm text-gray-500 mt-1">{rangeLabel}</div>
        <div className="mt-3 h-[1.5px] bg-primary" />
      </div>
      <div className="px-12 pb-12">
        <StructuredTable report={report} showCodes={showCodes} showCompare={showCompare} showZero={showZero} currency="" />
      </div>
      <div className="px-12 pb-6">
        <div className="h-[1px] bg-primary mb-3" />
        <div className="text-center text-xs text-gray-500">Page 1 of 1</div>
        <div className="text-center text-xs text-gray-500">Generated by {APP_NAME} on {new Date().toLocaleDateString()}</div>
      </div>
    </div>
  );
}

// ---------- Financial report builders (structured) ----------

// Reconciliation matrix (§9) surfaced as visible residuals — never plug a residual.
function ReconciliationReport({ d, currency }: { d: any; currency: string }) {
  if (!d) return <div className="text-sm text-muted-foreground">Loading…</div>;
  const accounts: RecAccount[] = ((d.accounts ?? []) as any[]).map((a) => ({
    id: a.id, type: a.type, name: a.name,
    is_cash: !!a.is_bank || /cash|undeposited/i.test(String(a.name || "")),
  }));
  const lines: RecLine[] = ((d.glCumulative ?? []) as any[]).map((l) => ({
    account_id: l.account_id, date: String(l.journal_entries?.date ?? ""),
    debit: Number(l.debit || 0), credit: Number(l.credit || 0),
  }));
  const openInv = ((d.allInvoices ?? []) as any[]).filter((i) => i.status !== "void").reduce((s, i) => s + (Number(i.total || 0) - Number(i.paid_amount || 0)), 0);
  const openBill = ((d.allBills ?? []) as any[]).filter((b) => b.status !== "void").reduce((s, b) => s + (Number(b.total || 0) - Number(b.paid_amount || 0)), 0);

  // Cross-path figures from the report builders so R3/R5 verify the builders agree
  // with the raw GL. (P&L net income vs GL; Movement-of-Equity ending vs Balance Sheet.)
  const pl = buildPnL(d, undefined, false);
  const plNI = pl.rows.find((r) => r.kind === "grand" && /net income/i.test(r.label))?.amount;
  const bs = buildBalanceSheet(d);
  const bsEquity = bs.rows.find((r) => r.kind === "total" && /total equity/i.test(r.label))?.amount;
  const sce = buildMovementOfEquity(d, undefined, false);
  const sceEnding = sce.rows.find((r) => r.kind === "grand" && /closing equity/i.test(r.label))?.amount;

  const checks = reconcile({
    accounts, lines, periodStart: d.from, openInvoices: openInv, openBills: openBill,
    arApApplicable: d.glManaged,
    reportPLNetIncome: plNI, sceEndingEquity: sceEnding, bsTotalEquity: bsEquity,
  });
  const ok = (r: number) => Math.abs(r) < 0.005;
  const allPass = checks.every((c) => c.pass);

  return (
    <Card>
      <CardHeader>
        <CardTitle className="text-base flex items-center gap-2">
          Reconciliation Checks
          <span className={`text-xs rounded px-2 py-0.5 ${allPass ? "bg-emerald-100 text-emerald-700" : "bg-red-100 text-red-700"}`}>
            {allPass ? "All passing" : "Residuals present"}
          </span>
        </CardTitle>
      </CardHeader>
      <CardContent className="p-0">
        <Table>
          <TableHeader>
            <TableRow>
              <TableHead className="w-16">Check</TableHead>
              <TableHead>Assertion</TableHead>
              <TableHead className="text-right">Residual</TableHead>
              <TableHead className="text-right">Status</TableHead>
            </TableRow>
          </TableHeader>
          <TableBody>
            {checks.map((c) => (
              <TableRow key={c.id}>
                <TableCell className="font-mono">{c.id}</TableCell>
                <TableCell>{c.label}</TableCell>
                <TableCell className={`text-right tabular-nums ${ok(c.residual) ? "" : "text-red-600 font-semibold"}`}>{money(c.residual, currency)}</TableCell>
                <TableCell className="text-right">{ok(c.residual) ? "✓" : "✗"}</TableCell>
              </TableRow>
            ))}
            {/* R6 / R9 are not numeric residuals in this model — shown for matrix completeness. */}
            <TableRow className="text-muted-foreground">
              <TableCell className="font-mono">R6</TableCell>
              <TableCell>GL closing balance = Trial Balance / Balance Sheet balance</TableCell>
              <TableCell className="text-right">—</TableCell>
              <TableCell className="text-right" title="Trial Balance and Balance Sheet are both derived directly from the GL, so they agree by construction.">n/a</TableCell>
            </TableRow>
            <TableRow className="text-muted-foreground">
              <TableCell className="font-mono">R9</TableCell>
              <TableCell>Direct-method CFO = Indirect-method CFO</TableCell>
              <TableCell className="text-right">—</TableCell>
              <TableCell className="text-right" title="Only the indirect-method cash flow is produced, so there is no second method to reconcile against.">n/a</TableCell>
            </TableRow>
          </TableBody>
        </Table>
        <p className="text-xs text-muted-foreground p-4">
          A non-zero residual is a bug signal (§9), not to be plugged. R1/R2 failing means the ledger is
          unbalanced (often an imported single-sided entry). R3 checks the P&L's net income against the raw GL;
          R5 checks the Movement of Equity against the Balance Sheet. R7/R8 failing means A/R or A/P is summing
          gross billings instead of open balances, or a sub-ledger doesn't tie to the GL control account.
          R6 is satisfied by construction (Trial Balance and Balance Sheet both derive from the GL); R9 is N/A
          because only the indirect-method cash flow is produced.
          {!d.glManaged && " R7/R8 are omitted for this company: its GL is imported, so its A/R/A/P are maintained in the GL rather than from the platform's invoice/bill sub-ledgers."}
        </p>
      </CardContent>
    </Card>
  );
}

function buildFinancial(id: ReportId, d: any, p: any | undefined, useCompare: boolean): StructuredReport {
  if (id === "pnl") return buildPnL(d, p, useCompare);
  if (id === "balance-sheet") return buildBalanceSheet(d, p, useCompare);
  if (id === "movement-of-equity") return buildMovementOfEquity(d, p, useCompare);
  return buildCashFlow(d, p, useCompare);
}

function buildMovementOfEquity(d: any, p: any | undefined, useCompare: boolean): StructuredReport {
  // GL-consistent with the Balance Sheet and P&L: net income is the GL's
  // current-year earnings (not a separate sub-ledger figure), and the equity
  // rolls forward to exactly the Balance Sheet's total equity (ties R5).
  const equityAccs = (d.accounts ?? []).filter((a: any) => a.type === "equity");
  const draws = equityAccs.filter((a: any) => /draw|dividend/i.test(a.name));
  const nonDraw = equityAccs.filter((a: any) => !/draw|dividend/i.test(a.name));

  // Per-dataset equity figures derived entirely from the GL (via bsBalances).
  const eq = (ds: any) => {
    const bs = bsBalances(ds);
    const bal = (a: any) => bs.glByAcct.get(a.id) ?? 0;
    const nonDrawGL = nonDraw.reduce((s: number, a: any) => s + bal(a), 0);
    const drawsGL = draws.reduce((s: number, a: any) => s + bal(a), 0);
    const opening = nonDrawGL + bs.rePrior;       // capital + opening RE + prior-year earnings
    const closing = opening + bs.cye + drawsGL;   // = Balance Sheet total equity (by construction)
    return { bal, drawsGL, opening, closing, netIncome: bs.cye, rePrior: bs.rePrior };
  };

  const cur = eq(d);
  const prev = useCompare && p ? eq(p) : undefined;
  const cmp = (v: number | undefined) => (prev ? v : undefined);

  const rows: StructuredRow[] = [
    { kind: "section",  label: "Opening Equity" },
    ...nonDraw.map((a: any) => ({ kind: "sub" as const, label: a.name, code: a.code ?? undefined, amount: cur.bal(a), compare: cmp(prev ? prev.bal(a) : undefined) })),
    { kind: "sub",      label: "Retained Earnings (prior years)", amount: cur.rePrior, compare: cmp(prev?.rePrior) },
    { kind: "total",    label: "Total Opening Equity", amount: cur.opening, compare: cmp(prev?.opening) },
    { kind: "spacer",   label: "" },

    { kind: "section",  label: "Period Activity" },
    { kind: "sub",      label: "Net Income", amount: cur.netIncome, compare: cmp(prev?.netIncome) },
    ...draws.map((a: any) => ({ kind: "sub" as const, label: `Less: ${a.name}`, amount: cur.bal(a), compare: cmp(prev ? prev.bal(a) : undefined) })),
    { kind: "total",    label: "Net Change in Equity", amount: cur.netIncome + cur.drawsGL, compare: cmp(prev ? prev.netIncome + prev.drawsGL : undefined) },
    { kind: "spacer",   label: "" },

    { kind: "grand",    label: "Closing Equity", amount: cur.closing, compare: cmp(prev?.closing) },
  ];

  return { title: "Movement of Equity", rows };
}

// ── P&L from the general ledger (grouped by parent account) ───────────────────

type GLAccountTotal = { name: string; code: string | null; amount: number; parentId: string | null };

/**
 * Group journal-entry lines by GL account, split into income/expense.
 * Income accounts net to credit−debit; expense accounts to debit−credit.
 */
function groupGLByAccount(lines: any[]) {
  const income = new Map<string, GLAccountTotal>();
  const expense = new Map<string, GLAccountTotal>();
  for (const l of lines ?? []) {
    const acc = l.accounts;
    if (!acc) continue;
    const debit = Number(l.debit ?? 0);
    const credit = Number(l.credit ?? 0);
    const parentId = acc.parent_account_id ?? null;
    if (acc.type === "income") {
      const e = income.get(acc.id) ?? { name: acc.name, code: acc.code ?? null, amount: 0, parentId };
      e.amount += credit - debit;
      income.set(acc.id, e);
    } else if (acc.type === "expense") {
      const e = expense.get(acc.id) ?? { name: acc.name, code: acc.code ?? null, amount: 0, parentId };
      e.amount += debit - credit;
      expense.set(acc.id, e);
    }
  }
  return { income, expense };
}

/**
 * Emit a P&L section, grouping accounts under their parent account. Top-level
 * accounts (no parent) are listed directly; child accounts are nested under a
 * parent group header with a "Total for <parent>" subtotal. Returns section totals.
 */
function pushPnLSection(
  rows: StructuredRow[], label: string,
  accMap: Map<string, GLAccountTotal>, prevMap: Map<string, GLAccountTotal>,
  parentName: Map<string, string>, useCompare: boolean
): { total: number; prevTotal: number } {
  rows.push({ kind: "section", label });

  const ungrouped: [string, GLAccountTotal][] = [];
  const groups = new Map<string, [string, GLAccountTotal][]>();
  for (const entry of accMap.entries()) {
    const pid = entry[1].parentId;
    if (pid && parentName.has(pid)) {
      const g = groups.get(pid) ?? [];
      g.push(entry); groups.set(pid, g);
    } else ungrouped.push(entry);
  }

  let total = 0, prevTotal = 0;
  const byName = (a: [string, GLAccountTotal], b: [string, GLAccountTotal]) => a[1].name.localeCompare(b[1].name);
  const emit = (id: string, v: GLAccountTotal) => {
    total += v.amount; prevTotal += prevMap.get(id)?.amount ?? 0;
    rows.push({ kind: "sub", label: v.name, code: v.code ?? undefined, amount: v.amount, compare: useCompare ? prevMap.get(id)?.amount : undefined });
  };

  for (const [id, v] of ungrouped.sort(byName)) emit(id, v);

  const sortedGroups = [...groups.entries()].sort((a, b) => (parentName.get(a[0]) ?? "").localeCompare(parentName.get(b[0]) ?? ""));
  for (const [pid, entries] of sortedGroups) {
    const gName = parentName.get(pid) ?? "Group";
    rows.push({ kind: "group", label: gName });
    let gTotal = 0, gPrev = 0;
    for (const [id, v] of entries.sort(byName)) {
      gTotal += v.amount; gPrev += prevMap.get(id)?.amount ?? 0;
      emit(id, v);
    }
    rows.push({ kind: "total", label: `Total for ${gName}`, amount: gTotal, compare: useCompare ? gPrev : undefined });
  }

  return { total, prevTotal };
}

// P&L sub-classification is read from the account's explicit `subtype` field
// (never inferred from names — see the P&L spec §4). Unclassified nominal
// accounts default to plain revenue / operating expense, which is correct for
// HOA charts that have no COGS, tax, or contra accounts.
const PNL_SUBTYPE_MAP: Record<string, PnlClassification> = {
  revenue: "REVENUE", sales: "REVENUE", income: "REVENUE", operating_income: "REVENUE",
  contra_revenue: "CONTRA_REVENUE", sales_returns: "CONTRA_REVENUE",
  sales_allowances: "CONTRA_REVENUE", sales_discounts: "CONTRA_REVENUE",
  cogs: "COGS", cost_of_goods_sold: "COGS", cost_of_sales: "COGS",
  contra_cogs: "CONTRA_COGS", purchase_discounts: "CONTRA_COGS", purchase_returns: "CONTRA_COGS",
  operating_expense: "OPERATING_EXPENSE", opex: "OPERATING_EXPENSE", sga: "OPERATING_EXPENSE",
  selling_general_admin: "OPERATING_EXPENSE", depreciation: "OPERATING_EXPENSE",
  other_income: "OTHER_INCOME", non_operating_income: "OTHER_INCOME", interest_income: "OTHER_INCOME",
  other_expense: "OTHER_EXPENSE", non_operating_expense: "OTHER_EXPENSE", interest_expense: "OTHER_EXPENSE",
  tax: "TAX_EXPENSE", income_tax: "TAX_EXPENSE", tax_expense: "TAX_EXPENSE",
};

function classifyAccount(a: any): PnlClassification {
  const key = String(a.subtype ?? "").trim().toLowerCase().replace(/[\s-]+/g, "_");
  if (key && PNL_SUBTYPE_MAP[key]) return PNL_SUBTYPE_MAP[key];
  return a.type === "income" ? "REVENUE" : "OPERATING_EXPENSE";
}

function runPnLEngine(d: any): PnlResult | { error: string } {
  const nominal = ((d.accounts ?? []) as any[]).filter((a) => a.type === "income" || a.type === "expense");
  const accts: PnlAccount[] = nominal.map((a) => ({ id: a.id, name: a.name, type: a.type, classification: classifyAccount(a) }));
  const allow = new Set(nominal.map((a) => a.id));
  const postings: PnlPosting[] = ((d.glLines ?? []) as any[])
    .filter((l) => l.accounts && allow.has(l.accounts.id))
    .map((l) => ({
      accountId: l.accounts.id,
      debitMinor: toMinor(l.debit ?? 0),
      creditMinor: toMinor(l.credit ?? 0),
      date: l.journal_entries?.date ?? d.from ?? d.asOf,
    }));
  try {
    return computePnL(postings, accts, { periodStart: d.from ?? d.asOf, periodEnd: d.asOf });
  } catch (e) {
    if (e instanceof PnlValidationError) return { error: e.message };
    return { error: e instanceof Error ? e.message : String(e) };
  }
}

function buildPnL(d: any, p: any | undefined, useCompare: boolean): StructuredReport {
  const cur = runPnLEngine(d);
  if ("error" in cur) {
    return {
      title: "Profit & Loss",
      rows: [
        { kind: "section", label: "Profit & Loss could not be computed (validation failed)" },
        { kind: "section", label: cur.error },
      ],
    };
  }
  const prevRes = useCompare && p ? runPnLEngine(p) : undefined;
  const prevOk = prevRes && !("error" in prevRes) ? (prevRes as PnlResult) : undefined;

  const s = cur.subtotals;
  const ps = prevOk?.subtotals;
  const D = fromMinor;
  const cmp = (v?: number) => (useCompare && v !== undefined ? D(v) : undefined);
  const rows: StructuredRow[] = [];
  const prevByAcct = new Map((prevOk?.lines ?? []).map((l) => [l.accountId, l.amountMinor]));

  // Emit per-account line items for the given classifications. `displaySign`
  // shows contra/expense reductions as negative figures in the column.
  const lineRows = (classes: PnlClassification[], displaySign: 1 | -1) => {
    for (const l of cur.lines.filter((x) => classes.includes(x.classification)).sort((a, b) => a.name.localeCompare(b.name))) {
      rows.push({
        kind: "sub", label: l.name, amount: D(l.amountMinor * displaySign),
        compare: useCompare ? D((prevByAcct.get(l.accountId) ?? 0) * displaySign) : undefined,
        accountId: l.accountId,
      });
    }
  };

  const has = (cs: PnlClassification[]) => cur.lines.some((l) => cs.includes(l.classification));
  const hasContra = has(["CONTRA_REVENUE"]);
  const hasCogs = has(["COGS", "CONTRA_COGS"]) || s.costOfGoodsSold !== 0;
  const hasOther = has(["OTHER_INCOME", "OTHER_EXPENSE"]) || s.otherNet !== 0;
  const hasTax = has(["TAX_EXPENSE"]) || s.incomeTaxExpense !== 0;

  // Revenue → Net Revenue
  rows.push({ kind: "section", label: "Revenue" });
  lineRows(["REVENUE"], 1);
  if (hasContra) lineRows(["CONTRA_REVENUE"], -1);
  rows.push({ kind: "total", label: "Net Revenue", amount: D(s.netRevenue), compare: cmp(ps?.netRevenue) });
  rows.push({ kind: "spacer", label: "" });

  // Cost of Goods Sold → Gross Profit (only when present)
  if (hasCogs) {
    rows.push({ kind: "section", label: "Cost of Goods Sold" });
    lineRows(["COGS"], 1);
    if (has(["CONTRA_COGS"])) lineRows(["CONTRA_COGS"], -1);
    rows.push({ kind: "total", label: "Total Cost of Goods Sold", amount: D(s.costOfGoodsSold), compare: cmp(ps?.costOfGoodsSold) });
    rows.push({ kind: "total", label: "Gross Profit", amount: D(s.grossProfit), compare: cmp(ps?.grossProfit) });
    rows.push({ kind: "spacer", label: "" });
  }

  // Operating Expenses → Operating Income (EBIT)
  rows.push({ kind: "section", label: "Operating Expenses" });
  lineRows(["OPERATING_EXPENSE"], 1);
  rows.push({ kind: "total", label: "Total Operating Expenses", amount: D(s.operatingExpenses), compare: cmp(ps?.operatingExpenses) });
  rows.push({ kind: "total", label: "Operating Income (EBIT)", amount: D(s.operatingIncome), compare: cmp(ps?.operatingIncome) });

  // Other Income / (Expense)
  if (hasOther) {
    rows.push({ kind: "spacer", label: "" });
    rows.push({ kind: "section", label: "Other Income / (Expense)" });
    lineRows(["OTHER_INCOME"], 1);
    lineRows(["OTHER_EXPENSE"], -1);
    rows.push({ kind: "total", label: "Total Other Income / (Expense)", amount: D(s.otherNet), compare: cmp(ps?.otherNet) });
  }

  // Pre-Tax Income (EBT) — shown when it differs from Operating Income
  if (hasOther || hasTax) {
    rows.push({ kind: "total", label: "Pre-Tax Income (EBT)", amount: D(s.preTaxIncome), compare: cmp(ps?.preTaxIncome) });
  }

  // Income Tax
  if (hasTax) {
    rows.push({ kind: "spacer", label: "" });
    rows.push({ kind: "section", label: "Income Tax" });
    lineRows(["TAX_EXPENSE"], 1);
    rows.push({ kind: "total", label: "Income Tax Expense", amount: D(s.incomeTaxExpense), compare: cmp(ps?.incomeTaxExpense) });
  }

  rows.push({ kind: "spacer", label: "" });
  rows.push({ kind: "grand", label: "Net Income", amount: D(s.netIncome), compare: cmp(ps?.netIncome) });

  // Margins (presentation-only, §5)
  const m = computeMargins(cur);
  if (m.netMargin !== null) {
    const pct = (x: number | null) => (x === null ? "—" : `${(x * 100).toFixed(1)}%`);
    rows.push({ kind: "spacer", label: "" });
    rows.push({ kind: "section", label: `Margins — Gross ${pct(m.grossMargin)} · Operating ${pct(m.operatingMargin)} · Net ${pct(m.netMargin)}` });
  }

  return { title: "Profit & Loss", rows };
}

// Per-account natural balances + retained-earnings split from a dataset's GL.
function bsBalances(ds: any) {
  const fyStart = `${String(ds?.asOf ?? "").slice(0, 4)}-01-01`;
  const isDebitNormal = (t: string) => t === "asset" || t === "expense";
  const glByAcct = new Map<string, number>();
  let incomeAll = 0, expenseAll = 0, incomePrior = 0, expensePrior = 0;
  for (const l of (ds?.glCumulative ?? []) as any[]) {
    const t = l.accounts?.type;
    if (!t) continue;
    const debit = Number(l.debit || 0), credit = Number(l.credit || 0);
    const nat = isDebitNormal(t) ? debit - credit : credit - debit;
    glByAcct.set(l.account_id, (glByAcct.get(l.account_id) ?? 0) + nat);
    const isPrior = String(l.journal_entries?.date ?? "") < fyStart;
    if (t === "income") { incomeAll += credit - debit; if (isPrior) incomePrior += credit - debit; }
    else if (t === "expense") { expenseAll += debit - credit; if (isPrior) expensePrior += debit - credit; }
  }
  const rePrior = incomePrior - expensePrior;
  const cye = (incomeAll - expenseAll) - rePrior;
  return { glByAcct, rePrior, cye };
}

function buildBalanceSheet(d: any, p?: any, useCompare?: boolean): StructuredReport {
  // GL-derived (opening balances posted to the GL). Current-Year Earnings is
  // computed independently from actuals (income − expense), never plugged.
  const accounts = (d.accounts ?? []) as any[];
  const cur = bsBalances(d);
  const prev = useCompare && p ? bsBalances(p) : undefined;
  const balOf = (a: any) => (cur.glByAcct.get(a.id) ?? 0);
  const balOfP = (a: any) => (prev ? (prev.glByAcct.get(a.id) ?? 0) : undefined);
  const cmp = (v: number | undefined) => (prev ? v : undefined);
  const byType = (t: string) => accounts.filter((a) => a.type === t);
  const sumBal = (rows: any[]) => rows.reduce((s, a) => s + balOf(a), 0);
  const sumBalP = (rows: any[]) => (prev ? rows.reduce((s, a) => s + (prev.glByAcct.get(a.id) ?? 0), 0) : undefined);

  const rows: StructuredRow[] = [];

  // Assets
  rows.push({ kind: "section", label: "Assets" });
  const assets = byType("asset");
  for (const a of assets) rows.push({ kind: "sub", label: a.name, code: a.code ?? undefined, amount: balOf(a), compare: cmp(balOfP(a)), accountId: a.id });
  const totalA = sumBal(assets);
  rows.push({ kind: "grand", label: "TOTAL ASSETS", amount: totalA, compare: cmp(sumBalP(assets)) });
  rows.push({ kind: "spacer", label: "" });

  // Liabilities
  rows.push({ kind: "section", label: "Liabilities" });
  const liabs = byType("liability");
  for (const a of liabs) rows.push({ kind: "sub", label: a.name, code: a.code ?? undefined, amount: balOf(a), compare: cmp(balOfP(a)), accountId: a.id });
  const totalL = sumBal(liabs);
  rows.push({ kind: "total", label: "Total Liabilities", amount: totalL, compare: cmp(sumBalP(liabs)) });
  rows.push({ kind: "spacer", label: "" });

  // Equity — equity accounts + calculated RE / current-year earnings
  rows.push({ kind: "section", label: "Equity" });
  const equityAccs = byType("equity");
  for (const a of equityAccs) rows.push({ kind: "sub", label: a.name, code: a.code ?? undefined, amount: balOf(a), compare: cmp(balOfP(a)), accountId: a.id });
  rows.push({ kind: "sub", label: "Retained Earnings (prior years)", amount: cur.rePrior, compare: cmp(prev?.rePrior) });
  rows.push({ kind: "sub", label: "Current Year Earnings", amount: cur.cye, compare: cmp(prev?.cye) });
  const totalE = sumBal(equityAccs) + cur.rePrior + cur.cye;
  const totalEP = prev ? (sumBalP(equityAccs)! + prev.rePrior + prev.cye) : undefined;
  rows.push({ kind: "total", label: "Total Equity", amount: totalE, compare: cmp(totalEP) });
  rows.push({ kind: "spacer", label: "" });

  rows.push({ kind: "grand", label: "TOTAL LIABILITIES & EQUITY", amount: totalL + totalE, compare: cmp(prev ? (sumBalP(liabs)! + totalEP!) : undefined) });

  // Residual surfaced (never plugged): Assets − (Liabilities + Equity).
  const cents = (n: number) => Math.round(n * 100);
  const residualCents = cents(totalA) - cents(totalL + totalE);
  const balanced = residualCents === 0;
  return {
    title: "Balance Sheet",
    rows,
    balanced,
    outOfBalanceAmount: balanced ? undefined : residualCents / 100,
  };
}


// Indirect-method cash flow built from the GL (§5). It ties to the change in the
// Balance Sheet cash accounts by construction (R4): because every entry balances,
// the cash impact of net income + all non-cash balance movements equals ΔCash.
type CashFlowCalc = {
  netIncome: number; operating: { label: string; amount: number }[];
  cfo: number; cfi: number; cff: number; netChange: number;
  beginCash: number; endCash: number; residual: number;
};

function computeCashFlow(d: any): CashFlowCalc {
  const from: string = d.from;
  const acctById = new Map<string, any>((d.accounts ?? []).map((a: any) => [a.id, a]));
  const isCash = (a: any) => !!a && (a.is_bank || /cash|undeposited/i.test(String(a.name || "")));
  const beginRaw = new Map<string, number>();
  const endRaw = new Map<string, number>();
  for (const l of (d.glCumulative ?? []) as any[]) {
    const raw = Number(l.debit || 0) - Number(l.credit || 0);
    endRaw.set(l.account_id, (endRaw.get(l.account_id) ?? 0) + raw);
    if (String(l.journal_entries?.date ?? "") < from) beginRaw.set(l.account_id, (beginRaw.get(l.account_id) ?? 0) + raw);
  }
  const ids = new Set<string>([...endRaw.keys(), ...beginRaw.keys()]);
  const deltaRaw = (id: string) => (endRaw.get(id) ?? 0) - (beginRaw.get(id) ?? 0);

  let beginCash = 0, endCash = 0, revenue = 0, expense = 0, cfi = 0, cff = 0;
  const operating: { label: string; amount: number }[] = [];
  for (const id of ids) {
    const a = acctById.get(id);
    if (!a) continue;
    if (isCash(a)) { beginCash += beginRaw.get(id) ?? 0; endCash += endRaw.get(id) ?? 0; continue; }
    if (a.type === "income") { revenue += -deltaRaw(id); continue; }
    if (a.type === "expense") { expense += deltaRaw(id); continue; }
    const impact = -deltaRaw(id);
    if (Math.abs(impact) < 0.005) continue;
    const name = String(a.name || "").toLowerCase();
    if (a.type === "asset") {
      const naturalUp = deltaRaw(id) > 0;
      if (/investment|property|equipment|fixed|capital asset/.test(name)) cfi += impact;
      else operating.push({ label: `${naturalUp ? "Increase" : "Decrease"} in ${a.name}`, amount: impact });
    } else if (a.type === "liability") {
      const naturalUp = -deltaRaw(id) > 0;
      if (/loan|note|mortgage|debt|bond/.test(name)) cff += impact;
      else operating.push({ label: `${naturalUp ? "Increase" : "Decrease"} in ${a.name}`, amount: impact });
    } else if (a.type === "equity") { cff += impact; }
  }
  const netIncome = revenue - expense;
  const cfo = netIncome + operating.reduce((s, r) => s + r.amount, 0);
  const netChange = cfo + cfi + cff;
  return { netIncome, operating, cfo, cfi, cff, netChange, beginCash, endCash, residual: netChange - (endCash - beginCash) };
}

function buildCashFlow(d: any, p: any | undefined, useCompare: boolean): StructuredReport {
  const cur = computeCashFlow(d);
  const prev = useCompare && p ? computeCashFlow(p) : undefined;
  const cmp = (v: number | undefined) => (useCompare && prev ? v : undefined);
  // Match prior-period operating line items by label for the compare column.
  const prevByLabel = new Map((prev?.operating ?? []).map((r) => [r.label, r.amount]));

  const rows: StructuredRow[] = [];
  rows.push({ kind: "section", label: "Operating Activities" });
  rows.push({ kind: "sub", label: "Net Income", amount: cur.netIncome, compare: cmp(prev?.netIncome) });
  for (const r of cur.operating) rows.push({ kind: "sub", label: r.label, amount: r.amount, compare: cmp(prevByLabel.get(r.label) ?? 0) });
  rows.push({ kind: "total", label: "Net Cash from Operating Activities", amount: cur.cfo, compare: cmp(prev?.cfo) });
  rows.push({ kind: "spacer", label: "" });
  rows.push({ kind: "section", label: "Investing Activities" });
  rows.push({ kind: "total", label: "Net Cash from Investing Activities", amount: cur.cfi, compare: cmp(prev?.cfi) });
  rows.push({ kind: "spacer", label: "" });
  rows.push({ kind: "section", label: "Financing Activities" });
  rows.push({ kind: "total", label: "Net Cash from Financing Activities", amount: cur.cff, compare: cmp(prev?.cff) });
  rows.push({ kind: "spacer", label: "" });
  rows.push({ kind: "sub", label: "Beginning Cash", amount: cur.beginCash, compare: cmp(prev?.beginCash) });
  rows.push({ kind: "sub", label: "Ending Cash", amount: cur.endCash, compare: cmp(prev?.endCash) });
  if (Math.abs(cur.residual) >= 0.005) {
    rows.push({ kind: "total", label: "⚠ Out of balance — R4 residual (CFO+CFI+CFF − ΔCash)", amount: cur.residual });
  }

  return {
    title: "Cash Flow Statement",
    rows,
    cashHighlight: { label: "Net Change in Cash", amount: cur.netChange },
  };
}

// ---------- Flat (non-financial) report builders ----------

type Flat = { title: string; columns: string[]; rows: (string | number)[][]; boldRows?: number[] };

function buildFlat(id: ReportId, d: any, cur: string): Flat | null {
  const m = (n: number) => money(n, cur);
  switch (id) {
    case "invoice-summary":
      return {
        title: "Invoice Summary", columns: ["Invoice #", "Customer", "Date", "Status", "Amount"],
        rows: d.invoices.map((i: any) => [i.number, i.customers?.name ?? "—", fmtDate(i.issue_date), i.status, m(Number(i.total))]),
      };
    case "customer-balances":
      return {
        title: "Customer Balances", columns: ["Customer", "Balance"],
        rows: d.customers.map((c: any) => [c.name, m(Number(c.balance ?? 0))]),
      };
    case "expense-summary": {
      const byCat: Record<string, number> = {};
      // Direct expenses from expenses table
      for (const e of d.expenses) byCat[e.category] = (byCat[e.category] ?? 0) + Number(e.amount);
      // Bill expenses (accrual — total billed, not just paid)
      for (const b of d.bills) {
        if (b.status === "void" || b.status === "draft") continue;
        const cat = b.vendors?.name ?? "Vendor Expenses";
        byCat[cat] = (byCat[cat] ?? 0) + Number(b.total);
      }
      const rows = Object.entries(byCat).sort((a, b) => b[1] - a[1]).map(([cat, amt]) => [cat, m(amt)]);
      const total = Object.values(byCat).reduce((s, v) => s + v, 0);
      return { title: "Expense Summary (Accrual)", columns: ["Category / Vendor", "Amount"], rows: [...rows, ["TOTAL", m(total)]], boldRows: [rows.length] };
    }
    case "vendor-balances": {
      const byVendor: Record<string, number> = {};
      for (const b of d.bills) {
        const name = b.vendors?.name ?? "—";
        const bal = Number(b.total) - Number(b.paid_amount ?? 0);
        if (bal > 0) byVendor[name] = (byVendor[name] ?? 0) + bal;
      }
      return {
        title: "Vendor Balances (open bills)", columns: ["Vendor", "Outstanding"],
        rows: Object.entries(byVendor).map(([n, v]) => [n, m(v)]),
      };
    }
    default: return null;
  }
}

function flatToStructured(flat: Flat, title: string): StructuredReport {
  const rows: StructuredRow[] = [{ kind: "section", label: flat.columns.join(" · ") }];
  for (const r of flat.rows) {
    const label = r.slice(0, r.length - 1).map(String).join(" · ");
    const last = r[r.length - 1];
    const num = typeof last === "number" ? last : parseFloat(String(last).replace(/[^0-9.\-]/g, "")) || 0;
    rows.push({ kind: "sub", label, amount: num });
  }
  return { title, rows };
}

// ---------- Budget vs Actuals ----------

/** Order accounts as a tree (parents first, children indented) instead of by number. */
function orderAccountsHierarchically(accs: any[]): any[] {
  const byId = new Map(accs.map((a) => [a.id, a]));
  const childrenByParent = new Map<string, any[]>();
  const roots: any[] = [];
  for (const a of accs) {
    if (a.parent_account_id && byId.has(a.parent_account_id)) {
      const arr = childrenByParent.get(a.parent_account_id) ?? [];
      arr.push(a); childrenByParent.set(a.parent_account_id, arr);
    } else {
      roots.push(a);
    }
  }
  const byCode = (a: any, b: any) => String(a.code ?? "").localeCompare(String(b.code ?? ""));
  roots.sort(byCode);
  const out: any[] = [];
  const visit = (node: any, depth: number) => {
    out.push({ ...node, _depth: depth });
    for (const k of (childrenByParent.get(node.id) ?? []).sort(byCode)) visit(k, depth + 1);
  };
  for (const r of roots) visit(r, 0);
  return out;
}

async function fetchBvaActuals(companyId: string, f: string, t: string) {
  const [inv, exp, txns, billItemsRes] = await Promise.all([
    accounting.from("invoices").select("total,status,issue_date").eq("company_id", companyId).gte("issue_date", f).lte("issue_date", t).not("status", "in", '("void","draft")'),
    accounting.from("expenses").select("amount,category,date").eq("company_id", companyId).gte("date", f).lte("date", t),
    accounting.from("transactions").select("coa_account_id,amount,type").eq("company_id", companyId).gte("date", f).lte("date", t).not("coa_account_id", "is", null),
    accounting.from("bill_items").select("account_id,amount,bills!inner(issue_date,company_id)").eq("bills.company_id", companyId).gte("bills.issue_date", f).lte("bills.issue_date", t).not("account_id", "is", null),
  ]);
  return { invoices: inv.data ?? [], expenses: exp.data ?? [], transactions: txns.data ?? [], billItems: billItemsRes.data ?? [] };
}

// Actuals per account for a given actualsData window (transactions, bill items,
// expenses, and accrual invoice income distributed by budget weight).
function computeBvaActuals(actualsData: any, grouped: Record<string, any[]>, budgetByAcct: Record<string, number>): Record<string, number> {
  const m: Record<string, number> = {};
  if (!actualsData) return m;
  const expAccs = grouped.expense ?? [];
  for (const tx of actualsData.transactions as any[]) {
    if (!tx.coa_account_id) continue;
    m[tx.coa_account_id] = (m[tx.coa_account_id] ?? 0) + Number(tx.amount);
  }
  for (const bi of actualsData.billItems as any[]) {
    if (!bi.account_id) continue;
    m[bi.account_id] = (m[bi.account_id] ?? 0) + Number(bi.amount);
  }
  for (const e of actualsData.expenses as any[]) {
    const cat = String(e.category ?? "").toLowerCase().trim();
    const match = expAccs.find((a) => a.name.toLowerCase().trim() === cat || (a.code && a.code.toLowerCase().trim() === cat));
    if (match) m[match.id] = (m[match.id] ?? 0) + Number(e.amount);
  }
  const totalPaidInvoices = actualsData.invoices
    .filter((i: any) => i.status !== "void" && i.status !== "draft")
    .reduce((s: number, i: any) => s + Number(i.total), 0);
  const alreadyCountedIncome = (grouped.income ?? []).reduce((s: number, a: any) => s + (m[a.id] ?? 0), 0);
  const remainingInvoiceIncome = Math.max(0, totalPaidInvoices - alreadyCountedIncome);
  if (remainingInvoiceIncome > 0 && (grouped.income ?? []).length > 0) {
    const incomeAccs = grouped.income ?? [];
    const totalBudgeted = incomeAccs.reduce((s: number, a: any) => s + (budgetByAcct[a.id] ?? 0), 0);
    for (const a of incomeAccs) {
      const weight = totalBudgeted > 0 ? (budgetByAcct[a.id] ?? 0) / totalBudgeted : 1 / incomeAccs.length;
      m[a.id] = (m[a.id] ?? 0) + remainingInvoiceIncome * weight;
    }
  }
  return m;
}

function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabel, logoUrl }: { companyId: string; from: string; to: string; currency: string; companyName: string; rangeLabel: string; logoUrl?: string | null }) {
  const { data: budgets = [] } = useQuery({
    queryKey: ["budgets-active", companyId],
    enabled: !!companyId,
    queryFn: async () => (await accounting.from("budgets").select("*").eq("company_id", companyId).eq("status", "active").order("created_at", { ascending: false })).data ?? [],
  });

  const [budgetId, setBudgetId] = useState<string>("");
  useEffect(() => { if (!budgetId && budgets.length) setBudgetId(budgets[0].id); }, [budgets, budgetId]);

  // Actuals comparison window — defaults to the report period, but the user can
  // choose any custom date range to compare against the budget.
  const [actFrom, setActFrom] = useState(from);
  const [actTo, setActTo] = useState(to);
  useEffect(() => { setActFrom(from); setActTo(to); }, [from, to]);
  const actualsLabel = `${actFrom} to ${actTo}`;

  const { data: accounts = [] } = useQuery({
    queryKey: ["accounts", companyId],
    enabled: !!companyId,
    queryFn: async () => (await accounting.from("accounts").select("*").eq("company_id", companyId).order("code")).data ?? [],
  });

  const { data: entries = [] } = useQuery({
    queryKey: ["budget-entries", budgetId],
    enabled: !!budgetId,
    queryFn: async () => (await accounting.from("budget_entries").select("*").eq("budget_id", budgetId)).data ?? [],
  });

  // Optional comparison window (B3): compare actuals to another date range.
  const [cmpOn, setCmpOn] = useState(false);
  const [cmpFrom, setCmpFrom] = useState("");
  const [cmpTo, setCmpTo] = useState("");

  const { data: actualsData } = useQuery({
    queryKey: ["bva-actuals", companyId, actFrom, actTo],
    enabled: !!companyId,
    queryFn: () => fetchBvaActuals(companyId, actFrom, actTo),
  });

  const { data: cmpActualsData } = useQuery({
    queryKey: ["bva-actuals-cmp", companyId, cmpFrom, cmpTo],
    enabled: !!companyId && cmpOn && !!cmpFrom && !!cmpTo,
    queryFn: () => fetchBvaActuals(companyId, cmpFrom, cmpTo),
  });

  const TYPES_LOCAL = [
    { value: "income", label: "Income", favorableWhen: "over" as const },
    { value: "expense", label: "Expenses", favorableWhen: "under" as const },
  ];

  const grouped = useMemo(() => {
    const out: Record<string, any[]> = { income: [], expense: [] };
    for (const a of accounts) if (a.type === "income" || a.type === "expense") out[a.type].push(a);
    return out;
  }, [accounts]);

  // Same accounts, ordered as a parent→child tree (each carries `_depth`).
  const groupedOrdered = useMemo(() => ({
    income: orderAccountsHierarchically(grouped.income ?? []),
    expense: orderAccountsHierarchically(grouped.expense ?? []),
  } as Record<string, any[]>), [grouped]);

  const selectedBudget = useMemo(() => (budgets as any[]).find((b) => b.id === budgetId), [budgets, budgetId]);

  // Budget pro-rated to the selected actuals window (B2): sum each budget period
  // weighted by how much of it overlaps [actFrom, actTo]. period_index maps to
  // months (monthly), quarters (quarterly), or the whole year (annual).
  const budgetByAcct = useMemo(() => {
    const m: Record<string, number> = {};
    const pt = String(selectedBudget?.period_type ?? "annual");
    const fy = Number(selectedBudget?.fiscal_year) || new Date(actFrom || actTo || Date.now()).getFullYear();
    const fromT = actFrom ? new Date(actFrom).getTime() : -Infinity;
    const toT = actTo ? new Date(actTo).getTime() : Infinity;
    const DAY = 86400000;
    const span = (idx: number): [number, number] => {
      if (pt === "monthly") return [new Date(fy, idx, 1).getTime(), new Date(fy, idx + 1, 0).getTime()];
      if (pt === "quarterly") return [new Date(fy, idx * 3, 1).getTime(), new Date(fy, idx * 3 + 3, 0).getTime()];
      return [new Date(fy, 0, 1).getTime(), new Date(fy, 11, 31).getTime()];
    };
    for (const e of (entries as any[])) {
      const [s, en] = span(Number(e.period_index) || 0);
      const overlap = Math.max(0, Math.min(en, toT) - Math.max(s, fromT));
      const full = en - s;
      const weight = full > 0 ? Math.min(1, (overlap + DAY) / (full + DAY)) : 1;
      if (weight <= 0) continue;
      m[e.account_id] = (m[e.account_id] ?? 0) + Number(e.amount) * weight;
    }
    return m;
  }, [entries, selectedBudget, actFrom, actTo]);

  const actualByAcct = useMemo(() => computeBvaActuals(actualsData, grouped, budgetByAcct), [actualsData, grouped, budgetByAcct]);
  const cmpActualByAcct = useMemo(() => computeBvaActuals(cmpActualsData, grouped, budgetByAcct), [cmpActualsData, grouped, budgetByAcct]);

  // Comparison-window budget (pro-rated like budgetByAcct, over [cmpFrom, cmpTo]).
  const cmpBudgetByAcct = useMemo(() => {
    const m: Record<string, number> = {};
    if (!cmpOn || !cmpFrom || !cmpTo) return m;
    const pt = String(selectedBudget?.period_type ?? "annual");
    const fy = Number(selectedBudget?.fiscal_year) || new Date(cmpFrom || cmpTo || Date.now()).getFullYear();
    const fromT = new Date(cmpFrom).getTime();
    const toT = new Date(cmpTo).getTime();
    const DAY = 86400000;
    const span = (idx: number): [number, number] => {
      if (pt === "monthly") return [new Date(fy, idx, 1).getTime(), new Date(fy, idx + 1, 0).getTime()];
      if (pt === "quarterly") return [new Date(fy, idx * 3, 1).getTime(), new Date(fy, idx * 3 + 3, 0).getTime()];
      return [new Date(fy, 0, 1).getTime(), new Date(fy, 11, 31).getTime()];
    };
    for (const e of (entries as any[])) {
      const [s, en] = span(Number(e.period_index) || 0);
      const overlap = Math.max(0, Math.min(en, toT) - Math.max(s, fromT));
      const full = en - s;
      const weight = full > 0 ? Math.min(1, (overlap + DAY) / (full + DAY)) : 1;
      if (weight <= 0) continue;
      m[e.account_id] = (m[e.account_id] ?? 0) + Number(e.amount) * weight;
    }
    return m;
  }, [entries, selectedBudget, cmpOn, cmpFrom, cmpTo]);

  // Full-year budget per account (no pro-ration) for the Annual Budget column.
  const annualBudgetByAcct = useMemo(() => {
    const m: Record<string, number> = {};
    for (const e of (entries as any[])) m[e.account_id] = (m[e.account_id] ?? 0) + Number(e.amount);
    return m;
  }, [entries]);

  const chartData = useMemo(() => {
    const sumGroup = (type: "income" | "expense") => {
      const accs = grouped[type] ?? [];
      const b = accs.reduce((s, a) => s + (budgetByAcct[a.id] ?? 0), 0);
      const ac = accs.reduce((s, a) => s + (actualByAcct[a.id] ?? 0), 0);
      return { b, ac };
    };
    const inc = sumGroup("income"); const exp = sumGroup("expense");
    return [
      { name: "Income", Budget: inc.b, Actual: inc.ac },
      { name: "Expenses", Budget: exp.b, Actual: exp.ac },
    ];
  }, [grouped, budgetByAcct, actualByAcct]);

  // Flattened rows (group totals + accounts) shared by the CSV and PDF exports.
  const exportRows = useMemo(() => {
    const rows: { label: string; budget: number; actual: number; variance: number; pct: string; group: boolean }[] = [];
    for (const t of TYPES_LOCAL) {
      const accs = grouped[t.value] ?? [];
      if (!accs.length) continue;
      const tb = accs.reduce((s, a) => s + (budgetByAcct[a.id] ?? 0), 0);
      const ta = accs.reduce((s, a) => s + (actualByAcct[a.id] ?? 0), 0);
      const tv = ta - tb;
      rows.push({ label: t.label, budget: tb, actual: ta, variance: tv, pct: tb ? `${((tv / tb) * 100).toFixed(1)}%` : "—", group: true });
      for (const a of groupedOrdered[t.value] ?? []) {
        const b = budgetByAcct[a.id] ?? 0; const ac = actualByAcct[a.id] ?? 0; const v = ac - b;
        const indent = "    ".repeat(a._depth ?? 0);
        rows.push({ label: `${indent}${a.code ? `${a.name} (${a.code})` : a.name}`, budget: b, actual: ac, variance: v, pct: b ? `${((v / b) * 100).toFixed(1)}%` : "—", group: false });
      }
    }
    return rows;
  }, [grouped, groupedOrdered, budgetByAcct, actualByAcct]);

  const fileBase = `budget-vs-actuals-${actFrom}-to-${actTo}`;

  const exportCSV = () => {
    const esc = (s: any) => `"${String(s).replace(/"/g, '""')}"`;
    const lines = [["Account", "Budget", "Actual", "Variance", "Variance %"].join(",")];
    for (const r of exportRows) lines.push([esc(r.label), r.budget.toFixed(2), r.actual.toFixed(2), r.variance.toFixed(2), esc(r.pct)].join(","));
    const blob = new Blob([lines.join("\n")], { type: "text/csv;charset=utf-8" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url; a.download = `${fileBase}.csv`; a.click();
    URL.revokeObjectURL(url);
  };

  // Export using the shared branded Budget vs. Actuals generator (current period +
  // optional comparison + Annual Budget columns), matching the main report style.
  const exportPDF = async () => {
    const nameById = new Map((accounts as any[]).map((a) => [a.id, a.name]));
    const parentIds = new Set((accounts as any[]).filter((a) => a.parent_account_id).map((a) => a.parent_account_id));
    const rows: any[] = [];
    for (const type of ["income", "expense"] as const) {
      for (const a of (grouped[type] ?? [])) {
        if (parentIds.has(a.id)) continue; // parent accounts render as group headers
        const budget = budgetByAcct[a.id] ?? 0;
        const actual = actualByAcct[a.id] ?? 0;
        const cmpA = cmpActualByAcct[a.id] ?? 0;
        const cmpB = cmpBudgetByAcct[a.id] ?? 0;
        const annual = annualBudgetByAcct[a.id] ?? 0;
        rows.push({
          id: a.id,
          category: a.code ? `${a.code} ${a.name}` : a.name,
          accountType: type,
          parentId: a.parent_account_id ?? null,
          parentCategory: a.parent_account_id ? (nameById.get(a.parent_account_id) ?? null) : null,
          budget, annualBudget: annual, actual, variance: actual - budget,
          pctOfBudget: budget ? (actual / budget) * 100 : 0,
          comparisonActual: cmpA, comparisonBudget: cmpB, comparisonVariance: cmpA - cmpB,
          comparisonPctOfBudget: cmpB ? (cmpA / cmpB) * 100 : 0, cmpDelta: 0,
        });
      }
    }
    const inc = rows.filter((r) => r.accountType === "income");
    const exp = rows.filter((r) => r.accountType !== "income");
    const sum = (rs: any[], k: string) => rs.reduce((s, r) => s + (Number(r[k]) || 0), 0);
    await generateBudgetVsActualPdf({
      association: { name: companyName, logo_url: logoUrl ?? null },
      fiscalYear: Number(selectedBudget?.fiscal_year) || new Date().getFullYear(),
      rangeLabel: actualsLabel,
      comparisonLabel: cmpOn && cmpFrom && cmpTo ? "Comparison" : null,
      comparisonRangeLabel: cmpOn && cmpFrom && cmpTo ? `${cmpFrom} to ${cmpTo}` : null,
      rows,
      totals: {
        incomeBudget: sum(inc, "budget"), incomeActual: sum(inc, "actual"),
        incomeCmp: sum(inc, "comparisonActual"), incomeCmpBudget: sum(inc, "comparisonBudget"),
        expenseBudget: sum(exp, "budget"), expenseActual: sum(exp, "actual"),
        expenseCmp: sum(exp, "comparisonActual"), expenseCmpBudget: sum(exp, "comparisonBudget"),
      },
      comparisonBudgetMonths: null,
    });
  };

  if (!budgets.length) {
    return (
      <Card><CardContent className="py-12 text-center text-sm text-muted-foreground">
        No active budgets yet. <Link to="/dashboard/accounting/budgets" className="text-primary underline">Create one</Link> to compare against actuals.
      </CardContent></Card>
    );
  }

  return (
    <div className="space-y-4">
      <Card>
        <CardContent className="flex flex-wrap items-center gap-3 py-4">
          <Label className="text-sm">Budget:</Label>
          <Select value={budgetId} onValueChange={setBudgetId}>
            <SelectTrigger className="h-9 w-[280px]"><SelectValue /></SelectTrigger>
            <SelectContent>
              {budgets.map((b: any) => <SelectItem key={b.id} value={b.id}>{b.name} (FY {b.fiscal_year})</SelectItem>)}
            </SelectContent>
          </Select>
          <div className="flex items-center gap-2">
            <Label className="text-sm">Actuals from</Label>
            <Input type="date" value={actFrom} onChange={(e) => setActFrom(e.target.value)} className="h-9 w-40" />
            <Label className="text-sm">to</Label>
            <Input type="date" value={actTo} onChange={(e) => setActTo(e.target.value)} className="h-9 w-40" />
          </div>
          <div className="flex items-center gap-2">
            <Label className="text-sm flex items-center gap-1">
              <input type="checkbox" checked={cmpOn} onChange={(e) => setCmpOn(e.target.checked)} /> Compare to
            </Label>
            {cmpOn && (<>
              <Input type="date" value={cmpFrom} onChange={(e) => setCmpFrom(e.target.value)} className="h-9 w-40" />
              <Label className="text-sm">to</Label>
              <Input type="date" value={cmpTo} onChange={(e) => setCmpTo(e.target.value)} className="h-9 w-40" />
            </>)}
          </div>
          <div className="ml-auto flex gap-2">
            <Button variant="outline" onClick={exportCSV}><Download className="mr-1 h-4 w-4" /> CSV</Button>
            <Button onClick={exportPDF}><FileDown className="mr-1 h-4 w-4" /> PDF</Button>
          </div>
          <div className="w-full text-xs text-muted-foreground">
            Budget pro-rated to the selected period ({String((selectedBudget as any)?.period_type ?? "annual")} budget).
          </div>
        </CardContent>
      </Card>

      <ReportSheet title="Budget vs. Actuals" subtitle="Accrual basis" companyName={companyName} period={actualsLabel} logoUrl={logoUrl}>
          <Table>
            <TableHeader>
              <TableRow>
                <TableHead>Account</TableHead>
                <TableHead className="text-right">Budget</TableHead>
                <TableHead className="text-right">Actual</TableHead>
                <TableHead className="text-right">Variance</TableHead>
                <TableHead className="text-right">Variance %</TableHead>
                {cmpOn && <TableHead className="text-right">Compare</TableHead>}
                {cmpOn && <TableHead className="text-right">Δ vs Compare</TableHead>}
              </TableRow>
            </TableHeader>
            <TableBody>
              {TYPES_LOCAL.map((t) => {
                const accs = grouped[t.value] ?? [];
                if (!accs.length) return null;
                const totalB = accs.reduce((s, a) => s + (budgetByAcct[a.id] ?? 0), 0);
                const totalA = accs.reduce((s, a) => s + (actualByAcct[a.id] ?? 0), 0);
                const totalC = accs.reduce((s, a) => s + (cmpActualByAcct[a.id] ?? 0), 0);
                const totalVar = totalA - totalB;
                const totalPct = totalB ? (totalVar / totalB) * 100 : 0;
                const totalFavorable = t.favorableWhen === "over" ? totalVar >= 0 : totalVar <= 0;
                return (
                  <Fragment key={t.value}>
                    <TableRow className="bg-muted/60 font-semibold">
                      <TableCell>{t.label}</TableCell>
                      <TableCell className="text-right tabular-nums">{money(totalB, currency)}</TableCell>
                      <TableCell className="text-right tabular-nums">{money(totalA, currency)}</TableCell>
                      <TableCell className={`text-right tabular-nums ${totalFavorable ? "text-emerald-600" : "text-red-600"}`}>{money(totalVar, currency)}</TableCell>
                      <TableCell className={`text-right tabular-nums ${totalFavorable ? "text-emerald-600" : "text-red-600"}`}>{totalB ? `${totalPct.toFixed(1)}%` : "—"}</TableCell>
                      {cmpOn && <TableCell className="text-right tabular-nums">{money(totalC, currency)}</TableCell>}
                      {cmpOn && <TableCell className="text-right tabular-nums">{money(totalA - totalC, currency)}</TableCell>}
                    </TableRow>
                    {(groupedOrdered[t.value] ?? []).map((a: any) => {
                      const b = budgetByAcct[a.id] ?? 0;
                      const ac = actualByAcct[a.id] ?? 0;
                      const c = cmpActualByAcct[a.id] ?? 0;
                      const v = ac - b;
                      const pct = b ? (v / b) * 100 : 0;
                      const fav = t.favorableWhen === "over" ? v >= 0 : v <= 0;
                      const depth = a._depth ?? 0;
                      return (
                        <TableRow key={a.id} className="hover:bg-muted/20">
                          <TableCell style={{ paddingLeft: `${16 + depth * 20}px` }}>
                            <span className={depth === 0 ? "font-semibold" : "font-medium"}>{a.name}</span>
                            {a.code && <span className="text-xs text-muted-foreground font-mono ml-2">{a.code}</span>}
                          </TableCell>
                          <TableCell className="text-right tabular-nums">{money(b, currency)}</TableCell>
                          <TableCell className="text-right tabular-nums">{money(ac, currency)}</TableCell>
                          <TableCell className={`text-right tabular-nums ${fav ? "text-emerald-600" : "text-red-600"}`}>{money(v, currency)}</TableCell>
                          <TableCell className={`text-right tabular-nums ${fav ? "text-emerald-600" : "text-red-600"}`}>{b ? `${pct.toFixed(1)}%` : "—"}</TableCell>
                          {cmpOn && <TableCell className="text-right tabular-nums">{money(c, currency)}</TableCell>}
                          {cmpOn && <TableCell className="text-right tabular-nums">{money(ac - c, currency)}</TableCell>}
                        </TableRow>
                      );
                    })}
                  </Fragment>
                );
              })}
            </TableBody>
          </Table>
      </ReportSheet>
    </div>
  );
}

// ── AP Aging ─────────────────────────────────────────────────────────────────

function APAgingTable({ rows, currency }: { rows: any[]; currency: string }) {
  const now = new Date();
  type APRow = { id: string; name: string; current: number; d30: number; d60: number; d90: number; d90p: number; total: number };
  const byVendor = new Map<string, APRow>();
  for (const bill of rows) {
    const open = Number(bill.total ?? 0) - Number(bill.paid_amount ?? 0);
    if (open <= 0) continue;
    const vid = bill.vendor_id ?? bill.vendors?.id ?? bill.id;
    const name = bill.vendors?.name ?? "Unknown Vendor";
    const due = bill.due_date ? new Date(bill.due_date) : new Date(bill.issue_date ?? Date.now());
    const days = Math.floor((now.getTime() - due.getTime()) / 86400000);
    const r = byVendor.get(vid) ?? { id: vid, name, current: 0, d30: 0, d60: 0, d90: 0, d90p: 0, total: 0 };
    if (days <= 0) r.current += open;
    else if (days <= 30) r.d30 += open;
    else if (days <= 60) r.d60 += open;
    else if (days <= 90) r.d90 += open;
    else r.d90p += open;
    r.total += open;
    byVendor.set(vid, r);
  }
  const list = Array.from(byVendor.values()).sort((a, b) => b.total - a.total);
  const totals = list.reduce(
    (s, r) => ({ current: s.current + r.current, d30: s.d30 + r.d30, d60: s.d60 + r.d60, d90: s.d90 + r.d90, d90p: s.d90p + r.d90p, total: s.total + r.total }),
    { current: 0, d30: 0, d60: 0, d90: 0, d90p: 0, total: 0 }
  );
  return (
    <Table>
      <TableHeader>
        <TableRow>
          <TableHead>Vendor</TableHead>
          <TableHead className="text-right">Current</TableHead>
          <TableHead className="text-right">1–30 days</TableHead>
          <TableHead className="text-right">31–60 days</TableHead>
          <TableHead className="text-right">61–90 days</TableHead>
          <TableHead className="text-right">90+ days</TableHead>
          <TableHead className="text-right font-semibold">Total</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {list.map((r) => (
          <TableRow key={r.id} className="hover:bg-muted/40">
            <TableCell className="font-medium">{r.name}</TableCell>
            <TableCell className="text-right tabular-nums">{money(r.current, currency)}</TableCell>
            <TableCell className="text-right tabular-nums text-amber-600">{money(r.d30, currency)}</TableCell>
            <TableCell className="text-right tabular-nums text-orange-600">{money(r.d60, currency)}</TableCell>
            <TableCell className="text-right tabular-nums text-red-500">{money(r.d90, currency)}</TableCell>
            <TableCell className="text-right tabular-nums text-red-700 font-medium">{money(r.d90p, currency)}</TableCell>
            <TableCell className="text-right tabular-nums font-semibold">{money(r.total, currency)}</TableCell>
          </TableRow>
        ))}
        {list.length === 0 && <TableRow><TableCell colSpan={7} className="text-center text-muted-foreground py-8">No outstanding payables.</TableCell></TableRow>}
        {list.length > 0 && (
          <TableRow className="font-semibold bg-muted/30">
            <TableCell>Total</TableCell>
            <TableCell className="text-right tabular-nums">{money(totals.current, currency)}</TableCell>
            <TableCell className="text-right tabular-nums">{money(totals.d30, currency)}</TableCell>
            <TableCell className="text-right tabular-nums">{money(totals.d60, currency)}</TableCell>
            <TableCell className="text-right tabular-nums">{money(totals.d90, currency)}</TableCell>
            <TableCell className="text-right tabular-nums">{money(totals.d90p, currency)}</TableCell>
            <TableCell className="text-right tabular-nums">{money(totals.total, currency)}</TableCell>
          </TableRow>
        )}
      </TableBody>
    </Table>
  );
}

// ── Homeowner Balance Summary ─────────────────────────────────────────────────

function HomeownerSummaryTable({ customers, invoices, currency }: { customers: any[]; invoices: any[]; currency: string }) {
  const byCustomer = useMemo(() => {
    const m = new Map<string, { open: number; count: number; lastPaid: string | null }>();
    for (const inv of invoices) {
      const cid = inv.customer_id;
      if (!cid) continue;
      const open = Number(inv.total ?? 0) - Number(inv.paid_amount ?? 0);
      const isPaid = inv.status === "paid";
      const cur = m.get(cid) ?? { open: 0, count: 0, lastPaid: null };
      if (open > 0) { cur.open += open; cur.count++; }
      if (isPaid && (!cur.lastPaid || inv.issue_date > cur.lastPaid)) cur.lastPaid = inv.issue_date;
      m.set(cid, cur);
    }
    return m;
  }, [invoices]);

  const rows = customers
    .map((c: any) => ({ ...c, ...(byCustomer.get(c.id) ?? { open: 0, count: 0, lastPaid: null }) }))
    .sort((a, b) => b.open - a.open);

  const totalBalance = rows.reduce((s, r) => s + Number(r.balance ?? 0), 0);
  const totalOpen = rows.reduce((s, r) => s + r.open, 0);

  return (
    <Table>
      <TableHeader>
        <TableRow>
          <TableHead>Homeowner</TableHead>
          <TableHead>Property</TableHead>
          <TableHead className="text-right">Open Invoices</TableHead>
          <TableHead className="text-right">Outstanding Balance</TableHead>
          <TableHead className="text-right">Last Payment</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {rows.map((r: any) => (
          <TableRow key={r.id} className="hover:bg-muted/40">
            <TableCell>
              <Link to={`/dashboard/accounting/customers/${r.id}`} className="font-medium text-primary hover:underline">{r.name}</Link>
            </TableCell>
            <TableCell className="text-sm text-muted-foreground">{r.property_address ?? "—"}{r.lot_number ? ` · Lot ${r.lot_number}` : ""}</TableCell>
            <TableCell className="text-right tabular-nums">{r.count || "—"}</TableCell>
            <TableCell className={`text-right tabular-nums font-medium ${r.open > 0 ? "text-red-600" : ""}`}>{money(r.open, currency)}</TableCell>
            <TableCell className="text-right text-sm text-muted-foreground">{r.lastPaid ? fmtDate(r.lastPaid) : "—"}</TableCell>
          </TableRow>
        ))}
        {rows.length === 0 && <TableRow><TableCell colSpan={5} className="text-center text-muted-foreground py-8">No homeowners.</TableCell></TableRow>}
        <TableRow className="font-semibold bg-muted/30">
          <TableCell colSpan={3}>Total ({rows.length} homeowners)</TableCell>
          <TableCell className="text-right tabular-nums">{money(totalOpen, currency)}</TableCell>
          <TableCell />
        </TableRow>
      </TableBody>
    </Table>
  );
}

// ── Delinquency Report ────────────────────────────────────────────────────────

function DelinquencyTable({ customers, invoices, currency }: { customers: any[]; invoices: any[]; currency: string }) {
  const now = new Date();
  type DelRow = { id: string; name: string; email: string | null; phone: string | null; property: string | null; overdue: number; oldest: number; invoiceCount: number };
  const byCustomer = new Map<string, DelRow>();

  for (const inv of invoices) {
    const open = Number(inv.total ?? 0) - Number(inv.paid_amount ?? 0);
    if (open <= 0) continue;
    const due = inv.due_date ? new Date(inv.due_date) : new Date(inv.issue_date ?? Date.now());
    const days = Math.floor((now.getTime() - due.getTime()) / 86400000);
    if (days <= 0) continue; // not overdue yet
    const cid = inv.customer_id;
    if (!cid) continue;
    const cust = customers.find((c: any) => c.id === cid);
    const r = byCustomer.get(cid) ?? {
      id: cid, name: cust?.name ?? inv.customers?.name ?? "—",
      email: cust?.email ?? null, phone: cust?.phone ?? null,
      property: cust?.property_address ?? null,
      overdue: 0, oldest: 0, invoiceCount: 0,
    };
    r.overdue += open;
    r.oldest = Math.max(r.oldest, days);
    r.invoiceCount++;
    byCustomer.set(cid, r);
  }

  const list = Array.from(byCustomer.values()).sort((a, b) => b.overdue - a.overdue);
  const totalOverdue = list.reduce((s, r) => s + r.overdue, 0);

  const ageBucket = (days: number) => {
    if (days <= 30) return { label: "1–30 days", cls: "bg-amber-100 text-amber-800" };
    if (days <= 60) return { label: "31–60 days", cls: "bg-orange-100 text-orange-800" };
    if (days <= 90) return { label: "61–90 days", cls: "bg-red-100 text-red-800" };
    return { label: "90+ days", cls: "bg-red-200 text-red-900 font-semibold" };
  };

  return (
    <div className="space-y-3">
      {totalOverdue > 0 && (
        <div className="rounded-md bg-red-50 border border-red-200 px-4 py-2 text-sm flex items-center justify-between">
          <span className="font-medium text-red-800">{list.length} homeowners with overdue balances</span>
          <span className="font-bold text-red-900">{money(totalOverdue, currency)} total overdue</span>
        </div>
      )}
      <Table>
        <TableHeader>
          <TableRow>
            <TableHead>Homeowner</TableHead>
            <TableHead>Property</TableHead>
            <TableHead>Contact</TableHead>
            <TableHead className="text-right">Invoices</TableHead>
            <TableHead>Age</TableHead>
            <TableHead className="text-right">Amount Overdue</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {list.map((r) => {
            const { label, cls } = ageBucket(r.oldest);
            return (
              <TableRow key={r.id} className="hover:bg-muted/40">
                <TableCell>
                  <Link to={`/dashboard/accounting/customers/${r.id}`} className="font-medium text-primary hover:underline">{r.name}</Link>
                </TableCell>
                <TableCell className="text-sm text-muted-foreground">{r.property ?? "—"}</TableCell>
                <TableCell className="text-sm">
                  {r.email && <div><a href={`mailto:${r.email}`} className="text-primary hover:underline">{r.email}</a></div>}
                  {r.phone && <div className="text-muted-foreground">{r.phone}</div>}
                </TableCell>
                <TableCell className="text-right tabular-nums">{r.invoiceCount}</TableCell>
                <TableCell><span className={`rounded-full px-2 py-0.5 text-xs ${cls}`}>{label}</span></TableCell>
                <TableCell className="text-right tabular-nums font-semibold text-red-700">{money(r.overdue, currency)}</TableCell>
              </TableRow>
            );
          })}
          {list.length === 0 && <TableRow><TableCell colSpan={6} className="text-center text-muted-foreground py-8">No overdue accounts. All homeowners are current.</TableCell></TableRow>}
          {list.length > 0 && (
            <TableRow className="font-semibold bg-muted/30">
              <TableCell colSpan={5}>Total overdue</TableCell>
              <TableCell className="text-right tabular-nums text-red-700">{money(totalOverdue, currency)}</TableCell>
            </TableRow>
          )}
        </TableBody>
      </Table>
    </div>
  );
}

// ── AR Aging ──────────────────────────────────────────────────────────────────

function ARAgingTable({ rows, currency, detailed = false }: { rows: any[]; currency: string; detailed?: boolean }) {
  const now = new Date();
  type AgingRow = { id: string; name: string; current: number; d30: number; d60: number; d90: number; d90p: number; total: number };
  const byCustomer = new Map<string, AgingRow>();
  for (const inv of rows) {
    const open = Number(inv.total ?? 0) - Number(inv.paid_amount ?? 0);
    if (open <= 0) continue;
    const cid = inv.customer_id ?? inv.customers?.id;
    if (!cid) continue;
    const name = inv.customers?.name ?? "—";
    const due = inv.due_date ? new Date(inv.due_date) : new Date(inv.issue_date);
    const days = Math.floor((now.getTime() - due.getTime()) / 86400000);
    const r = byCustomer.get(cid) ?? { id: cid, name, current: 0, d30: 0, d60: 0, d90: 0, d90p: 0, total: 0 };
    if (days <= 0) r.current += open;
    else if (days <= 30) r.d30 += open;
    else if (days <= 60) r.d60 += open;
    else if (days <= 90) r.d90 += open;
    else r.d90p += open;
    r.total += open;
    byCustomer.set(cid, r);
  }
  const list = Array.from(byCustomer.values()).sort((a, b) => b.total - a.total);
  const totals = list.reduce(
    (s, r) => ({ current: s.current + r.current, d30: s.d30 + r.d30, d60: s.d60 + r.d60, d90: s.d90 + r.d90, d90p: s.d90p + r.d90p, total: s.total + r.total }),
    { current: 0, d30: 0, d60: 0, d90: 0, d90p: 0, total: 0 }
  );
  return (
    <Table>
      <TableHeader>
        <TableRow>
          <TableHead>Homeowner</TableHead>
          <TableHead className="text-right">Current</TableHead>
          <TableHead className="text-right">1–30 days</TableHead>
          <TableHead className="text-right">31–60 days</TableHead>
          <TableHead className="text-right">61–90 days</TableHead>
          <TableHead className="text-right">90+ days</TableHead>
          <TableHead className="text-right font-semibold">Total</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {list.map((r) => (
          <TableRow key={r.id} className="cursor-pointer hover:bg-muted/40">
            <TableCell>
              <Link to={`/dashboard/accounting/customers/${r.id}`} className="text-primary hover:underline">{r.name}</Link>
            </TableCell>
            <TableCell className="text-right">{money(r.current, currency)}</TableCell>
            <TableCell className="text-right">{money(r.d30, currency)}</TableCell>
            <TableCell className="text-right">{money(r.d60, currency)}</TableCell>
            <TableCell className="text-right">{money(r.d90, currency)}</TableCell>
            <TableCell className="text-right">{money(r.d90p, currency)}</TableCell>
            <TableCell className="text-right font-medium">{money(r.total, currency)}</TableCell>
          </TableRow>
        ))}
        {list.length === 0 && <TableRow><TableCell colSpan={7} className="text-center text-muted-foreground py-8">No outstanding receivables.</TableCell></TableRow>}
        {list.length > 0 && (
          <TableRow className="font-semibold bg-muted/30">
            <TableCell>Total</TableCell>
            <TableCell className="text-right">{money(totals.current, currency)}</TableCell>
            <TableCell className="text-right">{money(totals.d30, currency)}</TableCell>
            <TableCell className="text-right">{money(totals.d60, currency)}</TableCell>
            <TableCell className="text-right">{money(totals.d90, currency)}</TableCell>
            <TableCell className="text-right">{money(totals.d90p, currency)}</TableCell>
            <TableCell className="text-right">{money(totals.total, currency)}</TableCell>
          </TableRow>
        )}
      </TableBody>
    </Table>
  );
}

