import { useState, useEffect, useCallback, useMemo, useRef, type FormEvent } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import {
  useSpendingSummary, useSpendingTransactions, useSpendingCategories, useCategoryGroups,
} from '@/hooks/useQueries';
import { toast } from 'sonner';
import {
  ChevronLeft, ChevronRight, ChevronDown, Tag, ReceiptText, TrendingDown, Pencil, Check, X,
  Trash2, BookmarkPlus, Settings2, Copy, AlertCircle, RefreshCw, ArrowRightLeft,
  Layers, Wallet,
} from 'lucide-react';
import { api } from '@/api';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/Skeleton';
import {
  DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuLabel,
  DropdownMenuSeparator, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { CategoryPicker } from '@/components/transactions/CategoryPicker';
import { formatUSD, asDollars } from '@/lib/money';
import { errMessage, settingEnabled } from '@/lib/utils';
import type {
  Category,
  SpendingCategoryEntry as CatEntry,
  SpendingSummary,
  SpendingTransaction as SpendingTx,
  SpendingTransactionsResponse as SpendingTxResponse,
  SpendingIncomeTx as IncomeTx,
  SpendingRule,
  CategoryGroup,
} from '@/types';

const MONTH_NAMES = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];

function fmt(v: number | null | undefined): string {
  return formatUSD(asDollars(Number(v) || 0));
}

type Level = 'over' | 'warn' | 'ok';

type RealCatEntry = CatEntry & { category_id: number };

type Budgets = Record<string, number | null>;

// pctBar() returns a progress-bar percent plus a YNAB-style status level:
//  - 'over': spending has exceeded the budget (Available < 0)
//  - 'warn': spending has crossed ~80% of the budget
//  - 'ok':   on track
function pctBar(amount: number, budget: number | null | undefined): { pct: number; level: Level; over: boolean } | null {
  if (budget == null) return null;
  const pct = Math.min(100, Math.round((amount / budget) * 100));
  const level: Level = amount > budget ? 'over' : (budget > 0 && amount / budget >= 0.8) ? 'warn' : 'ok';
  return { pct, level, over: level === 'over' };
}

const LEVEL_BAR_CLASS: Record<Level, string> = { over: 'bg-destructive', warn: 'bg-amber-500', ok: 'bg-primary' };
const LEVEL_TEXT_CLASS: Record<Level, string> = { over: 'text-destructive', warn: 'text-amber-500', ok: 'text-emerald-500' };

// ── Transaction row ──────────────────────────────────────────────────────────

function TxRow({ tx, categories, onCategorize }: {
  tx: SpendingTx;
  categories: Category[];
  onCategorize: (txId: number, categoryId: number | string | null, categoryName: string | null) => void;
}) {
  const [saving,         setSaving]         = useState(false);
  const [rememberPrompt, setRememberPrompt] = useState<{ categoryId: number | string | null; categoryName: string | null } | null>(null);
  const dismissTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);

  // Auto-dismiss the "remember" prompt after 7 seconds
  useEffect(() => {
    if (!rememberPrompt) return;
    dismissTimer.current = setTimeout(() => setRememberPrompt(null), 7000);
    return () => clearTimeout(dismissTimer.current);
  }, [rememberPrompt]);

  const handleSelect = async (categoryId: number | string | null) => {
    setSaving(true);
    setRememberPrompt(null);
    try {
      await api.categorizeTransaction(tx.id, { category_id: categoryId, save_rule: false });
      const catName = categories.find(c => c.id === categoryId)?.name ?? null;
      onCategorize(tx.id, categoryId, catName);
      // Offer to remember the merchant rule (only when assigning a real category)
      if (categoryId) setRememberPrompt({ categoryId, categoryName: catName });
    } catch (err) {
      toast.error(errMessage(err, 'Failed to categorize'));
    } finally {
      setSaving(false);
    }
  };

  const saveRule = async () => {
    if (!rememberPrompt) return;
    clearTimeout(dismissTimer.current);
    setRememberPrompt(null);
    try {
      await api.categorizeTransaction(tx.id, { category_id: rememberPrompt.categoryId, save_rule: true });
      toast.success(`Rule saved — future ${tx.payee} transactions will be auto-categorized.`);
    } catch (err) {
      toast.error(errMessage(err, 'Failed to save rule'));
    }
  };

  return (
    <div className="border-b border-border/30 last:border-0">
      <div className="flex items-center gap-3 px-4 py-2.5 hover:bg-muted/20 transition-colors">
        <div className="min-w-0 flex-1">
          <p className="text-sm truncate font-medium">{tx.payee}</p>
          <p className="text-xs text-muted-foreground">{tx.date}</p>
        </div>
        <span className="text-sm font-mono font-semibold text-destructive shrink-0">
          -{fmt(tx.amount)}
        </span>
        {saving
          ? <span className="text-xs text-muted-foreground w-28 text-center">Saving…</span>
          : <CategoryPicker categories={categories} current={tx.spending_category_id} currentLabel={tx.spending_category_name} onSelect={handleSelect} />
        }
      </div>
      {rememberPrompt && (
        <div className="mx-4 mb-2 flex items-center gap-2 rounded-md border border-primary/20 bg-primary/5 px-3 py-1.5">
          <BookmarkPlus className="h-3.5 w-3.5 text-primary shrink-0" />
          <span className="text-xs text-muted-foreground flex-1">
            Always categorize <span className="font-medium text-foreground">{tx.payee}</span> as{' '}
            <span className="font-medium text-foreground">{rememberPrompt.categoryName}</span>?
          </span>
          <button type="button" onClick={saveRule}
            className="text-xs font-medium text-primary hover:text-primary/80 transition-colors">
            Save rule
          </button>
          <button type="button" onClick={() => { clearTimeout(dismissTimer.current); setRememberPrompt(null); }}
            className="text-muted-foreground hover:text-foreground">
            <X className="h-3.5 w-3.5" />
          </button>
        </div>
      )}
    </div>
  );
}

// ── Budget edit inline ───────────────────────────────────────────────────────

