import { useState, useEffect, useMemo, useCallback } from 'react';
import { useSearchParams } from 'react-router-dom';
import { ChevronLeft, ChevronRight, AlertCircle, CheckCircle2, Plus, Search, RefreshCw, Landmark, ArrowUpToLine, ArrowUp, ArrowDown } from 'lucide-react';
import { toast } from 'sonner';
import { api } from '@/api.js';
import { useTracker, useDriftReport } from '@/hooks/useQueries';
import { useSearchPanelPreference } from '@/hooks/useSearchPanelPreference';
import BillModal from '@/components/BillModal';
import { makeBillDraft } from '@/lib/billDrafts';
import { scheduleLabel, scheduleValue } from '@/lib/billingSchedule';
import { cn, fmt } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import SearchFilterPanel from '@/components/SearchFilterPanel';
import { Skeleton } from '@/components/ui/Skeleton';
import {
  Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
} from '@/components/ui/select';
import {
  Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription,
} from '@/components/ui/dialog';

import StartingAmountsEditDialog from '@/components/tracker/StartingAmountsEditDialog';
import OverdueCommandCenter from '@/components/tracker/OverdueCommandCenter';
import DriftInsightPanel from '@/components/tracker/DriftInsightPanel';
import {
  MONTHS, FILTER_ALL,
  paymentDateForTrackerMonth, amountSearchText, rowEffectiveStatus, rowIsPaid, rowIsDebt,
  TRACKER_SORT_DEFAULT, TRACKER_SORT_ASC, TRACKER_SORT_DESC, TRACKER_SORT_OPTIONS,
  TRACKER_SORT_DEFAULT_DIRS, TRACKER_SORT_LABELS,
  normalizeTrackerSortKey, normalizeTrackerSortDir, sortTrackerRows,
} from '@/lib/trackerUtils';
import { FilterChip } from '@/components/tracker/FilterChip';
import { SummaryCard, TrendCard } from '@/components/tracker/SummaryCards';
import { PaymentLedgerDialog } from '@/components/tracker/PaymentLedgerDialog';
import { TrackerBucket as Bucket } from '@/components/tracker/TrackerBucket';
import IncomeBreakdownModal from '@/components/IncomeBreakdownModal';


function fmtBalanceAge(isoStr) {
  if (!isoStr) return null;
  return new Date(isoStr).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
}

