import { useCallback, useEffect, useRef, useState, type DragEvent, type DragEventHandler, type ReactNode } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import {
  useSnowball, useSnowballSettings, useSnowballActivePlan, useSnowballPlans, useCategories,
} from '@/hooks/useQueries';
import { ArrowDown, ArrowUp, GripVertical, TrendingDown, Zap, Save, CalendarCheck, AlertTriangle, PenLine, EyeOff, CheckCircle2, Circle, AlertCircle, RefreshCw } from 'lucide-react';
import { toast } from 'sonner';
import { api } from '@/api';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { Skeleton } from '@/components/ui/Skeleton';
import { cn, errMessage } from '@/lib/utils';
import { moveInArray } from '@/lib/reorder';
import BillModal from '@/components/BillModal';
import { makeBillDraft } from '@/lib/billDrafts';
import PlanStatusBanner, { type ActivePlan } from '@/components/snowball/PlanStatusBanner';
import PlanHistoryPanel, { type SnowballPlan } from '@/components/snowball/PlanHistoryPanel';
import * as AlertDialog from '@radix-ui/react-alert-dialog';
import { formatUSD, formatUSDWhole, asDollars } from '@/lib/money';
import type {
  Bill, Category,
  SnowballProjection as Projection,
  SnowballProjectionDetail as SnowballProj,
  AvalancheProjectionDetail as AvalancheProj,
  SnowballSettings,
} from '@/types';

// ── formatters ────────────────────────────────────────────────────────────────
function fmt(val: number | null | undefined): string {
  return formatUSD(val == null ? undefined : asDollars(val), { dash: true });
}
function fmtCompact(val: number | null | undefined): string {
  if (val == null || val === 0) return '—';
  return formatUSDWhole(asDollars(val));
}
function ordinal(n: number | null | undefined): string {
  const d = Number(n);
  if (!d) return '—';
  if (d > 3 && d < 21) return `${d}th`;
  switch (d % 10) {
    case 1: return `${d}st`; case 2: return `${d}nd`; case 3: return `${d}rd`; default: return `${d}th`;
  }
}
function sortRamseyDebts(debts: Bill[]): Bill[] {
  return [...debts].sort((a, b) => {
    if (a.current_balance == null && b.current_balance == null) return a.name.localeCompare(b.name);
    if (a.current_balance == null) return 1;
    if (b.current_balance == null) return -1;
    const diff = Number(a.current_balance) - Number(b.current_balance);
    return diff || a.name.localeCompare(b.name);
  });
}
function isRamseyOrdered(debts: Bill[]): boolean {
  const sorted = sortRamseyDebts(debts);
  return debts.every((debt, index) => debt.id === sorted[index]?.id);
}


interface ReadinessItem {
  id: string;
  label: string;
  ready: boolean;
  manual?: boolean;
  hint?: string;
}

interface DebtDragProps {
  draggable: boolean;
  isDragging?: boolean;
  isDropTarget?: boolean;
  onDragStart?: DragEventHandler<HTMLDivElement>;
  onDragEnter?: DragEventHandler<HTMLDivElement>;
  onDragOver?: DragEventHandler<HTMLDivElement>;
  onDragEnd?: DragEventHandler<HTMLDivElement>;
  onDrop?: DragEventHandler<HTMLDivElement>;
}

interface EditBillState {
  bill: Bill | null;
  initialBill?: Partial<Bill>;
}

// ── SectionDivider ────────────────────────────────────────────────────────────
function SectionDivider({ label }: { label: ReactNode }) {
  return (
    <div className="flex items-center gap-3">
      <span className="shrink-0 text-[10px] font-bold uppercase tracking-[0.12em] text-muted-foreground/50">
        {label}
      </span>
      <div className="h-px flex-1 bg-border/50" />
    </div>
  );
}

// ── StatCard ──────────────────────────────────────────────────────────────────
function StatCard({ label, value, sub, highlight }: {
  label: ReactNode;
  value: ReactNode;
  sub?: ReactNode;
  highlight?: boolean;
}) {
  return (
    <div className={cn('surface-elevated rounded-xl px-5 py-4 space-y-0.5', highlight && 'border border-emerald-500/30')}>
      <p className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">{label}</p>
      <p className={cn('text-2xl font-semibold tabular-nums', highlight && 'text-emerald-400')}>{value}</p>
      {sub && <p className="text-xs text-muted-foreground">{sub}</p>}
    </div>
  );
}

// ── Projection panel ──────────────────────────────────────────────────────────
function AvalancheComparison({ snowball, avalanche }: { snowball: SnowballProj; avalanche: AvalancheProj }) {
  if (!snowball.months_to_freedom || !avalanche.months_to_freedom) return null;
  const monthDiff    = snowball.months_to_freedom - avalanche.months_to_freedom;
  const interestDiff = Number(snowball.total_interest_paid ?? 0) - Number(avalanche.total_interest_paid ?? 0);
  const same         = Math.abs(monthDiff) < 1 && Math.abs(interestDiff) < 1;
  return (
    <div className="border-t border-border/40 px-5 py-3 space-y-1.5">
      <p className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
        vs. Avalanche (highest rate first)
      </p>
      <div className="flex items-baseline justify-between gap-2">
        <span className="text-sm text-muted-foreground">{avalanche.payoff_display}</span>
        <span className="text-xs tabular-nums text-muted-foreground">{fmt(avalanche.total_interest_paid)} interest</span>
      </div>
      {same ? (
        <p className="text-xs text-muted-foreground/70">Same result — your debts have similar rates.</p>
      ) : interestDiff > 0 ? (
        <p className="text-xs text-emerald-400">
          Avalanche saves {fmt(interestDiff)} interest
          {monthDiff > 0 ? ` · ${monthDiff} month${monthDiff > 1 ? 's' : ''} faster` : ''}
        </p>
      ) : (
        <p className="text-xs text-violet-400">
          Snowball finishes {Math.abs(monthDiff)} month{Math.abs(monthDiff) > 1 ? 's' : ''} faster ·
          Avalanche costs {fmt(Math.abs(interestDiff))} more
        </p>
      )}
    </div>
  );
}

