import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useEffect, useMemo, useRef, useState } from "react";
import { accounting } from "@/lib/accountingClient";
import { useCompanyId } from "./lib/useCompanyId";
import { useAuth } from "@/contexts/AuthContext";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, SelectGroup, SelectLabel } from "@/components/ui/select";
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/ui/table";
import { Switch } from "@/components/ui/switch";
import { Checkbox } from "@/components/ui/checkbox";
import { Plus, Trash2, Pencil, ArrowLeftRight, Printer, Link2, RefreshCw, Unlink, FileUp, Download, Loader2 } from "lucide-react";
import { toast } from "sonner";
import { money, fmtDate } from "./lib/format";
import { generateCheckPDF } from "./lib/checkPdf";
import { parseCsv, pick, parseDateStr } from "./lib/csv";
import { usePlaidLink } from "react-plaid-link";
import { createLinkToken, exchangePlaidToken, syncPlaidTransactions, disconnectPlaid } from "./lib/plaid";
import { applyPaymentToBill, matchOpenBills } from "./lib/autoBill";

type TxForm = {
  account_id: string;
  date: string;
  amount: string;
  type: "credit" | "debit";
  reference: string;
  coa_account_id: string;
  vendor_id: string;
  customer_id: string;
  memo: string;
  printCheck: boolean;
};

type TransferForm = {
  from_account_id: string;
  to_account_id: string;
  date: string;
  amount: string;
  memo: string;
};

const EMPTY_TX: TxForm = {
  account_id: "",
  date: new Date().toLocaleDateString("en-CA", { timeZone: "America/New_York" }),
  amount: "",
  type: "credit",
  reference: "",
  coa_account_id: "",
  vendor_id: "",
  customer_id: "",
  memo: "",
  printCheck: false,
};

const EMPTY_TRANSFER: TransferForm = {
  from_account_id: "",
  to_account_id: "",
  date: new Date().toLocaleDateString("en-CA", { timeZone: "America/New_York" }),
  amount: "",
  memo: "Account Transfer",
};

