"use client";

import { useMemo, useState } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Kpi as KpiCard, KpiStrip } from "@/components/ui/kpi";
import { useToast } from "@/components/ui/toast";
import { useLocalStorage } from "@/lib/hooks/use-local-storage";
import { ALL_ORGANIZATIONS } from "@/lib/sag/entities";
import { formatCurrency, formatDate } from "@/lib/utils";
import type { Bill, BillStatus } from "@/lib/finance/bill-types";
import type { ToastSalesRow } from "@/lib/sag/types";

/**
 * F7 — Tax Pack Export.
 *
 * Reads bills (`sag.bills.v2`), personal W-2 paystubs (`sag.income.sources`),
 * and any Toast POS rollups (`sag.toast.imports`) from localStorage and
 * produces a CPA-ready bundle covering the chosen period and entity.
 *
 * Bundle output:
 *   - summary.json — totals + period + entity filter
 *   - bills_paid.csv — every paid bill (line per payment)
 *   - bills_outstanding.csv — anything still due
 *   - income.csv — W-2 paystubs Glenn personally tracks
 *   - toast_revenue.csv — Toast POS rollup rows (if any uploaded)
 *   - by_category.csv — expense rollup by bill category
 *   - by_entity.csv — expense rollup per SAG entity
 *
 * **Zip fallback:** JSZip isn't a project dependency and we're instructed not
 * to add one. Instead this exporter writes two artifacts back-to-back —
 * `tax-pack-<period>.csv` (all CSV sections concatenated, separated by
 * `## SECTION NAME` comment rows the CPA can split apart) and a companion
 * `tax-pack-<period>.json` with the structured summary. The UI surfaces the
 * fallback choice clearly so Glenn knows what to expect.
 */

interface IncomePaystub {
  id: string;
  date: string;
  gross: number;
  net?: number;
  hours?: number;
  notes?: string;
}

interface IncomeSource {
  id: string;
  employerSlug: string;
  role?: string;
  cadence: string;
  payRate?: number;
  startedAt?: string;
  notes?: string;
  paystubs: IncomePaystub[];
}

interface ToastImport {
  id: string;
  name: string;
  uploadedAt: string;
  rowCount: number;
  rows: ToastSalesRow[];
}

type PeriodMode = "month" | "year" | "custom";

interface PeriodWindow {
  startISO: string; // inclusive YYYY-MM-DD
  endISO: string; // inclusive YYYY-MM-DD
  label: string; // short label used in filenames
  pretty: string; // human-readable display label
}

function clampToISODate(d: Date): string {
  return d.toISOString().slice(0, 10);
}

function buildWindow(
  mode: PeriodMode,
  year: number,
  month: number,
  customStart: string,
  customEnd: string
): PeriodWindow | null {
  if (mode === "month") {
    const start = new Date(Date.UTC(year, month - 1, 1));
    const end = new Date(Date.UTC(year, month, 0)); // last day of month
    return {
      startISO: clampToISODate(start),
      endISO: clampToISODate(end),
      label: `${year}-${String(month).padStart(2, "0")}`,
      pretty: `${start.toLocaleDateString("en-US", { month: "long", timeZone: "UTC" })} ${year}`,
    };
  }
  if (mode === "year") {
    return {
      startISO: `${year}-01-01`,
      endISO: `${year}-12-31`,
      label: `${year}`,
      pretty: `Calendar year ${year}`,
    };
  }
  // custom
  if (!customStart || !customEnd) return null;
  if (customEnd < customStart) return null;
  return {
    startISO: customStart,
    endISO: customEnd,
    label: `${customStart}_to_${customEnd}`,
    pretty: `${customStart} → ${customEnd}`,
  };
}

const MONTH_LABELS = [
  "January",
  "February",
  "March",
  "April",
  "May",
  "June",
  "July",
  "August",
  "September",
  "October",
  "November",
  "December",
];

