"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 { Kpi as KpiCard, KpiStrip } from "@/components/ui/kpi";
import { SkeletonRows } from "@/components/ui/skeleton";
import { useToast } from "@/components/ui/toast";
import { useLocalStorage } from "@/lib/hooks/use-local-storage";
import { ALL_ORGANIZATIONS, getOrganizationBySlug } from "@/lib/sag/entities";
import { formatCurrency } from "@/lib/utils";
import {
  detectSubscriptions,
  type DetectedSubscription,
  type DetectorTransaction,
  type SubscriptionCadence,
} from "@/lib/finance/detect-subscriptions";
import {
  confirmFromDetected,
  SUBSCRIPTIONS_STORAGE_KEY,
  type StoredSubscription,
} from "@/lib/finance/subscriptions-store";

/**
 * F1 — Subscription detection UI.
 *
 * Pulls 180 days of transactions from `/api/plaid/transactions` for every
 * linked Plaid item, hands them to `detectSubscriptions()`, and renders the
 * resulting clusters with a KPI strip, cadence/entity filters, and cancel
 * deep-links from the F2 seed list.
 *
 * Plaid transactions are NOT cached in localStorage today (see
 * `plaid-transactions.tsx`), so this component fetches them at mount and on
 * the explicit Refresh button. To keep parity with the other Plaid panels,
 * the access tokens themselves come from `sag.plaid.accessTokens` and the
 * account → entity mapping from `sag.plaid.accounts`.
 *
 * LocalStorage keys (read-only, owned by other components):
 *   - `sag.plaid.accounts`       (PlaidConnect / PlaidTransactions / CashForecast)
 *   - `sag.plaid.accessTokens`   (PlaidConnect)
 *
 * Persisted by THIS component:
 *   - `sag.finance.subscription_ignored`  (merchant ignore-list)
 *   - `sag.finance.subscriptions.ui`      (filter state, surface tweaks)
 */

// ──────────────────────────────────────────────────────────────────────────
// Types
// ──────────────────────────────────────────────────────────────────────────

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

interface UiState {
  cadenceFilter: "all" | SubscriptionCadence;
  entityFilter: "all" | string;
}

const DEFAULT_UI_STATE: UiState = {
  cadenceFilter: "all",
  entityFilter: "all",
};

const LOOKBACK_DAYS = 180; // 6 months — enough for quarterly subs to show up.

const CADENCE_LABEL: Record<SubscriptionCadence, string> = {
  weekly: "Weekly",
  biweekly: "Biweekly",
  monthly: "Monthly",
  quarterly: "Quarterly",
  annual: "Annual",
  irregular: "Irregular",
};

// ──────────────────────────────────────────────────────────────────────────
// Component
// ──────────────────────────────────────────────────────────────────────────