export default function AccountingBankingPage() {
  const { companyId, loading: companyLoading, error: companyError, associationId, associationName } = useCompanyId();
  const { user } = useAuth();
  const cid = companyId ?? "";
  const cur = "USD";
  const qc = useQueryClient();

  const [plaidLinkToken,   setPlaidLinkToken]   = useState<string | null>(null);
  const [plaidTargetAcct,  setPlaidTargetAcct]  = useState<string>("");
  const [syncingAcctId,    setSyncingAcctId]     = useState<string | null>(null);

  const { data: plaidConnections = [] } = useQuery({
    queryKey: ["plaid-connections", cid],
    enabled: !!cid,
    queryFn: async () =>
      (await accounting.from("plaid_connections").select("*").eq("company_id", cid)).data ?? [],
  });
  const plaidByAcct = new Map((plaidConnections as any[]).map((c) => [c.account_id, c]));

  const openPlaidLink = async (accountId: string) => {
    if (!user?.id) return toast.error("Must be logged in");
    setPlaidTargetAcct(accountId);
    try {
      const { link_token } = await createLinkToken(user.id, cid);
      setPlaidLinkToken(link_token);
    } catch (e: any) {
      toast.error(e?.message ?? "Failed to start Plaid Link");
    }
  };

  const syncAccount = async (accountId: string) => {
    setSyncingAcctId(accountId);
    try {
      const result = await syncPlaidTransactions(cid, accountId);
      toast.success(`Synced: +${result.added} new, ${result.modified} updated, ${result.removed} removed`);
      qc.invalidateQueries({ queryKey: ["transactions", cid] });
      qc.invalidateQueries({ queryKey: ["accounts", cid] });
      qc.invalidateQueries({ queryKey: ["plaid-connections", cid] });
    } catch (e: any) {
      toast.error(e?.message ?? "Sync failed");
    } finally {
      setSyncingAcctId(null);
    }
  };

  const disconnectAccount = async (accountId: string) => {
    if (!confirm("Disconnect this bank feed? Existing transactions are kept.")) return;
    try {
      await disconnectPlaid(accountId);
      toast.success("Bank feed disconnected");
      qc.invalidateQueries({ queryKey: ["plaid-connections", cid] });
    } catch (e: any) {
      toast.error(e?.message ?? "Disconnect failed");
    }
  };

  const [selectedAccountId, setSelectedAccountId] = useState<string>("");
  const [txDialog, setTxDialog] = useState<{ open: boolean; mode: "deposit" | "payment" | "edit" }>({ open: false, mode: "deposit" });
  const [editId, setEditId] = useState<string | null>(null);
  const [txForm, setTxForm] = useState<TxForm>(EMPTY_TX);
  const [transferOpen, setTransferOpen] = useState(false);
  const [transfer, setTransfer] = useState<TransferForm>(EMPTY_TRANSFER);
  const [acctDialog, setAcctDialog] = useState(false);
  const [acctForm, setAcctForm] = useState({ name: "", code: "", type: "asset" as const, is_bank: true });
  const [search, setSearch] = useState("");
  const [importOpen, setImportOpen] = useState(false);
  const [importing, setImporting] = useState(false);
  const [importResult, setImportResult] = useState<{ inserted: number; skipped: number } | null>(null);
  const importFileRef = useRef<HTMLInputElement>(null);
  const [selected, setSelected] = useState<Set<string>>(new Set());

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

  const bankAccounts = useMemo(() => (accounts as any[]).filter((a) => a.is_bank), [accounts]);
  const incomeAccounts = useMemo(() => (accounts as any[]).filter((a) => a.type === "income"), [accounts]);
  const expenseAccounts = useMemo(() => (accounts as any[]).filter((a) => a.type === "expense"), [accounts]);

  const { data: vendors = [] } = useQuery({
    queryKey: ["vendors-lookup", cid],
    enabled: !!cid,
    queryFn: async () =>
      (await accounting.from("vendors").select("id,name,address").eq("company_id", cid).order("name")).data ?? [],
  });

  const { data: customers = [] } = useQuery({
    queryKey: ["customers", cid],
    enabled: !!cid,
    queryFn: async () =>
      (await accounting.from("customers").select("id,name").eq("company_id", cid).order("name")).data ?? [],
  });

  const { data: checkSettings } = useQuery({
    queryKey: ["check-settings", cid],
    enabled: !!cid,
    queryFn: async () =>
      (await accounting.from("check_settings").select("*").eq("company_id", cid).maybeSingle()).data,
  });

  const activeAccountId = selectedAccountId || bankAccounts[0]?.id || "";

  const { data: txs = [] } = useQuery({
    queryKey: ["transactions", cid, activeAccountId],
    enabled: !!cid && !!activeAccountId,
    queryFn: async () =>
      (
        await accounting
          .from("transactions")
          .select("*, bank_account:accounts!account_id(name), coa:accounts!coa_account_id(name), vendors(name), customers(name)")
          .eq("company_id", cid)
          .eq("account_id", activeAccountId)
          .order("date", { ascending: true })
          .order("created_at", { ascending: true })
      ).data ?? [],
  });

  const register = useMemo(() => {
    let bal = 0;
    return (txs as any[]).map((tx) => {
      const amt = Number(tx.amount ?? 0);
      if (tx.type === "credit") bal += amt;
      else bal -= amt;
      return { ...tx, running: bal };
    });
  }, [txs]);

  const filteredRegister = useMemo(() => {
    if (!search.trim()) return register;
    const q = search.toLowerCase();
    return register.filter(
      (r) =>
        r.description?.toLowerCase().includes(q) ||
        r.category?.toLowerCase().includes(q) ||
        r.reference?.toLowerCase().includes(q)
    );
  }, [register, search]);

  const computedBalance = register.length > 0 ? register[register.length - 1].running : 0;
  const activeAccount = (accounts as any[]).find((a) => a.id === activeAccountId);

  const selectableIds = useMemo(
    () => filteredRegister.filter((r: any) => !r.reconciliation_id && !r.transfer_id).map((r: any) => r.id),
    [filteredRegister]
  );
  const allSelected = selectableIds.length > 0 && selectableIds.every((id: string) => selected.has(id));
  useEffect(() => { setSelected(new Set()); }, [activeAccountId]);

  const toggleAll = () => setSelected(allSelected ? new Set() : new Set(selectableIds));
  const toggleOne = (id: string) =>
    setSelected((prev) => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; });

  const bulkSetCategory = async (accountId: string) => {
    const acc = (accounts as any[]).find((a) => a.id === accountId);
    if (!acc || selected.size === 0) return;
    const ids = [...selected];
    const rows = (register as any[]).filter((r) => selected.has(r.id));

    // Accrual A/P: before categorizing a vendor debit to an expense account, check
    // whether it actually settles an open bill (matchOpenBills). A debit that
    // uniquely matches one is a bill payment — clear A/P (coa null, link the bill)
    // instead of re-hitting the expense, which was already booked on the bill.
    //   • exactly one match  → auto-apply to the bill
    //   • more than one match → leave for the user to resolve in Pay Bills (skip)
    //   • no match           → categorize as a direct expense (below)
    const billPayments: { id: string; bill: any; amount: number }[] = [];
    let ambiguous = 0;
    for (const r of rows) {
      if (r.type !== "debit" || !r.vendor_id) continue;
      const matches = await matchOpenBills({ companyId: cid, vendorId: r.vendor_id, amount: Number(r.amount), date: r.date });
      if (matches.length === 1) billPayments.push({ id: r.id, bill: matches[0], amount: Number(r.amount) });
      else if (matches.length > 1) ambiguous++;
    }
    const handled = new Set([...billPayments.map((p) => p.id)]);
    const categorizeIds = ids.filter((id) => !handled.has(id));

    if (categorizeIds.length) {
      const { error } = await accounting.from("transactions")
        .update({ coa_account_id: accountId, category: acc.name }).in("id", categorizeIds);
      if (error) return toast.error(error.message);
    }
    for (const p of billPayments) {
      const bal = Number(p.bill.total) - Number(p.bill.paid_amount ?? 0);
      const { error } = await accounting.from("transactions")
        .update({ coa_account_id: null, bill_id: p.bill.id, category: `Bill Payment · ${p.bill.number}` })
        .eq("id", p.id);
      if (error) return toast.error(error.message);
      await applyPaymentToBill(p.bill.id, Math.min(p.amount, bal));
    }

    const parts: string[] = [];
    if (categorizeIds.length) parts.push(`categorized ${categorizeIds.length}`);
    if (billPayments.length) parts.push(`applied ${billPayments.length} to open bills`);
    toast.success(parts.length ? parts.join(", ") : "No changes");
    if (ambiguous) toast.warning(`${ambiguous} debit${ambiguous !== 1 ? "s" : ""} match multiple open bills — resolve in Pay Bills`);
    qc.invalidateQueries({ queryKey: ["transactions", cid] });
    if (billPayments.length) qc.invalidateQueries({ queryKey: ["bills", cid] });
    setSelected(new Set());
  };

  const bulkSetDirection = async (type: "debit" | "credit") => {
    if (selected.size === 0) return;
    const ids = [...selected];
    const { error } = await accounting.from("transactions").update({ type }).in("id", ids);
    if (error) return toast.error(error.message);
    toast.success(`Set ${ids.length} transaction${ids.length !== 1 ? "s" : ""} to ${type === "credit" ? "deposit" : "payment"}`);
    qc.invalidateQueries({ queryKey: ["transactions", cid] });
    qc.invalidateQueries({ queryKey: ["accounts", cid] });
  };

  const saveTx = async () => {
    const { account_id, type, reference, date, coa_account_id, vendor_id, customer_id, memo } = txForm;
    const amount = Math.abs(Number(txForm.amount));

    if (!account_id) return toast.error("Bank account required");
    if (!amount || amount <= 0) return toast.error("Amount can't be zero");
    if (!coa_account_id) return toast.error(`${type === "credit" ? "Income" : "Expense"} account (COA) required`);
    if (type === "debit" && !vendor_id) return toast.error("Vendor required for payments");
    if (type === "credit" && !customer_id) return toast.error("Homeowner required for deposits");

    const coaName = (accounts as any[]).find((a) => a.id === coa_account_id)?.name ?? "";
    const vendorName = (vendors as any[]).find((v) => v.id === vendor_id)?.name ?? "";
    const customerName = (customers as any[]).find((c) => c.id === customer_id)?.name ?? "";
    const partyName = type === "debit" ? vendorName : customerName;
    const description = [partyName, coaName, memo].filter(Boolean).join(" · ");
    const category = coaName;

    // Vendor-payment recognition rule: count the expense for the bill when it is
    // entered (accrual), or — when no bill exists — when the payment is made.
    //   • Debit matches an OPEN bill (matchOpenBills: same vendor, amount within
    //     $0.01 or partial, date within ±30 days) → this payment clears Accounts
    //     Payable (coa null, vendor set → post_transaction_gl posts Dr A/P / Cr
    //     Bank); the expense was already recognized on the bill. We then apply it
    //     to the matched bill(s).
    //   • No matching bill → the payment IS the expense: keep the chosen expense
    //     account so it posts Dr Expense / Cr Bank on the payment date.
    // Customer deposits (credits) clear A/R via customer_id and are unchanged.
    const matchedBills = type === "debit" && vendor_id
      ? await matchOpenBills({ companyId: cid, vendorId: vendor_id, amount, date })
      : [];
    const debitClearsAp = matchedBills.length > 0;

    const payload: any = {
      account_id, date, description, amount, type, category, reference: reference || null,
      coa_account_id: debitClearsAp ? null : (coa_account_id || null),
      vendor_id: vendor_id || null,
      customer_id: customer_id || null,
      // Link the settled bill only when a single bill matches (the guard requires
      // coa null on any bill-linked row, which holds here). Multi-bill payments
      // stay unlinked but still clear A/P via the vendor branch.
      bill_id: matchedBills.length === 1 ? matchedBills[0].id : null,
    };

    if (editId) {
      const { error } = await accounting.from("transactions").update(payload).eq("id", editId);
      if (error) return toast.error(error.message);
      toast.success("Transaction updated");
    } else {
      const { data: inserted, error } = await accounting
        .from("transactions").insert({ ...payload, company_id: cid }).select("id").single();
      if (error || !inserted) return toast.error(error?.message ?? "Failed to record");
      toast.success(type === "credit" ? "Deposit recorded" : "Payment recorded");

      if (type === "debit" && txForm.printCheck) {
        const vendorRecord = (vendors as any[]).find((v) => v.id === vendor_id);
        await printCheckForPayment({
          vendorId: vendor_id,
          vendorName: vendorRecord?.name ?? "Payee",
          vendorAddress: vendorRecord?.address ?? undefined,
          amount,
          date,
          memo: memo || coaName,
          checkNumber: parseInt(txForm.reference) || (checkSettings as any)?.next_check_number || 1001,
          bankAccountId: account_id,
        });
      }

      // When the payment cleared A/P, apply it to the matched bill(s) oldest-first
      // so they show paid. The expense lives on the bill, so no expense is booked
      // here. With no matching bill the payment already posted the expense directly
      // (above) — nothing further to do.
      if (debitClearsAp) {
        let remaining = amount;
        for (const b of matchedBills) {
          if (remaining <= 0.005) break;
          const bal = Number(b.total) - Number(b.paid_amount ?? 0);
          const applied = Math.min(bal, remaining);
          await applyPaymentToBill(b.id, applied);
          remaining -= applied;
        }
        qc.invalidateQueries({ queryKey: ["bills", cid] });
      }
    }
    setTxDialog({ open: false, mode: "deposit" });
    setEditId(null);
    setTxForm(EMPTY_TX);
    qc.invalidateQueries({ queryKey: ["transactions", cid] });
    qc.invalidateQueries({ queryKey: ["accounts", cid] });
    qc.invalidateQueries({ queryKey: ["check-settings", cid] });
  };

  const printCheckForPayment = async ({
    vendorId, vendorName, vendorAddress, amount, date, memo, checkNumber, bankAccountId,
  }: {
    vendorId: string; vendorName: string; vendorAddress?: string;
    amount: number; date: string; memo: string; checkNumber: number; bankAccountId: string;
  }) => {
    const cs = checkSettings as any;
    const bankAccount = (bankAccounts as any[]).find((a) => a.id === bankAccountId);

    await accounting.from("checks").insert({
      company_id: cid,
      bank_account_id: bankAccountId,
      check_number: checkNumber,
      date,
      payee_vendor_id: vendorId || null,
      payee_name: vendorName,
      amount,
      memo: memo || undefined,
      status: "printed",
      printed_at: new Date().toISOString(),
    });

    if (cs?.id) {
      await accounting.from("check_settings").update({
        next_check_number: Math.max((cs.next_check_number ?? 1001), checkNumber + 1),
      }).eq("id", cs.id);
    }

    const dataUrl = generateCheckPDF([{
      companyName: associationName ?? "Association",
      companyAddress: undefined,
      bankName: cs?.bank_name ?? bankAccount?.name ?? undefined,
      bankAddress: cs?.bank_address ?? undefined,
      routingNumber: cs?.routing_number ?? undefined,
      accountNumber: cs?.account_number ?? undefined,
      fractionalRouting: cs?.fractional_routing ?? undefined,
      checkNumber,
      date: fmtDate(date),
      payee: vendorName,
      payeeAddress: vendorAddress || undefined,
      amount,
      memo: memo || undefined,
      printSignature: cs?.print_signature ?? false,
      signatureDataUrl: cs?.signature_url ?? undefined,
    }], {
      style: cs?.default_style ?? "voucher",
      position: cs?.default_position ?? "top",
      fontSize: cs?.font_size ?? "medium",
      offsetX: cs?.offset_x ?? 0,
      offsetY: cs?.offset_y ?? 0,
      micrOffsetY: cs?.micr_offset_y ?? 0,
    });

    const w = window.open("");
    if (w) {
      w.document.write(`<iframe src="${dataUrl}" style="border:0;width:100%;height:100vh" onload="this.contentWindow.print()"></iframe>`);
    }
  };

  const deleteTx = async (tx: any) => {
    if (tx.reconciliation_id) return toast.error("Reconciled transactions can't be deleted");
    if (!confirm("Delete this transaction?")) return;

    if (tx.transfer_id) {
      const { error } = await accounting.from("transactions").delete().eq("transfer_id", tx.transfer_id);
      if (error) return toast.error(error.message);
    } else {
      const { error } = await accounting.from("transactions").delete().eq("id", tx.id);
      if (error) return toast.error(error.message);
    }
    toast.success("Transaction deleted");
    qc.invalidateQueries({ queryKey: ["transactions", cid] });
    qc.invalidateQueries({ queryKey: ["accounts", cid] });
  };

  const openEdit = (tx: any) => {
    if (tx.reconciliation_id) return toast.error("Reconciled transactions can't be edited");
    setEditId(tx.id);
    setTxForm({
      account_id: tx.account_id ?? activeAccountId,
      date: tx.date,
      amount: tx.type === "debit" ? String(-Math.abs(Number(tx.amount))) : String(tx.amount),
      type: tx.type,
      reference: tx.reference ?? "",
      coa_account_id: tx.coa_account_id ?? "",
      vendor_id: tx.vendor_id ?? "",
      customer_id: tx.customer_id ?? "",
      memo: "",
      printCheck: false,
    });
    setTxDialog({ open: true, mode: "edit" });
  };

  const openNewTx = () => {
    setEditId(null);
    setTxForm({ ...EMPTY_TX, account_id: activeAccountId, type: "credit" });
    setTxDialog({ open: true, mode: "deposit" });
  };

  const onAmountChange = (v: string) => {
    setTxForm((prev) => {
      const t = v.trim();
      const nextType: "credit" | "debit" =
        t === "" ? prev.type : (t.startsWith("-") || Number(v) < 0 ? "debit" : "credit");
      return nextType !== prev.type
        ? { ...prev, amount: v, type: nextType, coa_account_id: "", vendor_id: "", customer_id: "" }
        : { ...prev, amount: v };
    });
  };

  const coaOptions = txForm.type === "credit" ? incomeAccounts : expenseAccounts;

  const saveTransfer = async () => {
    const { from_account_id, to_account_id, date, memo } = transfer;
    const amount = Number(transfer.amount);
    if (!from_account_id || !to_account_id) return toast.error("Both accounts required");
    if (from_account_id === to_account_id) return toast.error("Accounts must be different");
    if (!amount || amount <= 0) return toast.error("Amount must be greater than zero");

    const transferId = crypto.randomUUID();
    const fromAcc = (accounts as any[]).find((a) => a.id === from_account_id);
    const toAcc = (accounts as any[]).find((a) => a.id === to_account_id);

    const { error } = await accounting.from("transactions").insert([
      {
        company_id: cid,
        account_id: from_account_id,
        date,
        description: `Transfer to ${toAcc?.name ?? "account"}${memo ? ` — ${memo}` : ""}`,
        amount,
        type: "debit",
        category: "Transfer",
        transfer_id: transferId,
      },
      {
        company_id: cid,
        account_id: to_account_id,
        date,
        description: `Transfer from ${fromAcc?.name ?? "account"}${memo ? ` — ${memo}` : ""}`,
        amount,
        type: "credit",
        category: "Transfer",
        transfer_id: transferId,
      },
    ]);
    if (error) return toast.error(error.message);
    toast.success("Transfer recorded");
    setTransferOpen(false);
    setTransfer(EMPTY_TRANSFER);
    qc.invalidateQueries({ queryKey: ["transactions", cid] });
    qc.invalidateQueries({ queryKey: ["accounts", cid] });
  };

  const downloadRegisterTemplate = () => {
    const csv =
      "date,description,amount,reference,category,cleared\n" +
      "2026-01-15,Opening deposit,1200.00,DEP-001,Income,true\n" +
      "2026-01-18,Landscaping invoice,-350.00,1042,Maintenance,false\n";
    const blob = new Blob([csv], { type: "text/csv" });
    const a = document.createElement("a");
    a.href = URL.createObjectURL(blob);
    a.download = "bank-register-template.csv";
    a.click();
    URL.revokeObjectURL(a.href);
  };

  const handleRegisterImport = async (file: File) => {
    if (!activeAccountId) { toast.error("Select a bank account first"); return; }
    setImporting(true);
    setImportResult(null);
    try {
      const rows = parseCsv(await file.text());
      if (rows.length === 0) { toast.error("CSV is empty or unreadable"); return; }

      let skipped = 0;
      const payload = rows
        .map((r) => {
          const dateRaw = pick(r.date, r.transaction_date, r["transaction date"], r.posted, r["post date"]);
          if (!dateRaw) { skipped++; return null; }
          const debit = parseFloat(pick(r.debit, r.withdrawal, r.withdrawals, r["debit amount"]) || "0") || 0;
          const credit = parseFloat(pick(r.credit, r.deposit, r.deposits, r["credit amount"]) || "0") || 0;
          const signed = parseFloat(pick(r.amount) || "0") || 0;
          let amount: number;
          let type: "debit" | "credit";
          if (debit > 0 || credit > 0) {
            amount = debit > 0 ? debit : credit;
            type = debit > 0 ? "debit" : "credit";
          } else if (signed !== 0) {
            amount = Math.abs(signed);
            const t = pick(r.type).toLowerCase();
            type = t === "debit" || t === "credit" ? (t as "debit" | "credit") : (signed < 0 ? "debit" : "credit");
          } else {
            skipped++; return null;
          }
          const clearedRaw = pick(r.cleared, r.reconciled, r.status).toLowerCase();
          return {
            company_id: cid,
            account_id: activeAccountId,
            date: parseDateStr(dateRaw),
            description: pick(r.description, r.payee, r.memo, r.name) || "Imported transaction",
            amount,
            type,
            category: pick(r.category) || null,
            reference: pick(r.reference, r["check number"], r["check #"], r.check, r.cheque) || null,
            cleared: ["true", "yes", "1", "cleared", "reconciled", "c"].includes(clearedRaw),
          };
        })
        .filter((x): x is NonNullable<typeof x> => x !== null);

      if (payload.length === 0) { toast.warning("No rows imported — check the column names against the template."); return; }

      let inserted = 0;
      for (let i = 0; i < payload.length; i += 500) {
        const chunk = payload.slice(i, i + 500);
        const { error, count } = await accounting.from("transactions").insert(chunk, { count: "exact" });
        if (error) { toast.error(error.message); break; }
        inserted += count ?? chunk.length;
      }
      setImportResult({ inserted, skipped });
      if (inserted > 0) {
        toast.success(`Imported ${inserted} transaction${inserted !== 1 ? "s" : ""}${skipped ? ` (${skipped} skipped)` : ""}`);
        qc.invalidateQueries({ queryKey: ["transactions", cid] });
        qc.invalidateQueries({ queryKey: ["accounts", cid] });
      }
    } catch (e) {
      toast.error(e instanceof Error ? e.message : "Import failed");
    } finally {
      setImporting(false);
      if (importFileRef.current) importFileRef.current.value = "";
    }
  };

  const saveAccount = async () => {
    if (!acctForm.name.trim()) return toast.error("Name required");
    const { error } = await accounting.from("accounts").insert({ ...acctForm, company_id: cid });
    if (error) return toast.error(error.message);
    toast.success("Account added");
    setAcctDialog(false);
    setAcctForm({ name: "", code: "", type: "asset", is_bank: true });
    qc.invalidateQueries({ queryKey: ["accounts", cid] });
  };

  const dialogTitle = editId
    ? "Edit transaction"
    : (txForm.type === "debit" ? "New payment" : "New transaction");

  const totalDebits = filteredRegister.reduce((s, r) => s + (r.type === "debit" ? Number(r.amount) : 0), 0);
  const totalCredits = filteredRegister.reduce((s, r) => s + (r.type === "credit" ? Number(r.amount) : 0), 0);

  if (!associationId) return <p className="text-sm text-muted-foreground">Select an association.</p>;
  if (companyLoading) return <div className="flex justify-center py-12"><Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /></div>;
  if (companyError || !companyId) return <p className="text-sm text-muted-foreground text-center py-12">{companyError || "Accounting setup is not ready."}</p>;

  return (
    <div className="space-y-6">
      <div className="flex items-center justify-between">
        <h1 className="text-2xl font-semibold">Banking</h1>
        <div className="flex gap-2">
          <Button variant="outline" size="sm" onClick={() => setAcctDialog(true)}>
            <Plus className="h-4 w-4 mr-1" /> New account
          </Button>
        </div>
      </div>

      <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
        {bankAccounts.map((acc: any) => {
          const conn = plaidByAcct.get(acc.id);
          const isSyncing = syncingAcctId === acc.id;
          return (
          <Card
            key={acc.id}
            className={`cursor-pointer transition-colors ${acc.id === activeAccountId ? "ring-2 ring-primary" : "hover:bg-muted/50"}`}
            onClick={() => setSelectedAccountId(acc.id)}
          >
            <CardContent className="p-4 space-y-2">
              <div className="flex items-start justify-between gap-2">
                <div>
                  <div className="text-xs text-muted-foreground uppercase tracking-wide">{acc.type} · bank</div>
                  <div className="font-medium mt-0.5">{acc.name}</div>
                  <div className="text-xl font-semibold mt-1">{money(acc.balance, cur)}</div>
                </div>
                {conn && (
                  <Badge className="bg-emerald-100 text-emerald-700 border-0 shrink-0 text-[10px]">
                    <Link2 className="h-2.5 w-2.5 mr-1" />Connected
                  </Badge>
                )}
              </div>
              {conn ? (
                <div className="flex gap-1.5 pt-1" onClick={(e) => e.stopPropagation()}>
                  <Button size="sm" variant="outline" className="h-7 text-xs flex-1"
                    disabled={isSyncing}
                    onClick={() => syncAccount(acc.id)}>
                    <RefreshCw className={`h-3 w-3 mr-1 ${isSyncing ? "animate-spin" : ""}`} />
                    {isSyncing ? "Syncing…" : conn.last_sync_at ? `Synced ${new Date(conn.last_sync_at).toLocaleDateString()}` : "Sync now"}
                  </Button>
                  <Button size="sm" variant="ghost" className="h-7 text-xs text-destructive hover:text-destructive"
                    onClick={() => disconnectAccount(acc.id)}>
                    <Unlink className="h-3 w-3" />
                  </Button>
                </div>
              ) : (
                <div onClick={(e) => e.stopPropagation()}>
                  <Button size="sm" variant="outline" className="h-7 text-xs w-full"
                    onClick={() => openPlaidLink(acc.id)}>
                    <Link2 className="h-3 w-3 mr-1" /> Connect bank feed
                  </Button>
                </div>
              )}
            </CardContent>
          </Card>
          );
        })}
        {bankAccounts.length === 0 && (
          <Card className="col-span-full">
            <CardContent className="p-8 text-center text-muted-foreground">
              No bank accounts yet. Add one to get started.
            </CardContent>
          </Card>
        )}
      </div>

      {activeAccount && (
        <Card>
          <CardHeader className="pb-3">
            <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
              <div>
                <CardTitle>{activeAccount.name} — Register</CardTitle>
                <div className="text-sm text-muted-foreground mt-0.5">
                  Computed balance: <span className="font-semibold text-foreground">{money(computedBalance, cur)}</span>
                </div>
              </div>
              <div className="flex flex-wrap gap-2">
                <Button size="sm" onClick={openNewTx}>
                  <Plus className="h-4 w-4 mr-1" /> New transaction
                </Button>
                <Button size="sm" variant="outline" onClick={() => { setTransfer(EMPTY_TRANSFER); setTransferOpen(true); }}>
                  <ArrowLeftRight className="h-4 w-4 mr-1" /> Transfer
                </Button>
                <Button size="sm" variant="outline" onClick={() => { setImportResult(null); setImportOpen(true); }}>
                  <FileUp className="h-4 w-4 mr-1" /> Import
                </Button>
              </div>
            </div>
            <Input
              placeholder="Search description, category, reference…"
              value={search}
              onChange={(e) => setSearch(e.target.value)}
              className="max-w-sm mt-2"
            />
          </CardHeader>
          <CardContent className="p-0">
            {selected.size > 0 && (
              <div className="flex flex-wrap items-center gap-3 border-b bg-muted/40 px-4 py-2.5">
                <span className="text-sm font-medium">{selected.size} selected</span>
                <div className="flex items-center gap-1.5">
                  <span className="text-xs text-muted-foreground">Category</span>
                  <Select value="" onValueChange={bulkSetCategory}>
                    <SelectTrigger className="h-8 w-56 text-sm"><SelectValue placeholder="Set category…" /></SelectTrigger>
                    <SelectContent>
                      {incomeAccounts.length > 0 && (
                        <SelectGroup>
                          <SelectLabel>Income</SelectLabel>
                          {incomeAccounts.map((a: any) => (
                            <SelectItem key={a.id} value={a.id}>{a.code ? `${a.code} · ` : ""}{a.name}</SelectItem>
                          ))}
                        </SelectGroup>
                      )}
                      {expenseAccounts.length > 0 && (
                        <SelectGroup>
                          <SelectLabel>Expense</SelectLabel>
                          {expenseAccounts.map((a: any) => (
                            <SelectItem key={a.id} value={a.id}>{a.code ? `${a.code} · ` : ""}{a.name}</SelectItem>
                          ))}
                        </SelectGroup>
                      )}
                    </SelectContent>
                  </Select>
                </div>
                <div className="flex items-center gap-1.5">
                  <span className="text-xs text-muted-foreground">Direction</span>
                  <Select value="" onValueChange={(v) => bulkSetDirection(v as "debit" | "credit")}>
                    <SelectTrigger className="h-8 w-44 text-sm"><SelectValue placeholder="Set direction…" /></SelectTrigger>
                    <SelectContent>
                      <SelectItem value="credit">Deposit (money in)</SelectItem>
                      <SelectItem value="debit">Payment (money out)</SelectItem>
                    </SelectContent>
                  </Select>
                </div>
                <Button size="sm" variant="ghost" className="ml-auto" onClick={() => setSelected(new Set())}>Clear</Button>
              </div>
            )}
            <div className="overflow-x-auto">
              <Table>
                <TableHeader>
                  <TableRow>
                    <TableHead className="w-[36px] px-2">
                      <Checkbox checked={allSelected} onCheckedChange={toggleAll} disabled={selectableIds.length === 0} aria-label="Select all" />
                    </TableHead>
                    <TableHead className="w-[100px]">Date</TableHead>
                    <TableHead className="w-[80px]">Ref #</TableHead>
                    <TableHead>Description</TableHead>
                    <TableHead>Category</TableHead>
                    <TableHead className="text-right w-[110px]">Payment</TableHead>
                    <TableHead className="text-right w-[110px]">Deposit</TableHead>
                    <TableHead className="text-right w-[120px]">Balance</TableHead>
                    <TableHead className="w-[80px]"></TableHead>
                  </TableRow>
                </TableHeader>
                <TableBody>
                  {filteredRegister.map((row) => (
                    <TableRow key={row.id} className={`hover:bg-muted/40 ${selected.has(row.id) ? "bg-primary/5" : ""}`}>
                      <TableCell className="px-2">
                        {!row.reconciliation_id && !row.transfer_id && (
                          <Checkbox checked={selected.has(row.id)} onCheckedChange={() => toggleOne(row.id)} aria-label="Select transaction" />
                        )}
                      </TableCell>
                      <TableCell className="text-sm">{fmtDate(row.date)}</TableCell>
                      <TableCell className="text-sm text-muted-foreground">{row.reference ?? "—"}</TableCell>
                      <TableCell className="text-sm">
                        <span className="flex items-center gap-1.5">
                          {row.transfer_id && (
                            <Badge variant="outline" className="text-[10px] py-0 px-1">TFR</Badge>
                          )}
                          {row.reconciliation_id && (
                            <Badge variant="outline" className="text-[10px] py-0 px-1 bg-emerald-50 text-emerald-700 border-emerald-200">R</Badge>
                          )}
                          {row.description}
                        </span>
                      </TableCell>
                      <TableCell className="text-sm text-muted-foreground">{row.category ?? "—"}</TableCell>
                      <TableCell className="text-right text-sm text-red-600">
                        {row.type === "debit" ? money(row.amount, cur) : ""}
                      </TableCell>
                      <TableCell className="text-right text-sm text-emerald-600">
                        {row.type === "credit" ? money(row.amount, cur) : ""}
                      </TableCell>
                      <TableCell className={`text-right text-sm font-medium ${row.running < 0 ? "text-destructive" : ""}`}>
                        {money(row.running, cur)}
                      </TableCell>
                      <TableCell>
                        <div className="flex justify-end gap-1">
                          <Button size="icon" variant="ghost" className="h-7 w-7" onClick={() => openEdit(row)}>
                            <Pencil className="h-3.5 w-3.5" />
                          </Button>
                          <Button size="icon" variant="ghost" className="h-7 w-7 text-destructive hover:text-destructive" onClick={() => deleteTx(row)}>
                            <Trash2 className="h-3.5 w-3.5" />
                          </Button>
                        </div>
                      </TableCell>
                    </TableRow>
                  ))}
                  {filteredRegister.length === 0 && (
                    <TableRow>
                      <TableCell colSpan={9} className="text-center text-muted-foreground py-8">
                        No transactions yet. Record a deposit or payment to get started.
                      </TableCell>
                    </TableRow>
                  )}
                  {filteredRegister.length > 0 && (
                    <TableRow className="bg-muted font-medium">
                      <TableCell colSpan={5} className="text-sm">Totals</TableCell>
                      <TableCell className="text-right text-sm text-red-600">{money(totalDebits, cur)}</TableCell>
                      <TableCell className="text-right text-sm text-emerald-600">{money(totalCredits, cur)}</TableCell>
                      <TableCell className={`text-right text-sm ${computedBalance < 0 ? "text-destructive" : ""}`}>
                        {money(computedBalance, cur)}
                      </TableCell>
                      <TableCell />
                    </TableRow>
                  )}
                </TableBody>
              </Table>
            </div>
          </CardContent>
        </Card>
      )}

      {/* Deposit / Payment / Edit dialog */}
      <Dialog open={txDialog.open} onOpenChange={(o) => { if (!o) { setTxDialog({ open: false, mode: "deposit" }); setEditId(null); setTxForm(EMPTY_TX); } else setTxDialog((d) => ({ ...d, open: true })); }}>
        <DialogContent className="max-w-lg">
          <DialogHeader><DialogTitle>{dialogTitle}</DialogTitle></DialogHeader>
          <div className="space-y-3">

            {txDialog.mode === "edit" && (
              <div>
                <Label>Bank account *</Label>
                <Select value={txForm.account_id} onValueChange={(v) => setTxForm({ ...txForm, account_id: v })}>
                  <SelectTrigger><SelectValue placeholder="Select" /></SelectTrigger>
                  <SelectContent>{bankAccounts.map((a: any) => <SelectItem key={a.id} value={a.id}>{a.name}</SelectItem>)}</SelectContent>
                </Select>
              </div>
            )}

            <div className="grid grid-cols-2 gap-3">
              <div>
                <Label>Date *</Label>
                <Input type="date" value={txForm.date} onChange={(e) => setTxForm({ ...txForm, date: e.target.value })} />
              </div>
              <div>
                <Label>Amount <span className="text-muted-foreground text-xs">(+ deposit / − payment)</span> *</Label>
                <Input
                  type="number"
                  step="0.01"
                  placeholder="0.00"
                  className={Number(txForm.amount) < 0 ? "text-red-600" : ""}
                  value={txForm.amount}
                  onChange={(e) => onAmountChange(e.target.value)}
                />
              </div>
            </div>

            {txForm.type === "debit" ? (
              <div>
                <Label>Vendor *</Label>
                <Select value={txForm.vendor_id} onValueChange={(v) => setTxForm({ ...txForm, vendor_id: v })}>
                  <SelectTrigger><SelectValue placeholder="Select vendor" /></SelectTrigger>
                  <SelectContent>
                    {(vendors as any[]).map((v: any) => (
                      <SelectItem key={v.id} value={v.id}>{v.name}</SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              </div>
            ) : (
              <div>
                <Label>Homeowner *</Label>
                <Select value={txForm.customer_id} onValueChange={(v) => setTxForm({ ...txForm, customer_id: v })}>
                  <SelectTrigger><SelectValue placeholder="Select homeowner" /></SelectTrigger>
                  <SelectContent>
                    {(customers as any[]).map((c: any) => (
                      <SelectItem key={c.id} value={c.id}>{c.name}</SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              </div>
            )}

            <div>
              <Label>{txForm.type === "credit" ? "Income account *" : "Expense account *"}</Label>
              <Select value={txForm.coa_account_id} onValueChange={(v) => setTxForm({ ...txForm, coa_account_id: v })}>
                <SelectTrigger><SelectValue placeholder={`Select ${txForm.type === "credit" ? "income" : "expense"} account`} /></SelectTrigger>
                <SelectContent>
                  {coaOptions.map((a: any) => (
                    <SelectItem key={a.id} value={a.id}>
                      {a.code ? `${a.code} · ` : ""}{a.name}
                    </SelectItem>
                  ))}
                  {coaOptions.length === 0 && (
                    <SelectItem value="__none" disabled>
                      No {txForm.type === "credit" ? "income" : "expense"} accounts — add them in Chart of Accounts
                    </SelectItem>
                  )}
                </SelectContent>
              </Select>
            </div>

            <div className="grid grid-cols-2 gap-3">
              <div>
                <Label>{txForm.type === "debit" ? "Check # *" : "Ref #"}</Label>
                <Input
                  maxLength={40}
                  placeholder={txForm.type === "debit" ? "Check number" : "Optional"}
                  value={txForm.reference}
                  onChange={(e) => setTxForm({ ...txForm, reference: e.target.value })}
                  className="font-mono"
                />
              </div>
              <div>
                <Label>Memo <span className="text-muted-foreground text-xs">(optional)</span></Label>
                <Input maxLength={120} placeholder="Additional notes" value={txForm.memo} onChange={(e) => setTxForm({ ...txForm, memo: e.target.value })} />
              </div>
            </div>

            {txForm.type === "debit" && !editId && (
              <div className="flex items-center justify-between rounded-md border bg-muted/30 px-4 py-3">
                <div className="flex items-center gap-2">
                  <Printer className="h-4 w-4 text-muted-foreground" />
                  <div>
                    <div className="text-sm font-medium">Print check after saving</div>
                    <div className="text-xs text-muted-foreground">Opens print dialog with check #{txForm.reference || "—"}</div>
                  </div>
                </div>
                <Switch
                  checked={txForm.printCheck}
                  onCheckedChange={(v) => setTxForm({ ...txForm, printCheck: v })}
                />
              </div>
            )}
          </div>
          <DialogFooter>
            <Button variant="outline" onClick={() => { setTxDialog({ open: false, mode: "deposit" }); setEditId(null); setTxForm(EMPTY_TX); }}>Cancel</Button>
            <Button onClick={saveTx}>
              {txForm.type === "debit" && txForm.printCheck && !editId ? "Save & Print Check" : "Save"}
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>

      {/* Transfer dialog */}
      <Dialog open={transferOpen} onOpenChange={setTransferOpen}>
        <DialogContent>
          <DialogHeader><DialogTitle>Transfer between accounts</DialogTitle></DialogHeader>
          <div className="space-y-3">
            <div>
              <Label>From account</Label>
              <Select value={transfer.from_account_id} onValueChange={(v) => setTransfer({ ...transfer, from_account_id: v })}>
                <SelectTrigger><SelectValue placeholder="Select account" /></SelectTrigger>
                <SelectContent>{bankAccounts.map((a: any) => <SelectItem key={a.id} value={a.id}>{a.name}</SelectItem>)}</SelectContent>
              </Select>
            </div>
            <div>
              <Label>To account</Label>
              <Select value={transfer.to_account_id} onValueChange={(v) => setTransfer({ ...transfer, to_account_id: v })}>
                <SelectTrigger><SelectValue placeholder="Select account" /></SelectTrigger>
                <SelectContent>{bankAccounts.map((a: any) => <SelectItem key={a.id} value={a.id}>{a.name}</SelectItem>)}</SelectContent>
              </Select>
            </div>
            <div className="grid grid-cols-2 gap-3">
              <div>
                <Label>Date</Label>
                <Input type="date" value={transfer.date} onChange={(e) => setTransfer({ ...transfer, date: e.target.value })} />
              </div>
              <div>
                <Label>Amount</Label>
                <Input type="number" min={0} step="0.01" placeholder="0.00" value={transfer.amount} onChange={(e) => setTransfer({ ...transfer, amount: e.target.value })} />
              </div>
            </div>
            <div>
              <Label>Memo</Label>
              <Input maxLength={200} value={transfer.memo} onChange={(e) => setTransfer({ ...transfer, memo: e.target.value })} />
            </div>
          </div>
          <DialogFooter>
            <Button variant="outline" onClick={() => setTransferOpen(false)}>Cancel</Button>
            <Button onClick={saveTransfer}>Record transfer</Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>

      {/* Bank register import dialog */}
      <Dialog open={importOpen} onOpenChange={(o) => { setImportOpen(o); if (!o) setImportResult(null); }}>
        <DialogContent>
          <DialogHeader><DialogTitle>Import bank register</DialogTitle></DialogHeader>
          <div className="space-y-4">
            <p className="text-sm text-muted-foreground">
              Import transactions into <span className="font-medium text-foreground">{activeAccount?.name ?? "this account"}</span>.
              Columns: <code className="text-xs">date, description, amount, reference, category, cleared</code>.
              Use a <span className="font-medium text-foreground">signed amount</span> — <code className="text-xs">+</code> for
              money in, <code className="text-xs">−</code> for money out.
              Imported rows are uncategorized — set the offset account from the register afterward.
            </p>
            <div className="flex flex-wrap gap-2">
              <Button variant="outline" size="sm" onClick={downloadRegisterTemplate}>
                <Download className="h-4 w-4 mr-1" /> Download template
              </Button>
              <Button size="sm" disabled={importing || !activeAccountId} onClick={() => importFileRef.current?.click()}>
                {importing ? <Loader2 className="h-4 w-4 mr-1 animate-spin" /> : <FileUp className="h-4 w-4 mr-1" />}
                {importing ? "Importing…" : "Choose CSV file"}
              </Button>
              <input
                ref={importFileRef}
                type="file"
                accept=".csv,text/csv"
                className="hidden"
                onChange={(e) => { const f = e.target.files?.[0]; if (f) handleRegisterImport(f); }}
              />
            </div>
            {importResult && (
              <div className="rounded-md border bg-muted/40 px-3 py-2 text-sm">
                Imported <span className="font-semibold">{importResult.inserted}</span> transaction{importResult.inserted !== 1 ? "s" : ""}
                {importResult.skipped > 0 && <> · {importResult.skipped} skipped (missing date/amount)</>}.
              </div>
            )}
          </div>
          <DialogFooter>
            <Button variant="outline" onClick={() => setImportOpen(false)}>Close</Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>

      {/* New account dialog */}
      <Dialog open={acctDialog} onOpenChange={setAcctDialog}>
        <DialogContent>
          <DialogHeader><DialogTitle>New bank account</DialogTitle></DialogHeader>
          <div className="space-y-3">
            <div><Label>Name</Label><Input maxLength={80} value={acctForm.name} onChange={(e) => setAcctForm({ ...acctForm, name: e.target.value })} placeholder="e.g. Operating Checking" /></div>
            <div><Label>Code</Label><Input maxLength={20} value={acctForm.code} onChange={(e) => setAcctForm({ ...acctForm, code: e.target.value })} placeholder="e.g. 1010" /></div>
            <div>
              <Label>Account type</Label>
              <Select value={acctForm.type} onValueChange={(v) => setAcctForm({ ...acctForm, type: v as any })}>
                <SelectTrigger><SelectValue /></SelectTrigger>
                <SelectContent>
                  {["asset", "liability", "equity", "income", "expense"].map((x) => (
                    <SelectItem key={x} value={x}>{x}</SelectItem>
                  ))}
                </SelectContent>
              </Select>
            </div>
          </div>
          <DialogFooter><Button onClick={saveAccount}>Add account</Button></DialogFooter>
        </DialogContent>
      </Dialog>

      {/* Plaid Link — mounts only when a link token is ready */}
      {plaidLinkToken && (
        <PlaidLinkButton
          linkToken={plaidLinkToken}
          accountId={plaidTargetAcct}
          companyId={cid}
          onDone={() => {
            setPlaidLinkToken(null);
            qc.invalidateQueries({ queryKey: ["plaid-connections", cid] });
          }}
        />
      )}

    </div>
  );
}

function PlaidLinkButton({
  linkToken, accountId, companyId, onDone,
}: {
  linkToken: string;
  accountId: string;
  companyId: string;
  onDone: () => void;
}) {
  const { open, ready } = usePlaidLink({
    token: linkToken,
    onSuccess: async (publicToken, metadata) => {
      try {
        const account = metadata.accounts?.[0];
        await exchangePlaidToken({
          publicToken,
          companyId,
          accountId,
          plaidAccountId: account?.id ?? "",
          institutionName: metadata.institution?.name ?? undefined,
          institutionId:   metadata.institution?.institution_id ?? undefined,
          mask:            account?.mask ?? undefined,
        });
        toast.success(`Connected to ${metadata.institution?.name ?? "bank"} — click Sync now to import transactions`);
      } catch (e: any) {
        toast.error(e?.message ?? "Connection failed");
      }
      onDone();
    },
    onExit: (err) => {
      if (err) toast.error(`Plaid: ${err.display_message ?? err.error_message ?? "Closed"}`);
      onDone();
    },
  });

  useEffect(() => {
    if (ready) open();
  }, [ready, open]);

  return null;
}
