import jsPDF from "jspdf";
import autoTable from "jspdf-autotable";
import { accounting } from "@/lib/accountingClient";
import { fmtDate } from "./format";
import { drawBrandedFooter } from "./reportHeader";
import { appendStructuredReportPdf, type StructuredReport } from "./reportPdf";
import {
  fetchAssociationId, fetchOwnerLedger, fetchUnitsAndOwners,
  unitLabel, money, num,
  type OwnerLedgerEntry, type UnitInfo, type OwnerInfo,
} from "./ownerLedger";

const TEAL: [number, number, number] = [0, 137, 123];
const DEBIT_NATURAL = ["asset", "expense"];

/** Reports that can be added to a saved batch, in packet order. */
export const BATCHABLE_REPORTS: { id: string; name: string }[] = [
  { id: "balance-sheet", name: "Balance Sheet" },
  { id: "pnl", name: "Profit & Loss" },
  { id: "cash-flow", name: "Cash Flow Statement" },
  { id: "movement-of-equity", name: "Movement of Equity" },
  { id: "trial-balance", name: "Trial Balance" },
  { id: "general-ledger", name: "General Ledger" },
  { id: "cash-disbursement", name: "Cash Disbursement" },
  { id: "ar-aging-property", name: "AR Aging (Property)" },
  { id: "prepaid-homeowners", name: "Pre-Paid Homeowners" },
  { id: "reserve-fund", name: "Reserve Fund Schedule" },
];
const FINANCIAL_IDS = new Set(["balance-sheet", "pnl", "cash-flow", "movement-of-equity"]);

export type BatchCtx = {
  companyId: string;
  companyName: string;
  from: string;
  to: string;
  currency: string;
  // Injected from the reports page to avoid an import cycle.
  fetchReportData: (cid: string, from: string, to: string) => Promise<any>;
  buildFinancial: (id: any, d: any, p: any, useCompare: boolean) => StructuredReport;
};

const PAGE = 1000;
async function fetchAllGL(cid: string, to: string, select: string, from?: string): Promise<any[]> {
  const out: any[] = [];
  for (let offset = 0; ; offset += PAGE) {
    let q = accounting.from("journal_entry_lines").select(select)
      .eq("journal_entries.company_id", cid).lte("journal_entries.date", to);
    if (from) q = q.gte("journal_entries.date", from);
    const { data, error } = await q.order("id", { ascending: true }).range(offset, offset + PAGE - 1);
    if (error) throw error;
    const rows = (data ?? []) as any[];
    out.push(...rows);
    if (rows.length < PAGE) break;
  }
  return out;
}

const numAcc = (n: number) => {
  const v = Math.round((n + Number.EPSILON) * 100) / 100 || 0;
  const abs = Math.abs(v).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
  return v < 0 ? `(${abs})` : abs;
};

// ── Per-report appenders (each starts on the current page) ────────────────────

async function appendFinancial(doc: jsPDF, id: string, ctx: BatchCtx) {
  const data = await ctx.fetchReportData(ctx.companyId, ctx.from, ctx.to);
  const report = ctx.buildFinancial(id, data, undefined, false);
  appendStructuredReportPdf(doc, report, {
    companyName: ctx.companyName, appName: "Cozy Books",
    rangeLabel: id === "balance-sheet" ? `As of ${fmtDate(ctx.to)}` : `${fmtDate(ctx.from)} – ${fmtDate(ctx.to)}`,
    currency: ctx.currency, showCodes: true, showCompare: false, showZero: false, skipFooter: true,
  });
}