function BudgetEditor({ categoryId, year, month, initial, onSaved }: {
  categoryId: number;
  year: number;
  month: number;
  initial: number | null;
  onSaved: (categoryId: number, amount: number | null) => void;
}) {
  const [editing, setEditing] = useState(false);
  const [val, setVal] = useState<string>(initial != null ? String(initial) : '');

  const save = async () => {
    const amount = val === '' ? null : parseFloat(val);
    if (val !== '' && (amount == null || isNaN(amount) || amount < 0)) { toast.error('Enter a valid amount'); return; }
    try {
      await api.setSpendingBudget({ category_id: categoryId, year, month, amount });
      onSaved(categoryId, amount);
      setEditing(false);
    } catch { toast.error('Failed to save budget'); }
  };

  if (!editing) return (
    <button
      type="button"
      onClick={() => setEditing(true)}
      className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
    >
      {initial != null ? `Budget: ${fmt(initial)}` : 'Set budget'}
      <Pencil className="h-3 w-3" />
    </button>
  );

  return (
    <div className="flex items-center gap-1">
      <input
        type="number"
        value={val}
        onChange={e => setVal(e.target.value)}
        onKeyDown={e => { if (e.key === 'Enter') save(); if (e.key === 'Escape') setEditing(false); }}
        placeholder="0.00"
        autoFocus
        className="w-20 rounded border border-border/60 bg-background px-1.5 py-0.5 text-xs font-mono focus:outline-none focus:ring-1 focus:ring-ring"
      />
      <button type="button" onClick={save} className="text-emerald-500 hover:text-emerald-400"><Check className="h-3.5 w-3.5" /></button>
      <button type="button" onClick={() => setEditing(false)} className="text-muted-foreground hover:text-foreground"><X className="h-3.5 w-3.5" /></button>
    </div>
  );
}

// ── Income section ───────────────────────────────────────────────────────────

function IncomeSection({ year, month, totalIncome }: { year: number; month: number; totalIncome?: number }) {
  const [open,    setOpen]    = useState(false);
  const [rows,    setRows]    = useState<IncomeTx[]>([]);
  const [page,    setPage]    = useState(1);
  const [pages,   setPages]   = useState(1);
  const [loading, setLoading] = useState(false);

  const load = useCallback(async (p = 1) => {
    setLoading(true);
    try {
      const d = await api.spendingIncome({ year, month, page: p });
      setRows(d.transactions || []);
      setPages(d.pages || 1);
      setPage(p);
    } catch (err) {
      toast.error(errMessage(err, 'Failed to load income transactions'));
    } finally {
      setLoading(false);
    }
  }, [year, month]);

  useEffect(() => { if (open) load(1); }, [open, load]);

  if (!totalIncome) return null;

  return (
    <div className="rounded-xl border border-emerald-500/25 bg-card/80 overflow-hidden">
      <button
        type="button"
        onClick={() => setOpen(v => !v)}
        className="w-full flex items-center gap-2 px-4 py-3 text-left hover:bg-muted/20 transition-colors"
      >
        <TrendingDown className="h-4 w-4 text-emerald-500 rotate-180" />
        <span className="text-sm font-medium flex-1">Income & Deposits</span>
        <span className="text-sm font-mono font-semibold text-emerald-500">{fmt(totalIncome)}</span>
        <ChevronDown className={`h-4 w-4 text-muted-foreground transition-transform ml-2 ${open ? 'rotate-180' : ''}`} />
      </button>

      {open && (
        <div className="border-t border-emerald-500/20">
          {loading ? (
            <p className="text-sm text-muted-foreground text-center py-6">Loading…</p>
          ) : rows.length === 0 ? (
            <p className="text-sm text-muted-foreground text-center py-6">No income transactions found.</p>
          ) : (
            <>
              <div className="divide-y divide-border/30 max-h-72 overflow-y-auto">
                {rows.map(tx => (
                  <div key={tx.id} className="flex items-center gap-3 px-4 py-2.5 hover:bg-muted/20 transition-colors">
                    <div className="min-w-0 flex-1">
                      <p className="text-sm truncate font-medium">{tx.payee}</p>
                      <p className="text-xs text-muted-foreground">{tx.date}</p>
                    </div>
                    <span className="text-sm font-mono font-semibold text-emerald-500 shrink-0">
                      +{fmt(tx.amount)}
                    </span>
                  </div>
                ))}
              </div>
              {pages > 1 && (
                <div className="flex items-center justify-center gap-2 px-4 py-2.5 border-t border-border/30">
                  <Button variant="ghost" size="sm" disabled={page <= 1} onClick={() => load(page - 1)}>
                    <ChevronLeft className="h-4 w-4" />
                  </Button>
                  <span className="text-xs text-muted-foreground">{page} / {pages}</span>
                  <Button variant="ghost" size="sm" disabled={page >= pages} onClick={() => load(page + 1)}>
                    <ChevronRight className="h-4 w-4" />
                  </Button>
                </div>
              )}
            </>
          )}
          <p className="px-4 py-2 text-[11px] text-muted-foreground border-t border-border/30">
            Positive unmatched transactions — deposits, refunds, transfers in. Bill-matched payments are excluded.
          </p>
        </div>
      )}
    </div>
  );
}

// ── Rules manager ────────────────────────────────────────────────────────────