function ProjectionPanel({ projection, projectionLoading, projectionError, onRetry, billCount }: {
  projection: Projection | null;
  projectionLoading: boolean;
  projectionError: boolean;
  onRetry: () => void;
  billCount: number;
}) {
  if (projectionLoading && !projection) {
    return (
      <div className="surface-elevated rounded-xl p-5 space-y-3">
        <Skeleton className="h-5 w-36" />
        <div className="space-y-2">{[...Array(3)].map((_, i) => <Skeleton key={i} className="h-10" />)}</div>
      </div>
    );
  }
  if (projectionError && !projection) {
    return (
      <div className="surface-elevated rounded-xl p-5 text-center">
        <AlertCircle className="mx-auto h-6 w-6 text-muted-foreground/50" />
        <p className="mt-2 text-sm text-muted-foreground">Couldn’t load the payoff projection.</p>
        <Button size="sm" variant="outline" className="mt-3 gap-1.5" onClick={onRetry}>
          <RefreshCw className="h-3.5 w-3.5" /> Try again
        </Button>
      </div>
    );
  }
  if (!projection) return null;
  const sb = projection.snowball;
  const av = projection.avalanche;
  if (!sb) return null;
  const hasProjection = sb.debts.length > 0;
  const needsBalances = billCount > 0 && !hasProjection && sb.skipped.length > 0;
  return (
    <div className="surface-elevated rounded-xl overflow-hidden">
      <div className="flex items-start justify-between gap-4 px-5 py-4 border-b border-border/40">
        <div className="flex items-center gap-2">
          <CalendarCheck className="h-4 w-4 text-primary shrink-0" />
          <span className="text-sm font-semibold">Payoff Projection</span>
        </div>
        {sb.payoff_display && (
          <div className="text-right shrink-0">
            <p className="text-[10px] uppercase tracking-wider text-muted-foreground">Snowball · Debt-Free</p>
            <p className="text-base font-semibold text-emerald-400">{sb.payoff_display}</p>
          </div>
        )}
      </div>
      {projectionError && (
        <div className="flex items-center gap-2 px-5 py-2 bg-muted/40 border-b border-border/40 text-xs text-muted-foreground">
          <AlertCircle className="h-3.5 w-3.5 shrink-0" />
          Couldn’t refresh — showing the last result.
          <button type="button" onClick={onRetry} className="ml-auto underline hover:text-foreground">Retry</button>
        </div>
      )}
      {sb.capped && (
        <div className="flex items-start gap-2 px-5 py-3 bg-amber-500/10 border-b border-amber-500/20 text-xs text-amber-400">
          <AlertTriangle className="h-3.5 w-3.5 mt-0.5 shrink-0" />
          One or more debts won’t pay off at this rate. Add extra monthly budget or increase minimum payments.
        </div>
      )}
      {needsBalances && (
        <div className="px-5 py-8 text-center text-sm text-muted-foreground">
          Click any balance to enter it and see your payoff timeline.
        </div>
      )}
      {hasProjection && (
        <div className="divide-y divide-border/30">
          {sb.debts.map((d, i) => (
            <div key={d.id} className="flex items-center gap-3 px-5 py-3">
              <span className="text-xs font-bold text-muted-foreground w-5 shrink-0 tabular-nums">#{i + 1}</span>
              <span className="flex-1 text-sm font-medium truncate min-w-0">{d.name}</span>
              <div className="text-right shrink-0 space-y-0.5">
                {d.payoff_display ? (
                  <>
                    <p className="text-sm font-semibold">{d.payoff_display}</p>
                    <p className="text-[10px] text-muted-foreground">
                      {d.months} mo · {fmtCompact(d.total_interest)} interest
                    </p>
                  </>
                ) : (
                  <p className="text-xs text-muted-foreground">unknown balance</p>
                )}
              </div>
            </div>
          ))}
        </div>
      )}
      {hasProjection && (
        <div className="flex items-center justify-between px-5 py-3 border-t border-border/40 bg-muted/20">
          <span className="text-xs text-muted-foreground">Total interest paid</span>
          <span className="text-sm font-semibold tabular-nums">{fmt(sb.total_interest_paid)}</span>
        </div>
      )}
      {hasProjection && av && <AvalancheComparison snowball={sb} avalanche={av} />}
      {sb.skipped.length > 0 && hasProjection && (
        <div className="px-5 pb-3 text-[10px] text-muted-foreground/60">
          {sb.skipped.length} bill{sb.skipped.length > 1 ? 's' : ''} excluded (no balance):
          {' '}{sb.skipped.map(s => s.name).join(', ')}
        </div>
      )}
    </div>
  );
}

// ── Readiness strip ───────────────────────────────────────────────────────────
function ReadinessStrip({ items, readyCount, totalCount, allReady, onToggle, disabled }: {
  items: ReadinessItem[];
  readyCount: number;
  totalCount: number;
  allReady: boolean;
  onToggle: (id: string, ready: boolean) => void;
  disabled?: boolean;
}) {
  return (
    <div className={cn(
      'surface-elevated rounded-xl px-4 py-3 flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between',
      allReady && 'border border-emerald-500/30',
    )}>
      <div className="flex items-center gap-3 shrink-0">
        <div className={cn(
          'flex h-9 w-9 items-center justify-center rounded-lg border',
          allReady ? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-400' : 'border-border/60 bg-muted/30 text-muted-foreground',
        )}>
          {allReady ? <CheckCircle2 className="h-4 w-4" /> : <Circle className="h-4 w-4" />}
        </div>
        <div>
          <p className="text-sm font-semibold">Snowball Readiness</p>
          <p className={cn('text-xs', allReady ? 'text-emerald-400' : 'text-muted-foreground')}>
            {allReady ? 'Ready to roll: attack the smallest balance first.' : `${readyCount} of ${totalCount} ready`}
          </p>
        </div>
      </div>
      <div className="flex flex-wrap gap-2">
        {items.map(item => {
          const Icon = item.ready ? CheckCircle2 : Circle;
          const content = (
            <>
              <Icon className="h-3.5 w-3.5 shrink-0" />
              <span className="truncate">{item.label}</span>
            </>
          );
          const className = cn(
            'inline-flex h-8 max-w-full items-center gap-1.5 rounded-full border px-3 text-xs font-medium transition-colors',
            item.ready
              ? 'border-emerald-500/25 bg-emerald-500/10 text-emerald-400'
              : 'border-border/60 bg-muted/30 text-muted-foreground',
            item.manual && !disabled && 'hover:bg-muted/60 cursor-pointer',
            item.manual && disabled && 'opacity-60 cursor-not-allowed',
          );

          if (item.manual) {
            return (
              <button
                key={item.id}
                type="button"
                className={className}
                onClick={() => onToggle(item.id, !item.ready)}
                disabled={disabled}
                title={item.hint}
              >
                {content}
              </button>
            );
          }

          return (
            <span key={item.id} className={className} title={item.hint}>
              {content}
            </span>
          );
        })}
      </div>
    </div>
  );
}