async function appendTrialBalance(doc: jsPDF, ctx: BatchCtx) {
  const [{ data: accts }, glLines] = await Promise.all([
    accounting.from("accounts").select("id,name,code,type").eq("company_id", ctx.companyId).eq("is_archived", false).order("code"),
    fetchAllGL(ctx.companyId, ctx.to, "debit,credit,account_id,accounts!inner(type)"),
  ]);
  const net = new Map<string, number>();
  for (const l of glLines) net.set(l.account_id, (net.get(l.account_id) ?? 0) + Number(l.debit || 0) - Number(l.credit || 0));
  let td = 0, tc = 0;
  const body = (accts ?? []).map((a: any) => {
    const n = net.get(a.id) ?? 0;
    const debit = n > 0.004 ? n : 0;
    const credit = n < -0.004 ? -n : 0;
    td += debit; tc += credit;
    return [`${a.code ? a.code + "  " : ""}${a.name}`, debit ? numAcc(debit) : "", credit ? numAcc(credit) : ""];
  }).filter((r) => r[1] || r[2]);
  body.push([{ content: "TOTAL", styles: { fontStyle: "bold" } } as any,
             { content: numAcc(td), styles: { fontStyle: "bold", halign: "right" } } as any,
             { content: numAcc(tc), styles: { fontStyle: "bold", halign: "right" } } as any]);
  sectionTitle(doc, "Trial Balance", `As of ${fmtDate(ctx.to)}`, ctx);
  autoTable(doc, {
    startY: (doc as any).__y, head: [["Account", "Debit", "Credit"]], body,
    styles: { fontSize: 8.5, cellPadding: 3 }, headStyles: { fillColor: TEAL, textColor: 255 },
    columnStyles: { 1: { halign: "right" }, 2: { halign: "right" } }, margin: { left: 40, right: 40 },
  });
}

async function appendGeneralLedger(doc: jsPDF, ctx: BatchCtx) {
  const [{ data: accts }, glLines] = await Promise.all([
    accounting.from("accounts").select("id,name,code,type").eq("company_id", ctx.companyId).eq("is_archived", false).order("code"),
    fetchAllGL(ctx.companyId, ctx.to, "id,debit,credit,description,account_id,journal_entries!inner(company_id,date,reference)"),
  ]);
  const acctById = new Map((accts ?? []).map((a: any) => [a.id, a]));
  const groups = new Map<string, { account: any; opening: number; entries: any[]; closing: number }>();
  const rows = glLines.map((l: any) => ({
    date: String(l.journal_entries?.date ?? "").slice(0, 10),
    description: l.description ?? null, reference: l.journal_entries?.reference ?? null,
    account_id: l.account_id, debit: Number(l.debit || 0), credit: Number(l.credit || 0),
  })).sort((a, b) => a.date.localeCompare(b.date));
  for (const t of rows) {
    const a = acctById.get(t.account_id); if (!a) continue;
    const g = groups.get(t.account_id) ?? { account: a, opening: 0, entries: [], closing: 0 };
    const nat = DEBIT_NATURAL.includes(a.type) ? t.debit - t.credit : t.credit - t.debit;
    if (t.date < ctx.from) g.opening += nat; else g.entries.push(t);
    groups.set(t.account_id, g);
  }
  const list = [...groups.values()].filter((g) => g.entries.length > 0 || Math.abs(g.opening) > 0.004)
    .sort((a, b) => (a.account.code ?? "").localeCompare(b.account.code ?? ""));

  sectionTitle(doc, "General Ledger", `${fmtDate(ctx.from)} – ${fmtDate(ctx.to)}`, ctx);
  let y = (doc as any).__y;
  for (const g of list) {
    const nat = DEBIT_NATURAL.includes(g.account.type);
    let bal = g.opening;
    const body: any[] = [[
      { content: fmtDate(ctx.from), styles: { fontStyle: "bold", fillColor: [245, 245, 245] } },
      { content: "Opening Balance", colSpan: 3, styles: { fontStyle: "bold", fillColor: [245, 245, 245] } },
      { content: numAcc(g.opening), styles: { fontStyle: "bold", halign: "right", fillColor: [245, 245, 245] } },
    ]];
    for (const t of g.entries) {
      bal += nat ? t.debit - t.credit : t.credit - t.debit;
      body.push([fmtDate(t.date), t.description ?? "", t.debit ? numAcc(t.debit) : "", t.credit ? numAcc(t.credit) : "", numAcc(bal)]);
    }
    body.push([{ content: "Closing Balance", colSpan: 4, styles: { fontStyle: "bold", halign: "right" } } as any,
               { content: numAcc(bal), styles: { fontStyle: "bold", halign: "right" } } as any]);
    autoTable(doc, {
      startY: y,
      head: [[{ content: `${g.account.code ? g.account.code + " · " : ""}${g.account.name}`, colSpan: 5, styles: { halign: "left", fillColor: TEAL, textColor: 255 } }],
             ["Date", "Description", "Debit", "Credit", "Balance"]],
      body, styles: { fontSize: 8, cellPadding: 2.5 },
      headStyles: { fillColor: [232, 240, 240], textColor: 20 },
      columnStyles: { 0: { cellWidth: 64 }, 2: { halign: "right" }, 3: { halign: "right" }, 4: { halign: "right" } },
      margin: { left: 40, right: 40 },
    });
    y = (doc as any).lastAutoTable.finalY + 14;
    if (y > doc.internal.pageSize.getHeight() - 80) { doc.addPage(); y = 54; }
  }
}