function RulesManager({ categories }: { categories: Category[] }) {
  const [open,    setOpen]    = useState(false);
  const [rules,   setRules]   = useState<SpendingRule[]>([]);
  const [loading, setLoading] = useState(false);
  const [newMerchant,  setNewMerchant]  = useState('');
  const [newCategory,  setNewCategory]  = useState('');
  const [adding, setAdding] = useState(false);

  const load = useCallback(async () => {
    setLoading(true);
    try { setRules((await api.spendingCategoryRules()).rules || []); }
    catch { toast.error('Failed to load rules'); }
    finally { setLoading(false); }
  }, []);

  useEffect(() => { if (open) load(); }, [open, load]);

  const deleteRule = async (id: number) => {
    try {
      await api.deleteSpendingRule(id);
      setRules(prev => prev.filter(r => r.id !== id));
    } catch { toast.error('Failed to delete rule'); }
  };

  const addRule = async (e: FormEvent) => {
    e.preventDefault();
    if (!newMerchant.trim() || !newCategory) return;
    setAdding(true);
    try {
      await api.addSpendingRule({ merchant: newMerchant.trim(), category_id: parseInt(newCategory, 10) });
      setNewMerchant(''); setNewCategory('');
      await load();
      toast.success('Rule saved.');
    } catch (err) {
      toast.error(errMessage(err, 'Failed to save rule'));
    } finally { setAdding(false); }
  };

  return (
    <div className="rounded-xl border border-border/60 bg-card/80 overflow-hidden">
      <button
        type="button"
        onClick={() => setOpen(v => !v)}
        className="w-full flex items-center gap-2 px-4 py-3 text-left hover:bg-muted/20 transition-colors"
      >
        <Settings2 className="h-4 w-4 text-muted-foreground" />
        <span className="text-sm font-medium flex-1">Merchant Rules</span>
        {rules.length > 0 && !open && (
          <Badge variant="secondary" className="text-xs">{rules.length}</Badge>
        )}
        <ChevronDown className={`h-4 w-4 text-muted-foreground transition-transform ${open ? 'rotate-180' : ''}`} />
      </button>

      {open && (
        <div className="border-t border-border/40">
          {/* Add new rule */}
          <form onSubmit={addRule} className="flex items-center gap-2 px-4 py-3 border-b border-border/30">
            <input
              value={newMerchant}
              onChange={e => setNewMerchant(e.target.value)}
              placeholder="Merchant name (e.g. walmart)"
              className="flex-1 min-w-0 rounded-md border border-border/60 bg-background px-2.5 py-1.5 text-xs focus:outline-none focus:ring-1 focus:ring-ring"
            />
            <select
              value={newCategory}
              onChange={e => setNewCategory(e.target.value)}
              className="rounded-md border border-border/60 bg-background px-2 py-1.5 text-xs focus:outline-none focus:ring-1 focus:ring-ring"
            >
              <option value="">Category…</option>
              {categories.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
            </select>
            <Button type="submit" size="sm" disabled={adding || !newMerchant.trim() || !newCategory}>
              {adding ? 'Saving…' : 'Add'}
            </Button>
          </form>

          {/* Existing rules */}
          {loading ? (
            <p className="text-sm text-muted-foreground text-center py-6">Loading…</p>
          ) : rules.length === 0 ? (
            <p className="text-sm text-muted-foreground text-center py-6">
              No rules saved yet. Categorize a transaction and click "Save rule" to create one.
            </p>
          ) : (
            <div className="divide-y divide-border/30 max-h-64 overflow-y-auto">
              {rules.map(r => (
                <div key={r.id} className="flex items-center gap-3 px-4 py-2.5">
                  <span className="font-mono text-xs text-muted-foreground flex-1 truncate">{r.merchant}</span>
                  <span className="text-xs shrink-0">→</span>
                  <span className="text-xs font-medium shrink-0">{r.category_name}</span>
                  <button type="button" onClick={() => deleteRule(r.id)}
                    className="ml-1 text-muted-foreground hover:text-destructive transition-colors shrink-0">
                    <Trash2 className="h-3.5 w-3.5" />
                  </button>
                </div>
              ))}
            </div>
          )}
        </div>
      )}
    </div>
  );
}

// ── Spending settings menu ───────────────────────────────────────────────────

function SpendingSettingsMenu({ settings, onToggle, saving }: {
  settings: Record<string, unknown>;
  onToggle: (key: string, checked: boolean) => void;
  saving: boolean;
}) {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button type="button" size="sm" variant="outline" className="h-8 gap-1.5 px-2.5 text-xs" disabled={saving}>
          <Settings2 className="h-3.5 w-3.5" />
          <span className="hidden sm:inline">View</span>
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end" className="w-60">
        <DropdownMenuLabel>Spending page options</DropdownMenuLabel>
        <DropdownMenuSeparator />
        <DropdownMenuCheckboxItem
          checked={settingEnabled(settings.spending_show_ready_to_assign)}
          onSelect={e => e.preventDefault()}
          onCheckedChange={checked => onToggle('spending_show_ready_to_assign', checked)}
        >
          Ready to Assign
        </DropdownMenuCheckboxItem>
        <DropdownMenuCheckboxItem
          checked={settingEnabled(settings.spending_show_avg)}
          onSelect={e => e.preventDefault()}
          onCheckedChange={checked => onToggle('spending_show_avg', checked)}
        >
          3-month averages
        </DropdownMenuCheckboxItem>
        <DropdownMenuCheckboxItem
          checked={settingEnabled(settings.spending_show_cover_overspend)}
          onSelect={e => e.preventDefault()}
          onCheckedChange={checked => onToggle('spending_show_cover_overspend', checked)}
        >
          Cover overspending
        </DropdownMenuCheckboxItem>
        <DropdownMenuCheckboxItem
          checked={settingEnabled(settings.spending_group_categories, false)}
          onSelect={e => e.preventDefault()}
          onCheckedChange={checked => onToggle('spending_group_categories', checked)}
        >
          Group categories
        </DropdownMenuCheckboxItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

// ── Cover overspending ───────────────────────────────────────────────────────

function CoverOverspendPicker({ cat, siblings, budgets, year, month, onCovered }: {
  cat: RealCatEntry;
  siblings: RealCatEntry[];
  budgets: Budgets;
  year: number;
  month: number;
  onCovered: (updates: Record<string, number>) => void;
}) {
  const [open,   setOpen]   = useState(false);
  const [saving, setSaving] = useState(false);

  const catBudget = budgets[cat.category_id] ?? cat.budget ?? 0;
  const overspend = Math.max(0, cat.amount - catBudget);

  const options = siblings
    .filter(s => s.category_id !== cat.category_id)
    .map(s => ({ ...s, available: (budgets[s.category_id] ?? s.budget ?? 0) - s.amount }))
    .filter(s => s.available > 0);

  const cover = async (source: RealCatEntry & { available: number }) => {
    const sourceBudget = budgets[source.category_id] ?? source.budget ?? 0;
    const amount = Math.min(overspend, source.available);
    setSaving(true);
    try {
      const newSourceBudget = sourceBudget - amount;
      const newCatBudget = catBudget + amount;
      await api.setSpendingBudget({ category_id: source.category_id, year, month, amount: newSourceBudget });
      await api.setSpendingBudget({ category_id: cat.category_id, year, month, amount: newCatBudget });
      onCovered({ [source.category_id]: newSourceBudget, [cat.category_id]: newCatBudget });
      toast.success(`Covered ${fmt(amount)} from ${source.category_name}.`);
      setOpen(false);
    } catch (err) {
      toast.error(errMessage(err, 'Failed to cover overspending'));
    } finally {
      setSaving(false);
    }
  };

  if (overspend <= 0) return null;

  if (options.length === 0) {
    return <span className="text-[11px] text-destructive/70">No category has budget left to cover this</span>;
  }

  return (
    <div className="relative">
      <button
        type="button"
        onClick={() => setOpen(v => !v)}
        disabled={saving}
        className="flex items-center gap-1 text-[11px] font-medium text-destructive hover:text-destructive/80 transition-colors disabled:opacity-50"
      >
        <ArrowRightLeft className="h-3 w-3" /> Cover from…
      </button>
      {open && (
        <div className="absolute z-10 right-0 mt-1 w-52 rounded-md border border-border/60 bg-popover shadow-lg py-1">
          {options.map(s => (
            <button
              key={s.category_id}
              type="button"
              onClick={() => cover(s)}
              className="w-full flex items-center justify-between gap-2 px-2.5 py-1.5 text-xs text-left hover:bg-muted/40 transition-colors"
            >
              <span className="truncate">{s.category_name}</span>
              <span className="text-muted-foreground font-mono shrink-0">{fmt(s.available)} avail</span>
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

// ── Category groups manager ──────────────────────────────────────────────────

function CategoryGroupManager({ categories, groups, onGroupsChanged, onCategoriesChanged }: {
  categories: Category[];
  groups: CategoryGroup[];
  onGroupsChanged: () => Promise<void> | void;
  onCategoriesChanged: () => Promise<void> | void;
}) {
  const [open,          setOpen]          = useState(false);
  const [newGroupName,  setNewGroupName]  = useState('');
  const [adding,        setAdding]        = useState(false);
  const [editingId,     setEditingId]     = useState<number | null>(null);
  const [editName,      setEditName]      = useState('');

  const addGroup = async (e: FormEvent) => {
    e.preventDefault();
    if (!newGroupName.trim()) return;
    setAdding(true);
    try {
      await api.createCategoryGroup({ name: newGroupName.trim() });
      setNewGroupName('');
      await onGroupsChanged();
      toast.success('Group created.');
    } catch (err) {
      toast.error(errMessage(err, 'Failed to create group'));
    } finally {
      setAdding(false);
    }
  };

  const renameGroup = async (id: number) => {
    if (!editName.trim()) return;
    try {
      await api.updateCategoryGroup(id, { name: editName.trim() });
      setEditingId(null);
      await onGroupsChanged();
    } catch (err) {
      toast.error(errMessage(err, 'Failed to rename group'));
    }
  };

  const deleteGroup = async (id: number) => {
    try {
      await api.deleteCategoryGroup(id);
      await onGroupsChanged();
      toast.success('Group deleted.');
    } catch (err) {
      toast.error(errMessage(err, 'Failed to delete group'));
    }
  };

  const setCategoryGroup = async (cat: Category, groupId: string) => {
    try {
      await api.updateCategory(cat.id, { name: cat.name, spending_enabled: true, group_id: groupId === '' ? null : Number(groupId) });
      await onCategoriesChanged();
    } catch (err) {
      toast.error(errMessage(err, 'Failed to update category group'));
    }
  };

  return (
    <div className="rounded-xl border border-border/60 bg-card/80 overflow-hidden">
      <button
        type="button"
        onClick={() => setOpen(v => !v)}
        className="w-full flex items-center gap-2 px-4 py-3 text-left hover:bg-muted/20 transition-colors"
      >
        <Layers className="h-4 w-4 text-muted-foreground" />
        <span className="text-sm font-medium flex-1">Category Groups</span>
        {groups.length > 0 && !open && (
          <Badge variant="secondary" className="text-xs">{groups.length}</Badge>
        )}
        <ChevronDown className={`h-4 w-4 text-muted-foreground transition-transform ${open ? 'rotate-180' : ''}`} />
      </button>

      {open && (
        <div className="border-t border-border/40">
          <form onSubmit={addGroup} className="flex items-center gap-2 px-4 py-3 border-b border-border/30">
            <input
              value={newGroupName}
              onChange={e => setNewGroupName(e.target.value)}
              placeholder="New group name (e.g. Bills, Everyday)"
              className="flex-1 min-w-0 rounded-md border border-border/60 bg-background px-2.5 py-1.5 text-xs focus:outline-none focus:ring-1 focus:ring-ring"
            />
            <Button type="submit" size="sm" disabled={adding || !newGroupName.trim()}>
              {adding ? 'Adding…' : 'Add group'}
            </Button>
          </form>

          {groups.length === 0 ? (
            <p className="text-sm text-muted-foreground text-center py-6">
              No groups yet. Create one above, then assign categories to it below.
            </p>
          ) : (
            <div className="divide-y divide-border/30">
              {groups.map(g => (
                <div key={g.id} className="flex items-center gap-2 px-4 py-2">
                  {editingId === g.id ? (
                    <>
                      <input
                        value={editName}
                        onChange={e => setEditName(e.target.value)}
                        autoFocus
                        onKeyDown={e => { if (e.key === 'Enter') renameGroup(g.id); if (e.key === 'Escape') setEditingId(null); }}
                        className="flex-1 rounded-md border border-border/60 bg-background px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-ring"
                      />
                      <button type="button" onClick={() => renameGroup(g.id)} className="text-emerald-500 hover:text-emerald-400"><Check className="h-3.5 w-3.5" /></button>
                      <button type="button" onClick={() => setEditingId(null)} className="text-muted-foreground hover:text-foreground"><X className="h-3.5 w-3.5" /></button>
                    </>
                  ) : (
                    <>
                      <span className="text-sm font-medium flex-1 truncate">{g.name}</span>
                      <button type="button" onClick={() => { setEditingId(g.id); setEditName(g.name); }} className="text-muted-foreground hover:text-foreground"><Pencil className="h-3.5 w-3.5" /></button>
                      <button type="button" onClick={() => deleteGroup(g.id)} className="text-muted-foreground hover:text-destructive"><Trash2 className="h-3.5 w-3.5" /></button>
                    </>
                  )}
                </div>
              ))}
            </div>
          )}

          {categories.length > 0 && (
            <div className="border-t border-border/30 px-4 py-3 space-y-2">
              <p className="text-xs text-muted-foreground font-medium uppercase tracking-wide">Assign categories</p>
              {categories.map(c => (
                <div key={c.id} className="flex items-center justify-between gap-2">
                  <span className="text-sm truncate">{c.name}</span>
                  <select
                    value={c.group_id ?? ''}
                    onChange={e => setCategoryGroup(c, e.target.value)}
                    className="rounded-md border border-border/60 bg-background px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-ring"
                  >
                    <option value="">No group</option>
                    {groups.map(g => <option key={g.id} value={g.id}>{g.name}</option>)}
                  </select>
                </div>
              ))}
            </div>
          )}
        </div>
      )}
    </div>
  );
}

// ── Main page ────────────────────────────────────────────────────────────────

interface GroupBucket { group: { id: number | string; name: string }; entries: RealCatEntry[] }

export default function SpendingPage() {
  const now = new Date();
  const [year,  setYear]  = useState(now.getFullYear());
  const [month, setMonth] = useState(now.getMonth() + 1);

  const queryClient = useQueryClient();
  const [txPage,       setTxPage]       = useState(1);
  const [activeCat,    setActiveCat]    = useState<number | null | undefined>(undefined); // undefined = all
  const { data: summaryRaw = null, isPending: loading, error: summaryErrObj } = useSpendingSummary(year, month);
  const summary = summaryRaw;
  const { data: txDataRaw, isFetching: txLoading, error: txErrObj } = useSpendingTransactions({ year, month, activeCat, page: txPage });
  const txData = txDataRaw;
  const transactions = useMemo(() => txData?.transactions || [], [txData]);
  const txTotal = txData?.total || 0;
  const txPages = txData?.pages || 1;
  const { data: categories = [], error: catErrObj } = useSpendingCategories();
  const { data: categoryGroupsRaw = [] } = useCategoryGroups();
  const categoryGroups = categoryGroupsRaw;
  const summaryError = summaryErrObj ? (summaryErrObj.message || 'Failed to load spending summary') : null;
  const txError = txErrObj ? (txErrObj.message || 'Failed to load transactions') : null;
  const catError = catErrObj ? (catErrObj.message || 'Failed to load categories') : null;
  // Optimistic edits write through the query cache so existing call sites work.
  const setSummary = useCallback(
    (u: (prev: SpendingSummary | null | undefined) => SpendingSummary | null | undefined) =>
      queryClient.setQueryData<SpendingSummary | null>(['spending-summary', year, month], prev => u(prev)),
    [queryClient, year, month]);
  const setTransactions = useCallback(
    (u: (prev: SpendingTx[]) => SpendingTx[]) =>
      queryClient.setQueryData<SpendingTxResponse>(
        ['spending-transactions', year, month, activeCat ?? 'all', txPage],
        prev => (prev ? { ...prev, transactions: u(prev.transactions || []) } : prev)),
    [queryClient, year, month, activeCat, txPage]);
  const [budgets,      setBudgets]      = useState<Budgets>({}); // categoryId → amount (seeded from summary)
  const [copying,      setCopying]      = useState(false);
  const [spendingSettings,      setSpendingSettings]      = useState<Record<string, unknown>>({});
  const [savingSpendingSetting, setSavingSpendingSetting] = useState(false);

  // loadCategories is stable — categories don't vary by month
  const loadCategories = useCallback(
    () => queryClient.invalidateQueries({ queryKey: ['spending-categories'] }), [queryClient]);
  const loadCategoryGroups = useCallback(
    () => queryClient.invalidateQueries({ queryKey: ['category-groups'] }), [queryClient]);
  const loadSummary = useCallback(
    () => queryClient.invalidateQueries({ queryKey: ['spending-summary', year, month] }), [queryClient, year, month]);
  // Pagination moves the page (query re-fetches on the new key); a same-page call
  // (e.g. the Refresh button) forces a refetch of the current page.
  const loadTransactions = useCallback((page = 1) => {
    if (page === txPage) queryClient.invalidateQueries({ queryKey: ['spending-transactions'] });
    else setTxPage(page);
  }, [txPage, queryClient]);

  useEffect(() => {
    // best-effort: the page-options are cosmetic; on a load failure we fall back
    // to defaults rather than block the page or nag with a mount-time toast.
    api.settings().then(s => setSpendingSettings((s as Record<string, unknown>) || {})).catch(() => {});
  }, []);

  // Reset to page 1 whenever the month or category filter changes.
  useEffect(() => { setTxPage(1); }, [year, month, activeCat]);

  // Seed the editable budgets map from the loaded summary whenever it changes
  // (this is what loadSummary used to do inline).
  useEffect(() => {
    if (!summary) return;
    const bmap: Budgets = {};
    (summary.by_category || []).forEach(c => { if (c.category_id != null && c.budget != null) bmap[c.category_id] = c.budget; });
    setBudgets(bmap);
  }, [summary]);

  async function saveSpendingSetting(patch: Record<string, string>, successMessage?: string) {
    setSavingSpendingSetting(true);
    setSpendingSettings(prev => ({ ...prev, ...patch }));
    try {
      const next = await api.saveSettings(patch);
      setSpendingSettings(prev => ({ ...prev, ...(next as Record<string, unknown>) }));
      if (successMessage) toast.success(successMessage);
    } catch (err) {
      toast.error(errMessage(err, 'Failed to update spending setting'));
      api.settings().then(s => setSpendingSettings(prev => ({ ...prev, ...(s as Record<string, unknown>) }))).catch(() => {});
    } finally {
      setSavingSpendingSetting(false);
    }
  }

  const handleCopyBudgets = async () => {
    setCopying(true);
    try {
      const d = await api.copySpendingBudgets({ year, month });
      if (d.copied === 0) {
        toast.info('No budgets found in the previous month to copy.');
      } else {
        // Update local budget state from server response
        const bmap: Budgets = {};
        (d.budgets || []).forEach(b => { bmap[b.category_id] = b.amount; });
        setBudgets(bmap);
        setSummary(prev => prev ? {
          ...prev,
          by_category: (prev.by_category || []).map(c => {
            const key = c.category_id;
            const val = key == null ? undefined : bmap[key];
            return val != null ? { ...c, budget: val } : c;
          }),
        } : prev);
        toast.success(`${d.copied} budget${d.copied !== 1 ? 's' : ''} copied from last month.`);
      }
    } catch (err) {
      toast.error(errMessage(err, 'Failed to copy budgets'));
    } finally {
      setCopying(false);
    }
  };

  const navMonth = (dir: number) => {
    let m = month + dir, y = year;
    if (m > 12) { m = 1; y++; }
    if (m < 1)  { m = 12; y--; }
    setMonth(m); setYear(y); setActiveCat(undefined); setTxPage(1);
  };

  const handleCategorize = (txId: number, categoryId: number | string | null, categoryName: string | null) => {
    setTransactions(prev => prev.map(t =>
      t.id === txId ? { ...t, spending_category_id: categoryId, spending_category_name: categoryName } : t
    ));
    loadSummary();
  };

  const handleBudgetSaved = (categoryId: number, amount: number | null) => {
    setBudgets(prev => ({ ...prev, [categoryId]: amount }));
    setSummary(prev => {
      if (!prev) return prev;
      return {
        ...prev,
        by_category: (prev.by_category || []).map(c =>
          c.category_id === categoryId ? { ...c, budget: amount } : c
        ),
      };
    });
  };

  const handleBudgetsCovered = (updates: Record<string, number>) => {
    setBudgets(prev => ({ ...prev, ...updates }));
    setSummary(prev => {
      if (!prev) return prev;
      return {
        ...prev,
        by_category: (prev.by_category || []).map(c => {
          const key = c.category_id;
          const val = key == null ? undefined : updates[key];
          return val != null ? { ...c, budget: val } : c;
        }),
      };
    });
  };

  const refreshAfterGroupChange = async () => {
    await loadCategoryGroups();
    await loadSummary();
  };

  const refreshAfterCategoryChange = async () => {
    await loadCategories();
    await loadSummary();
  };

  const selectCat = (catId: number | null) => {
    setActiveCat(prev => prev === catId ? undefined : catId);
    setTxPage(1);
  };

  if (loading && !summary) {
    return (
      <div className="min-h-screen bg-[radial-gradient(circle_at_top_right,oklch(var(--primary)/0.05),transparent_30rem)] pb-16">
        <div className="mx-auto max-w-4xl px-4 py-6 sm:px-6 space-y-6">
          <div className="flex items-center justify-between gap-4">
            <div className="space-y-2">
              <Skeleton variant="line" className="h-6 w-28" />
              <Skeleton variant="line" className="h-4 w-56" />
            </div>
            <Skeleton variant="button" className="w-32" />
          </div>
          <Skeleton variant="card" className="h-28" />
          <div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
            {Array.from({ length: 5 }).map((_, i) => <Skeleton key={i} variant="card" className="h-20" />)}
          </div>
          <Skeleton variant="card" className="h-72" />
          <Skeleton variant="card" className="h-64" />
        </div>
      </div>
    );
  }

  const realCatEntries = (summary?.by_category || []).filter(
    (c): c is RealCatEntry => typeof c.category_id === 'number');
  const uncatEntry = summary?.by_category?.find(c => c.category_id === null) || null;
  const otherEntry = summary?.by_category?.find(c => c.category_id === 'other') || null;

  const totalBudgeted  = Object.values(budgets).reduce<number>((sum, b) => sum + (Number(b) || 0), 0);
  const totalSpending  = summary?.total_spending || 0;
  const income         = summary?.income || 0;
  const readyToAssign  = income - totalBudgeted;
  const remaining      = totalBudgeted - totalSpending;

  const showReadyToAssign = settingEnabled(spendingSettings.spending_show_ready_to_assign);
  const showAvg           = settingEnabled(spendingSettings.spending_show_avg);
  const showCover         = settingEnabled(spendingSettings.spending_show_cover_overspend);
  const showGroups        = settingEnabled(spendingSettings.spending_group_categories, false);

  const groupedEntries: GroupBucket[] | null = (() => {
    if (!showGroups || categoryGroups.length === 0) return null;
    const byGroup = new Map<number, GroupBucket>(categoryGroups.map(g => [g.id, { group: g, entries: [] }]));
    const ungrouped: RealCatEntry[] = [];
    realCatEntries.forEach(c => {
      if (c.group_id && byGroup.has(c.group_id)) byGroup.get(c.group_id)!.entries.push(c);
      else ungrouped.push(c);
    });
    const groups = [...byGroup.values()].filter(g => g.entries.length > 0);
    if (ungrouped.length > 0) groups.push({ group: { id: 'ungrouped', name: 'Ungrouped' }, entries: ungrouped });
    return groups;
  })();

  const renderCategoryRow = (cat: RealCatEntry) => {
    const budget    = budgets[cat.category_id] ?? cat.budget ?? null;
    const bar       = pctBar(cat.amount, budget);
    const isActive  = activeCat === cat.category_id;
    const available = budget != null ? budget - cat.amount : null;
    const level     = bar?.level ?? 'ok';

    return (
      <div key={cat.category_id} className={`transition-colors ${isActive ? 'bg-primary/8' : 'hover:bg-muted/30'}`}>
        <button type="button" onClick={() => selectCat(cat.category_id)} className="w-full text-left px-4 pt-3">
          <div className="flex items-center justify-between gap-2">
            <span className="text-sm font-medium truncate">{cat.category_name}</span>
            <span className="text-sm font-mono font-semibold shrink-0">{fmt(cat.amount)}</span>
          </div>
          {bar && (
            <div className="mt-1.5 h-1.5 w-full rounded-full bg-muted/50">
              <div className={`h-full rounded-full transition-all ${LEVEL_BAR_CLASS[level]}`} style={{ width: `${bar.pct}%` }} />
            </div>
          )}
          <div className="flex items-center justify-between mt-1 gap-2">
            <span className="text-[11px] text-muted-foreground">
              {cat.tx_count} transaction{cat.tx_count !== 1 ? 's' : ''}
              {showAvg && (cat.avg_3mo ?? 0) > 0 && (
                <span className="ml-2 text-muted-foreground/70">avg {fmt(cat.avg_3mo)}/mo</span>
              )}
            </span>
            {available != null && (
              <span className={`text-[11px] font-medium ${LEVEL_TEXT_CLASS[level]}`}>
                {available < 0 ? `${fmt(Math.abs(available))} over` : `${fmt(available)} left`}
              </span>
            )}
          </div>
        </button>
        <div className="flex items-center justify-between gap-2 px-4 pb-3 pt-1.5">
          <BudgetEditor
            categoryId={cat.category_id}
            year={year} month={month}
            initial={budgets[cat.category_id] ?? null}
            onSaved={handleBudgetSaved}
          />
          {showCover && level === 'over' && (
            <CoverOverspendPicker cat={cat} siblings={realCatEntries} budgets={budgets} year={year} month={month} onCovered={handleBudgetsCovered} />
          )}
        </div>
      </div>
    );
  };

  const renderUncatRow = () => (
    <button
      type="button"
      onClick={() => selectCat(null)}
      className={`w-full text-left px-4 py-3 transition-colors ${activeCat === null ? 'bg-primary/8' : 'hover:bg-muted/30'}`}
    >
      <div className="flex items-center justify-between gap-2">
        <span className="text-sm font-medium text-muted-foreground flex items-center gap-2">
          <Tag className="h-3.5 w-3.5" /> Uncategorized
        </span>
        <span className="text-sm font-mono font-semibold text-muted-foreground">{fmt(uncatEntry?.amount)}</span>
      </div>
      <p className="text-[11px] text-muted-foreground mt-0.5">{uncatEntry?.tx_count} transaction{uncatEntry?.tx_count !== 1 ? 's' : ''}</p>
    </button>
  );

  const renderOtherRow = () => (
    <div
      title="Categorized outside Spending — enable Spending on this category to manage it here."
      className="px-4 py-3"
    >
      <div className="flex items-center justify-between gap-2">
        <span className="text-sm font-medium text-muted-foreground flex items-center gap-2">
          <Layers className="h-3.5 w-3.5" /> Other categories
        </span>
        <span className="text-sm font-mono font-semibold text-muted-foreground">{fmt(otherEntry?.amount)}</span>
      </div>
      <p className="text-[11px] text-muted-foreground mt-0.5">{otherEntry?.tx_count} transaction{otherEntry?.tx_count !== 1 ? 's' : ''}</p>
    </div>
  );

  const activeCatLabel = activeCat === null
    ? 'Uncategorized'
    : activeCat !== undefined
      ? realCatEntries.find(c => c.category_id === activeCat)?.category_name
      : null;

  return (
    <div className="min-h-screen bg-[radial-gradient(circle_at_top_right,oklch(var(--primary)/0.05),transparent_30rem)] pb-16">
      <div className="mx-auto max-w-4xl px-4 py-6 sm:px-6 space-y-6">

        {/* Header */}
        <div className="flex items-center justify-between gap-4">
          <div>
            <h1 className="text-xl font-semibold">Spending</h1>
            <p className="text-sm text-muted-foreground">Unmatched bank transactions by category</p>
          </div>
          <div className="flex items-center gap-1">
            <Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => navMonth(-1)} aria-label="Previous month">
              <ChevronLeft className="h-4 w-4" />
            </Button>
            <span className="text-sm font-medium w-24 text-center">
              {MONTH_NAMES[month - 1]} {year}
            </span>
            <Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => navMonth(1)} aria-label="Next month">
              <ChevronRight className="h-4 w-4" />
            </Button>
            <SpendingSettingsMenu settings={spendingSettings} onToggle={(key, checked) => saveSpendingSetting({ [key]: checked ? 'true' : 'false' })} saving={savingSpendingSetting} />
          </div>
        </div>

        {/* Summary load error */}
        {summaryError && (
          <div className="flex items-center gap-3 rounded-xl border border-destructive/25 bg-destructive/10 px-4 py-3">
            <AlertCircle className="h-4 w-4 text-destructive shrink-0" />
            <span className="text-sm text-destructive flex-1">{summaryError}</span>
            <Button variant="outline" size="sm" onClick={() => loadSummary()} className="h-7 gap-1.5 text-xs">
              <RefreshCw className="h-3.5 w-3.5" /> Retry
            </Button>
          </div>
        )}

        {/* Ready to Assign */}
        {showReadyToAssign && (
          <div className={`rounded-xl border px-4 py-4 ${readyToAssign >= 0 ? 'border-emerald-500/25 bg-emerald-500/5' : 'border-amber-500/25 bg-amber-500/10'}`}>
            <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Ready to Assign</p>
            <p className={`text-3xl font-bold mt-1 ${readyToAssign >= 0 ? 'text-emerald-500' : 'text-amber-500'}`}>
              {fmt(Math.abs(readyToAssign))}
            </p>
            <p className="text-xs text-muted-foreground mt-1">
              {readyToAssign >= 0 ? 'left to budget this month' : 'over-assigned — budgets exceed income'}
            </p>
          </div>
        )}

        {/* Overview strip */}
        <div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
          <div className="rounded-xl border border-border/60 bg-card/80 px-4 py-3">
            <p className="text-xs text-muted-foreground font-medium uppercase tracking-wide">Budgeted</p>
            <p className="text-2xl font-bold mt-1">{fmt(totalBudgeted)}</p>
          </div>
          <div className="rounded-xl border border-border/60 bg-card/80 px-4 py-3">
            <p className="text-xs text-muted-foreground font-medium uppercase tracking-wide">Spent</p>
            <p className="text-2xl font-bold mt-1">{fmt(totalSpending)}</p>
          </div>
          <div className="rounded-xl border border-border/60 bg-card/80 px-4 py-3">
            <p className="text-xs text-muted-foreground font-medium uppercase tracking-wide">Remaining</p>
            <p className={`text-2xl font-bold mt-1 ${remaining < 0 ? 'text-destructive' : 'text-emerald-500'}`}>{fmt(remaining)}</p>
          </div>
          <div className="rounded-xl border border-border/60 bg-card/80 px-4 py-3">
            <p className="text-xs text-muted-foreground font-medium uppercase tracking-wide">Income</p>
            <p className="text-2xl font-bold mt-1 text-emerald-500">{fmt(income)}</p>
          </div>
          <div className="rounded-xl border border-border/60 bg-card/80 px-4 py-3 col-span-2 sm:col-span-1">
            <p className="text-xs text-muted-foreground font-medium uppercase tracking-wide">Uncategorized</p>
            <p className="text-2xl font-bold mt-1">{fmt(summary?.uncategorized_amount)}</p>
            {(summary?.uncategorized_count ?? 0) > 0 && (
              <p className="text-xs text-muted-foreground">{summary?.uncategorized_count} transaction{summary?.uncategorized_count !== 1 ? 's' : ''}</p>
            )}
          </div>
        </div>

        {/* No spending categories notice */}
        {categories.length === 0 && (
          <div className="flex items-start gap-3 rounded-xl border border-amber-500/25 bg-amber-500/10 px-4 py-3">
            <Tag className="h-4 w-4 text-amber-500 shrink-0 mt-0.5" />
            <div className="text-sm text-amber-600 dark:text-amber-400">
              No spending categories are enabled yet. Go to{' '}
              <a href="/categories" className="underline underline-offset-2 font-medium">Categories</a>
              {' '}and enable "Spending" on the categories you want to use here.
            </div>
          </div>
        )}
        {catError && (
          <div className="flex items-center gap-3 rounded-xl border border-destructive/25 bg-destructive/10 px-4 py-3">
            <AlertCircle className="h-4 w-4 text-destructive shrink-0" />
            <span className="text-sm text-destructive flex-1">{catError}</span>
            <Button variant="outline" size="sm" onClick={() => loadCategories()} className="h-7 gap-1.5 text-xs">
              <RefreshCw className="h-3.5 w-3.5" /> Retry
            </Button>
          </div>
        )}

        {/* Category breakdown */}
        <div className="rounded-xl border border-border/60 bg-card/80 overflow-hidden">
          <div className="px-4 py-3 border-b border-border/40 flex items-center gap-2">
            <TrendingDown className="h-4 w-4 text-muted-foreground" />
            <span className="text-sm font-medium">By Category</span>
            <div className="ml-auto flex items-center gap-2">
              {activeCat !== undefined && (
                <button type="button" onClick={() => setActiveCat(undefined)}
                  className="text-xs text-muted-foreground hover:text-foreground underline-offset-4 hover:underline">
                  Show all
                </button>
              )}
              <button
                type="button"
                onClick={handleCopyBudgets}
                disabled={copying}
                title="Copy budgets from last month"
                className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors disabled:opacity-40"
              >
                <Copy className="h-3.5 w-3.5" />
                {copying ? 'Copying…' : 'Copy last month'}
              </button>
            </div>
          </div>

          {realCatEntries.length === 0 && !uncatEntry && !otherEntry ? (
            <div className="flex flex-col items-center gap-2 py-10 text-center">
              <Wallet className="h-8 w-8 text-muted-foreground/50" />
              <p className="text-sm text-muted-foreground">No spending found for this month yet.</p>
              <p className="text-xs text-muted-foreground/70">Set a budget or categorize transactions below to see them here.</p>
            </div>
          ) : groupedEntries ? (
            <div className="divide-y divide-border/30">
              {groupedEntries.map(({ group, entries }) => {
                const subtotalAmount = entries.reduce((s, c) => s + c.amount, 0);
                const subtotalBudget = entries.reduce((s, c) => s + (budgets[c.category_id] ?? c.budget ?? 0), 0);
                return (
                  <div key={group.id}>
                    <div className="px-4 py-2 bg-muted/20 flex items-center justify-between">
                      <span className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">{group.name}</span>
                      <span className="text-xs font-mono text-muted-foreground">{fmt(subtotalAmount)} / {fmt(subtotalBudget)}</span>
                    </div>
                    <div className="divide-y divide-border/30">
                      {entries.map(renderCategoryRow)}
                    </div>
                  </div>
                );
              })}
              {(uncatEntry || otherEntry) && (
                <div className="divide-y divide-border/30">
                  {uncatEntry && renderUncatRow()}
                  {otherEntry && renderOtherRow()}
                </div>
              )}
            </div>
          ) : (
            <div className="divide-y divide-border/30">
              {realCatEntries.map(renderCategoryRow)}
              {uncatEntry && renderUncatRow()}
              {otherEntry && renderOtherRow()}
            </div>
          )}
        </div>

        {/* Transaction list */}
        <div className="rounded-xl border border-border/60 bg-card/80 overflow-hidden">
          <div className="px-4 py-3 border-b border-border/40 flex items-center gap-2">
            <ReceiptText className="h-4 w-4 text-muted-foreground" />
            <span className="text-sm font-medium">
              Transactions
              {activeCatLabel && (
                <span className="ml-1.5 text-muted-foreground font-normal">
                  — {activeCatLabel}
                </span>
              )}
            </span>
            <Badge variant="secondary" className="ml-auto text-xs">{txTotal}</Badge>
          </div>

          {txError ? (
            <div className="flex items-center gap-3 px-4 py-6">
              <AlertCircle className="h-4 w-4 text-destructive shrink-0" />
              <span className="text-sm text-destructive flex-1">{txError}</span>
              <Button variant="outline" size="sm" onClick={() => loadTransactions(txPage)} className="h-7 gap-1.5 text-xs">
                <RefreshCw className="h-3.5 w-3.5" /> Retry
              </Button>
            </div>
          ) : txLoading ? (
            <div className="divide-y divide-border/30">
              {Array.from({ length: 5 }).map((_, i) => (
                <div key={i} className="flex items-center gap-3 px-4 py-2.5">
                  <div className="min-w-0 flex-1 space-y-1.5">
                    <Skeleton variant="line" className="h-3.5 w-2/3" />
                    <Skeleton variant="line" className="h-3 w-1/4" />
                  </div>
                  <Skeleton variant="line" className="h-3.5 w-16" />
                  <Skeleton variant="line" className="h-7 w-28" />
                </div>
              ))}
            </div>
          ) : transactions.length === 0 ? (
            <div className="flex flex-col items-center gap-2 py-10 text-center">
              <ReceiptText className="h-8 w-8 text-muted-foreground/50" />
              <p className="text-sm text-muted-foreground">No transactions found{activeCatLabel ? ` for ${activeCatLabel}` : ''}.</p>
            </div>
          ) : (
            <>
              <div>
                {transactions.map(tx => (
                  <TxRow
                    key={tx.id}
                    tx={tx}
                    categories={categories}
                    onCategorize={handleCategorize}
                  />
                ))}
              </div>
              {txPages > 1 && (
                <div className="flex items-center justify-center gap-2 px-4 py-3 border-t border-border/30">
                  <Button variant="ghost" size="sm" disabled={txPage <= 1} onClick={() => loadTransactions(txPage - 1)}>
                    <ChevronLeft className="h-4 w-4" />
                  </Button>
                  <span className="text-xs text-muted-foreground">Page {txPage} of {txPages}</span>
                  <Button variant="ghost" size="sm" disabled={txPage >= txPages} onClick={() => loadTransactions(txPage + 1)}>
                    <ChevronRight className="h-4 w-4" />
                  </Button>
                </div>
              )}
            </>
          )}
        </div>

        {/* Income & deposits */}
        <IncomeSection year={year} month={month} totalIncome={summary?.income} />

        {/* Category groups management */}
        {showGroups && (
          <CategoryGroupManager
            categories={categories}
            groups={categoryGroups}
            onGroupsChanged={refreshAfterGroupChange}
            onCategoriesChanged={refreshAfterCategoryChange}
          />
        )}

        {/* Merchant rules manager */}
        <RulesManager categories={categories} />

      </div>
    </div>
  );
}