export function SubscriptionDetector() {
  const [accounts] = useLocalStorage<PlaidAccount[]>("sag.plaid.accounts", []);
  const [accessTokens] = useLocalStorage<Record<string, string>>(
    "sag.plaid.accessTokens",
    {}
  );
  const [ignored, setIgnored] = useLocalStorage<Record<string, boolean>>(
    "sag.finance.subscription_ignored",
    {}
  );
  const [ui, setUi] = useLocalStorage<UiState>(
    "sag.finance.subscriptions.ui",
    DEFAULT_UI_STATE
  );
  // Manual store — used here only to filter out clusters the user has
  // already confirmed (and to keep that set in sync after a confirm click).
  // Writes go through `confirmFromDetected()` so the canonical helper layer
  // owns the schema; this state mirror is just for instant UI feedback.
  const [trackedSubs, setTrackedSubs] = useLocalStorage<StoredSubscription[]>(
    SUBSCRIPTIONS_STORAGE_KEY,
    []
  );
  const { toast } = useToast();

  const [txns, setTxns] = useState<DetectorTransaction[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string>("");
  const [lastFetchedAt, setLastFetchedAt] = useState<string | null>(null);

  // Unique tokens across the linked Plaid items.
  const itemTokens = useMemo(() => {
    const seen = new Set<string>();
    const out: Array<{ itemId: string; token: string }> = [];
    for (const acc of accounts) {
      const tok = accessTokens[acc.itemId] ?? acc.accessToken;
      if (!tok || seen.has(tok)) continue;
      seen.add(tok);
      out.push({ itemId: acc.itemId, token: tok });
    }
    return out;
  }, [accounts, accessTokens]);

  // accountId → entitySlug map for clustering & display.
  const accountEntitySlugs = useMemo(() => {
    const map: Record<string, string | undefined> = {};
    for (const acc of accounts) map[acc.accountId] = acc.entitySlug;
    return map;
  }, [accounts]);

  async function fetchAllTransactions() {
    if (itemTokens.length === 0) {
      setTxns([]);
      return;
    }
    setLoading(true);
    setError("");
    const aggregated: DetectorTransaction[] = [];
    try {
      const startDate = nDaysAgoISO(LOOKBACK_DAYS);
      const endDate = nDaysAgoISO(0);
      for (const { token } of itemTokens) {
        const resp = await fetch("/api/plaid/transactions", {
          method: "POST",
          headers: { "content-type": "application/json" },
          body: JSON.stringify({ accessToken: token, startDate, endDate }),
        });
        const data = await resp.json();
        if (!resp.ok) {
          throw new Error(data.error || "Plaid request failed");
        }
        const incoming = Array.isArray(data.transactions)
          ? (data.transactions as DetectorTransaction[])
          : [];
        aggregated.push(...incoming);
      }
      setTxns(aggregated);
      setLastFetchedAt(new Date().toISOString());
    } catch (e) {
      setError(e instanceof Error ? e.message : "Failed to load transactions");
    } finally {
      setLoading(false);
    }
  }

  // Initial load. Re-runs when the set of linked items changes.
  useEffect(() => {
    const t = setTimeout(() => void fetchAllTransactions(), 0);
    return () => clearTimeout(t);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [itemTokens.length]);

  // Detection ids already promoted into the manual store. Used to suppress
  // those clusters from the detector view so the user can't double-add.
  const confirmedDetectionIds = useMemo(() => {
    const out = new Set<string>();
    for (const s of trackedSubs) {
      if (s.confirmedFromDetected) out.add(s.confirmedFromDetected);
    }
    return out;
  }, [trackedSubs]);

  // Run detection over the fetched transactions.
  const subscriptions = useMemo(
    () =>
      detectSubscriptions(txns, {
        accountEntitySlugs,
        ignoredMerchantsNormalized: ignored,
      }).filter((s) => !confirmedDetectionIds.has(s.id)),
    [txns, accountEntitySlugs, ignored, confirmedDetectionIds]
  );

  // Entity options for the filter — only entities that actually have a sub.
  const entityOptions = useMemo(() => {
    const slugs = new Set<string>();
    for (const s of subscriptions) {
      if (s.entitySlug) slugs.add(s.entitySlug);
    }
    return Array.from(slugs)
      .map((slug) => ALL_ORGANIZATIONS.find((o) => o.slug === slug))
      .filter((o): o is NonNullable<typeof o> => !!o)
      .sort((a, b) => a.name.localeCompare(b.name));
  }, [subscriptions]);

  const visible = useMemo(() => {
    return subscriptions.filter((s) => {
      if (ui.cadenceFilter !== "all" && s.cadence !== ui.cadenceFilter) return false;
      if (ui.entityFilter !== "all") {
        if (ui.entityFilter === "__untagged__") {
          if (s.entitySlug) return false;
        } else if (s.entitySlug !== ui.entityFilter) return false;
      }
      return true;
    });
  }, [subscriptions, ui]);

  const kpis = useMemo(() => {
    let monthly = 0;
    let biggest = 0;
    let biggestName = "";
    for (const s of visible) {
      monthly += s.estMonthlyBurn;
      if (s.estMonthlyBurn > biggest) {
        biggest = s.estMonthlyBurn;
        biggestName = s.merchant;
      }
    }
    return {
      monthly,
      annual: monthly * 12,
      count: visible.length,
      biggest,
      biggestName,
    };
  }, [visible]);

  function markIgnored(s: DetectedSubscription) {
    setIgnored({ ...ignored, [s.merchantNormalized]: true });
  }

  function unignoreAll() {
    setIgnored({});
  }

  function confirmDetected(s: DetectedSubscription) {
    const saved = confirmFromDetected(s.id, s.merchant, s.estMonthlyBurn, {
      cadence: s.cadence,
      entitySlug: s.entitySlug,
    });
    setTrackedSubs((prev) => {
      const idx = prev.findIndex((x) => x.id === saved.id);
      if (idx >= 0) {
        const next = [...prev];
        next[idx] = saved;
        return next;
      }
      return [saved, ...prev];
    });
    toast({
      title: "Confirmed as subscription",
      description: `${s.merchant} · ${formatCurrency(s.estMonthlyBurn)}/mo tracked`,
      variant: "success",
    });
  }

  const ignoredCount = Object.values(ignored).filter(Boolean).length;
  const hasAnyAccounts = accounts.length > 0;
  const hasUsableTokens = itemTokens.length > 0;

  // ────────────────────────────────────────────────────────────────────────
  // Render
  // ────────────────────────────────────────────────────────────────────────

  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">
                Cadence
              </label>
              <select
                value={ui.cadenceFilter}
                onChange={(e) =>
                  setUi({ ...ui, cadenceFilter: e.target.value as UiState["cadenceFilter"] })
                }
                className="h-9 rounded-md border border-input bg-background px-3 text-sm"
              >
                <option value="all">All cadences</option>
                <option value="weekly">Weekly</option>
                <option value="biweekly">Biweekly</option>
                <option value="monthly">Monthly</option>
                <option value="quarterly">Quarterly</option>
                <option value="annual">Annual</option>
                <option value="irregular">Irregular</option>
              </select>
            </div>

            <div className="flex flex-col gap-1">
              <label className="section-label">
                Entity
              </label>
              <select
                value={ui.entityFilter}
                onChange={(e) => setUi({ ...ui, entityFilter: e.target.value })}
                className="h-9 rounded-md border border-input bg-background px-3 text-sm min-w-[14rem]"
              >
                <option value="all">All entities</option>
                <option value="__untagged__">Untagged accounts</option>
                {entityOptions.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">
              {ignoredCount > 0 && (
                <Button variant="ghost" size="sm" onClick={unignoreAll}>
                  Restore {ignoredCount} ignored
                </Button>
              )}
              <Button
                variant="outline"
                size="sm"
                onClick={() => void fetchAllTransactions()}
                disabled={loading || !hasUsableTokens}
              >
                {loading ? "Loading…" : "↻ Refresh"}
              </Button>
            </div>
          </div>

          <div className="mt-3 flex items-center justify-between text-[11px] text-muted-foreground">
            <span>
              Lookback window: {LOOKBACK_DAYS} days · {txns.length} transactions scanned
            </span>
            {lastFetchedAt && (
              <span className="font-mono">
                Last refreshed {new Date(lastFetchedAt).toLocaleString()}
              </span>
            )}
          </div>
        </CardContent>
      </Card>

      {/* Empty-state banner: no Plaid accounts at all */}
      {!hasAnyAccounts && (
        <EmptyState
          icon="🔌"
          size="compact"
          title="Link a Plaid account to detect subscriptions"
          description={`Subscription detection runs over your last ${LOOKBACK_DAYS} days of bank transactions. Connect at least one account on the Connections page to see recurring vendors, monthly burn, and one-click cancel guides.`}
          action={
            <Button asChild variant="brand" size="sm">
              <Link href="/app/settings/connections">Link Plaid →</Link>
            </Button>
          }
        />
      )}

      {/* Empty-state banner: accounts linked but no access tokens (re-link needed) */}
      {hasAnyAccounts && !hasUsableTokens && (
        <EmptyState
          icon="🔑"
          size="compact"
          title="Re-link your Plaid items to load transactions"
          description="We can see your linked accounts but don't have the access tokens needed to pull transactions. Re-link via Connections to enable detection."
          action={
            <Button asChild variant="brand" size="sm">
              <Link href="/app/settings/connections">Re-link →</Link>
            </Button>
          }
        />
      )}

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

      {/* KPI strip */}
      <KpiStrip cols={4}>
        <KpiCard label="Monthly burn" value={formatCurrency(kpis.monthly)} />
        <KpiCard label="Detected subs" value={String(kpis.count)} />
        <KpiCard
          label="Biggest sub"
          value={kpis.biggest > 0 ? formatCurrency(kpis.biggest) : "—"}
          hint={kpis.biggestName || undefined}
        />
        <KpiCard label="Annual projection" value={formatCurrency(kpis.annual)} />
      </KpiStrip>

      {/* Table */}
      <Card>
        <CardContent className="p-5">
          <div className="flex items-center justify-between mb-3">
            <div>
              <h2 className="text-sm font-semibold">Detected subscriptions</h2>
              <p className="text-[11px] text-muted-foreground">
                Sorted by estimated monthly burn. <strong>Confirm</strong> moves a
                row into the Tracked tab (and the cash forecast). Already-confirmed
                clusters are filtered out below.
              </p>
            </div>
            <Badge variant="outline" className="text-[10px]">
              {visible.length} of {subscriptions.length}
            </Badge>
          </div>

          {loading && txns.length === 0 ? (
            <SkeletonRows rows={5} />
          ) : visible.length === 0 ? (
            <p className="py-10 text-center text-xs text-muted-foreground">
              {subscriptions.length === 0
                ? hasUsableTokens
                  ? "No recurring vendors detected in the last 180 days. A subscription needs at least 3 charges spaced ≥25 days apart with consistent amounts."
                  : "Link a Plaid account to start detection."
                : "Subscriptions exist but the current filters hide them. Adjust the cadence or entity filter above."}
            </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">Merchant</th>
                    <th className="py-2 px-2 text-left font-medium">Entity</th>
                    <th className="py-2 px-2 text-left font-medium">Cadence</th>
                    <th className="py-2 px-2 text-right font-medium">Median $</th>
                    <th className="py-2 px-2 text-right font-medium">Monthly burn</th>
                    <th className="py-2 px-2 text-left font-medium">Last seen</th>
                    <th className="py-2 px-2 text-left font-medium">Cancel</th>
                    <th className="py-2 pl-2 text-right font-medium">Actions</th>
                  </tr>
                </thead>
                <tbody>
                  {visible.map((s) => (
                    <SubscriptionRow
                      key={s.id}
                      sub={s}
                      onIgnore={() => markIgnored(s)}
                      onConfirm={() => confirmDetected(s)}
                    />
                  ))}
                </tbody>
              </table>
            </div>
          )}
        </CardContent>
      </Card>

      {/* Footnote */}
      <Card className="border-dashed">
        <CardContent className="p-4 text-[11px] text-muted-foreground space-y-1.5">
          <div>
            Detected with a +/-5% amount tolerance over &ge;3 occurrences and &ge;25-day
            cadence. Cancel guidance is best-effort &mdash; verify on the vendor&apos;s site
            before relying on it.
          </div>
          <div>
            Data flow: pulls {LOOKBACK_DAYS} days from{" "}
            <code className="text-foreground">/api/plaid/transactions</code> using tokens at{" "}
            <code className="text-foreground">sag.plaid.accessTokens</code>; accounts at{" "}
            <code className="text-foreground">sag.plaid.accounts</code> supply the entity
            mapping. Detection is pure (
            <code className="text-foreground">src/lib/finance/detect-subscriptions.ts</code>
            ); ignore-list persists at{" "}
            <code className="text-foreground">sag.finance.subscription_ignored</code>.
          </div>
        </CardContent>
      </Card>
    </div>
  );
}

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

function SubscriptionRow({
  sub,
  onIgnore,
  onConfirm,
}: {
  sub: DetectedSubscription;
  onIgnore: () => void;
  onConfirm: () => void;
}) {
  const entity = sub.entitySlug ? getOrganizationBySlug(sub.entitySlug) : undefined;
  const guide = sub.cancelGuide;

  return (
    <tr className="border-b last:border-0 align-middle">
      <td className="py-2 pr-3">
        <div className="font-medium truncate max-w-[14rem]" title={sub.merchant}>
          {sub.merchant}
        </div>
        <div className="text-[10px] text-muted-foreground">
          {sub.occurrences} charges since {sub.firstSeen}
        </div>
      </td>
      <td className="py-2 px-2">
        {entity ? (
          <span className="truncate inline-block max-w-[10rem]" title={entity.name}>
            {entity.emoji ?? "🏢"} {entity.name}
          </span>
        ) : (
          <span className="text-muted-foreground">—</span>
        )}
      </td>
      <td className="py-2 px-2">
        <CadenceBadge cadence={sub.cadence} />
        <div className="text-[10px] text-muted-foreground tabular-nums mt-0.5">
          ~{sub.medianDaysBetween.toFixed(1)} days
        </div>
      </td>
      <td className="py-2 px-2 text-right font-mono tabular-nums">
        {formatCurrency(sub.medianAmount)}
      </td>
      <td className="py-2 px-2 text-right font-mono tabular-nums font-semibold">
        {formatCurrency(sub.estMonthlyBurn)}
      </td>
      <td className="py-2 px-2 font-mono text-[11px] tabular-nums whitespace-nowrap">
        {sub.lastSeen}
      </td>
      <td className="py-2 px-2">
        {guide ? (
          <div className="flex items-center gap-1.5">
            <a
              href={guide.cancellationUrl}
              target="_blank"
              rel="noopener noreferrer"
              className="text-primary underline-offset-2 hover:underline"
              title={guide.notes}
            >
              {guide.vendor}
            </a>
            {guide.bestGuess && (
              <Badge
                variant="warning"
                className="text-[9px] px-1 py-0"
                title="Best-guess URL — verify before cancelling"
              >
                ?
              </Badge>
            )}
          </div>
        ) : (
          <span className="text-muted-foreground">No guide</span>
        )}
      </td>
      <td className="py-2 pl-2 text-right">
        <div className="inline-flex items-center gap-1 justify-end">
          <Button
            variant="ghost"
            size="sm"
            onClick={onConfirm}
            className="h-7 px-2 text-[10px]"
            title="Promote this detected sub into the manual tracker so the cash forecast pulls it"
          >
            Confirm
          </Button>
          <Button
            variant="ghost"
            size="sm"
            onClick={onIgnore}
            className="h-7 px-2 text-[10px]"
            title="Mark as not a subscription"
          >
            Not a sub
          </Button>
        </div>
      </td>
    </tr>
  );
}

function CadenceBadge({ cadence }: { cadence: SubscriptionCadence }) {
  const variant: React.ComponentProps<typeof Badge>["variant"] =
    cadence === "irregular" ? "warning" : cadence === "annual" ? "info" : "secondary";
  return (
    <Badge variant={variant} className="text-[10px]">
      {CADENCE_LABEL[cadence]}
    </Badge>
  );
}

function nDaysAgoISO(n: number): string {
  const d = new Date();
  d.setDate(d.getDate() - n);
  return d.toISOString().slice(0, 10);
}