async function appendCashDisbursement(doc: jsPDF, ctx: BatchCtx) {
  const [{ data: accts }, lines] = await Promise.all([
    accounting.from("accounts").select("id,code,name,is_bank").eq("company_id", ctx.companyId),
    accounting.from("journal_entry_lines")
      .select("debit,credit,description,account_id,journal_entries!inner(id,company_id,date,description,reference,external_source)")
      .eq("journal_entries.company_id", ctx.companyId).gte("journal_entries.date", ctx.from).lte("journal_entries.date", ctx.to).limit(50000),
  ]);
  const acctById = new Map((accts ?? []).map((a: any) => [a.id, a]));
  const byJe = new Map<string, { je: any; lines: any[] }>();
  for (const l of (lines ?? []) as any[]) {
    const je = l.journal_entries; if (!je?.id) continue;
    const g = byJe.get(je.id) ?? { je, lines: [] }; g.lines.push(l); byJe.set(je.id, g);
  }
  const groups = new Map<string, { label: string; entries: any[]; subtotal: number }>();
  let grand = 0;
  for (const { je, lines: ls } of byJe.values()) {
    if (je.external_source === "acmacc_xfer") continue;
    const bankCredits = ls.filter((l) => Number(l.credit || 0) > 0 && acctById.get(l.account_id)?.is_bank);
    const nonBankDebits = ls.filter((l) => Number(l.debit || 0) > 0 && !acctById.get(l.account_id)?.is_bank);
    if (!bankCredits.length || !nonBankDebits.length) continue;
    const amount = bankCredits.reduce((s, l) => s + Number(l.credit || 0), 0);
    const mainBank = bankCredits.reduce((a, b) => (Number(b.credit) > Number(a.credit) ? b : a));
    const ba = acctById.get(mainBank.account_id);
    const label = ba ? `${ba.code ? ba.code + " - " : ""}${ba.name}` : "Unknown account";
    const g = groups.get(label) ?? { label, entries: [], subtotal: 0 };
    g.entries.push({ date: String(je.date).slice(0, 10), ref: je.reference || "—", desc: je.description || "—", amount });
    g.subtotal += amount; grand += amount; groups.set(label, g);
  }
  const list = [...groups.values()].sort((a, b) => a.label.localeCompare(b.label));
  for (const g of list) g.entries.sort((a, b) => a.date.localeCompare(b.date));

  sectionTitle(doc, "Cash Disbursement", `${fmtDate(ctx.from)} – ${fmtDate(ctx.to)}`, ctx);
  const body: any[] = [];
  for (const g of list) {
    body.push([{ content: g.label, colSpan: 4, styles: { fontStyle: "bold", textColor: TEAL, fontSize: 10 } }]);
    for (const e of g.entries) body.push([fmtDate(e.date), e.ref, e.desc, { content: numAcc(e.amount), styles: { halign: "right" } }]);
    body.push([{ content: `Total ${g.label}`, colSpan: 3, styles: { fontStyle: "bold", halign: "right" } },
               { content: numAcc(g.subtotal), styles: { fontStyle: "bold", halign: "right" } }]);
  }
  body.push([{ content: "Total Disbursements", colSpan: 3, styles: { fontStyle: "bold", halign: "right", fillColor: [237, 239, 242] } },
             { content: numAcc(grand), styles: { fontStyle: "bold", halign: "right", fillColor: [237, 239, 242] } }]);
  autoTable(doc, {
    startY: (doc as any).__y, head: [["Paid Date", "CheckNo", "Description", "Amount"]], body,
    styles: { fontSize: 8, cellPadding: 3 }, headStyles: { fillColor: TEAL, textColor: 255 },
    columnStyles: { 0: { cellWidth: 70 }, 1: { cellWidth: 70 }, 3: { halign: "right", cellWidth: 80 } },
    margin: { left: 40, right: 40 },
  });
}

