"use client";

import Link from "next/link";
import { useMemo } 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 { MiniKpi, MiniKpiGrid } from "@/components/ui/mini-kpi";
import { Switch } from "@/components/ui/switch";
import { useLocalStorage } from "@/lib/hooks/use-local-storage";
import { ALL_ORGANIZATIONS } from "@/lib/sag/entities";
import { formatCurrency } from "@/lib/utils";
import { cn } from "@/lib/utils";
import type { Bill } from "@/lib/finance/bill-types";
import {
  SUBSCRIPTIONS_STORAGE_KEY,
  type StoredSubscription,
} from "@/lib/finance/subscriptions-store";

/**
 * F3 — 30 / 60 / 90 day cash forecast.
 *
 * Builds a per-entity projected balance trajectory by:
 *   1. Pulling current balances from `sag.plaid.accounts` (each Plaid account
 *      is optionally tagged with an entity slug; balances roll up).
 *   2. Walking 90 days forward, subtracting scheduled bills on the day their
 *      `dueDate` falls (so long as the bill is not Paid / Cancelled).
 *   3. Subtracting a flat per-day burn for any recurring bills tagged
 *      `Subscriptions` — `monthly amount / 30`. This is the F1 input the spec
 *      flags as optional; if the user has no Subscription-category recurring
 *      bills yet, the toggle disables itself.
 *
 * Chart is pure Tailwind — no SVG, no chart library. Each entity gets a row
 * with four bars (Today / +30 / +60 / +90), width normalized against the
 * largest positive balance across the visible set, colored by the entity's
 * `primaryColor` (or a deterministic hash hue when none is set). Cells go red
 * on negative balances and the summary card highlights any entity that
 * crosses zero inside the window.
 *
 * LocalStorage keys (read-only here — owned by other components):
 *   - `sag.plaid.accounts`        (PlaidConnect)
 *   - `sag.bills.v2`              (BillsInbox / IfrReport / TaxPackExport)
 *   - `sag.subscriptions.v1`      (SubscriptionManager — manual store)
 *
 * Persisted-here (this component owns these):
 *   - `sag.finance.forecast.ui`  (entity filter + toggle state)
 *
 * Burn computation: both the `Subscriptions`-category recurring bills AND
 * the manual `sag.subscriptions.v1` store feed the daily-burn slice. The two
 * toggles are independent — turn manual off if you trust the detector only,
 * or turn the bills path off if you've migrated everything to the manual
 * store. They sum cleanly because the manual store has no overlap with
 * Subscriptions-category bills (different surfaces, different ids).
 */

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

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

interface ForecastUiState {
  entityFilter: string;
  includeBills: boolean;
  includeBurn: boolean;
  /**
   * When true, the daily-burn slice also sums `sag.subscriptions.v1` (the
   * manual tracker). Defaults to true so anyone who's curated that store
   * sees their data reflected without flipping a toggle.
   */
  includeManualSubs: boolean;
}

interface EntityForecast {
  slug: string;
  name: string;
  emoji: string;
  color: string;
  startingBalance: number;
  /** Day-indexed projection, 0 = today, 90 = day 90 inclusive. */
  trajectory: number[];
  day30: number;
  day60: number;
  day90: number;
  /** ISO date when balance first crosses below 0, or null if it never does. */
  negativeCrossoverDate: string | null;
  /** Total scheduled-bill outflow inside the 90-day window. */
  scheduledOutflow: number;
  /** Total recurring-subscription burn inside the 90-day window. */
  burnOutflow: number;
  /** Plaid account names contributing to the starting balance. */
  accountNames: string[];
}

// ──────────────────────────────────────────────────────────────────────────
// Constants & helpers
// ──────────────────────────────────────────────────────────────────────────

const HORIZON_DAYS = 90;

/** Industries whose entities don't typically carry their own operating cash. */
const PURE_HOLDING_INDUSTRIES = new Set(["Holding"]);

/** Statuses we treat as future outflows. Paid / Cancelled bills are skipped. */
const FORWARD_BILL_STATUSES = new Set(["Unscheduled", "Pending Review", "Scheduled", "Overdue"]);