// ── Main page ──────────────────────────────────────────────────────────────
function LateAttributionDialog({ attr, remaining, busy, onAccept, onDismiss }) {
  if (!attr) return null;
  const fmtDate = (d) => new Date(d + 'T00:00:00').toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
  const priorMonth = new Date(attr.suggested_date + 'T00:00:00').toLocaleDateString('en-US', { month: 'long' });
  return (
    <Dialog open onOpenChange={onDismiss}>
      <DialogContent className="sm:max-w-md">
        <DialogHeader>
          <DialogTitle>Payment posted after month end</DialogTitle>
          <DialogDescription>
            A <strong>{attr.bill_name}</strong> payment of <strong>{fmt(attr.amount)}</strong> posted on{' '}
            <strong>{fmtDate(attr.original_date)}</strong> — after the previous month closed.
            Should it count for {priorMonth}?
          </DialogDescription>
        </DialogHeader>
        <div className="rounded-lg border border-border/60 bg-muted/30 p-3 space-y-1">
          <p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">What this does</p>
          <p className="text-sm">Moves the paid date to <strong>{fmtDate(attr.suggested_date)}</strong> so it appears in the prior month's tracker. Amount and bank link are unchanged.</p>
        </div>
        {remaining > 0 && (
          <p className="text-xs text-muted-foreground">{remaining} more similar payment{remaining > 1 ? 's' : ''} to review after this.</p>
        )}
        <DialogFooter className="gap-2 sm:gap-0">
          <Button variant="outline" onClick={onDismiss} disabled={busy}>
            Keep as {fmtDate(attr.original_date).replace(/,?\s*\d{4}/, '').trim()}
          </Button>
          <Button onClick={() => onAccept(attr)} disabled={busy}>
            {busy ? 'Moving…' : `Count for ${priorMonth}`}
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

export default function TrackerPage() {
  const [searchParams, setSearchParams] = useSearchParams();
  const now = new Date();

  // All navigation + filter state lives in the URL so views are bookmarkable/shareable.
  const year   = Number(searchParams.get('year'))  || now.getFullYear();
  const month  = Number(searchParams.get('month')) || (now.getMonth() + 1);
  const search = searchParams.get('q') || '';
  const filters = {
    category:        searchParams.get('fc') || FILTER_ALL,
    cycle:           searchParams.get('cy') || FILTER_ALL,
    autopay:         searchParams.get('ap') === '1',
    firstBucket:     searchParams.get('b1') === '1',
    fifteenthBucket: searchParams.get('b2') === '1',
    unpaid:          searchParams.get('un') === '1',
    overdue:         searchParams.get('ov') === '1',
    debt:            searchParams.get('de') === '1',
  };
  const sortKey = normalizeTrackerSortKey(searchParams.get('sort') || TRACKER_SORT_DEFAULT);
  const hasSort = sortKey !== TRACKER_SORT_DEFAULT;
  const sortDir = hasSort
    ? normalizeTrackerSortDir(searchParams.get('dir') || TRACKER_SORT_DEFAULT_DIRS[sortKey] || TRACKER_SORT_ASC)
    : TRACKER_SORT_ASC;

  // replace: true keeps history clean for rapid navigation (e.g. search keystrokes)
  const updateParams = useCallback((patch) => {
    setSearchParams(prev => {
      const next = new URLSearchParams(prev);
      Object.entries(patch).forEach(([k, v]) => {
        if (v == null || v === '' || v === false) next.delete(k);
        else next.set(k, v === true ? '1' : String(v));
      });
      return next;
    }, { replace: true });
  }, [setSearchParams]);

  // Edit Bill modal: { bill, categories } when open, null when closed
  const [bankSyncStatus,    setBankSyncStatus]    = useState(null);
  const [bankSyncing,       setBankSyncing]       = useState(false);
  const [lateAttributions,  setLateAttributions] = useState([]); // pending month-attribution prompts
  const [attrBusy,          setAttrBusy]         = useState(null); // payment_id being resolved
  const [pinUpcoming,    setPinUpcoming]    = useState(() => localStorage.getItem('tracker_pin_upcoming') === 'true');
  const [editBillData, setEditBillData] = useState(null);
  // Edit Starting Amounts modal: true when open, false when closed
  const [editStartingOpen, setEditStartingOpen] = useState(false);
  const [incomeModalOpen,  setIncomeModalOpen]  = useState(false);
  const [orderedRows, setOrderedRows] = useState(null);
  const [movingBillId, setMovingBillId] = useState(null);
  const [searchPanelCollapsed, setSearchPanelCollapsed] = useSearchPanelPreference();

  // Row to open in PaymentLedgerDialog via the overdue command center
  const [commandCenterPayRow, setCommandCenterPayRow] = useState(null);

  // Use React Query for data fetching
  const { data, isLoading: loading, isError, error, refetch, dataUpdatedAt } = useTracker(year, month);
  const { data: driftData, refetch: refetchDrift } = useDriftReport();

  useEffect(() => {
    setOrderedRows(null);
    setMovingBillId(null);
  }, [dataUpdatedAt, year, month]);

  // Load SimpleFIN status once to decide whether to show the sync button
  useEffect(() => {
    api.simplefinStatus()
      .then(setBankSyncStatus)
      .catch(() => setBankSyncStatus(null));
  }, []);

  // Listen for late-attribution events fired by BillModal's single-bill sync
  useEffect(() => {
    function handler(e) {
      const attrs = e.detail?.attributions;
      if (Array.isArray(attrs) && attrs.length > 0) {
        setLateAttributions(prev => [...prev, ...attrs]);
      }
    }
    window.addEventListener('tracker:late-attributions', handler);
    return () => window.removeEventListener('tracker:late-attributions', handler);
  }, []);

  function navigate(delta) {
    let nm = month + delta;
    let ny = year;
    if (nm > 12) { ny += 1; nm = 1; }
    if (nm < 1)  { ny -= 1; nm = 12; }
    updateParams({ year: ny, month: nm });
  }

  async function handleBankSync() {
    setBankSyncing(true);
    try {
      const result = await api.syncAllSources();
      const matched      = result.auto_matched ?? 0;
      const newTx        = result.transactions_new ?? 0;
      const billNames    = result.matched_bills ?? [];
      const attributions = result.late_attributions ?? [];

      if (matched > 0 && billNames.length > 0) {
        toast.success(
          `Synced — ${billNames.join(', ')} ✓` +
          (matched > billNames.length ? ` (+${matched - billNames.length} more)` : ''),
          { duration: 5000 }
        );
      } else if (matched > 0) {
        toast.success(`Synced — ${matched} payment${matched === 1 ? '' : 's'} matched`);
      } else if (newTx > 0) {
        toast.success(`Synced — ${newTx} new transaction${newTx === 1 ? '' : 's'}, no automatic matches`);
      } else {
        toast.success('Synced — no new transactions');
      }

      // Surface late-attribution prompts (payments that just crossed a month boundary)
      if (attributions.length > 0) setLateAttributions(attributions);

      refetch();
    } catch (err) {
      toast.error(err.message || 'Bank sync failed');
    } finally {
      setBankSyncing(false);
    }
  }

  function togglePinUpcoming() {
    setPinUpcoming(prev => {
      const next = !prev;
      localStorage.setItem('tracker_pin_upcoming', String(next));
      return next;
    });
  }

  // Show sync button when SimpleFIN is enabled, connected, and user has matching rules
  const showBankSync = bankSyncStatus?.enabled &&
    bankSyncStatus?.has_connections &&
    bankSyncStatus?.has_merchant_rules;

  async function handleOpenEditBill(row) {
    try {
      const [bill, categories] = await Promise.all([
        api.bill(row.id),
        api.categories(),
      ]);
      setEditBillData({ bill, categories });
    } catch (err) {
      toast.error(err.message);
    }
  }

  async function handleOpenAddBill() {
    try {
      const categories = await api.categories();
      setEditBillData({ bill: null, categories });
    } catch (err) {
      toast.error(err.message || 'Failed to open bill editor');
    }
  }

  function goToday() {
    const n = new Date();
    updateParams({ year: n.getFullYear(), month: n.getMonth() + 1 });
  }

  const rows         = orderedRows || data?.rows || [];
  const summary      = data?.summary || {};
  const bankTracking = data?.bank_tracking;
  const cashflow     = data?.cashflow;
  const toggleFilter = (key) => {
    const paramMap = { autopay: 'ap', firstBucket: 'b1', fifteenthBucket: 'b2', unpaid: 'un', overdue: 'ov', debt: 'de' };
    updateParams({ [paramMap[key]]: !filters[key] });
  };
  const setFilterValue = (key, value) => {
    const paramMap = { category: 'fc', cycle: 'cy' };
    updateParams({ [paramMap[key]]: value === FILTER_ALL ? null : value });
  };
  const setSort = (key) => {
    const normalizedKey = normalizeTrackerSortKey(key);
    if (normalizedKey === TRACKER_SORT_DEFAULT) {
      updateParams({ sort: null, dir: null });
      return;
    }
    updateParams({
      sort: normalizedKey,
      dir: TRACKER_SORT_DEFAULT_DIRS[normalizedKey] || TRACKER_SORT_ASC,
    });
  };
  const toggleSortDirection = () => {
    if (!hasSort) return;
    updateParams({ dir: sortDir === TRACKER_SORT_ASC ? TRACKER_SORT_DESC : TRACKER_SORT_ASC });
  };
  const handleSortHeader = (key) => {
    const normalizedKey = normalizeTrackerSortKey(key);
    if (normalizedKey === TRACKER_SORT_DEFAULT) return;
    if (sortKey === normalizedKey) {
      updateParams({ dir: sortDir === TRACKER_SORT_ASC ? TRACKER_SORT_DESC : TRACKER_SORT_ASC });
      return;
    }
    updateParams({
      sort: normalizedKey,
      dir: TRACKER_SORT_DEFAULT_DIRS[normalizedKey] || TRACKER_SORT_ASC,
    });
  };
  const hasFilters = !!(
    search.trim()
    || filters.category !== FILTER_ALL
    || filters.cycle !== FILTER_ALL
    || filters.autopay
    || filters.firstBucket
    || filters.fifteenthBucket
    || filters.unpaid
    || filters.overdue
    || filters.debt
    || hasSort
  );
  const resetFilters = () => {
    updateParams({ q: null, fc: null, cy: null, ap: null, b1: null, b2: null, un: null, ov: null, de: null, sort: null, dir: null });
  };
  const categoryOptions = useMemo(() => {
    const map = new Map();
    rows.forEach(row => {
      if (row.category_id && row.category_name) map.set(String(row.category_id), row.category_name);
    });
    return Array.from(map, ([id, name]) => ({ id, name })).sort((a, b) => a.name.localeCompare(b.name));
  }, [rows]);
  const cycleOptions = useMemo(() => (
    Array.from(new Set(rows.map(scheduleValue))).sort()
  ), [rows]);
  const filteredRows = useMemo(() => {
    const q = search.trim().toLowerCase();
    return rows.filter(row => {
      const effectiveStatus = rowEffectiveStatus(row);
      if (filters.category !== FILTER_ALL && String(row.category_id ?? '') !== filters.category) return false;
      if (filters.cycle !== FILTER_ALL && scheduleValue(row) !== filters.cycle) return false;
      if (filters.autopay && !row.autopay_enabled) return false;
      if (filters.debt && !rowIsDebt(row)) return false;
      if (filters.unpaid && (row.is_skipped || rowIsPaid(row))) return false;
      if (filters.overdue && !(effectiveStatus === 'late' || effectiveStatus === 'missed')) return false;
      if (filters.firstBucket && !filters.fifteenthBucket && row.bucket !== '1st') return false;
      if (filters.fifteenthBucket && !filters.firstBucket && row.bucket !== '15th') return false;

      if (!q) return true;
      const haystack = [
        row.name,
        row.category_name,
        row.notes,
        row.monthly_notes,
        scheduleValue(row),
        scheduleLabel(row),
        row.bucket,
        row.status,
        amountSearchText(row.expected_amount, row.actual_amount, row.total_paid, row.balance, row.current_balance, row.minimum_payment),
      ].filter(Boolean).join(' ').toLowerCase();
      return haystack.includes(q);
    });
  }, [filters, rows, search]);
  // When pin-upcoming is on, sort by urgency so overdue/due-soon bills surface
  // at the top of each bucket. Bucket split runs after so each bucket is sorted independently.
  const URGENCY_ORDER = { missed: 0, late: 1, due_soon: 2, upcoming: 3 };
  const sortedRows = hasSort
    ? sortTrackerRows(filteredRows, sortKey, sortDir)
    : pinUpcoming
      ? [...filteredRows].sort((a, b) => {
        const ua = URGENCY_ORDER[a.status] ?? 99;
        const ub = URGENCY_ORDER[b.status] ?? 99;
        if (ua !== ub) return ua - ub;
        return (a.due_day ?? 99) - (b.due_day ?? 99);
      })
      : filteredRows;

  const first   = sortedRows.filter(r => r.bucket === '1st');
  const second  = sortedRows.filter(r => r.bucket === '15th');
  const reorderEnabled = !hasFilters && !loading && !isError && !pinUpcoming;

  async function persistTrackerOrder(nextRows, movedBillId) {
    const payload = Object.fromEntries(nextRows.map((row, index) => [row.id, index]));
    setOrderedRows(nextRows);
    setMovingBillId(movedBillId);
    try {
      await api.reorderBills(payload);
      toast.success('Bill order saved');
      refetch();
    } catch (err) {
      setOrderedRows(null);
      toast.error(err.message || 'Failed to save bill order');
    } finally {
      setMovingBillId(null);
    }
  }

  function handleReorderBucket(bucket, orderedBucketRows) {
    const sourceRows = rows;
    const nextRows = [...sourceRows];
    const replacement = [...orderedBucketRows];
    for (let i = 0; i < nextRows.length; i += 1) {
      if (nextRows[i].bucket === bucket) nextRows[i] = replacement.shift();
    }
    const moved = orderedBucketRows.find((row, index) => row.id !== (sourceRows.filter(item => item.bucket === bucket)[index]?.id));
    persistTrackerOrder(nextRows, moved?.id || orderedBucketRows[0]?.id);
  }

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

      {/* ── Header ── */}
      <div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
        <div>
          <p className="text-[11px] font-semibold uppercase tracking-[0.15em] text-muted-foreground mb-0.5">
            Monthly Overview
          </p>
          <h1 className="text-2xl font-bold tracking-tight">
            {MONTHS[month - 1]}
            <span className="text-muted-foreground font-normal ml-2 text-xl">{year}</span>
          </h1>
          <p className="text-xs text-muted-foreground mt-0.5">
            {rows.length} {rows.length === 1 ? 'bill' : 'bills'}
          </p>
        </div>

        <div className="flex flex-wrap items-center gap-2">
          <Button
            size="sm"
            variant={pinUpcoming ? 'default' : 'outline'}
            onClick={togglePinUpcoming}
            className="h-9 gap-1.5 px-3"
            title={pinUpcoming ? 'Showing urgent bills first — click to restore normal order' : 'Pin overdue and due-soon bills to the top'}
          >
            <ArrowUpToLine className="h-4 w-4" />
            <span className="hidden sm:inline">{pinUpcoming ? 'Pinned' : 'Pin Due'}</span>
          </Button>

          {showBankSync && (
            <Button
              size="sm"
              variant="outline"
              onClick={handleBankSync}
              disabled={bankSyncing}
              className="h-9 gap-1.5 px-3"
              title="Scan bank transactions and match payments"
            >
              {bankSyncing
                ? <RefreshCw className="h-4 w-4 animate-spin" />
                : <Landmark className="h-4 w-4" />}
              <span className="hidden sm:inline">{bankSyncing ? 'Syncing…' : 'Sync Bank'}</span>
            </Button>
          )}
          <Button
            size="sm"
            onClick={handleOpenAddBill}
            className="h-9 gap-1.5 px-3 shadow-sm"
            aria-label="Add bill"
            title="Add bill"
          >
            <Plus className="h-4 w-4" />
            <span className="hidden sm:inline">Add Bill</span>
          </Button>

          <div className="flex items-center gap-1 bg-muted/50 border border-border rounded-lg p-1">
            <Button
              size="icon" variant="ghost"
              onClick={() => navigate(-1)}
              className="h-7 w-7 hover:bg-white/5"
              aria-label="Previous month"
            >
              <ChevronLeft className="h-4 w-4" />
            </Button>
            <span className="min-w-[7.5rem] px-1 text-center text-xs font-semibold tabular-nums select-none">
              {MONTHS[month - 1]} {year}
            </span>
            <Button
              size="icon" variant="ghost"
              onClick={() => navigate(1)}
              className="h-7 w-7 hover:bg-white/5"
              aria-label="Next month"
            >
              <ChevronRight className="h-4 w-4" />
            </Button>
          </div>
          <Button
            size="sm" variant="outline"
            onClick={goToday}
            className="h-9 px-3 text-xs"
          >
            Today
          </Button>
        </div>
      </div>

      {/* ── B: Bank status bar ── */}
      {bankTracking?.enabled && (
        <div className={cn(
          'flex flex-wrap items-center justify-between gap-x-4 gap-y-1 rounded-xl border px-4 py-2.5 text-xs',
          Number(bankTracking.remaining ?? 0) >= 0
            ? 'border-emerald-500/20 bg-emerald-500/5 text-emerald-700 dark:text-emerald-400'
            : 'border-destructive/20 bg-destructive/5 text-destructive',
        )}>
          <div className="flex items-center gap-2">
            <span className="relative flex h-2 w-2">
              <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-current opacity-50" />
              <span className="relative inline-flex h-2 w-2 rounded-full bg-current" />
            </span>
            <span className="font-semibold">{bankTracking.account_name}</span>
            <span className="text-muted-foreground">·</span>
            <span>{fmt(bankTracking.balance ?? 0)} balance</span>
            {bankTracking.last_updated && (
              <>
                <span className="text-muted-foreground">·</span>
                <span className="text-muted-foreground">as of {fmtBalanceAge(bankTracking.last_updated)}</span>
              </>
            )}
            {Number(bankTracking.pending_payments ?? 0) > 0 && (
              <>
                <span className="text-muted-foreground">·</span>
                <span className="text-amber-600 dark:text-amber-400">{fmt(bankTracking.pending_payments)} pending</span>
              </>
            )}
          </div>
          <span className="font-semibold tabular-nums">
            {Number(bankTracking.remaining ?? 0) < 0 ? '−' : ''}{fmt(Math.abs(Number(bankTracking.remaining ?? 0)))} projected
          </span>
        </div>
      )}

      <SearchFilterPanel
        title="Search & sort"
        collapsed={searchPanelCollapsed}
        onCollapsedChange={setSearchPanelCollapsed}
        hasFilters={hasFilters}
        resultLabel={`${filteredRows.length} of ${rows.length} shown`}
        sortLabel={hasSort ? `sorted by ${TRACKER_SORT_LABELS[sortKey]} ${sortDir === TRACKER_SORT_ASC ? 'ascending' : 'descending'}` : null}
        onClear={resetFilters}
      >
        <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-[minmax(220px,1fr)_220px_180px_220px_auto] xl:items-center">
          <label className="relative">
            <Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
            <Input
              value={search}
              onChange={e => updateParams({ q: e.target.value || null })}
              placeholder="Search this month by bill, category, notes, or amount"
              className="h-10 pl-9"
            />
          </label>
          <Select value={filters.category} onValueChange={value => setFilterValue('category', value)}>
            <SelectTrigger className="h-10">
              <SelectValue placeholder="Category" />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value={FILTER_ALL}>All categories</SelectItem>
              {categoryOptions.map(category => (
                <SelectItem key={category.id} value={category.id}>{category.name}</SelectItem>
              ))}
            </SelectContent>
          </Select>
          <Select value={filters.cycle} onValueChange={value => setFilterValue('cycle', value)}>
            <SelectTrigger className="h-10 capitalize">
              <SelectValue placeholder="Billing schedule" />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value={FILTER_ALL}>All cycles</SelectItem>
              {cycleOptions.map(cycle => (
                <SelectItem key={cycle} value={cycle}>{scheduleLabel(cycle)}</SelectItem>
              ))}
            </SelectContent>
          </Select>
          <Select value={sortKey} onValueChange={setSort}>
            <SelectTrigger className="h-10">
              <SelectValue placeholder="Sort by" />
            </SelectTrigger>
            <SelectContent>
              {TRACKER_SORT_OPTIONS.map(option => (
                <SelectItem key={option.key} value={option.key}>{option.label}</SelectItem>
              ))}
            </SelectContent>
          </Select>
          <Button
            type="button"
            variant="outline"
            disabled={!hasSort}
            onClick={toggleSortDirection}
            className="h-10 justify-center gap-2 text-xs"
            title={hasSort ? `Sort ${sortDir === TRACKER_SORT_ASC ? 'ascending' : 'descending'}` : 'Choose a sort first'}
          >
            {sortDir === TRACKER_SORT_ASC ? <ArrowUp className="h-3.5 w-3.5" /> : <ArrowDown className="h-3.5 w-3.5" />}
            <span>{sortDir === TRACKER_SORT_ASC ? 'Asc' : 'Desc'}</span>
          </Button>
        </div>
        <div className="flex flex-wrap items-center gap-2">
          <FilterChip active={filters.unpaid} onClick={() => toggleFilter('unpaid')}>Unpaid</FilterChip>
          <FilterChip active={filters.overdue} onClick={() => toggleFilter('overdue')}>Overdue</FilterChip>
          <FilterChip active={filters.autopay} onClick={() => toggleFilter('autopay')}>Autopay</FilterChip>
          <FilterChip active={filters.firstBucket} onClick={() => toggleFilter('firstBucket')}>1st bucket</FilterChip>
          <FilterChip active={filters.fifteenthBucket} onClick={() => toggleFilter('fifteenthBucket')}>15th bucket</FilterChip>
          <FilterChip active={filters.debt} onClick={() => toggleFilter('debt')}>Debt</FilterChip>
          <span className="ml-auto text-xs text-muted-foreground tabular-nums">
            {filteredRows.length} of {rows.length} shown
            {hasSort && (
              <span className="ml-2 hidden sm:inline">
                · sorted by {TRACKER_SORT_LABELS[sortKey]} {sortDir === TRACKER_SORT_ASC ? 'ascending' : 'descending'}
              </span>
            )}
          </span>
        </div>
      </SearchFilterPanel>

      {/* ── Summary cards (backend already excludes skipped from totals) ── */}
      {loading ? (
        <div className="grid grid-cols-2 gap-3 lg:flex" aria-busy="true">
          <Skeleton variant="card" className="h-32" />
          <Skeleton variant="card" className="h-32" />
          <Skeleton variant="card" className="h-32" />
          <Skeleton variant="card" className="h-32" />
          <Skeleton variant="card" className="h-32" />
          {summary.trend && <Skeleton variant="card" className="h-32" />}
        </div>
      ) : (
        <div className="grid grid-cols-2 gap-3 lg:flex">
          {bankTracking?.enabled ? (
            <button
              type="button"
              onClick={() => setIncomeModalOpen(true)}
              className={cn(
                'flex-1 min-w-0 relative overflow-hidden rounded-xl border px-5 py-4 shadow-sm shadow-black/15 transition-all duration-300 text-left',
                'hover:ring-2 hover:ring-primary/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
                Number(bankTracking.remaining ?? 0) >= 0
                  ? 'border-emerald-500/30 bg-card/95'
                  : 'border-destructive/30 bg-card/95',
              )}
              title="Click to see income breakdown"
            >
              <div className="absolute top-0 left-0 right-0 h-[3px] bg-gradient-to-r from-emerald-500 to-teal-400" />
              <div className="flex items-center gap-2 mb-3">
                <Landmark className={cn('h-4 w-4', Number(bankTracking.remaining ?? 0) >= 0 ? 'text-emerald-500' : 'text-destructive')} />
                <p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground truncate">
                  {bankTracking.account_name}
                </p>
                <span className="ml-auto flex items-center gap-1 text-[10px] text-emerald-600 dark:text-emerald-400 font-medium">
                  <span className="relative flex h-1.5 w-1.5">
                    <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75" />
                    <span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-emerald-500" />
                  </span>
                  Live Sync
                </span>
              </div>
              <p className="text-[1.75rem] font-bold tracking-tight font-mono leading-none text-foreground">
                {fmt(bankTracking.effective_balance ?? 0)}
              </p>
              <p className="mt-2 text-[11px] text-muted-foreground">
                {Number(bankTracking.remaining ?? 0) < 0 ? '−' : ''}{fmt(Math.abs(Number(bankTracking.remaining ?? 0)))} projected after bills
              </p>
              {bankTracking.last_updated && (
                <p className="mt-1 text-[10px] text-muted-foreground/60">
                  as of {fmtBalanceAge(bankTracking.last_updated)}
                </p>
              )}
            </button>
          ) : (
            <SummaryCard
              type="starting"
              value={summary.total_starting}
              hint={(() => {
                if (!summary.has_starting_amounts) return 'Set monthly starting cash';
                if (cashflow?.has_data && cashflow.period_projected !== undefined) {
                  const proj = Number(cashflow.period_projected);
                  const sign = proj < 0 ? '−' : '';
                  return `→ ${sign}${fmt(Math.abs(proj))} projected by ${cashflow.period_end_label}`;
                }
                return '';
              })()}
              onEdit={() => setEditStartingOpen(true)}
            />
          )}
          <SummaryCard type="paid"      value={summary.total_paid} />
          <SummaryCard type="overdue"   value={summary.overdue} />
          <SummaryCard type="paid"      value={summary.previous_month_total} hint="Previous month"/>
          {summary.trend && <TrendCard trend={summary.trend} />}
        </div>
      )}

      {/* ── Overdue Command Center ── */}
      {!isError && !loading && (summary?.count_late ?? 0) > 0 && (
        <OverdueCommandCenter
          rows={rows}
          year={year}
          month={month}
          refresh={refetch}
          onPayNow={(row) => setCommandCenterPayRow(row)}
        />
      )}

      {/* ── Drift / Price-Change Insights ── */}
      {!isError && !loading && (driftData?.bills?.length ?? 0) > 0 && (
        <DriftInsightPanel
          driftBills={driftData.bills}
          refresh={() => { refetch(); refetchDrift(); }}
        />
      )}

      {/* ── Fetch error state ── */}
      {isError && (
        <div className="flex flex-col items-center justify-center py-20 text-center rounded-xl border border-destructive/20 bg-destructive/5">
          <div className="h-10 w-10 rounded-full bg-destructive/10 flex items-center justify-center mb-3">
            <AlertCircle className="h-5 w-5 text-destructive" />
          </div>
          <p className="text-sm font-medium text-foreground">Failed to load tracker data</p>
          <p className="mt-1 text-xs text-muted-foreground">{error?.message || 'An unexpected error occurred.'}</p>
          <Button
            size="sm"
            variant="outline"
            onClick={() => refetch()}
            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>
      )}

      {/* ── Empty state ── */}
      {!isError && rows.length === 0 && data !== null && (
        <div className="flex flex-col items-center justify-center py-20 text-center rounded-xl border border-border bg-muted/20">
          <div className="h-10 w-10 rounded-full bg-muted flex items-center justify-center mb-3">
            <CheckCircle2 className="h-5 w-5 text-muted-foreground" />
          </div>
          <p className="text-sm font-medium text-muted-foreground">No bills this month</p>
          <a href="/bills" className="mt-1.5 text-xs text-muted-foreground underline underline-offset-4 hover:text-foreground transition-colors">
            Add a bill
          </a>
        </div>
      )}

      {/* ── Buckets — year and month passed so Row can open the correct monthly state dialog ── */}
      {!isError && loading && (
        <div className="space-y-5" aria-busy="true">
          <div className="rounded-xl border border-border overflow-hidden bg-card p-4">
            <div className="flex items-center justify-between mb-4">
              <div className="h-4 w-32 rounded-md bg-muted" />
              <div className="h-4 w-16 rounded-md bg-muted" />
            </div>
            <div className="space-y-3">
              {Array.from({ length: 3 }).map((_, i) => (
                <div key={i} className="flex items-center gap-3 animate-pulse">
                  <div className="h-1.5 w-1.5 rounded-full bg-muted" />
                  <div className="h-4 w-64 rounded-md bg-muted" />
                </div>
              ))}
            </div>
          </div>
          <div className="rounded-xl border border-border overflow-hidden bg-card p-4">
            <div className="flex items-center justify-between mb-4">
              <div className="h-4 w-32 rounded-md bg-muted" />
              <div className="h-4 w-16 rounded-md bg-muted" />
            </div>
            <div className="space-y-3">
              {Array.from({ length: 3 }).map((_, i) => (
                <div key={i} className="flex items-center gap-3 animate-pulse">
                  <div className="h-1.5 w-1.5 rounded-full bg-muted" />
                  <div className="h-4 w-64 rounded-md bg-muted" />
                </div>
              ))}
            </div>
          </div>
        </div>
      )}
      {!isError && (first.length > 0 || second.length > 0) && (
        <div className="space-y-5">
          {first.length  > 0 && <Bucket label="1st – 14th"  rows={first}  year={year} month={month} refresh={refetch} onEditBill={handleOpenEditBill} loading={loading} reorderEnabled={reorderEnabled} movingBillId={movingBillId} sortKey={sortKey} sortDir={sortDir} onSort={handleSortHeader} onReorderRows={(next) => handleReorderBucket('1st', next)} />}
          {second.length > 0 && <Bucket label="15th – 31st" rows={second} year={year} month={month} refresh={refetch} onEditBill={handleOpenEditBill} loading={loading} reorderEnabled={reorderEnabled} movingBillId={movingBillId} sortKey={sortKey} sortDir={sortDir} onSort={handleSortHeader} onReorderRows={(next) => handleReorderBucket('15th', next)} />}
        </div>
      )}

      {/* Edit Bill modal — opened by clicking a bill name in any tracker row */}
      {editBillData && (
        <BillModal
          key={editBillData.bill?.id ? `edit-${editBillData.bill.id}` : `new-${editBillData.initialBill?.name || 'blank'}`}
          bill={editBillData.bill}
          initialBill={editBillData.initialBill}
          categories={editBillData.categories}
          onClose={() => setEditBillData(null)}
          onSave={() => { setEditBillData(null); refetch(); }}
          onDuplicate={bill => setEditBillData({
            bill: null,
            initialBill: makeBillDraft(bill, { copy: true, categories: editBillData.categories }),
            categories: editBillData.categories,
          })}
        />
      )}

      {/* Edit Starting Amounts modal */}
      <StartingAmountsEditDialog
        open={editStartingOpen}
        onClose={() => setEditStartingOpen(false)}
        year={year}
        month={month}
        onSave={() => { setEditStartingOpen(false); refetch(); }}
      />

      {/* Income breakdown modal — opens when clicking the bank balance card */}
      {bankTracking?.enabled && (
        <IncomeBreakdownModal
          open={incomeModalOpen}
          onClose={() => setIncomeModalOpen(false)}
          year={year}
          month={month}
          bankTracking={bankTracking}
        />
      )}

      {/* Late-attribution dialog — fires after sync when a payment just crossed a month boundary */}
      {lateAttributions.length > 0 && (
        <LateAttributionDialog
          attr={lateAttributions[0]}
          remaining={lateAttributions.length - 1}
          busy={attrBusy === lateAttributions[0]?.payment_id}
          onAccept={async (attr) => {
            setAttrBusy(attr.payment_id);
            try {
              await api.attributePaymentToMonth(attr.payment_id, attr.suggested_date);
              const month = new Date(attr.suggested_date + 'T00:00:00').toLocaleDateString('en-US', { month: 'long' });
              toast.success(`${attr.bill_name} payment moved to ${month}`);
              setLateAttributions(prev => prev.slice(1)); // dismiss only on success
              refetch();
            } catch (err) {
              toast.error(err.message || 'Failed to reclassify payment — try again');
              // keep the attribution in queue so user can retry
            } finally {
              setAttrBusy(null);
            }
          }}
          onDismiss={() => setLateAttributions(prev => prev.slice(1))}
        />
      )}

      {/* PaymentLedgerDialog opened via Overdue Command Center "Pay Now" */}
      {commandCenterPayRow && (
        <PaymentLedgerDialog
          row={commandCenterPayRow}
          year={year}
          month={month}
          threshold={commandCenterPayRow.actual_amount ?? commandCenterPayRow.expected_amount}
          defaultPaymentDate={paymentDateForTrackerMonth(year, month, commandCenterPayRow.due_day)}
          onClose={() => setCommandCenterPayRow(null)}
          onSaved={() => { setCommandCenterPayRow(null); refetch(); }}
        />
      )}

    </div>
  );
}