// AR Aging (Property) — FIFO-aged open balances per unit
async function appendArAging(doc: jsPDF, ctx: BatchCtx) {
  const assocId = await fetchAssociationId(ctx.companyId);
  if (!assocId) return sectionNote(doc, "AR Aging", "No association linked.", ctx);
  const [entries, { units, owners }] = await Promise.all([
    fetchOwnerLedger(assocId, ctx.to), fetchUnitsAndOwners(assocId),
  ]);
  const unitById = new Map<string, UnitInfo>(units.map((u) => [u.id, u]));
  const ownerByUnit = new Map<string, OwnerInfo>();
  for (const o of owners) if (o.unit_id && !ownerByUnit.has(o.unit_id)) ownerByUnit.set(o.unit_id, o);
  const byUnit = new Map<string, OwnerLedgerEntry[]>();
  for (const e of entries) {
    const key = e.unit_id ? `u:${e.unit_id}` : e.owner_id ? `o:${e.owner_id}` : null; if (!key) continue;
    (byUnit.get(key) ?? byUnit.set(key, []).get(key)!).push(e);
  }
  const bIdx = (chargeDate: string) => {
    const days = Math.floor((new Date(ctx.to + "T00:00:00").getTime() - new Date(chargeDate + "T00:00:00").getTime()) / 86400000);
    return days <= 30 ? 0 : days <= 60 ? 1 : days <= 90 ? 2 : 3;
  };
  const rows: { label: string; buckets: number[]; total: number }[] = [];
  const totals = [0, 0, 0, 0];
  for (const [key, list] of byUnit) {
    list.sort((a, b) => a.date.localeCompare(b.date));
    let pool = list.reduce((s, e) => s + e.credit, 0);
    const buckets = [0, 0, 0, 0]; let total = 0;
    for (const e of list) {
      if (e.debit <= 0) continue;
      let open = e.debit;
      if (pool > 0) { const ap = Math.min(pool, open); pool -= ap; open -= ap; }
      if (open <= 0.004) continue;
      const i = bIdx(e.date); buckets[i] += open; total += open; totals[i] += open;
    }
    if (total <= 0.004) continue;
    const unitId = key.startsWith("u:") ? key.slice(2) : null;
    const unit = unitId ? unitById.get(unitId) : undefined;
    const owner = unitId ? ownerByUnit.get(unitId) : null;
    rows.push({ label: unitLabel(unit, owner?.last_name ?? null), buckets, total });
  }
  rows.sort((a, b) => a.label.localeCompare(b.label));
  const grand = totals.reduce((s, n) => s + n, 0);

  sectionTitle(doc, "AR Aging", `As of ${fmtDate(ctx.to)}`, ctx);
  const dash = (n: number) => (n ? money(n) : "-");
  const body = rows.map((r) => [r.label, dash(r.buckets[0]), dash(r.buckets[1]), dash(r.buckets[2]), dash(r.buckets[3]), money(r.total)]);
  body.push([{ content: "Total", styles: { fontStyle: "bold" } } as any,
    ...totals.map((n) => ({ content: money(n), styles: { fontStyle: "bold", halign: "right" } })),
    { content: money(grand), styles: { fontStyle: "bold", halign: "right" } } as any]);
  autoTable(doc, {
    startY: (doc as any).__y, head: [["Property", "0-30", "Over 30", "Over 60", "Over 90", "Balance"]], body,
    styles: { fontSize: 8, cellPadding: 3 }, headStyles: { fillColor: TEAL, textColor: 255 },
    columnStyles: { 0: { cellWidth: 200 }, 1: { halign: "right" }, 2: { halign: "right" }, 3: { halign: "right" }, 4: { halign: "right" }, 5: { halign: "right" } },
    margin: { left: 40, right: 40 },
  });
}