const DEFAULT_UI_STATE: ForecastUiState = {
  entityFilter: "all",
  includeBills: true,
  includeBurn: true,
  includeManualSubs: true,
};

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

/** Add `n` days to an ISO date string, returning a new ISO date string. */
function addDays(iso: string, n: number): string {
  const d = new Date(`${iso}T00:00:00Z`);
  d.setUTCDate(d.getUTCDate() + n);
  return d.toISOString().slice(0, 10);
}

/** Deterministic 0-360 hue from a slug — used when an entity has no brand color. */
function slugHue(slug: string): number {
  let h = 0;
  for (let i = 0; i < slug.length; i++) {
    h = (h * 31 + slug.charCodeAt(i)) >>> 0;
  }
  return h % 360;
}

function entityColor(slug: string, primary?: string): string {
  if (primary && /^#[0-9a-f]{3,8}$/i.test(primary)) return primary;
  return `hsl(${slugHue(slug)} 65% 45%)`;
}

/** Operating entities: active or forming, with a real industry behind them. */
function getOperatingEntities() {
  return ALL_ORGANIZATIONS.filter((o) => {
    if (PURE_HOLDING_INDUSTRIES.has(o.industry)) return false;
    if (o.status === "Dissolved" || o.status === "Administratively Dissolved") return false;
    if (o.parentRelationship === "owner-investment") return false;
    if (o.parentRelationship === "external-employer") return false;
    return true;
  });
}

/** Estimated monthly amount for a recurring bill (only for burn-rate sums). */
function monthlyEquivalent(bill: Bill): number {
  if (!bill.recurring || !bill.amount) return 0;
  switch ((bill.recurrencePattern ?? "monthly").toLowerCase()) {
    case "weekly":
      return bill.amount * (52 / 12);
    case "monthly":
      return bill.amount;
    case "quarterly":
      return bill.amount / 3;
    case "annual":
      return bill.amount / 12;
    default:
      return bill.amount;
  }
}

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

export function CashForecast() {
  const [plaidAccounts] = useLocalStorage<PlaidAccount[]>("sag.plaid.accounts", []);
  const [bills] = useLocalStorage<Bill[]>("sag.bills.v2", []);
  const [manualSubs] = useLocalStorage<StoredSubscription[]>(
    SUBSCRIPTIONS_STORAGE_KEY,
    []
  );
  const [ui, setUi] = useLocalStorage<ForecastUiState>(
    "sag.finance.forecast.ui",
    DEFAULT_UI_STATE
  );

  const today = useMemo(() => todayIso(), []);
  const operatingEntities = useMemo(() => getOperatingEntities(), []);

  // Daily burn from recurring `Subscriptions`-category bills. We only count
  // bills with category = "Subscriptions" so a one-off "Subscriptions"
  // expense doesn't double-count when paired with the scheduled-bills path.
  const billsDailyBurnBySlug = useMemo(() => {
    const map = new Map<string, number>();
    for (const b of bills) {
      if (!b.recurring) continue;
      if (b.category !== "Subscriptions") continue;
      if (b.status === "Cancelled") continue;
      const monthly = monthlyEquivalent(b);
      if (monthly <= 0) continue;
      map.set(b.entitySlug, (map.get(b.entitySlug) ?? 0) + monthly / 30);
    }
    return map;
  }, [bills]);

  // Daily burn from the manual subscription store. Untagged subs do not
  // roll up anywhere — same policy the bills path uses (bills always have an
  // entitySlug, so this just matches).
  const manualDailyBurnBySlug = useMemo(() => {
    const map = new Map<string, number>();
    for (const s of manualSubs) {
      if (!s.entitySlug) continue;
      if (s.monthlyAmount <= 0) continue;
      map.set(s.entitySlug, (map.get(s.entitySlug) ?? 0) + s.monthlyAmount / 30);
    }
    return map;
  }, [manualSubs]);

  // Combined daily-burn map respecting the two independent toggles.
  const subscriptionDailyBurnBySlug = useMemo(() => {
    const map = new Map<string, number>();
    for (const [slug, burn] of billsDailyBurnBySlug) {
      map.set(slug, (map.get(slug) ?? 0) + burn);
    }
    if (ui.includeManualSubs) {
      for (const [slug, burn] of manualDailyBurnBySlug) {
        map.set(slug, (map.get(slug) ?? 0) + burn);
      }
    }
    return map;
  }, [billsDailyBurnBySlug, manualDailyBurnBySlug, ui.includeManualSubs]);

  const hasBurnData = useMemo(() => {
    for (const burn of subscriptionDailyBurnBySlug.values()) {
      if (burn > 0) return true;
    }
    return false;
  }, [subscriptionDailyBurnBySlug]);

  const hasManualSubs = manualSubs.some((s) => s.monthlyAmount > 0);

  // Plaid accounts grouped by entity. Untagged accounts roll into the SAG
  // parent so they still show up somewhere (rather than vanishing).
  const balancesBySlug = useMemo(() => {
    const map = new Map<string, { total: number; names: string[] }>();
    for (const acc of plaidAccounts) {
      const slug = acc.entitySlug || "south-armz-global";
      const prev = map.get(slug) ?? { total: 0, names: [] };
      prev.total += acc.balance ?? 0;
      const label = acc.mask ? `${acc.name} ····${acc.mask}` : acc.name;
      prev.names.push(label);
      map.set(slug, prev);
    }
    return map;
  }, [plaidAccounts]);

  const hasPlaidData = plaidAccounts.length > 0;

  // Forward-looking bills indexed by their due date (YYYY-MM-DD).
  const billsByDueDate = useMemo(() => {
    const map = new Map<string, Bill[]>();
    if (!ui.includeBills) return map;
    for (const b of bills) {
      if (!FORWARD_BILL_STATUSES.has(b.status)) continue;
      // Skip recurring Subscriptions — they're absorbed by the daily burn
      // when the burn toggle is on, otherwise dropping them avoids the same
      // monthly charge hitting the forecast twice in 90 days.
      if (b.recurring && b.category === "Subscriptions") continue;
      const arr = map.get(b.dueDate) ?? [];
      arr.push(b);
      map.set(b.dueDate, arr);
    }
    return map;
  }, [bills, ui.includeBills]);

  // ────────────────────────────────────────────────────────────────────────
  // Build per-entity trajectories.
  // ────────────────────────────────────────────────────────────────────────
  const forecasts = useMemo<EntityForecast[]>(() => {
    const out: EntityForecast[] = [];
    for (const org of operatingEntities) {
      const bal = balancesBySlug.get(org.slug);
      const startingBalance = bal?.total ?? 0;
      const dailyBurn =
        ui.includeBurn && hasBurnData ? (subscriptionDailyBurnBySlug.get(org.slug) ?? 0) : 0;

      const trajectory: number[] = new Array(HORIZON_DAYS + 1);
      let running = startingBalance;
      trajectory[0] = running;
      let scheduledOutflow = 0;
      let burnOutflow = 0;
      let negativeCrossoverDate: string | null = null;

      for (let day = 1; day <= HORIZON_DAYS; day++) {
        const iso = addDays(today, day);
        // Subtract any bills due on this date for this entity.
        const dueToday = billsByDueDate.get(iso);
        if (dueToday) {
          for (const b of dueToday) {
            if (b.entitySlug !== org.slug) continue;
            running -= b.amount;
            scheduledOutflow += b.amount;
          }
        }
        // Subscription burn — flat daily slice.
        if (dailyBurn > 0) {
          running -= dailyBurn;
          burnOutflow += dailyBurn;
        }
        trajectory[day] = running;
        if (negativeCrossoverDate === null && running < 0) {
          negativeCrossoverDate = iso;
        }
      }

      // Only include entities that have either a Plaid balance, scheduled
      // outflows, or recurring burn — otherwise the row is dead weight (every
      // checkpoint is exactly $0). This keeps the chart focused on entities
      // with real cash activity.
      const hasActivity =
        startingBalance !== 0 || scheduledOutflow > 0 || burnOutflow > 0;
      if (!hasActivity) continue;

      out.push({
        slug: org.slug,
        name: org.name,
        emoji: org.emoji ?? "🏢",
        color: entityColor(org.slug, org.primaryColor),
        startingBalance,
        trajectory,
        day30: trajectory[30],
        day60: trajectory[60],
        day90: trajectory[90],
        negativeCrossoverDate,
        scheduledOutflow,
        burnOutflow,
        accountNames: bal?.names ?? [],
      });
    }
    // Largest starting balance first so the chart reads like a treemap.
    out.sort((a, b) => b.startingBalance - a.startingBalance);
    return out;
  }, [
    operatingEntities,
    balancesBySlug,
    subscriptionDailyBurnBySlug,
    billsByDueDate,
    ui.includeBurn,
    hasBurnData,
    today,
  ]);

  const visibleForecasts = useMemo(() => {
    if (ui.entityFilter === "all") return forecasts;
    return forecasts.filter((f) => f.slug === ui.entityFilter);
  }, [forecasts, ui.entityFilter]);

  // Chart scale — use the largest absolute balance across all visible
  // checkpoints so widths are comparable across the four columns.
  const chartScale = useMemo(() => {
    let max = 0;
    for (const f of visibleForecasts) {
      for (const v of [f.startingBalance, f.day30, f.day60, f.day90]) {
        const abs = Math.abs(v);
        if (abs > max) max = abs;
      }
    }
    return max || 1;
  }, [visibleForecasts]);

  // Roll-ups for the KPI strip.
  const totals = useMemo(() => {
    let today = 0;
    let day30 = 0;
    let day60 = 0;
    let day90 = 0;
    let scheduledOutflow = 0;
    let burnOutflow = 0;
    let entitiesGoingNegative = 0;
    for (const f of visibleForecasts) {
      today += f.startingBalance;
      day30 += f.day30;
      day60 += f.day60;
      day90 += f.day90;
      scheduledOutflow += f.scheduledOutflow;
      burnOutflow += f.burnOutflow;
      if (f.negativeCrossoverDate) entitiesGoingNegative++;
    }
    return {
      today,
      day30,
      day60,
      day90,
      scheduledOutflow,
      burnOutflow,
      entitiesGoingNegative,
    };
  }, [visibleForecasts]);

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

  return (
    <div className="space-y-6 max-w-6xl">
      {/* Filter / toggle 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">
                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-[16rem]"
              >
                <option value="all">All operating entities</option>
                {operatingEntities.map((o) => (
                  <option key={o.slug} value={o.slug}>
                    {o.emoji} {o.name}
                  </option>
                ))}
              </select>
            </div>

            <div className="flex flex-col gap-1">
              <label className="section-label">
                Scheduled bills
              </label>
              <button
                type="button"
                onClick={() => setUi({ ...ui, includeBills: !ui.includeBills })}
                className={cn(
                  "h-9 rounded-md border px-3 text-xs font-medium transition-colors",
                  ui.includeBills
                    ? "border-primary bg-primary/10 text-foreground"
                    : "border-input bg-background text-muted-foreground hover:bg-accent"
                )}
                aria-pressed={ui.includeBills}
              >
                {ui.includeBills ? "Included" : "Excluded"}
              </button>
            </div>

            <div className="flex flex-col gap-1">
              <label className="section-label">
                Subscription burn
              </label>
              <button
                type="button"
                onClick={() => hasBurnData && setUi({ ...ui, includeBurn: !ui.includeBurn })}
                disabled={!hasBurnData}
                className={cn(
                  "h-9 rounded-md border px-3 text-xs font-medium transition-colors",
                  !hasBurnData
                    ? "border-input bg-muted text-muted-foreground cursor-not-allowed"
                    : ui.includeBurn
                      ? "border-primary bg-primary/10 text-foreground"
                      : "border-input bg-background text-muted-foreground hover:bg-accent"
                )}
                aria-pressed={ui.includeBurn && hasBurnData}
                title={
                  hasBurnData
                    ? "Toggle daily subscription burn"
                    : "Add at least one recurring Subscriptions-category bill to enable"
                }
              >
                {!hasBurnData ? "No data" : ui.includeBurn ? "Included" : "Excluded"}
              </button>
            </div>

            <div className="flex flex-col gap-1">
              <label
                className="section-label"
                htmlFor="forecast-include-manual-subs"
              >
                Manual subscriptions
              </label>
              <div className="h-9 inline-flex items-center gap-2 rounded-md border border-input bg-background px-3 text-xs font-medium">
                <Switch
                  id="forecast-include-manual-subs"
                  checked={ui.includeManualSubs && hasManualSubs}
                  onCheckedChange={(checked) =>
                    setUi({ ...ui, includeManualSubs: checked })
                  }
                  disabled={!hasManualSubs}
                  aria-label="Include manual subscriptions"
                />
                <span
                  className={cn(
                    !hasManualSubs
                      ? "text-muted-foreground"
                      : ui.includeManualSubs
                        ? "text-foreground"
                        : "text-muted-foreground"
                  )}
                >
                  {!hasManualSubs
                    ? "No data"
                    : ui.includeManualSubs
                      ? "Included"
                      : "Excluded"}
                </span>
              </div>
            </div>

            <div className="ml-auto flex flex-col gap-1 text-right">
              <span className="section-label">
                Horizon
              </span>
              <span className="text-xs font-mono text-foreground">
                {today} → {addDays(today, HORIZON_DAYS)}
              </span>
            </div>
          </div>
        </CardContent>
      </Card>

      {/* No-Plaid empty state */}
      {!hasPlaidData && (
        <EmptyState
          icon="🔌"
          size="compact"
          title="Connect Plaid to power the forecast"
          description="The 30 / 60 / 90 projection starts from your current bank balances. Without at least one Plaid-linked account, every entity below opens at $0 — bills and burn still draw it negative, so the chart can show you the run-rate, but it can't show your real runway. Link an account to unlock real numbers."
          action={
            <Button asChild variant="brand" size="sm">
              <Link href="/app/settings/connections">Link Plaid →</Link>
            </Button>
          }
        />
      )}

      {/* KPI strip */}
      <KpiStrip cols={4}>
        <KpiCard label="Today" value={formatCurrency(totals.today)} />
        <KpiCard
          label="Day 30"
          value={formatCurrency(totals.day30)}
          tone={totals.day30 < 0 ? "rose" : totals.day30 < totals.today ? "amber" : "neutral"}
          hint={deltaSub(totals.today, totals.day30)}
        />
        <KpiCard
          label="Day 60"
          value={formatCurrency(totals.day60)}
          tone={totals.day60 < 0 ? "rose" : totals.day60 < totals.today ? "amber" : "neutral"}
          hint={deltaSub(totals.today, totals.day60)}
        />
        <KpiCard
          label="Day 90"
          value={formatCurrency(totals.day90)}
          tone={totals.day90 < 0 ? "rose" : totals.day90 < totals.today ? "amber" : "neutral"}
          hint={deltaSub(totals.today, totals.day90)}
        />
      </KpiStrip>

      {/* Outflow split & risk count */}
      <section className="grid gap-4 md:grid-cols-3">
        <Card>
          <CardContent className="p-4">
            <div className="section-label">
              Scheduled bill outflow (90d)
            </div>
            <div className="mt-1 text-xl font-semibold tabular-nums">
              {formatCurrency(totals.scheduledOutflow)}
            </div>
            <div className="mt-0.5 text-[10px] text-muted-foreground">
              {ui.includeBills ? "From bills with a due date in the window" : "Excluded from current view"}
            </div>
          </CardContent>
        </Card>
        <Card>
          <CardContent className="p-4">
            <div className="section-label">
              Subscription burn (90d)
            </div>
            <div className="mt-1 text-xl font-semibold tabular-nums">
              {formatCurrency(totals.burnOutflow)}
            </div>
            <div className="mt-0.5 text-[10px] text-muted-foreground">
              {!hasBurnData
                ? "No recurring Subscriptions bills or tracked subs yet"
                : ui.includeBurn
                  ? `Monthly recurring ÷ 30, summed across 90 days${
                      ui.includeManualSubs && hasManualSubs
                        ? " (incl. manual tracker)"
                        : ""
                    }`
                  : "Excluded from current view"}
            </div>
          </CardContent>
        </Card>
        <Card>
          <CardContent className="p-4">
            <div className="section-label">
              Entities going negative
            </div>
            <div
              className={cn(
                "mt-1 text-xl font-semibold tabular-nums",
                totals.entitiesGoingNegative > 0 ? "text-destructive" : "text-foreground"
              )}
            >
              {totals.entitiesGoingNegative} / {visibleForecasts.length}
            </div>
            <div className="mt-0.5 text-[10px] text-muted-foreground">
              Cross zero before day 90 at current run-rate
            </div>
          </CardContent>
        </Card>
      </section>

      {/* Pure-Tailwind stacked-area chart */}
      <Card>
        <CardContent className="p-5">
          <div className="flex items-center justify-between mb-3">
            <div>
              <h2 className="text-sm font-semibold">Per-entity projection</h2>
              <p className="text-[11px] text-muted-foreground">
                Each row shows that entity&apos;s balance at four checkpoints. Bar width
                is normalized against the largest balance in the view; red = negative.
              </p>
            </div>
            <Badge variant="outline" className="text-[10px]">
              {visibleForecasts.length} entit{visibleForecasts.length === 1 ? "y" : "ies"}
            </Badge>
          </div>

          {visibleForecasts.length === 0 ? (
            <p className="py-8 text-center text-xs text-muted-foreground">
              No entities with cash activity in this view. Link a Plaid account or schedule
              a bill to populate the chart.
            </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 w-[18rem]">Entity</th>
                    <th className="py-2 px-2 text-left font-medium">Today</th>
                    <th className="py-2 px-2 text-left font-medium">+30 days</th>
                    <th className="py-2 px-2 text-left font-medium">+60 days</th>
                    <th className="py-2 px-2 text-left font-medium">+90 days</th>
                  </tr>
                </thead>
                <tbody>
                  {visibleForecasts.map((f) => (
                    <tr key={f.slug} className="border-b last:border-0 align-middle">
                      <td className="py-2 pr-3">
                        <div className="flex items-center gap-2">
                          <span
                            className="h-3 w-3 rounded-sm shrink-0"
                            style={{ backgroundColor: f.color }}
                            aria-hidden
                          />
                          <span className="truncate">
                            {f.emoji} {f.name}
                          </span>
                        </div>
                      </td>
                      <td className="py-2 px-2">
                        <BalanceBar value={f.startingBalance} scale={chartScale} color={f.color} />
                      </td>
                      <td className="py-2 px-2">
                        <BalanceBar value={f.day30} scale={chartScale} color={f.color} />
                      </td>
                      <td className="py-2 px-2">
                        <BalanceBar value={f.day60} scale={chartScale} color={f.color} />
                      </td>
                      <td className="py-2 px-2">
                        <BalanceBar value={f.day90} scale={chartScale} color={f.color} />
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          )}
        </CardContent>
      </Card>

      {/* Per-entity summary cards */}
      {visibleForecasts.length > 0 && (
        <section className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
          {visibleForecasts.map((f) => (
            <Card key={f.slug}>
              <CardContent className="p-4 space-y-2">
                <div className="flex items-center justify-between gap-2">
                  <div className="flex items-center gap-2 min-w-0">
                    <span
                      className="h-3 w-3 rounded-sm shrink-0"
                      style={{ backgroundColor: f.color }}
                      aria-hidden
                    />
                    <span className="text-sm font-medium truncate">
                      {f.emoji} {f.name}
                    </span>
                  </div>
                  {f.negativeCrossoverDate ? (
                    <Badge variant="destructive" className="text-[10px]">
                      ↓ {f.negativeCrossoverDate}
                    </Badge>
                  ) : f.day90 < f.startingBalance ? (
                    <Badge variant="warning" className="text-[10px]">
                      drawdown
                    </Badge>
                  ) : (
                    <Badge variant="outline" className="text-[10px]">
                      stable
                    </Badge>
                  )}
                </div>

                <MiniKpiGrid>
                  <MiniKpi label="Starting" value={formatCurrency(f.startingBalance)} />
                  <MiniKpi
                    label="Day 90"
                    value={formatCurrency(f.day90)}
                    tone={
                      f.day90 < 0
                        ? "rose"
                        : f.day90 < f.startingBalance
                          ? "amber"
                          : undefined
                    }
                  />
                  <MiniKpi label="Bills (90d)" value={formatCurrency(f.scheduledOutflow)} />
                  <MiniKpi label="Burn (90d)" value={formatCurrency(f.burnOutflow)} />
                </MiniKpiGrid>

                {f.negativeCrossoverDate && (
                  <p className="text-[10px] text-destructive">
                    Crosses zero on{" "}
                    <span className="font-mono">{f.negativeCrossoverDate}</span>. Cover
                    the gap before then or reschedule the bills hitting that week.
                  </p>
                )}

                {f.accountNames.length > 0 ? (
                  <p className="text-[10px] text-muted-foreground truncate">
                    Sourced from: {f.accountNames.join(", ")}
                  </p>
                ) : (
                  <p className="text-[10px] text-muted-foreground">
                    No Plaid account tagged to this entity — starting balance assumed $0.
                  </p>
                )}
              </CardContent>
            </Card>
          ))}
        </section>
      )}

      {/* Footnote */}
      <Card className="border-dashed">
        <CardContent className="p-4 text-[11px] text-muted-foreground space-y-1.5">
          <div>
            <strong className="text-foreground">How this is built —</strong> starting
            balances come from <code className="text-foreground">sag.plaid.accounts</code>;
            scheduled bills from <code className="text-foreground">sag.bills.v2</code>{" "}
            (filtered to forward statuses: Unscheduled, Pending Review, Scheduled,
            Overdue). Recurring bills with the <em>Subscriptions</em> category are
            modeled as a flat daily burn (monthly amount ÷ 30) and skipped from the
            scheduled-bills path so they don&apos;t double-count. The manual
            subscription store at{" "}
            <code className="text-foreground">sag.subscriptions.v1</code> (when the{" "}
            <em>Manual subscriptions</em> toggle is on) adds on top of that.
          </div>
          <div>
            <strong className="text-foreground">What this is not —</strong> no inflow
            modeling (revenue, payroll deposits, transfers). This is a worst-case
            outflow runway against current cash, not a P&amp;L projection. Treat
            day-90 numbers as the floor.
          </div>
        </CardContent>
      </Card>
    </div>
  );
}

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

function BalanceBar({
  value,
  scale,
  color,
}: {
  value: number;
  scale: number;
  color: string;
}) {
  const pct = Math.min(100, Math.max(0, (Math.abs(value) / scale) * 100));
  const isNeg = value < 0;
  return (
    <div className="flex items-center gap-2 min-w-[8rem]">
      <div
        className={cn(
          "relative h-5 flex-1 rounded-sm overflow-hidden",
          isNeg ? "bg-destructive/10" : "bg-muted"
        )}
      >
        <div
          className="absolute inset-y-0 left-0 rounded-sm"
          style={{
            width: `${pct}%`,
            backgroundColor: isNeg ? undefined : color,
            opacity: isNeg ? 1 : 0.85,
          }}
        />
        {isNeg && (
          <div
            className="absolute inset-y-0 left-0 bg-destructive rounded-sm"
            style={{ width: `${pct}%` }}
          />
        )}
      </div>
      <span
        className={cn(
          "tabular-nums text-[11px] shrink-0 w-24 text-right",
          isNeg ? "text-destructive font-medium" : "text-foreground"
        )}
      >
        {formatCurrency(value)}
      </span>
    </div>
  );
}

function deltaSub(start: number, future: number): string {
  const delta = future - start;
  if (delta === 0) return "no change";
  const arrow = delta < 0 ? "↓" : "↑";
  return `${arrow} ${formatCurrency(Math.abs(delta))} vs today`;
}