export function TaxPackExport() {
  const [bills] = useLocalStorage<Bill[]>("sag.bills.v2", []);
  const [incomeSources] = useLocalStorage<IncomeSource[]>("sag.income.sources", []);
  const [toastImports] = useLocalStorage<ToastImport[]>("sag.toast.imports", []);

  const today = useMemo(() => new Date(), []);
  const [periodMode, setPeriodMode] = useState<PeriodMode>("month");
  const [year, setYear] = useState<number>(today.getUTCFullYear());
  const [month, setMonth] = useState<number>(today.getUTCMonth() + 1);
  const [customStart, setCustomStart] = useState<string>("");
  const [customEnd, setCustomEnd] = useState<string>("");
  const [entityFilter, setEntityFilter] = useState<string>("all");
  const [lastRun, setLastRun] = useState<{
    at: string;
    label: string;
    fileCount: number;
  } | null>(null);
  const { toast } = useToast();

  const window = useMemo(
    () => buildWindow(periodMode, year, month, customStart, customEnd),
    [periodMode, year, month, customStart, customEnd]
  );

  /** Bills are filtered by *payment date* if paid, otherwise *due date*. */
  const filteredBills = useMemo(() => {
    if (!window) return [] as Bill[];
    return bills.filter((b) => {
      if (entityFilter !== "all" && b.entitySlug !== entityFilter) return false;
      const reference = b.payment?.paidDate ?? b.dueDate;
      return reference >= window.startISO && reference <= window.endISO;
    });
  }, [bills, entityFilter, window]);

  const paidBills = useMemo(
    () =>
      filteredBills
        .filter((b) => b.status === "Paid")
        .sort((a, b) =>
          (a.payment?.paidDate ?? a.dueDate).localeCompare(b.payment?.paidDate ?? b.dueDate)
        ),
    [filteredBills]
  );

  const outstandingBills = useMemo(
    () =>
      filteredBills
        .filter((b) => b.status !== "Paid" && b.status !== "Cancelled")
        .sort((a, b) => a.dueDate.localeCompare(b.dueDate)),
    [filteredBills]
  );

  const incomeRecords = useMemo(() => {
    if (!window) return [] as Array<{ source: IncomeSource; stub: IncomePaystub }>;
    const out: Array<{ source: IncomeSource; stub: IncomePaystub }> = [];
    for (const s of incomeSources) {
      // Personal W-2 income only attaches to the matching employer slug. If a
      // specific entity is selected we still surface income for that entity;
      // otherwise we include everything.
      if (entityFilter !== "all" && s.employerSlug !== entityFilter) continue;
      for (const stub of s.paystubs) {
        if (stub.date < window.startISO || stub.date > window.endISO) continue;
        out.push({ source: s, stub });
      }
    }
    out.sort((a, b) => a.stub.date.localeCompare(b.stub.date));
    return out;
  }, [incomeSources, entityFilter, window]);

  const toastRevenue = useMemo(() => {
    if (!window) return [] as Array<{ upload: ToastImport; row: ToastSalesRow }>;
    const out: Array<{ upload: ToastImport; row: ToastSalesRow }> = [];
    for (const upload of toastImports) {
      const uploadedDate = upload.uploadedAt.slice(0, 10);
      if (uploadedDate < window.startISO || uploadedDate > window.endISO) continue;
      // Toast uploads have no row-level entity slug — keep them only if the
      // entity filter is "all" so we don't mis-attribute revenue.
      if (entityFilter !== "all") continue;
      for (const row of upload.rows) {
        // Skip the duplicate "No Dining Option" row (Toast emits two per cat).
        if (row.diningOption === "No Dining Option") continue;
        out.push({ upload, row });
      }
    }
    return out;
  }, [toastImports, entityFilter, window]);

  const byCategory = useMemo(() => {
    const map = new Map<string, { totalPaid: number; billCount: number }>();
    for (const b of paidBills) {
      const k = b.category ?? "Uncategorized";
      const prev = map.get(k) ?? { totalPaid: 0, billCount: 0 };
      map.set(k, {
        totalPaid: prev.totalPaid + (b.payment?.amount ?? b.amount),
        billCount: prev.billCount + 1,
      });
    }
    return Array.from(map.entries())
      .map(([category, v]) => ({ category, ...v }))
      .sort((a, b) => b.totalPaid - a.totalPaid);
  }, [paidBills]);

  const byEntity = useMemo(() => {
    const map = new Map<
      string,
      { totalPaid: number; totalOutstanding: number; billCount: number }
    >();
    for (const b of paidBills) {
      const prev = map.get(b.entitySlug) ?? {
        totalPaid: 0,
        totalOutstanding: 0,
        billCount: 0,
      };
      map.set(b.entitySlug, {
        ...prev,
        totalPaid: prev.totalPaid + (b.payment?.amount ?? b.amount),
        billCount: prev.billCount + 1,
      });
    }
    for (const b of outstandingBills) {
      const prev = map.get(b.entitySlug) ?? {
        totalPaid: 0,
        totalOutstanding: 0,
        billCount: 0,
      };
      map.set(b.entitySlug, {
        ...prev,
        totalOutstanding: prev.totalOutstanding + b.amount,
        billCount: prev.billCount + 1,
      });
    }
    return Array.from(map.entries())
      .map(([slug, v]) => ({ slug, ...v }))
      .sort((a, b) => b.totalPaid - a.totalPaid);
  }, [paidBills, outstandingBills]);

  const totals = useMemo(() => {
    const paidAmount = paidBills.reduce(
      (a, b) => a + (b.payment?.amount ?? b.amount),
      0
    );
    const outstandingAmount = outstandingBills.reduce((a, b) => a + b.amount, 0);
    const incomeAmount = incomeRecords.reduce((a, r) => a + r.stub.gross, 0);
    const toastGross = toastRevenue.reduce((a, r) => a + r.row.grossSales, 0);
    const toastNet = toastRevenue.reduce((a, r) => a + r.row.netSales, 0);
    return {
      paidAmount,
      outstandingAmount,
      paymentCount: paidBills.length,
      outstandingCount: outstandingBills.length,
      incomeAmount,
      paystubCount: incomeRecords.length,
      toastGrossSales: toastGross,
      toastNetSales: toastNet,
      toastRowCount: toastRevenue.length,
    };
  }, [paidBills, outstandingBills, incomeRecords, toastRevenue]);

  const statusBreakdown = useMemo(() => {
    const map = new Map<BillStatus, number>();
    for (const b of filteredBills) {
      map.set(b.status, (map.get(b.status) ?? 0) + 1);
    }
    return Array.from(map.entries()).sort((a, b) => b[1] - a[1]);
  }, [filteredBills]);

  function generate() {
    if (!window) return;
    const generatedAt = new Date().toISOString();

    const summary = {
      generatedAt,
      generator: "SAG Manager · Tax Pack Export (F7)",
      period: {
        mode: periodMode,
        label: window.label,
        pretty: window.pretty,
        startISO: window.startISO,
        endISO: window.endISO,
      },
      entityFilter,
      entityName:
        entityFilter === "all"
          ? "All entities"
          : (ALL_ORGANIZATIONS.find((o) => o.slug === entityFilter)?.name ?? entityFilter),
      totals,
      statusBreakdown: Object.fromEntries(statusBreakdown),
      byCategory,
      byEntity,
      counts: {
        paidBills: paidBills.length,
        outstandingBills: outstandingBills.length,
        paystubs: incomeRecords.length,
        toastRows: toastRevenue.length,
      },
      // Bundle the full records inline so the JSON file is self-contained.
      records: {
        billsPaid: paidBills.map((b) => ({
          vendor: b.vendor,
          amount: b.payment?.amount ?? b.amount,
          paidDate: b.payment?.paidDate ?? null,
          method: b.payment?.method ?? null,
          fromAccount: b.payment?.fromAccount ?? null,
          confirmation: b.payment?.confirmationNumber ?? null,
          entitySlug: b.entitySlug,
          category: b.category ?? null,
          notes: b.notes ?? null,
          accountNumber: b.accountNumber ?? null,
        })),
        billsOutstanding: outstandingBills.map((b) => ({
          vendor: b.vendor,
          amount: b.amount,
          dueDate: b.dueDate,
          status: b.status,
          entitySlug: b.entitySlug,
          category: b.category ?? null,
          notes: b.notes ?? null,
        })),
        income: incomeRecords.map((r) => ({
          employerSlug: r.source.employerSlug,
          role: r.source.role ?? null,
          paystubDate: r.stub.date,
          gross: r.stub.gross,
          net: r.stub.net ?? null,
          hours: r.stub.hours ?? null,
          notes: r.stub.notes ?? null,
        })),
        toastRevenue: toastRevenue.map((r) => ({
          uploadedAt: r.upload.uploadedAt,
          uploadName: r.upload.name,
          salesCategory: r.row.salesCategory,
          itemQty: r.row.itemQty,
          netSales: r.row.netSales,
          grossSales: r.row.grossSales,
          discountAmount: r.row.discountAmount,
          taxAmount: r.row.taxAmount,
        })),
      },
    };

    const sections: Array<{ name: string; rows: Array<Array<string | number>> }> = [];

    // bills_paid.csv
    sections.push({
      name: "bills_paid.csv",
      rows: [
        [
          "vendor",
          "amount",
          "paidDate",
          "method",
          "fromAccount",
          "confirmation",
          "entity",
          "category",
          "accountNumber",
          "notes",
        ],
        ...paidBills.map((b) => [
          b.vendor,
          (b.payment?.amount ?? b.amount).toFixed(2),
          b.payment?.paidDate ?? "",
          b.payment?.method ?? "",
          b.payment?.fromAccount ?? "",
          b.payment?.confirmationNumber ?? "",
          b.entitySlug,
          b.category ?? "",
          b.accountNumber ?? "",
          b.notes ?? "",
        ]),
      ],
    });

    // bills_outstanding.csv
    sections.push({
      name: "bills_outstanding.csv",
      rows: [
        ["vendor", "amount", "dueDate", "status", "entity", "category", "notes"],
        ...outstandingBills.map((b) => [
          b.vendor,
          b.amount.toFixed(2),
          b.dueDate,
          b.status,
          b.entitySlug,
          b.category ?? "",
          b.notes ?? "",
        ]),
      ],
    });

    // income.csv
    sections.push({
      name: "income.csv",
      rows: [
        ["employer", "role", "paystubDate", "gross", "net", "hours", "notes"],
        ...incomeRecords.map((r) => [
          r.source.employerSlug,
          r.source.role ?? "",
          r.stub.date,
          r.stub.gross.toFixed(2),
          r.stub.net != null ? r.stub.net.toFixed(2) : "",
          r.stub.hours != null ? String(r.stub.hours) : "",
          r.stub.notes ?? "",
        ]),
      ],
    });

    // toast_revenue.csv
    sections.push({
      name: "toast_revenue.csv",
      rows: [
        [
          "uploadedAt",
          "uploadName",
          "salesCategory",
          "itemQty",
          "netSales",
          "grossSales",
          "discountAmount",
          "taxAmount",
        ],
        ...toastRevenue.map((r) => [
          r.upload.uploadedAt,
          r.upload.name,
          r.row.salesCategory,
          String(r.row.itemQty),
          r.row.netSales.toFixed(2),
          r.row.grossSales.toFixed(2),
          r.row.discountAmount.toFixed(2),
          r.row.taxAmount.toFixed(2),
        ]),
      ],
    });

    // by_category.csv
    sections.push({
      name: "by_category.csv",
      rows: [
        ["category", "totalPaid", "billCount"],
        ...byCategory.map((c) => [c.category, c.totalPaid.toFixed(2), String(c.billCount)]),
      ],
    });

    // by_entity.csv
    sections.push({
      name: "by_entity.csv",
      rows: [
        ["entity", "totalPaid", "totalOutstanding", "billCount"],
        ...byEntity.map((e) => [
          e.slug,
          e.totalPaid.toFixed(2),
          e.totalOutstanding.toFixed(2),
          String(e.billCount),
        ]),
      ],
    });

    // Concatenated CSV bundle (one file, `## SECTION` comment rows divide it).
    const csvParts: string[] = [];
    csvParts.push(
      `## Tax pack export — ${window.pretty} — ${summary.entityName} — generated ${generatedAt}`
    );
    csvParts.push("");
    for (const s of sections) {
      csvParts.push(`## ${s.name}`);
      csvParts.push(s.rows.map(csvRow).join("\n"));
      csvParts.push("");
    }
    const csvBlob = new Blob([csvParts.join("\n")], { type: "text/csv;charset=utf-8" });
    const baseName = `tax-pack-${window.label}${entityFilter !== "all" ? `-${entityFilter}` : ""}`;
    triggerDownload(csvBlob, `${baseName}.csv`);

    const jsonBlob = new Blob([JSON.stringify(summary, null, 2)], {
      type: "application/json",
    });
    // Tiny stagger so browsers don't deduplicate the second download click.
    setTimeout(() => triggerDownload(jsonBlob, `${baseName}.json`), 250);

    setLastRun({ at: generatedAt, label: window.pretty, fileCount: 2 });
    toast({
      title: "Tax pack downloaded",
      description: `${window.pretty} · 2 files`,
      variant: "success",
    });
  }

  const yearOptions = useMemo(() => {
    const current = today.getUTCFullYear();
    const years: number[] = [];
    for (let y = current + 1; y >= current - 5; y--) years.push(y);
    return years;
  }, [today]);

  return (
    <div className="space-y-6 max-w-6xl">
      <Card>
        <CardContent className="p-5 space-y-4">
          <div className="flex flex-wrap items-end gap-4">
            <div className="flex flex-col gap-1">
              <label className="section-label">
                Period type
              </label>
              <select
                value={periodMode}
                onChange={(e) => setPeriodMode(e.target.value as PeriodMode)}
                className="h-9 rounded-md border border-input bg-background px-3 text-sm"
              >
                <option value="month">Specific month</option>
                <option value="year">Full year</option>
                <option value="custom">Custom range</option>
              </select>
            </div>

            {periodMode === "month" && (
              <>
                <div className="flex flex-col gap-1">
                  <label className="section-label">
                    Month
                  </label>
                  <select
                    value={month}
                    onChange={(e) => setMonth(Number(e.target.value))}
                    className="h-9 rounded-md border border-input bg-background px-3 text-sm"
                  >
                    {MONTH_LABELS.map((label, idx) => (
                      <option key={label} value={idx + 1}>
                        {label}
                      </option>
                    ))}
                  </select>
                </div>
                <div className="flex flex-col gap-1">
                  <label className="section-label">
                    Year
                  </label>
                  <select
                    value={year}
                    onChange={(e) => setYear(Number(e.target.value))}
                    className="h-9 rounded-md border border-input bg-background px-3 text-sm"
                  >
                    {yearOptions.map((y) => (
                      <option key={y} value={y}>
                        {y}
                      </option>
                    ))}
                  </select>
                </div>
              </>
            )}

            {periodMode === "year" && (
              <div className="flex flex-col gap-1">
                <label className="section-label">
                  Tax year
                </label>
                <select
                  value={year}
                  onChange={(e) => setYear(Number(e.target.value))}
                  className="h-9 rounded-md border border-input bg-background px-3 text-sm"
                >
                  {yearOptions.map((y) => (
                    <option key={y} value={y}>
                      {y}
                    </option>
                  ))}
                </select>
              </div>
            )}

            {periodMode === "custom" && (
              <>
                <div className="flex flex-col gap-1">
                  <label className="section-label">
                    Start date
                  </label>
                  <Input
                    type="date"
                    value={customStart}
                    onChange={(e) => setCustomStart(e.target.value)}
                    className="h-9 w-40"
                  />
                </div>
                <div className="flex flex-col gap-1">
                  <label className="section-label">
                    End date
                  </label>
                  <Input
                    type="date"
                    value={customEnd}
                    onChange={(e) => setCustomEnd(e.target.value)}
                    className="h-9 w-40"
                  />
                </div>
              </>
            )}

            <div className="flex flex-col gap-1">
              <label className="section-label">
                Entity
              </label>
              <select
                value={entityFilter}
                onChange={(e) => setEntityFilter(e.target.value)}
                className="h-9 rounded-md border border-input bg-background px-3 text-sm min-w-[16rem]"
              >
                <option value="all">All entities</option>
                {ALL_ORGANIZATIONS.map((o) => (
                  <option key={o.slug} value={o.slug}>
                    {o.emoji} {o.name}
                  </option>
                ))}
              </select>
            </div>

            <div className="ml-auto flex items-center gap-2">
              <Button variant="brand" size="sm" onClick={generate} disabled={!window}>
                ⬇ Generate tax pack
              </Button>
            </div>
          </div>

          {!window && periodMode === "custom" && (
            <p className="text-xs text-destructive">
              Pick a start and end date (end ≥ start) to enable the export.
            </p>
          )}

          {window && (
            <div className="text-xs text-muted-foreground tabular-nums">
              Window: <span className="font-mono">{window.startISO}</span> →{" "}
              <span className="font-mono">{window.endISO}</span> · {window.pretty}
            </div>
          )}
        </CardContent>
      </Card>

      <Card className="border-dashed">
        <CardContent className="p-4 text-xs text-muted-foreground space-y-1.5">
          <div>
            <strong className="text-foreground">Bundle format —</strong> JSZip isn&apos;t a
            project dependency, so the export downloads as two paired files instead of a
            single .zip:{" "}
            <code className="text-foreground">tax-pack-&lt;period&gt;.csv</code> (all six CSV
            sections concatenated, separated by{" "}
            <code className="text-foreground">## section.csv</code> comment rows the CPA can
            split with any spreadsheet tool) and{" "}
            <code className="text-foreground">tax-pack-&lt;period&gt;.json</code> (the full
            structured summary). When JSZip is added later, this same component can swap to a
            true .zip without changing the data layout.
          </div>
          <div>
            Source data: local-only.{" "}
            <code className="text-foreground">sag.bills.v2</code> ·{" "}
            <code className="text-foreground">sag.income.sources</code> ·{" "}
            <code className="text-foreground">sag.toast.imports</code>. Bills are filtered by
            payment date if paid, due date otherwise. Toast rows are only included when the
            entity filter is &quot;All entities&quot; (Toast exports don&apos;t carry an
            entity slug at the row level).
          </div>
        </CardContent>
      </Card>

      {lastRun && (
        <Card className="border-green-500/30 bg-green-500/5">
          <CardContent className="p-3 text-xs">
            <Badge variant="success" className="mr-2">
              ready
            </Badge>
            Downloaded {lastRun.fileCount} file{lastRun.fileCount === 1 ? "" : "s"} for{" "}
            <span className="font-medium">{lastRun.label}</span> at{" "}
            {formatDate(lastRun.at)}.
          </CardContent>
        </Card>
      )}

      <KpiStrip cols={4}>
        <KpiCard
          label="Bills paid"
          value={formatCurrency(totals.paidAmount)}
          hint={`${totals.paymentCount} payment${totals.paymentCount === 1 ? "" : "s"}`}
        />
        <KpiCard
          label="Outstanding"
          value={formatCurrency(totals.outstandingAmount)}
          hint={`${totals.outstandingCount} open`}
          tone="amber"
        />
        <KpiCard
          label="W-2 income"
          value={formatCurrency(totals.incomeAmount)}
          hint={`${totals.paystubCount} paystub${totals.paystubCount === 1 ? "" : "s"}`}
          tone="emerald"
        />
        <KpiCard
          label="Toast gross sales"
          value={formatCurrency(totals.toastGrossSales)}
          hint={
            totals.toastRowCount === 0
              ? entityFilter === "all"
                ? "No uploads in window"
                : "Entity filter hides Toast"
              : `${totals.toastRowCount} categories`
          }
          tone="violet"
        />
      </KpiStrip>

      <Card>
        <CardContent className="p-5">
          <div className="flex items-center justify-between mb-3">
            <h2 className="text-sm font-semibold">By entity</h2>
            <span className="section-label">
              what the bundle&apos;s{" "}
              <code className="text-foreground">by_entity.csv</code> contains
            </span>
          </div>
          {byEntity.length === 0 ? (
            <p className="text-xs text-muted-foreground">No bills in this window.</p>
          ) : (
            <div className="overflow-x-auto">
              <table className="w-full text-sm">
                <thead className="section-label border-b">
                  <tr>
                    <th className="px-2 py-2 text-left">Entity</th>
                    <th className="px-2 py-2 text-right">Paid</th>
                    <th className="px-2 py-2 text-right">Outstanding</th>
                    <th className="px-2 py-2 text-right">Bills</th>
                  </tr>
                </thead>
                <tbody>
                  {byEntity.map((row) => {
                    const org = ALL_ORGANIZATIONS.find((o) => o.slug === row.slug);
                    return (
                      <tr key={row.slug} className="border-b last:border-0">
                        <td className="px-2 py-2 text-xs">
                          {org ? `${org.emoji} ${org.name}` : row.slug}
                        </td>
                        <td className="px-2 py-2 text-right tabular-nums">
                          {formatCurrency(row.totalPaid)}
                        </td>
                        <td className="px-2 py-2 text-right tabular-nums text-muted-foreground">
                          {formatCurrency(row.totalOutstanding)}
                        </td>
                        <td className="px-2 py-2 text-right tabular-nums text-muted-foreground">
                          {row.billCount}
                        </td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>
          )}
        </CardContent>
      </Card>

      <Card>
        <CardContent className="p-5">
          <div className="flex items-center justify-between mb-3">
            <h2 className="text-sm font-semibold">By category</h2>
            <span className="section-label">
              what the bundle&apos;s{" "}
              <code className="text-foreground">by_category.csv</code> contains
            </span>
          </div>
          {byCategory.length === 0 ? (
            <p className="text-xs text-muted-foreground">No paid bills in this window.</p>
          ) : (
            <div className="overflow-x-auto">
              <table className="w-full text-sm">
                <thead className="section-label border-b">
                  <tr>
                    <th className="px-2 py-2 text-left">Category</th>
                    <th className="px-2 py-2 text-right">Paid</th>
                    <th className="px-2 py-2 text-right">Bills</th>
                  </tr>
                </thead>
                <tbody>
                  {byCategory.map((row) => (
                    <tr key={row.category} className="border-b last:border-0">
                      <td className="px-2 py-2 text-xs">{row.category}</td>
                      <td className="px-2 py-2 text-right tabular-nums">
                        {formatCurrency(row.totalPaid)}
                      </td>
                      <td className="px-2 py-2 text-right tabular-nums text-muted-foreground">
                        {row.billCount}
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          )}
        </CardContent>
      </Card>

      <Card>
        <CardContent className="p-5">
          <div className="flex items-center justify-between mb-3">
            <h2 className="text-sm font-semibold">Status breakdown</h2>
            <span className="section-label">
              every bill in window, by status
            </span>
          </div>
          {statusBreakdown.length === 0 ? (
            <p className="text-xs text-muted-foreground">No bills in this window.</p>
          ) : (
            <div className="flex flex-wrap gap-2">
              {statusBreakdown.map(([status, count]) => (
                <Badge
                  key={status}
                  variant={
                    status === "Paid"
                      ? "success"
                      : status === "Overdue"
                        ? "destructive"
                        : status === "Cancelled"
                          ? "secondary"
                          : "warning"
                  }
                >
                  {status} · {count}
                </Badge>
              ))}
            </div>
          )}
        </CardContent>
      </Card>
    </div>
  );
}

function csvRow(cells: Array<string | number>): string {
  return cells
    .map((cell) => {
      const s = typeof cell === "number" ? String(cell) : cell;
      return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
    })
    .join(",");
}

function triggerDownload(blob: Blob, filename: string) {
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = filename;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}