async function appendPrepaid(doc: jsPDF, ctx: BatchCtx) {
  const assocId = await fetchAssociationId(ctx.companyId);
  if (!assocId) return sectionNote(doc, "Pre Paid Homeowners", "No association linked.", ctx);
  const [entries, { units, owners }] = await Promise.all([fetchOwnerLedger(assocId, ctx.to), fetchUnitsAndOwners(assocId)]);
  const unitById = new Map<string, UnitInfo>(units.map((u) => [u.id, u]));
  const ownerByUnit = new Map<string, OwnerInfo>();
  for (const o of owners) if (o.unit_id && !ownerByUnit.has(o.unit_id)) ownerByUnit.set(o.unit_id, o);
  const bal = new Map<string, number>();
  for (const e of entries) {
    const key = e.unit_id ? `u:${e.unit_id}` : e.owner_id ? `o:${e.owner_id}` : null; if (!key) continue;
    bal.set(key, (bal.get(key) ?? 0) + e.debit - e.credit);
  }
  const rows: { account: string; property: string; owner: string; credit: number }[] = [];
  for (const [key, b] of bal) {
    if (b >= -0.004) continue;
    const unitId = key.startsWith("u:") ? key.slice(2) : null;
    const unit = unitId ? unitById.get(unitId) : undefined;
    const owner = unitId ? ownerByUnit.get(unitId) : null;
    rows.push({
      account: unit?.account_number || unit?.unit_number || "—",
      property: [unit?.address, unit?.unit_number].filter(Boolean).join(" ") || "—",
      owner: owner ? `${owner.first_name ?? ""} ${owner.last_name ?? ""}`.trim() || "—" : "—",
      credit: -b,
    });
  }
  rows.sort((a, b) => b.credit - a.credit);
  const total = rows.reduce((s, r) => s + r.credit, 0);
  sectionTitle(doc, "Pre Paid Homeowners", `For ${fmtDate(ctx.to)}`, ctx);
  const body = rows.map((r) => [r.account, r.property, r.owner, num(r.credit)]);
  body.push([{ content: "", styles: {} } as any, { content: "", styles: {} } as any,
    { content: "Total", styles: { fontStyle: "bold", halign: "right" } } as any,
    { content: num(total), styles: { fontStyle: "bold", halign: "right" } } as any]);
  autoTable(doc, {
    startY: (doc as any).__y, head: [["Account", "Property", "Owner Name", "Credit Amount"]], body,
    styles: { fontSize: 8.5, cellPadding: 4 }, headStyles: { fillColor: TEAL, textColor: 255 },
    columnStyles: { 3: { halign: "right" } }, margin: { left: 40, right: 40 },
  });
}

async function appendReserveFund(doc: jsPDF, ctx: BatchCtx) {
  const { data: reserveAccts } = await accounting.from("accounts")
    .select("id,name,code,type,is_reserve").eq("company_id", ctx.companyId).eq("is_reserve", true).eq("is_archived", false).order("code");
  const ids = (reserveAccts ?? []).map((a: any) => a.id);
  if (!ids.length) return sectionNote(doc, "Reserve Fund Schedule", "No reserve accounts flagged.", ctx);
  const { data: glLines } = await accounting.from("journal_entry_lines")
    .select("debit,credit,account_id,journal_entries!inner(company_id,date)")
    .eq("journal_entries.company_id", ctx.companyId).in("account_id", ids).lte("journal_entries.date", ctx.to).limit(50000);
  const bal = new Map<string, number>();
  for (const l of (glLines ?? []) as any[]) {
    const a = (reserveAccts ?? []).find((x: any) => x.id === l.account_id); if (!a) continue;
    const nat = DEBIT_NATURAL.includes(a.type) ? Number(l.debit || 0) - Number(l.credit || 0) : Number(l.credit || 0) - Number(l.debit || 0);
    bal.set(l.account_id, (bal.get(l.account_id) ?? 0) + nat);
  }
  let total = 0;
  const body = (reserveAccts ?? []).map((a: any) => {
    const b = bal.get(a.id) ?? 0; total += b;
    return [`${a.code ? a.code + "  " : ""}${a.name}`, numAcc(b)];
  });
  body.push([{ content: "TOTAL RESERVE FUNDS", styles: { fontStyle: "bold" } } as any,
             { content: numAcc(total), styles: { fontStyle: "bold", halign: "right" } } as any]);
  sectionTitle(doc, "Reserve Fund Schedule", `As of ${fmtDate(ctx.to)}`, ctx);
  autoTable(doc, {
    startY: (doc as any).__y, head: [["Account", "Current Balance"]], body,
    styles: { fontSize: 9, cellPadding: 4 }, headStyles: { fillColor: TEAL, textColor: 255 },
    columnStyles: { 1: { halign: "right" } }, margin: { left: 40, right: 40 },
  });
}

