"use client";

import Link from "next/link";
import { useEffect, useMemo, useState } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/ui/empty-state";
import { Input } from "@/components/ui/input";
import { Kpi as KpiCard, KpiStrip } from "@/components/ui/kpi";
import { useLocalStorage } from "@/lib/hooks/use-local-storage";
import { ALL_ORGANIZATIONS } from "@/lib/sag/entities";
import { toastNameToSlug } from "@/lib/sag/toast-csv";
import { formatCurrency, cn } from "@/lib/utils";
import type { ToastSalesRow } from "@/lib/sag/types";
import {
  matchToastDeposits,
  toastDepositId,
  type PlaidCredit,
  type ReconcileMatch,
  type ToastDeposit,
} from "@/lib/finance/match-toast-deposits";

/**
 * F8 — Toast deposits ↔ Truist credits reconciliation.
 *
 * Two data feeds:
 *
 *   1. Toast daily settles — derived from `sag.toast.imports` (the
 *      uploader stores each Toast Sales-Category CSV as a `SavedImport`
 *      with `uploadedAt` + the parsed `ToastSalesRow[]`). Toast's standard
 *      export bundles per-category totals for the *upload day*; Toast then
 *      typically settles that day's net sales to the bank on T+1. We model
 *      each rollup row in each saved import as one synthetic daily settle
 *      dated `uploadedAt - 1 day` (the "yesterday's sales were uploaded
 *      today" assumption) and entity-tagged via `toastNameToSlug`. When
 *      Toast's true per-day export schema lands, this same component swaps
 *      its derivation without changing the matcher contract.
 *
 *   2. Plaid credits — fetched on demand against every linked Plaid item.
 *      `/api/plaid/transactions` returns Plaid's convention (positive =
 *      outflow). We filter to `amount < 0` and flip the sign before
 *      handing the rows to the matcher.
 *
 * Persisted state:
 *   - `sag.finance.toast_reconcile_resolved` — array of Toast settle ids
 *     Glenn has manually marked resolved; excluded from the discrepancy
 *     KPI and visibly tagged in the table.
 */

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

interface PlaidAccount {
  accountId: string;
  itemId: string;
  accessToken?: string;
  name: string;
  mask?: string;
  balance?: number | null;
  entitySlug?: string;
}

interface PlaidApiTxn {
  transactionId: string;
  accountId: string;
  date: string;
  amount: number;
  merchantName: string;
  category?: string;
  pending: boolean;
  currency?: string;
}

const RESOLVED_KEY = "sag.finance.toast_reconcile_resolved";

function nDaysAgoIso(n: number): string {
  const d = new Date();
  d.setUTCDate(d.getUTCDate() - n);
  return d.toISOString().slice(0, 10);
}

function addDaysIso(iso: string, n: number): string {
  const d = new Date(`${iso}T00:00:00Z`);
  d.setUTCDate(d.getUTCDate() + n);
  return d.toISOString().slice(0, 10);
}

/**
 * Build a synthetic `ToastDeposit` per (upload × entity row). Toast's
 * Sales-Category CSV exports one rollup row per entity per upload; we use
 * `uploadedAt - 1d` as the settle date and the row's `netSales` as the
 * expected Truist credit (the standard Toast Capital settlement figure).
 */
function deriveToastDeposits(imports: ToastSavedImport[]): ToastDeposit[] {
  const out: ToastDeposit[] = [];
  for (const imp of imports) {
    const settleDate = addDaysIso(imp.uploadedAt.slice(0, 10), -1);
    for (const row of imp.rows) {
      // Skip Toast's duplicate "No Dining Option" sibling row — that's the
      // same numbers as the blank-dining rollup, just emitted twice.
      if (row.diningOption === "No Dining Option") continue;
      const slug = toastNameToSlug(row.salesCategory);
      if (!slug) continue; // can't reconcile what we can't attribute.
      if (row.netSales <= 0) continue; // refund-heavy days produce $0 settles.
      out.push({
        date: settleDate,
        entitySlug: slug,
        grossSales: row.grossSales,
        netDeposit: row.netSales,
      });
    }
  }
  return out;
}