// ── Page ──────────────────────────────────────────────────────────────────────
export default function SnowballPage() {
  const queryClient = useQueryClient();
  const { data: billsRaw = [], isPending: billsPending, error: snowballError } = useSnowball();
  const bills = billsRaw ?? [];
  const { data: categoriesRaw = [] } = useCategories();
  const categories = (categoriesRaw ?? []) as Category[];
  const { data: settingsRaw } = useSnowballSettings();
  const settings = settingsRaw;
  const { data: activePlanRaw = null } = useSnowballActivePlan();
  const activePlan = (activePlanRaw ?? null) as SnowballPlan | null;
  const { data: allPlansRaw = [] } = useSnowballPlans();
  const allPlans = (allPlansRaw ?? []) as SnowballPlan[];
  const loading = billsPending;
  const loadError = snowballError ? (snowballError.message || 'Failed to load snowball data') : null;
  // Optimistic list/plan edits write through the query cache so existing call
  // sites (setBills(prev => …), setActivePlan(…), setAllPlans(prev => …)) work.
  const setBills = useCallback((u: Bill[] | ((prev: Bill[]) => Bill[])) =>
    queryClient.setQueryData<Bill[]>(['snowball'], prev => (typeof u === 'function' ? u(prev || []) : u)), [queryClient]);
  const setActivePlan = useCallback((plan: SnowballPlan | null) =>
    queryClient.setQueryData<SnowballPlan | null>(['snowball-active-plan'], plan), [queryClient]);
  const setAllPlans = useCallback((u: (prev: SnowballPlan[]) => SnowballPlan[]) =>
    queryClient.setQueryData<SnowballPlan[]>(['snowball-plans'], prev => u(prev || [])), [queryClient]);
  const [saving,     setSaving]     = useState(false);
  const [dirty,      setDirty]      = useState(false);
  const [editBill,   setEditBill]   = useState<EditBillState | null>(null);

  const [extraPayment,   setExtraPayment]   = useState('');
  const [ramseyMode,     setRamseyMode]     = useState(true);
  const [readyCurrentOnBills, setReadyCurrentOnBills] = useState(false);
  const [readyEmergencyFund,  setReadyEmergencyFund]  = useState(false);
  const [savingSettings, setSavingSettings] = useState(false);
  const extraPaymentRef = useRef('');

  const [projection,        setProjection]        = useState<Projection | null>(null);
  const [projectionLoading, setProjectionLoading] = useState(false);
  const [projectionError,   setProjectionError]   = useState(false);
  const typedExtraRef = useRef('');

  const [editingBalance, setEditingBalance] = useState<{ billId: number | null; value: string }>({ billId: null, value: '' });

  const [startingPlan,      setStartingPlan]      = useState(false);
  const [readinessWarnOpen, setReadinessWarnOpen] = useState(false);
  const [draggingId,        setDraggingId]        = useState<number | null>(null);
  const [dropTargetId,      setDropTargetId]      = useState<number | null>(null);

  // ── loading ───────────────────────────────────────────────────────────────
  const loadProjection = useCallback(async () => {
    setProjectionLoading(true);
    try {
      const extra = parseFloat(typedExtraRef.current);
      const params = Number.isFinite(extra) && extra >= 0 ? { extra } : {};
      setProjection(await api.snowballProjection(params));
      setProjectionError(false);
    } catch {
      setProjectionError(true);
    } finally { setProjectionLoading(false); }
  }, []);

  const load = useCallback(() => Promise.all([
    queryClient.invalidateQueries({ queryKey: ['snowball'] }),
    queryClient.invalidateQueries({ queryKey: ['categories'] }),
    queryClient.invalidateQueries({ queryKey: ['snowball-settings'] }),
  ]), [queryClient]);

  // Seed the editable settings form from the loaded settings whenever they change.
  useEffect(() => {
    if (!settings) return;
    setDirty(false);
    const ep = Number(settings.extra_payment) > 0 ? String(settings.extra_payment) : '';
    setRamseyMode(settings.ramsey_mode !== false);
    setReadyCurrentOnBills(!!settings.ready_current_on_bills);
    setReadyEmergencyFund(!!settings.ready_emergency_fund);
    setExtraPayment(ep);
    extraPaymentRef.current = ep;
  }, [settings]);

  // The live projection stays a debounced client computation (not page data).
  useEffect(() => { loadProjection(); }, [loadProjection]);

  // ── auto-arrange ──────────────────────────────────────────────────────────
  const handleAutoArrange = () => {
    setBills(prev => sortRamseyDebts(prev));
    setDirty(true);
    toast.success('Arranged smallest-to-largest balance');
  };

  const moveDebt = (fromIndex: number, toIndex: number) => {
    if (ramseyMode || saving || fromIndex === toIndex) return;
    setBills(prev => moveInArray(prev, fromIndex, toIndex));
    setDirty(true);
  };

  const dragPropsFor = (bill: Bill): DebtDragProps => {
    if (ramseyMode || saving) return { draggable: false };
    return {
      draggable: true,
      isDragging: draggingId === bill.id,
      isDropTarget: dropTargetId === bill.id && draggingId !== bill.id,
      onDragStart: (event: DragEvent<HTMLDivElement>) => {
        setDraggingId(bill.id);
        event.dataTransfer.effectAllowed = 'move';
        event.dataTransfer.setData('text/plain', String(bill.id));
      },
      onDragEnter: () => {
        if (draggingId && draggingId !== bill.id) setDropTargetId(bill.id);
      },
      onDragOver: (event: DragEvent<HTMLDivElement>) => {
        event.preventDefault();
        event.dataTransfer.dropEffect = 'move';
        if (draggingId && draggingId !== bill.id) setDropTargetId(bill.id);
      },
      onDrop: (event: DragEvent<HTMLDivElement>) => {
        event.preventDefault();
        const sourceId = Number(event.dataTransfer.getData('text/plain') || draggingId);
        const fromIndex = bills.findIndex(item => item.id === sourceId);
        if (fromIndex >= 0) moveDebt(fromIndex, bills.findIndex(item => item.id === bill.id));
        setDraggingId(null);
        setDropTargetId(null);
      },
      onDragEnd: () => {
        setDraggingId(null);
        setDropTargetId(null);
      },
    };
  };

  // ── save order ────────────────────────────────────────────────────────────
  const handleSaveOrder = async () => {
    setSaving(true);
    try {
      await api.saveSnowballOrder(bills.map((b, i) => ({ id: b.id, snowball_order: i })));
      setDirty(false);
      toast.success('Order saved');
      loadProjection();
    } catch (err) { toast.error(errMessage(err, 'Failed to save order')); }
    finally { setSaving(false); }
  };

  // ── extra payment ─────────────────────────────────────────────────────────
  const handleSaveExtraPayment = async () => {
    const val = extraPayment.trim();
    if (val !== '' && (isNaN(parseFloat(val)) || parseFloat(val) < 0)) {
      toast.error('Extra payment must be a non-negative number'); return;
    }
    if (val === extraPaymentRef.current) return;
    setSavingSettings(true);
    try {
      const result = await api.saveSnowballSettings({ extra_payment: val === '' ? 0 : parseFloat(val) });
      const saved = Number(result.extra_payment) > 0 ? String(result.extra_payment) : '';
      extraPaymentRef.current = saved;
      setExtraPayment(saved);
      toast.success('Extra payment saved');
      loadProjection();
    } catch (err) { toast.error(errMessage(err, 'Failed to save')); }
    finally { setSavingSettings(false); }
  };

  const handleRamseyModeChange = async (checked: boolean) => {
    setSavingSettings(true);
    try {
      const result = await api.saveSnowballSettings({ ramsey_mode: checked });
      const nextMode = result.ramsey_mode !== false;
      setRamseyMode(nextMode);
      setDirty(false);
      if (nextMode) setBills(prev => sortRamseyDebts(prev));
      else load();
      loadProjection();
      toast.success(nextMode ? 'Ramsey Mode enabled' : 'Custom order enabled');
    } catch (err) {
      toast.error(errMessage(err, 'Failed to save Snowball mode'));
    } finally {
      setSavingSettings(false);
    }
  };

  const handleReadinessToggle = async (id: string, checked: boolean) => {
    const payload = id === 'current_on_bills'
      ? { ready_current_on_bills: checked }
      : { ready_emergency_fund: checked };
    const previous = id === 'current_on_bills' ? readyCurrentOnBills : readyEmergencyFund;

    if (id === 'current_on_bills') setReadyCurrentOnBills(checked);
    else setReadyEmergencyFund(checked);

    setSavingSettings(true);
    try {
      const result = await api.saveSnowballSettings(payload);
      setReadyCurrentOnBills(!!result.ready_current_on_bills);
      setReadyEmergencyFund(!!result.ready_emergency_fund);
    } catch (err) {
      if (id === 'current_on_bills') setReadyCurrentOnBills(previous);
      else setReadyEmergencyFund(previous);
      toast.error(errMessage(err, 'Failed to save readiness'));
    } finally {
      setSavingSettings(false);
    }
  };

  // ── inline balance edit ───────────────────────────────────────────────────
  const startEditBalance = (bill: Bill) =>
    setEditingBalance({ billId: bill.id, value: bill.current_balance != null ? String(bill.current_balance) : '' });

  const commitBalance = async (billId: number) => {
    const raw = editingBalance.value.trim();
    const num = raw === '' ? null : parseFloat(raw);
    if (raw !== '' && (num == null || isNaN(num) || num < 0)) { toast.error('Balance must be a non-negative number'); return; }
    const current = bills.find(b => b.id === billId);
    if (num === current?.current_balance) { setEditingBalance({ billId: null, value: '' }); return; }
    try {
      await api.updateBillBalance(billId, num);
      setBills(prev => {
        const next = prev.map(b => b.id === billId ? { ...b, current_balance: (num == null ? null : asDollars(num)) } : b);
        return ramseyMode ? sortRamseyDebts(next) : next;
      });
      setEditingBalance({ billId: null, value: '' });
      loadProjection();
    } catch (err) { toast.error(errMessage(err, 'Failed to update balance')); }
  };

  const removeFromSnowball = async (bill: Bill) => {
    try {
      await api.updateBillSnowball(bill.id, { snowball_include: false, snowball_exempt: true });
      setBills(prev => prev.filter(b => b.id !== bill.id));
      setDirty(true);
      toast.success(`${bill.name} removed from Snowball`);
      loadProjection();
    } catch (err) {
      toast.error(errMessage(err, 'Failed to remove bill from Snowball'));
    }
  };

  // ── live projection (debounced server call as user types extra amount) ──────
  useEffect(() => {
    typedExtraRef.current = extraPayment; // keep loadProjection() in sync with the input
    const t = setTimeout(async () => {
      const extra = parseFloat(extraPayment);
      const params = Number.isFinite(extra) && extra >= 0 ? { extra } : {};
      try {
        setProjectionLoading(true);
        const result = await api.snowballProjection(params);
        setProjection(result);
        setProjectionError(false);
      } catch {
        setProjectionError(true); // keep last projection, but surface the failure
      } finally {
        setProjectionLoading(false);
      }
    }, 220);
    return () => clearTimeout(t);
  }, [extraPayment]);

  // Attack card payoff date and display projection come directly from server result
  const liveAttackPayoff = projection?.snowball?.debts?.[0]?.payoff_display ?? null;
  const displayProjection = projection;

  // ── plan lifecycle ────────────────────────────────────────────────────────
  const handleStartPlan = async () => {
    setStartingPlan(true);
    try {
      const plan = await api.startSnowballPlan({ method: ramseyMode ? 'snowball' : 'custom' }) as SnowballPlan;
      setActivePlan(plan);
      setAllPlans(prev => [plan, ...prev.filter(p => !['active', 'paused'].includes(p.status))]);
      toast.success('Snowball plan started!');
    } catch (err) { toast.error(errMessage(err, 'Failed to start plan')); }
    finally { setStartingPlan(false); }
  };

  const handlePausePlan = async () => {
    if (!activePlan) return;
    try {
      const updated = await api.pauseSnowballPlan(activePlan.id) as SnowballPlan;
      setActivePlan(updated);
      setAllPlans(prev => prev.map(p => p.id === updated.id ? updated : p));
      toast.success('Plan paused');
    } catch (err) { toast.error(errMessage(err, 'Failed to pause')); }
  };

  const handleResumePlan = async () => {
    if (!activePlan) return;
    try {
      const updated = await api.resumeSnowballPlan(activePlan.id) as SnowballPlan;
      setActivePlan(updated);
      setAllPlans(prev => prev.map(p => p.id === updated.id ? updated : p));
      toast.success('Plan resumed');
    } catch (err) { toast.error(errMessage(err, 'Failed to resume')); }
  };

  const handleCompletePlan = async () => {
    if (!activePlan) return;
    try {
      const updated = await api.completeSnowballPlan(activePlan.id) as SnowballPlan;
      setActivePlan(null);
      setAllPlans(prev => prev.map(p => p.id === updated.id ? updated : p));
      toast.success('Plan marked as complete!');
    } catch (err) { toast.error(errMessage(err, 'Failed to complete plan')); }
  };

  const handleAbandonPlan = async () => {
    if (!activePlan) return;
    try {
      const updated = await api.abandonSnowballPlan(activePlan.id) as SnowballPlan;
      setActivePlan(null);
      setAllPlans(prev => prev.map(p => p.id === updated.id ? updated : p));
      toast.success('Plan abandoned');
    } catch (err) { toast.error(errMessage(err, 'Failed to abandon plan')); }
  };

  const handleStartPlanClick = () => {
    if (readinessAllReady) { handleStartPlan(); }
    else { setReadinessWarnOpen(true); }
  };

  // ── stats ─────────────────────────────────────────────────────────────────
  const totalBalance    = bills.reduce((s, b) => s + (b.current_balance || 0), 0);
  const totalMinPayment = bills.reduce((s, b) => s + (b.minimum_payment || 0), 0);
  const unknownCount    = bills.filter(b => b.current_balance == null).length;
  const missingMinCount = bills.filter(b => Number(b.current_balance) > 0 && b.minimum_payment == null).length;
  const extraAmt        = parseFloat(extraPayment) || 0;
  const attackBill      = bills[0] ?? null;
  const attackAmount    = attackBill ? (attackBill.minimum_payment || 0) + extraAmt : 0;
  const customOrderDrift = !ramseyMode && bills.length > 1 && !isRamseyOrdered(bills);
  const mortgageIncluded = bills.some(b => /mortgage|housing/i.test(b.category_name || b.name || ''));
  const readinessItems: ReadinessItem[] = [
    {
      id: 'current_on_bills',
      label: 'Current on bills',
      ready: readyCurrentOnBills,
      manual: true,
      hint: 'Confirm all bills are current before starting the snowball.',
    },
    {
      id: 'emergency_fund',
      label: '$1,000 emergency fund',
      ready: readyEmergencyFund,
      manual: true,
      hint: 'Confirm the starter emergency fund is set aside.',
    },
    {
      id: 'debts_entered',
      label: 'Debts entered',
      ready: bills.length > 0 && !mortgageIncluded,
      hint: mortgageIncluded
        ? 'Mortgage or housing debt is included; Ramsey Baby Step 2 excludes the house.'
        : 'Enter consumer debts and leave the mortgage out of this snowball.',
    },
    {
      id: 'minimums_entered',
      label: 'Minimums entered',
      ready: bills.length > 0 && missingMinCount === 0,
      hint: 'Every debt with a balance needs a minimum payment.',
    },
    {
      id: 'extra_set',
      label: 'Extra amount set',
      ready: extraAmt > 0,
      hint: 'Set the extra monthly budget you can throw at the current target.',
    },
  ];
  const readinessReadyCount = readinessItems.filter(item => item.ready).length;
  const readinessAllReady = readinessReadyCount === readinessItems.length;

  // ── loading skeleton ──────────────────────────────────────────────────────
  if (loading) {
    return (
      <div className="space-y-6">
        <Skeleton className="h-8 w-48" />
        <div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
          {[...Array(4)].map((_, i) => <Skeleton key={i} className="h-24 rounded-xl" />)}
        </div>
        <div className="space-y-2">
          {[...Array(3)].map((_, i) => <Skeleton key={i} className="h-24 rounded-xl" />)}
        </div>
      </div>
    );
  }

  if (loadError) {
    return (
      <div className="flex flex-col items-center justify-center py-24 text-center rounded-xl border border-destructive/20 bg-destructive/5">
        <AlertCircle className="h-10 w-10 text-destructive mb-3" />
        <p className="text-sm font-medium text-foreground">Failed to load snowball data</p>
        <p className="mt-1 text-xs text-muted-foreground">{loadError}</p>
        <Button size="sm" variant="outline" onClick={load}
          className="mt-4 gap-1.5 text-xs border-destructive/30 hover:bg-destructive/10 hover:text-destructive hover:border-destructive/50">
          <RefreshCw className="h-3 w-3" />
          Try again
        </Button>
      </div>
    );
  }

  const inp = 'bg-background/50 border-border/60 h-9 text-sm font-mono';

  return (
    <div className="space-y-6">

      {/* Active plan banner */}
      {activePlan && (
        <PlanStatusBanner
          plan={activePlan as unknown as ActivePlan}
          onPause={handlePausePlan}
          onResume={handleResumePlan}
          onComplete={handleCompletePlan}
          onAbandon={handleAbandonPlan}
          onNewPlan={handleStartPlan}
        />
      )}

      {/* Header */}
      <div>
        <h1 className="text-2xl font-semibold tracking-tight flex items-center gap-2">
          <TrendingDown className="h-6 w-6 text-primary" />
          Debt Snowball
        </h1>
        <p className="text-sm text-muted-foreground mt-1">
          Dave Ramsey method — attack the smallest balance first, roll payments as each debt clears.
          Marking a payment automatically reduces the outstanding balance.
        </p>
      </div>

      {/* Stats */}
      {bills.length > 0 && (
        <>
          <SectionDivider label="Overview" />
          <div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
            <StatCard label="Total Debt" value={fmt(totalBalance)}
              sub={unknownCount > 0 ? `+ ${unknownCount} unknown` : undefined} />
            <StatCard label="Monthly Minimums" value={fmt(totalMinPayment)} />
            <StatCard label="Extra / Month" value={extraAmt > 0 ? fmt(extraAmt) : '—'} sub="snowball accelerator" />
            <StatCard label="Next Win" value={liveAttackPayoff || 'Add details'}
              sub={attackBill ? `${attackBill.name} · attacking ${fmt(attackAmount)}/mo` : undefined}
              highlight={!!liveAttackPayoff} />
          </div>
        </>
      )}

      {/* Settings + Readiness */}
      {bills.length > 0 && (
        <>
          <SectionDivider label="Configuration" />
          <div className="space-y-3">
            <div className="flex flex-wrap items-end gap-3">
              <div className="surface-elevated rounded-xl px-4 py-3 flex items-center gap-3 min-h-[58px]">
                <Switch
                  id="ramsey-mode"
                  checked={ramseyMode}
                  onCheckedChange={handleRamseyModeChange}
                  disabled={savingSettings}
                />
                <div>
                  <Label htmlFor="ramsey-mode" className="text-xs font-semibold cursor-pointer">Ramsey Mode</Label>
                  <p className="text-[10px] text-muted-foreground">
                    {ramseyMode ? 'Smallest balance first' : 'Custom drag order'}
                  </p>
                </div>
              </div>
              <div className="surface-elevated min-h-[58px] rounded-xl border border-primary/25 bg-primary/[0.04] px-4 py-3 shadow-sm shadow-primary/5">
                <div className="flex items-center justify-between gap-4">
                  <div>
                    <Label htmlFor="extra-payment" className="flex items-center gap-1.5 text-[10px] font-semibold uppercase tracking-wider text-primary">
                      <Zap className="h-3.5 w-3.5" />
                      Extra applied monthly
                    </Label>
                    <p className="mt-0.5 text-[10px] text-muted-foreground">Added to the current target debt.</p>
                  </div>
                  <div className="text-right">
                    <p className="tracker-number text-base font-bold text-primary">{extraAmt > 0 ? fmt(extraAmt) : '$0'}</p>
                    <p className="text-[10px] text-muted-foreground">per month</p>
                  </div>
                </div>
                <Input
                  id="extra-payment"
                  type="number" min="0" step="1" placeholder="0.00"
                  value={extraPayment}
                  onChange={e => setExtraPayment(e.target.value)}
                  onBlur={handleSaveExtraPayment}
                  className={cn(inp, 'mt-2 w-full border-primary/25 bg-background/75')}
                  disabled={savingSettings}
                />
              </div>
              <div className="flex items-center gap-2 pb-0.5">
                <Button
                  type="button"
                  variant="outline"
                  size="sm"
                  onClick={handleAutoArrange}
                  className="gap-2"
                  disabled={ramseyMode}
                >
                  <Zap className="h-3.5 w-3.5" /> Restore Ramsey Order
                </Button>
                <Button type="button" size="sm" disabled={ramseyMode || !dirty || saving} onClick={handleSaveOrder} className="gap-2">
                  <Save className="h-3.5 w-3.5" /> {saving ? 'Saving…' : 'Save Order'}
                </Button>
                {dirty && <span className="text-xs text-amber-400">Unsaved changes</span>}
              </div>
            </div>

            <ReadinessStrip
              items={readinessItems}
              readyCount={readinessReadyCount}
              totalCount={readinessItems.length}
              allReady={readinessAllReady}
              onToggle={handleReadinessToggle}
              disabled={savingSettings}
            />
            {!activePlan && readinessReadyCount >= 3 && (
              <div className="flex justify-end">
                <Button size="sm" onClick={handleStartPlanClick} disabled={startingPlan} className="gap-1.5">
                  <Zap className="h-3.5 w-3.5" />
                  {startingPlan ? 'Starting…' : 'Start Snowball Plan'}
                </Button>
              </div>
            )}
          </div>
        </>
      )}

      {bills.length > 0 && (customOrderDrift || missingMinCount > 0) && (
        <div className="surface-elevated rounded-xl border border-amber-500/25 bg-amber-500/10 px-4 py-3 space-y-2">
          {customOrderDrift && (
            <div className="flex items-start gap-2 text-xs text-amber-300">
              <AlertTriangle className="h-3.5 w-3.5 mt-0.5 shrink-0" />
              Custom order is active, so the payoff list no longer strictly follows smallest-balance-first.
            </div>
          )}
          {missingMinCount > 0 && (
            <div className="flex items-start gap-2 text-xs text-amber-300">
              <AlertTriangle className="h-3.5 w-3.5 mt-0.5 shrink-0" />
              {missingMinCount} debt{missingMinCount > 1 ? 's need' : ' needs'} a minimum payment before the projection can model the snowball cleanly.
            </div>
          )}
        </div>
      )}

      {/* Empty state */}
      {bills.length === 0 && (
        <div className="flex flex-col items-center justify-center rounded-xl bg-muted/20 py-20 text-center gap-3">
          <TrendingDown className="h-10 w-10 text-muted-foreground/30" />
          <p className="text-sm font-medium text-muted-foreground">No debt bills found</p>
          <p className="text-xs text-muted-foreground/70 max-w-sm">
            Bills in Credit Cards, Loans, or Debt categories appear here automatically.
            You can also enable "Include in Snowball" when editing any bill.
          </p>
        </div>
      )}

      {/* Cards + projection */}
      {bills.length > 0 && (
        <>
        <SectionDivider label="Attack Order" />
        <div className="grid gap-6 lg:grid-cols-[1fr_340px]">

          {/* Cards list */}
          <div className="space-y-2">
            {bills.map((bill, index) => {
              const isAttack      = index === 0;
              const isEditingBal  = editingBalance.billId === bill.id;
              const dragProps = dragPropsFor(bill);

              // Pull this debt's payoff info from the live projection (attack card only)
              const attackProjection = isAttack
                ? displayProjection?.snowball?.debts?.[0]
                : null;

              return (
                <div
                  key={bill.id}
                  draggable={dragProps?.draggable}
                  onDragStart={dragProps?.onDragStart}
                  onDragEnter={dragProps?.onDragEnter}
                  onDragOver={dragProps?.onDragOver}
                  onDragEnd={dragProps?.onDragEnd}
                  onDrop={dragProps?.onDrop}
                  className={cn(
                    'surface-elevated rounded-xl border select-none transition-colors duration-150',
                    isAttack ? 'border-emerald-500/30 bg-emerald-950/5' : 'border-border/40',
                    dragProps?.isDragging && 'opacity-45',
                    dragProps?.isDropTarget && 'ring-2 ring-inset ring-primary/35',
                  )}
                >
                  <div className="flex items-stretch">

                    {/* Grip */}
                    <div className="flex items-center gap-0.5 px-3 transition-colors touch-none shrink-0">
                      <div
                        className={cn(
                          'transition-colors',
                          ramseyMode
                            ? 'text-muted-foreground/10 cursor-not-allowed'
                            : 'text-muted-foreground/55 hover:text-muted-foreground/80 cursor-grab active:cursor-grabbing',
                        )}
                        title={ramseyMode ? 'Ramsey Mode controls order' : 'Drag to reorder'}
                      >
                        <GripVertical className="h-5 w-5" />
                      </div>
                      <div className="flex flex-col">
                        <button
                          type="button"
                          onClick={() => moveDebt(index, index - 1)}
                          disabled={ramseyMode || saving || index === 0}
                          className="rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-25"
                          title="Move debt up"
                          aria-label={`Move ${bill.name} up`}
                        >
                          <ArrowUp className="h-3 w-3" />
                        </button>
                        <button
                          type="button"
                          onClick={() => moveDebt(index, index + 1)}
                          disabled={ramseyMode || saving || index === bills.length - 1}
                          className="rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-25"
                          title="Move debt down"
                          aria-label={`Move ${bill.name} down`}
                        >
                          <ArrowDown className="h-3 w-3" />
                        </button>
                      </div>
                    </div>

                    {/* Main content */}
                    <div className="flex-1 py-3 min-w-0">

                      {/* Name row */}
                      <div className="flex items-center gap-2 min-w-0">
                        {isAttack ? (
                          <span className="inline-flex items-center gap-1 rounded-full bg-emerald-500/15 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-emerald-400 shrink-0">
                            <Zap className="h-2.5 w-2.5" /> Now
                          </span>
                        ) : (
                          <span className="text-xs font-semibold text-muted-foreground/50 tabular-nums shrink-0 w-5">
                            #{index + 1}
                          </span>
                        )}
                        <span className="font-semibold text-sm truncate">{bill.name}</span>
                        {bill.category_name && (
                          <span className="text-[10px] text-muted-foreground/60 shrink-0 hidden sm:inline">
                            {bill.category_name}
                          </span>
                        )}
                      </div>

                      {/* Stats row */}
                      <div className="mt-1.5 flex flex-wrap gap-x-4 gap-y-1 text-sm items-center">

                        {/* Balance — inline editable */}
                        <div className="flex items-center gap-1.5">
                          <span className="text-xs text-muted-foreground">Balance</span>
                          {isEditingBal ? (
                            <Input
                              autoFocus
                              type="number" min="0" step="0.01"
                              value={editingBalance.value}
                              onChange={e => setEditingBalance(p => ({ ...p, value: e.target.value }))}
                              onBlur={() => commitBalance(bill.id)}
                              onKeyDown={e => {
                                if (e.key === 'Enter')  e.currentTarget.blur();
                                if (e.key === 'Escape') setEditingBalance({ billId: null, value: '' });
                              }}
                              className={cn(inp, 'h-7 w-28 text-xs py-0 px-2')}
                            />
                          ) : (
                            <button
                              type="button"
                              onClick={() => startEditBalance(bill)}
                              title="Click to update balance"
                              className={cn(
                                'font-semibold tabular-nums rounded px-1 -mx-1 hover:bg-muted/60 transition-colors',
                                isAttack && bill.current_balance != null && 'text-emerald-400',
                                bill.current_balance == null && 'text-muted-foreground/50 italic text-xs',
                              )}
                            >
                              {bill.current_balance != null ? fmt(bill.current_balance) : 'add balance'}
                            </button>
                          )}
                        </div>

                        {bill.minimum_payment != null && (
                          <div>
                            <span className="text-xs text-muted-foreground">Min </span>
                            <span className="font-medium tabular-nums">{fmt(bill.minimum_payment)}</span>
                          </div>
                        )}

                        {Number(bill.current_balance) > 0 && bill.minimum_payment == null && (
                          <div className="inline-flex items-center gap-1 text-xs text-amber-400">
                            <AlertTriangle className="h-3 w-3" />
                            Needs minimum payment
                          </div>
                        )}

                        {isAttack && extraAmt > 0 && (
                          <div>
                            <span className="text-xs text-muted-foreground">Throwing </span>
                            <span className="font-semibold tabular-nums text-emerald-400">
                              {fmt((bill.minimum_payment || 0) + extraAmt)}
                            </span>
                            <span className="text-xs text-muted-foreground"> /mo</span>
                          </div>
                        )}

                        {bill.interest_rate != null && (
                          <div>
                            <span className="text-xs text-muted-foreground">APR </span>
                            <span className={cn(
                              'font-medium tabular-nums',
                              bill.interest_rate >= 25 ? 'text-rose-400' :
                              bill.interest_rate >= 15 ? 'text-amber-400' :
                              'text-muted-foreground',
                            )}>
                              {bill.interest_rate}%
                            </span>
                          </div>
                        )}

                        <div>
                          <span className="text-xs text-muted-foreground">Due </span>
                          <span className="font-medium">{ordinal(bill.due_day)}</span>
                        </div>
                      </div>

                      {/* Attack payoff line — date is live (updates while typing), interest from server */}
                      {isAttack && (liveAttackPayoff || attackProjection?.payoff_display) && (
                        <div className="mt-1.5 flex items-center gap-1.5 text-xs text-emerald-400/80">
                          <span className="font-medium">
                            ↳ Clears {liveAttackPayoff ?? attackProjection?.payoff_display}
                          </span>
                          {Number(attackProjection?.total_interest) > 0 && (
                            <span className="text-muted-foreground">
                              · {fmtCompact(attackProjection?.total_interest)} interest
                            </span>
                          )}
                        </div>
                      )}
                    </div>

                    {/* Action icons — fixed right column */}
                    <div className="flex flex-col items-center justify-center gap-1 px-3 shrink-0">
                      <button
                        type="button"
                        onClick={() => setEditBill({ bill })}
                        title="Edit bill"
                        className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-muted/70 hover:text-foreground"
                      >
                        <PenLine className="h-3.5 w-3.5" />
                      </button>
                      <button
                        type="button"
                        onClick={() => removeFromSnowball(bill)}
                        title="Hide from Snowball"
                        className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-amber-500/10 hover:text-amber-400"
                      >
                        <EyeOff className="h-3.5 w-3.5" />
                      </button>
                    </div>

                  </div>
                </div>
              );
            })}

            <p className="pt-1 text-center text-xs font-medium text-muted-foreground">
              {ramseyMode
                ? 'Ramsey Mode keeps debts sorted by smallest balance · Click a balance to update it'
                : 'Drag the grip handle to reorder · Click a balance to update it · Save Order to persist'}
            </p>
          </div>

          {/* Projection (sticky sidebar on large screens) */}
          <div className="lg:sticky lg:top-24 lg:self-start">
            <ProjectionPanel
              projection={displayProjection}
              projectionLoading={projectionLoading}
              projectionError={projectionError}
              onRetry={loadProjection}
              billCount={bills.length}
            />
          </div>
        </div>
        </>
      )}

      {/* Plan history */}
      <PlanHistoryPanel plans={allPlans} />

      {/* Readiness warning dialog */}
      <AlertDialog.Root open={readinessWarnOpen} onOpenChange={setReadinessWarnOpen}>
        <AlertDialog.Portal>
          <AlertDialog.Overlay className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm" />
          <AlertDialog.Content className="fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2 w-full max-w-md rounded-xl border border-border bg-popover p-6 shadow-2xl space-y-4 focus:outline-none">
            <div className="flex items-center gap-2">
              <AlertTriangle className="h-4 w-4 text-amber-400 shrink-0" />
              <AlertDialog.Title className="text-base font-semibold">
                Checklist not complete
              </AlertDialog.Title>
            </div>
            <AlertDialog.Description asChild>
              <div className="text-sm text-muted-foreground space-y-3">
                <p>The following readiness items are still pending:</p>
                <ul className="space-y-1.5">
                  {readinessItems.filter(i => !i.ready).map(item => (
                    <li key={item.id} className="flex items-center gap-2 text-amber-400 text-xs">
                      <AlertTriangle className="h-3.5 w-3.5 shrink-0" />
                      {item.label}
                    </li>
                  ))}
                </ul>
                <p>Starting now may affect your plan&apos;s accuracy. You can still proceed.</p>
              </div>
            </AlertDialog.Description>
            <div className="flex gap-2 justify-end pt-1">
              <AlertDialog.Cancel asChild>
                <Button variant="outline" size="sm">Go back</Button>
              </AlertDialog.Cancel>
              <AlertDialog.Action asChild>
                <Button
                  size="sm"
                  className="gap-1.5"
                  onClick={() => { setReadinessWarnOpen(false); handleStartPlan(); }}
                >
                  <Zap className="h-3.5 w-3.5" />
                  Start anyway
                </Button>
              </AlertDialog.Action>
            </div>
          </AlertDialog.Content>
        </AlertDialog.Portal>
      </AlertDialog.Root>

      {/* Edit modal */}
      {editBill && (
        <BillModal
          key={editBill.bill?.id ? `edit-${editBill.bill.id}` : `new-${editBill.initialBill?.name || 'blank'}`}
          bill={editBill.bill}
          initialBill={editBill.initialBill}
          categories={categories}
          onClose={() => setEditBill(null)}
          onSave={() => { setEditBill(null); load(); loadProjection(); }}
          onDuplicate={bill => setEditBill({ bill: null, initialBill: makeBillDraft(bill, { copy: true, categories }) as Partial<Bill> })}
        />
      )}
    </div>
  );
}