// ── Shared section heading + cover ────────────────────────────────────────────

function sectionTitle(doc: jsPDF, title: string, subtitle: string, ctx: BatchCtx) {
  const W = doc.internal.pageSize.getWidth();
  doc.setFont("helvetica", "bold"); doc.setFontSize(16); doc.setTextColor(20);
  doc.text(title, W / 2, 48, { align: "center" });
  doc.setFont("helvetica", "normal"); doc.setFontSize(10); doc.setTextColor(90);
  doc.text(subtitle, W / 2, 64, { align: "center" });
  doc.setFontSize(9); doc.text(ctx.companyName, W / 2, 78, { align: "center" });
  (doc as any).__y = 96;
}
function sectionNote(doc: jsPDF, title: string, note: string, ctx: BatchCtx) {
  sectionTitle(doc, title, "", ctx);
  doc.setFontSize(10); doc.setTextColor(120);
  doc.text(note, 40, (doc as any).__y + 10);
}

function drawCover(doc: jsPDF, ctx: BatchCtx, reportIds: string[]) {
  const W = doc.internal.pageSize.getWidth();
  let y = 160;
  doc.setFont("helvetica", "bold"); doc.setFontSize(24); doc.setTextColor(20);
  doc.text(ctx.companyName, W / 2, y, { align: "center" }); y += 32;
  doc.setFont("helvetica", "normal"); doc.setFontSize(14); doc.setTextColor(90);
  doc.text("Financial Report Package", W / 2, y, { align: "center" }); y += 24;
  doc.setFontSize(11); doc.text(`${fmtDate(ctx.from)} – ${fmtDate(ctx.to)}`, W / 2, y, { align: "center" }); y += 50;
  doc.setFont("helvetica", "bold"); doc.setFontSize(11); doc.setTextColor(20);
  doc.text("Contents", W / 2, y, { align: "center" }); y += 18;
  doc.setFont("helvetica", "normal"); doc.setFontSize(10); doc.setTextColor(70);
  const labels = new Map(BATCHABLE_REPORTS.map((r) => [r.id, r.name]));
  for (const id of reportIds) { doc.text(`•  ${labels.get(id) ?? id}`, W / 2, y, { align: "center" }); y += 16; }
}

/** Build one combined PDF for a saved batch. Each report starts on a fresh page. */
export async function generateBatchPdf(ctx: BatchCtx, reportIds: string[]): Promise<jsPDF> {
  const ordered = BATCHABLE_REPORTS.map((r) => r.id).filter((id) => reportIds.includes(id));
  const doc = new jsPDF({ unit: "pt", format: "letter" });
  drawCover(doc, ctx, ordered);

  for (const id of ordered) {
    doc.addPage();
    try {
      if (FINANCIAL_IDS.has(id)) await appendFinancial(doc, id, ctx);
      else if (id === "trial-balance") await appendTrialBalance(doc, ctx);
      else if (id === "general-ledger") await appendGeneralLedger(doc, ctx);
      else if (id === "cash-disbursement") await appendCashDisbursement(doc, ctx);
      else if (id === "ar-aging-property") await appendArAging(doc, ctx);
      else if (id === "prepaid-homeowners") await appendPrepaid(doc, ctx);
      else if (id === "reserve-fund") await appendReserveFund(doc, ctx);
    } catch (e) {
      sectionNote(doc, BATCHABLE_REPORTS.find((r) => r.id === id)?.name ?? id,
        `Could not generate: ${e instanceof Error ? e.message : String(e)}`, ctx);
    }
  }

  drawBrandedFooter(doc); // one global "Page X of Y" across the whole packet
  return doc;
}