export function ToastReconcile() {
  const [toastImports] = useLocalStorage<ToastSavedImport[]>("sag.toast.imports", []);
  const [accounts] = useLocalStorage<PlaidAccount[]>("sag.plaid.accounts", []);
  const [accessTokens] = useLocalStorage<Record<string, string>>(
    "sag.plaid.accessTokens",
    {}
  );
  const [resolved, setResolved] = useLocalStorage<string[]>(RESOLVED_KEY, []);

  const [startDate, setStartDate] = useState<string>(() => nDaysAgoIso(30));
  const [endDate, setEndDate] = useState<string>(() => nDaysAgoIso(0));
  const [entityFilter, setEntityFilter] = useState<string>("all");
  const [expandedId, setExpandedId] = useState<string | null>(null);
  const [plaidCredits, setPlaidCredits] = useState<PlaidCredit[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string>("");
  const [fetchedAt, setFetchedAt] = useState<string | null>(null);

  const allToastDeposits = useMemo(() => deriveToastDeposits(toastImports), [toastImports]);

  const tokens = useMemo(() => {
    return Array.from(
      new Set(
        accounts
          .map((a) => accessTokens[a.itemId] ?? a.accessToken)
          .filter((t): t is string => !!t)
      )
    );
  }, [accounts, accessTokens]);

  /** Plaid window covers the matching window: Toast date+1 ... +3, so we
   *  extend the user's end-date by +3 days when querying Plaid. */
  const plaidWindowEnd = useMemo(() => addDaysIso(endDate, 3), [endDate]);

  // Entities that actually have Toast data in `sag.toast.imports`. Drives
  // the entity filter so we don't surface SAG entities Toast never sees
  // (e.g. 1NC Blockchain).
  const entityOptions = useMemo(() => {
    const slugs = new Set(allToastDeposits.map((d) => d.entitySlug));
    return ALL_ORGANIZATIONS.filter((o) => slugs.has(o.slug));
  }, [allToastDeposits]);

  // Toast rows in the chosen window + entity filter.
  const toastInWindow = useMemo(() => {
    return allToastDeposits.filter((d) => {
      if (d.date < startDate || d.date > endDate) return false;
      if (entityFilter !== "all" && d.entitySlug !== entityFilter) return false;
      return true;
    });
  }, [allToastDeposits, startDate, endDate, entityFilter]);

  async function fetchPlaid() {
    if (tokens.length === 0) {
      setError("No Plaid accounts linked yet — connect Truist in /app/finance first.");
      return;
    }
    setLoading(true);
    setError("");
    try {
      const all: PlaidCredit[] = [];
      for (const token of tokens) {
        const resp = await fetch("/api/plaid/transactions", {
          method: "POST",
          headers: { "content-type": "application/json" },
          body: JSON.stringify({
            accessToken: token,
            startDate: addDaysIso(startDate, 1),
            endDate: plaidWindowEnd,
          }),
        });
        const data = (await resp.json()) as { transactions?: PlaidApiTxn[]; error?: string };
        if (!resp.ok) throw new Error(data.error || "Plaid request failed");
        for (const t of data.transactions ?? []) {
          // Plaid: positive = outflow, negative = inflow. We only want
          // credits (inflows) and we store them as positive numbers so the
          // matcher / UI can read them naturally.
          if (t.amount >= 0) continue;
          all.push({
            id: t.transactionId,
            date: t.date,
            amount: Math.abs(t.amount),
            name: t.merchantName,
            accountId: t.accountId,
          });
        }
      }
      setPlaidCredits(all);
      setFetchedAt(new Date().toISOString());
    } catch (e) {
      setError(e instanceof Error ? e.message : "Failed to load bank data");
    } finally {
      setLoading(false);
    }
  }

  // Auto-fetch once on mount if we have both Toast data and Plaid tokens.
  // Deferred to the next tick so the synchronous-setState-in-effect lint
  // rule stays happy — same shape as `<PlaidTransactions />`.
  useEffect(() => {
    if (allToastDeposits.length === 0) return;
    if (tokens.length === 0) return;
    if (fetchedAt !== null) return;
    const t = setTimeout(() => {
      void fetchPlaid();
    }, 0);
    return () => clearTimeout(t);
    // eslint-disable-next-line react-hooks/exhaustive-deps -- one-shot on first mount
  }, [allToastDeposits.length, tokens.length]);

  const matches = useMemo(
    () => matchToastDeposits(toastInWindow, plaidCredits),
    [toastInWindow, plaidCredits]
  );

  const resolvedSet = useMemo(() => new Set(resolved), [resolved]);

  // KPI strip: counts respect the "resolved" exclusion for discrepancies.
  const kpis = useMemo(() => {
    let matched = 0;
    let discrepancy = 0;
    let missing = 0;
    let duplicate = 0;
    let totalNet = 0;
    let resolvedCount = 0;
    for (const m of matches) {
      totalNet += m.toast.netDeposit;
      const id = toastDepositId(m.toast);
      const isResolved = resolvedSet.has(id);
      if (m.status === "matched") {
        matched++;
        continue;
      }
      if (isResolved) {
        resolvedCount++;
        continue;
      }
      if (m.status === "discrepancy") discrepancy++;
      else if (m.status === "missing") missing++;
      else if (m.status === "duplicate") duplicate++;
    }
    return { matched, discrepancy, missing, duplicate, totalNet, resolvedCount };
  }, [matches, resolvedSet]);

  function toggleResolved(id: string) {
    if (resolvedSet.has(id)) {
      setResolved(resolved.filter((r) => r !== id));
    } else {
      setResolved([...resolved, id]);
    }
  }

  // Empty state: no Toast data uploaded yet.
  if (toastImports.length === 0 || allToastDeposits.length === 0) {
    return (
      <EmptyState
        icon="🧾"
        title="No Toast data to reconcile yet"
        description={
          <>
            Upload at least one Toast Sales-Category export so this page has daily
            settle rows to line up against your Truist credits. The reconciler reads
            from <code className="text-foreground">sag.toast.imports</code> and pulls
            bank credits live from any linked Plaid item.
          </>
        }
        action={
          <Button asChild variant="brand" size="sm">
            <Link href="/app/toast">Open Toast uploader →</Link>
          </Button>
        }
        className="max-w-3xl"
      />
    );
  }

  const noPlaidTokens = tokens.length === 0;

  return (
    <div className="space-y-6 max-w-6xl">
      {/* Filter bar */}
      <Card>
        <CardContent className="p-5">
          <div className="flex flex-wrap items-end gap-4">
            <div className="flex flex-col gap-1">
              <label className="section-label">
                Start date
              </label>
              <Input
                type="date"
                value={startDate}
                onChange={(e) => setStartDate(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={endDate}
                onChange={(e) => setEndDate(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-[14rem]"
              >
                <option value="all">All Toast entities</option>
                {entityOptions.map((o) => (
                  <option key={o.slug} value={o.slug}>
                    {o.emoji} {o.name}
                  </option>
                ))}
              </select>
            </div>
            <div className="ml-auto flex items-end gap-2">
              <Button
                variant="brand"
                size="sm"
                disabled={loading || noPlaidTokens}
                onClick={fetchPlaid}
              >
                {loading ? "Loading…" : fetchedAt ? "↻ Refresh bank data" : "Pull bank credits"}
              </Button>
            </div>
          </div>
          {fetchedAt && (
            <p className="mt-3 text-[11px] text-muted-foreground">
              Bank data fetched {new Date(fetchedAt).toLocaleString()} ·{" "}
              {plaidCredits.length} credit{plaidCredits.length === 1 ? "" : "s"} in
              window {addDaysIso(startDate, 1)} → {plaidWindowEnd}.
            </p>
          )}
        </CardContent>
      </Card>

      {/* No-Plaid empty state */}
      {noPlaidTokens && (
        <EmptyState
          icon="🔌"
          size="compact"
          title="Connect Truist via Plaid to pull credits"
          description={
            <>
              This page needs a linked Plaid item so it can pull Truist credits in the
              matching window. Without one, the table below will show every Toast settle
              as <em>missing</em>.
            </>
          }
          action={
            <Button asChild variant="brand" size="sm">
              <Link href="/app/finance">Link bank →</Link>
            </Button>
          }
        />
      )}

      {error && (
        <Card className="border-destructive/40 bg-destructive/5">
          <CardContent className="p-4 text-xs text-destructive">{error}</CardContent>
        </Card>
      )}

      {/* KPI strip */}
      <KpiStrip cols={4}>
        <KpiCard label="Matched" value={`${kpis.matched}`} tone="emerald" hint="Within ±$5 of Toast net" />
        <KpiCard
          label="Discrepancies"
          value={`${kpis.discrepancy}`}
          tone={kpis.discrepancy > 0 ? "amber" : "neutral"}
          hint={
            kpis.resolvedCount > 0
              ? `${kpis.resolvedCount} resolved (hidden)`
              : "Bank credit > $5 off Toast"
          }
        />
        <KpiCard
          label="Missing"
          value={`${kpis.missing + kpis.duplicate}`}
          tone={kpis.missing + kpis.duplicate > 0 ? "rose" : "neutral"}
          hint={
            kpis.duplicate > 0
              ? `${kpis.duplicate} duplicate · ${kpis.missing} not landed`
              : "No bank credit found in window"
          }
        />
        <KpiCard
          label="Toast net (period)"
          value={formatCurrency(kpis.totalNet)}
          hint={`${matches.length} settle${matches.length === 1 ? "" : "s"} in window`}
        />
      </KpiStrip>

      {/* Match table */}
      <Card>
        <CardContent className="p-5">
          <div className="flex items-center justify-between mb-3">
            <div>
              <h2 className="text-sm font-semibold">Settle ↔ credit pairing</h2>
              <p className="text-[11px] text-muted-foreground">
                Click any row to see the full Plaid transaction. Resolved discrepancies
                stay visible but stop counting.
              </p>
            </div>
            <Badge variant="outline" className="text-[10px]">
              {matches.length} row{matches.length === 1 ? "" : "s"}
            </Badge>
          </div>

          {matches.length === 0 ? (
            <p className="py-8 text-center text-xs text-muted-foreground">
              No Toast settles in this date window. Widen the range or pick a different entity.
            </p>
          ) : (
            <div className="overflow-x-auto">
              <table className="w-full min-w-[720px] text-xs">
                <thead className="section-label border-b">
                  <tr>
                    <th className="py-2 pr-3 text-left font-medium">Toast date</th>
                    <th className="py-2 px-2 text-left font-medium">Entity</th>
                    <th className="py-2 px-2 text-right font-medium">Gross</th>
                    <th className="py-2 px-2 text-right font-medium">Net</th>
                    <th className="py-2 px-2 text-left font-medium">Plaid date</th>
                    <th className="py-2 px-2 text-right font-medium">Plaid amt</th>
                    <th className="py-2 px-2 text-right font-medium">Δ</th>
                    <th className="py-2 px-2 text-left font-medium">Status</th>
                  </tr>
                </thead>
                <tbody>
                  {matches.map((m) => {
                    const id = toastDepositId(m.toast);
                    const org = ALL_ORGANIZATIONS.find((o) => o.slug === m.toast.entitySlug);
                    const isResolved = resolvedSet.has(id);
                    const isOpen = expandedId === id;
                    return (
                      <RowGroup
                        key={id}
                        id={id}
                        match={m}
                        org={org}
                        isResolved={isResolved}
                        isOpen={isOpen}
                        onToggleOpen={() => setExpandedId(isOpen ? null : id)}
                        onToggleResolved={() => toggleResolved(id)}
                        accountLabel={(() => {
                          if (!m.plaid) return null;
                          const acc = accounts.find((a) => a.accountId === m.plaid?.accountId);
                          if (!acc) return null;
                          return acc.mask ? `${acc.name} ····${acc.mask}` : acc.name;
                        })()}
                      />
                    );
                  })}
                </tbody>
              </table>
            </div>
          )}
        </CardContent>
      </Card>

      {/* Methodology footnote */}
      <Card className="border-dashed">
        <CardContent className="p-4 text-[11px] text-muted-foreground space-y-1.5">
          <div>
            <strong className="text-foreground">Deposit window —</strong> Toast Capital
            typically settles a day&apos;s sales to the bank between <strong>T+1 and T+3</strong>
            (T+1 on weekdays, T+2 over weekends, T+3 around bank holidays). A bank credit
            qualifies if it lands inside that window AND either its merchant name matches{" "}
            <code className="text-foreground">/toast|tst.*deposit|tst.*settle/i</code> or the
            amount is within ±$5 of Toast&apos;s expected net.
          </div>
          <div>
            <strong className="text-foreground">Toast settle dates —</strong> The Sales-Category
            CSVs Glenn uploads don&apos;t carry a per-row settle date. We treat each upload as
            covering the day before it landed (the standard &ldquo;yesterday&apos;s sales,
            uploaded today&rdquo; cadence). Once Toast&apos;s day-grain export is wired, this
            page will use the real settle date without changing the matcher.
          </div>
          <div>
            <strong className="text-foreground">Sources —</strong>{" "}
            <code className="text-foreground">sag.toast.imports</code> ·{" "}
            <code className="text-foreground">sag.plaid.accounts</code> ·{" "}
            <code className="text-foreground">sag.plaid.accessTokens</code> · Plaid credits
            fetched live via <code className="text-foreground">/api/plaid/transactions</code>.
            Resolved discrepancies persist in{" "}
            <code className="text-foreground">{RESOLVED_KEY}</code>.
          </div>
        </CardContent>
      </Card>
    </div>
  );
}

// ──────────────────────────────────────────────────────────────────────────
// Sub-components
// ──────────────────────────────────────────────────────────────────────────

function RowGroup({
  id,
  match,
  org,
  isResolved,
  isOpen,
  onToggleOpen,
  onToggleResolved,
  accountLabel,
}: {
  id: string;
  match: ReconcileMatch;
  org: (typeof ALL_ORGANIZATIONS)[number] | undefined;
  isResolved: boolean;
  isOpen: boolean;
  onToggleOpen: () => void;
  onToggleResolved: () => void;
  accountLabel: string | null;
}) {
  const absDelta = Math.abs(match.delta);
  const isFlaggedStatus =
    match.status === "discrepancy" || match.status === "missing" || match.status === "duplicate";
  return (
    <>
      <tr
        className={cn(
          "border-b last:border-0 align-middle cursor-pointer hover:bg-muted/30",
          isResolved && "opacity-60"
        )}
        onClick={onToggleOpen}
        aria-expanded={isOpen}
      >
        <td className="py-2 pr-3 font-mono text-[11px] tabular-nums whitespace-nowrap">
          {match.toast.date}
        </td>
        <td className="py-2 px-2">
          <span className="inline-flex items-center gap-1.5 truncate">
            <span aria-hidden>{org?.emoji ?? "🏢"}</span>
            <span className="truncate">{org?.name ?? match.toast.entitySlug}</span>
          </span>
        </td>
        <td className="py-2 px-2 text-right tabular-nums text-muted-foreground">
          {formatCurrency(match.toast.grossSales)}
        </td>
        <td className="py-2 px-2 text-right tabular-nums font-medium">
          {formatCurrency(match.toast.netDeposit)}
        </td>
        <td className="py-2 px-2 font-mono text-[11px] tabular-nums whitespace-nowrap">
          {match.plaid ? match.plaid.date : "—"}
        </td>
        <td className="py-2 px-2 text-right tabular-nums">
          {match.plaid ? formatCurrency(match.plaid.amount) : "—"}
        </td>
        <td
          className={cn(
            "py-2 px-2 text-right tabular-nums",
            absDelta > 5
              ? "text-destructive font-medium"
              : match.plaid
                ? "text-muted-foreground"
                : "text-destructive"
          )}
        >
          {match.plaid ? formatCurrency(match.delta) : "—"}
        </td>
        <td className="py-2 px-2">
          <StatusBadge status={match.status} resolved={isResolved} />
        </td>
      </tr>
      {isOpen && (
        <tr className="bg-muted/20 border-b">
          <td colSpan={8} className="px-4 py-3 text-[11px] space-y-2">
            {match.notes && (
              <p className="text-muted-foreground">
                <strong className="text-foreground">Why this status —</strong> {match.notes}
              </p>
            )}
            {match.plaid ? (
              <div className="grid grid-cols-2 md:grid-cols-4 gap-2">
                <Detail label="Plaid id" value={match.plaid.id} mono />
                <Detail label="Account" value={accountLabel ?? match.plaid.accountId} />
                <Detail label="Merchant" value={match.plaid.name} />
                <Detail label="Posted" value={match.plaid.date} mono />
              </div>
            ) : (
              <p className="text-muted-foreground italic">
                No Plaid credit identified yet in the T+1 to T+3 window.
              </p>
            )}
            {isFlaggedStatus && (
              <div className="pt-1">
                <Button
                  variant={isResolved ? "outline" : "brand"}
                  size="sm"
                  onClick={(e) => {
                    e.stopPropagation();
                    onToggleResolved();
                  }}
                >
                  {isResolved ? "↩ Unmark resolved" : "✓ Mark resolved"}
                </Button>
                <span className="ml-2 text-[10px] text-muted-foreground">
                  Resolved rows stay in the table but drop out of the discrepancy
                  count. Stored in{" "}
                  <code className="text-foreground">{RESOLVED_KEY}</code>.
                </span>
              </div>
            )}
            <p className="text-[10px] text-muted-foreground tabular-nums">
              Row id: <code className="text-foreground">{id}</code>
            </p>
          </td>
        </tr>
      )}
    </>
  );
}

function StatusBadge({
  status,
  resolved,
}: {
  status: ReconcileMatch["status"];
  resolved: boolean;
}) {
  if (resolved) {
    return (
      <Badge variant="secondary" className="text-[10px]">
        resolved
      </Badge>
    );
  }
  if (status === "matched") {
    return (
      <Badge variant="success" className="text-[10px]">
        matched
      </Badge>
    );
  }
  if (status === "discrepancy") {
    return (
      <Badge variant="warning" className="text-[10px]">
        discrepancy
      </Badge>
    );
  }
  // Both missing and duplicate are red — they represent "this needs human
  // attention before the deposit can be considered landed".
  return (
    <Badge variant="destructive" className="text-[10px]">
      {status}
    </Badge>
  );
}

function Detail({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
  return (
    <div className="rounded-md bg-background border px-2 py-1.5">
      <div className="text-[9px] uppercase tracking-wider text-muted-foreground">
        {label}
      </div>
      <div className={cn("text-xs truncate", mono && "font-mono tabular-nums")}>
        {value}
      </div>
    </div>
  );
}
