import { useEffect, useState, useCallback, useMemo, useRef, useDeferredValue } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { useBills, useCategories, useBillTemplates, useDeletedBills } from '@/hooks/useQueries';
import { Plus, ChevronRight, SlidersHorizontal, Search, Trash2 } from 'lucide-react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import SearchFilterPanel from '@/components/SearchFilterPanel';

import {
  Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
} from '@/components/ui/dialog';
import {
  AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle,
  AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction,
} from '@/components/ui/alert-dialog';
import {
  Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
} from '@/components/ui/select';
import { api } from '@/api';
import { cn, errMessage } from '@/lib/utils';
import { useSearchPanelPreference } from '@/hooks/useSearchPanelPreference';
import { moveInArray, movedItemId, reorderPayload } from '@/lib/reorder';
import BillsTableInner from '@/components/BillsTableInner';
import BillModal from '@/components/BillModal';
import RecentlyDeletedBillsDialog from '@/components/RecentlyDeletedBillsDialog';
import { makeBillDraft, type SourceBill } from '@/lib/billDrafts';
import { scheduleLabel, scheduleValue } from '@/lib/billingSchedule';
import type { Bill } from '@/types';
import type { DragProps, MoveControls } from '@/components/tracker/MobileTrackerRow';

interface BuiltInTemplate { id: string; name: string; data: SourceBill; categoryKeywords: string[] }
interface BillTemplate { id: number | string; name: string; data?: SourceBill; categoryKeywords?: string[] }

const VISIBILITY_OPTIONS = [
  { value: 'default', label: 'Default', description: 'Use the app default behavior for inactive bill history.' },
  { value: 'all',     label: 'Show all history', description: 'Show all historical tracker data for this inactive bill.' },
  { value: 'none',    label: 'Show no history', description: 'Hide historical tracker data for this inactive bill.' },
  { value: 'ranges',  label: 'Show only selected date ranges', description: 'Show history only for the ranges listed below.' },
];
const MONTH_OPTIONS: [string, string][] = [
  ['1', 'Jan'], ['2', 'Feb'], ['3', 'Mar'], ['4', 'Apr'], ['5', 'May'], ['6', 'Jun'],
  ['7', 'Jul'], ['8', 'Aug'], ['9', 'Sep'], ['10', 'Oct'], ['11', 'Nov'], ['12', 'Dec'],
];
const FILTER_ALL = 'all';

const BUILT_IN_TEMPLATES: BuiltInTemplate[] = [
  {
    id: 'builtin-utility',
    name: 'Utility',
    data: {
      name: 'Utility Bill',
      due_day: 15,
      expected_amount: 0,
      billing_cycle: 'monthly',
      cycle_type: 'monthly',
      cycle_day: '1',
      autopay_enabled: false,
      autodraft_status: 'none',
      has_2fa: false,
      notes: 'Utility service bill',
    },
    categoryKeywords: ['utility', 'utilities', 'electric', 'water'],
  },
  {
    id: 'builtin-credit-card',
    name: 'Credit Card',
    data: {
      name: 'Credit Card',
      due_day: 20,
      expected_amount: 0,
      billing_cycle: 'monthly',
      cycle_type: 'monthly',
      cycle_day: '1',
      autopay_enabled: false,
      autodraft_status: 'none',
      has_2fa: true,
      current_balance: 0,
      minimum_payment: 0,
      interest_rate: 0,
      snowball_include: true,
      notes: 'Credit card minimum payment',
    },
    categoryKeywords: ['credit card', 'credit cards', 'debt'],
  },
  {
    id: 'builtin-subscription',
    name: 'Subscription',
    data: {
      name: 'Subscription',
      due_day: 1,
      expected_amount: 0,
      billing_cycle: 'monthly',
      cycle_type: 'monthly',
      cycle_day: '1',
      autopay_enabled: true,
      autodraft_status: 'assumed_paid',
      has_2fa: false,
      notes: 'Recurring subscription',
    },
    categoryKeywords: ['subscription', 'subscriptions', 'entertainment'],
  },
  {
    id: 'builtin-loan',
    name: 'Loan',
    data: {
      name: 'Loan Payment',
      due_day: 1,
      expected_amount: 0,
      billing_cycle: 'monthly',
      cycle_type: 'monthly',
      cycle_day: '1',
      autopay_enabled: false,
      autodraft_status: 'none',
      has_2fa: false,
      current_balance: 0,
      minimum_payment: 0,
      interest_rate: 0,
      snowball_include: true,
      notes: 'Installment loan payment',
    },
    categoryKeywords: ['loan', 'loans', 'debt'],
  },
];

// BillModal is imported from @/components/BillModal

interface HistoryRange {
  id?: number;
  _key: string;
  start_year: string;
  start_month: string;
  end_year: string;
  end_month: string;
  label: string;
}
interface RawRange {
  id?: number;
  _key?: string;
  start_year?: number | string | null;
  start_month?: number | string | null;
  end_year?: number | string | null;
  end_month?: number | string | null;
  label?: string | null;
}

function blankRange(): HistoryRange {
  const year = String(new Date().getFullYear());
  return {
    _key: `new-${Date.now()}-${Math.random().toString(16).slice(2)}`,
    start_year: year,
    start_month: String(new Date().getMonth() + 1),
    end_year: '',
    end_month: '',
    label: '',
  };
}

function normalizeRange(range: RawRange): HistoryRange {
  return {
    ...range,
    _key: range._key || String(range.id),
    start_year: String(range.start_year ?? ''),
    start_month: String(range.start_month ?? ''),
    end_year: range.end_year == null ? '' : String(range.end_year),
    end_month: range.end_month == null ? '' : String(range.end_month),
    label: range.label || '',
  };
}

function rangePayload(range: HistoryRange) {
  return {
    start_year: parseInt(range.start_year, 10),
    start_month: parseInt(range.start_month, 10),
    end_year: range.end_year ? parseInt(range.end_year, 10) : null,
    end_month: range.end_month ? parseInt(range.end_month, 10) : null,
    label: range.label?.trim() || null,
  };
}

function validateRange(range: HistoryRange): string | null {
  const payload = rangePayload(range);
  if (!Number.isInteger(payload.start_year) || payload.start_year < 2000 || payload.start_year > 2100) {
    return 'Start year must be between 2000 and 2100.';
  }
  if (!Number.isInteger(payload.start_month) || payload.start_month < 1 || payload.start_month > 12) {
    return 'Start month must be between 1 and 12.';
  }
  if ((payload.end_year == null) !== (payload.end_month == null)) {
    return 'End year and end month must both be provided or both left blank.';
  }
  if (payload.end_year != null && payload.end_month != null) {
    if (payload.end_year < 2000 || payload.end_year > 2100) {
      return 'End year must be between 2000 and 2100.';
    }
    if (payload.end_month < 1 || payload.end_month > 12) {
      return 'End month must be between 1 and 12.';
    }
    if ((payload.end_year * 12 + payload.end_month) < (payload.start_year * 12 + payload.start_month)) {
      return 'Range end must be on or after the start.';
    }
  }
  return null;
}

// ── Display preferences ───────────────────────────────────────────────────────

const PREFS_KEY = 'bills-display-prefs-v1';

interface DisplayPrefs {
  showCategory: boolean;
  showDueDay: boolean;
  showAmount: boolean;
  showCycle: boolean;
  showApr: boolean;
  showBalance: boolean;
  showMinPayment: boolean;
  showAutopay: boolean;
  show2fa: boolean;
  showSubscription: boolean;
}

const PREFS_DEFAULTS: DisplayPrefs = {
  showCategory:   true,
  showDueDay:     true,
  showAmount:     true,
  showCycle:      true,
  showApr:        true,
  showBalance:    true,
  showMinPayment: true,
  showAutopay:      true,
  show2fa:          true,
  showSubscription: true,
};

const PREFS_LABELS: [keyof DisplayPrefs, string][] = [
  ['showCategory',     'Category'],
  ['showDueDay',       'Due day'],
  ['showAmount',       'Amount'],
  ['showCycle',        'Billing schedule'],
  ['showApr',          'APR'],
  ['showBalance',      'Balance'],
  ['showMinPayment',   'Min payment'],
  ['showAutopay',      'Autopay badge'],
  ['show2fa',          '2FA badge'],
  ['showSubscription', 'Subscription badge'],
];

function useDisplayPrefs() {
  const [prefs, setPrefs] = useState<DisplayPrefs>(() => {
    try {
      const raw = localStorage.getItem(PREFS_KEY);
      return raw ? { ...PREFS_DEFAULTS, ...JSON.parse(raw) } : PREFS_DEFAULTS;
    } catch {
      return PREFS_DEFAULTS;
    }
  });

  const toggle = (key: keyof DisplayPrefs) => {
    setPrefs(prev => {
      const next = { ...prev, [key]: !prev[key] };
      try { localStorage.setItem(PREFS_KEY, JSON.stringify(next)); } catch { /* ignore quota errors */ }
      return next;
    });
  };

  return { prefs, toggle };
}

function DisplayPrefsPanel({ prefs, onToggle }: { prefs: DisplayPrefs; onToggle: (key: keyof DisplayPrefs) => void }) {
  const [open, setOpen] = useState(false);
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!open) return;
    const handler = (e: MouseEvent) => {
      if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
    };
    document.addEventListener('mousedown', handler);
    return () => document.removeEventListener('mousedown', handler);
  }, [open]);

  return (
    <div className="relative" ref={ref}>
      <button
        type="button"
        onClick={() => setOpen(v => !v)}
        className={cn(
          'h-9 px-3 rounded-md border border-border/70 bg-card/80 text-xs font-medium',
          'flex items-center gap-2 transition-colors',
          open
            ? 'bg-accent text-foreground'
            : 'text-muted-foreground hover:bg-accent hover:text-foreground',
        )}
        aria-label="Display options"
      >
        <SlidersHorizontal className="h-3.5 w-3.5" />
        <span className="hidden sm:inline">Columns</span>
      </button>

      {open && (
        <div className="absolute right-0 top-full mt-1 z-50 w-48 rounded-xl border border-border/60 bg-card/95 backdrop-blur-xl shadow-xl p-3 space-y-0.5">
          <p className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground px-1 pb-1.5">
            Display options
          </p>
          {PREFS_LABELS.map(([key, label]) => (
            <label
              key={key}
              className="flex items-center gap-2.5 rounded-md px-1.5 py-1.5 text-sm cursor-pointer hover:bg-accent/50 transition-colors"
            >
              <input
                type="checkbox"
                checked={!!prefs[key]}
                onChange={() => onToggle(key)}
                className="h-3.5 w-3.5 rounded border-border accent-primary"
              />
              <span className={cn('text-xs', prefs[key] ? 'text-foreground' : 'text-muted-foreground/60')}>
                {label}
              </span>
            </label>
          ))}
        </div>
      )}
    </div>
  );
}

function amountSearchText(...values: (number | string | null | undefined)[]): string {
  return values
    .filter(value => value !== null && value !== undefined && Number.isFinite(Number(value)))
    .flatMap(value => {
      const num = Number(value);
      return [String(num), num.toFixed(2), `$${num.toFixed(2)}`];
    })
    .join(' ');
}

function billIsDebt(bill: Bill): boolean {
  const category = String(bill.category_name || '').toLowerCase();
  return Number(bill.current_balance) > 0
    || bill.minimum_payment != null
    || ['credit card', 'credit cards', 'loan', 'loans', 'debt', 'mortgage'].some(token => category.includes(token));
}

function FilterChip({ active, children, onClick }: { active: boolean; children: React.ReactNode; onClick: () => void }) {
  return (
    <button
      type="button"
      onClick={onClick}
      className={cn(
        'h-8 rounded-full border px-3 text-xs font-medium transition-colors',
        active
          ? 'border-primary/50 bg-primary/15 text-primary'
          : 'border-border/70 bg-card/70 text-muted-foreground hover:bg-accent hover:text-foreground',
      )}
    >
      {children}
    </button>
  );
}

const BILLS_SORT_KEY = 'bills_sort_mode';
const BILLS_CADENCE_ORDER = ['weekly', 'biweekly', 'monthly', 'quarterly', 'annual', 'other'];

function normalizedBillCadence(bill: Bill): string {
  const raw = String(bill?.cycle_type || bill?.billing_cycle || '').toLowerCase();
  if (raw.includes('week') && raw.includes('bi')) return 'biweekly';
  if (raw === 'biweekly') return 'biweekly';
  if (raw.includes('week')) return 'weekly';
  if (raw.includes('quarter')) return 'quarterly';
  if (raw.includes('annual') || raw.includes('year')) return 'annual';
  if (raw.includes('month') || !raw) return 'monthly';
  return 'other';
}

function billCadenceIndex(bill: Bill): number {
  const index = BILLS_CADENCE_ORDER.indexOf(normalizedBillCadence(bill));
  return index >= 0 ? index : BILLS_CADENCE_ORDER.length - 1;
}

function sortBillsByCadence(items: Bill[]): Bill[] {
  return [...items].sort((a, b) => (
    billCadenceIndex(a) - billCadenceIndex(b)
    || (Number(a.due_day) || 0) - (Number(b.due_day) || 0)
    || String(a.name || '').localeCompare(String(b.name || ''))
  ));
}

function SortModeButton({ active, children, onClick }: { active: boolean; children: React.ReactNode; onClick: () => void }) {
  return (
    <button
      type="button"
      onClick={onClick}
      className={cn(
        'h-7 rounded-md px-3 text-xs font-semibold transition-colors',
        active
          ? 'bg-background text-foreground shadow-sm'
          : 'text-muted-foreground hover:text-foreground',
      )}
      aria-pressed={active}
    >
      {children}
    </button>
  );
}

// ─────────────────────────────────────────────────────────────────────────────

function HistoryVisibilityDialog({ bill, onClose, onSaved }: { bill: Bill; onClose: () => void; onSaved: () => void }) {
  const [visibility, setVisibility] = useState<string>((bill?.history_visibility as string) || 'default');
  const [ranges, setRanges] = useState<HistoryRange[]>([]);
  const [deletedIds, setDeletedIds] = useState<number[]>([]);
  const [loading, setLoading] = useState(true);
  const [saving, setSaving] = useState(false);

  useEffect(() => {
    let mounted = true;
    setLoading(true);
    api.billHistoryRanges(bill.id)
      .then(raw => {
        if (!mounted) return;
        const data = raw as { history_visibility?: string; ranges?: RawRange[] };
        setVisibility(data.history_visibility || (bill.history_visibility as string) || 'default');
        setRanges((data.ranges || []).map(normalizeRange));
      })
      .catch(err => {
        toast.error(errMessage(err, 'Failed to load history visibility.'));
        onClose();
      })
      .finally(() => { if (mounted) setLoading(false); });
    return () => { mounted = false; };
  }, [bill, onClose]);

  const updateRange = (key: string, patch: Partial<HistoryRange>) => {
    setRanges(prev => prev.map(r => r._key === key ? { ...r, ...patch } : r));
  };

  const deleteRange = (range: HistoryRange) => {
    if (range.id) setDeletedIds(prev => [...prev, range.id!]);
    setRanges(prev => prev.filter(r => r._key !== range._key));
  };

  const save = async () => {
    if (visibility === 'ranges') {
      if (ranges.length === 0) {
        toast.error('Add at least one date range or choose another visibility option.');
        return;
      }
      const invalid = ranges.map(validateRange).find(Boolean);
      if (invalid) {
        toast.error(invalid);
        return;
      }
    }

    setSaving(true);
    try {
      await api.updateBill(bill.id, { history_visibility: visibility });
      for (const id of deletedIds) {
        await api.deleteBillHistoryRange(bill.id, id);
      }
      if (visibility === 'ranges') {
        for (const range of ranges) {
          const payload = rangePayload(range);
          if (range.id) {
            await api.updateBillHistoryRange(bill.id, range.id, payload);
          } else {
            await api.createBillHistoryRange(bill.id, payload);
          }
        }
      }
      toast.success('History visibility saved.');
      onSaved();
      onClose();
    } catch (err) {
      toast.error(errMessage(err, 'Failed to save history visibility.'));
    } finally {
      setSaving(false);
    }
  };

  return (
    <Dialog open onOpenChange={v => { if (!v && !saving) onClose(); }}>
      <DialogContent className="sm:max-w-4xl border-border/60 bg-card/95 backdrop-blur-xl">
        <DialogHeader>
          <DialogTitle className="text-base font-semibold tracking-tight">
            History Visibility: {bill.name}
          </DialogTitle>
        </DialogHeader>

        {loading ? (
          <div className="py-12 text-center text-sm text-muted-foreground">Loading history visibility...</div>
        ) : (
          <div className="space-y-5">
            <div className="rounded-lg border border-border/60 bg-muted/25 p-4">
              <p className="text-sm font-medium">Inactive bill history</p>
              <p className="mt-1 text-xs text-muted-foreground">
                Choose whether this inactive bill should appear in historical tracker views. Active bill behavior is unchanged.
              </p>
            </div>

            <div className="grid gap-3 md:grid-cols-2">
              {VISIBILITY_OPTIONS.map(option => (
                <label
                  key={option.value}
                  className={cn(
                    'flex cursor-pointer gap-3 rounded-lg border px-4 py-3 transition-colors',
                    visibility === option.value
                      ? 'border-primary/40 bg-primary/10'
                      : 'border-border/60 bg-muted/15 hover:bg-muted/30'
                  )}
                >
                  <input
                    type="radio"
                    name="history_visibility"
                    value={option.value}
                    checked={visibility === option.value}
                    onChange={() => setVisibility(option.value)}
                    className="mt-1 h-4 w-4 accent-primary"
                  />
                  <span>
                    <span className="block text-sm font-medium">{option.label}</span>
                    <span className="mt-0.5 block text-xs text-muted-foreground">{option.description}</span>
                  </span>
                </label>
              ))}
            </div>

            {visibility === 'ranges' && (
              <div className="space-y-3">
                <div className="flex items-center justify-between">
                  <div>
                    <p className="text-sm font-medium">Selected date ranges</p>
                    <p className="text-xs text-muted-foreground">End month/year are optional for open-ended ranges.</p>
                  </div>
                  <Button type="button" size="sm" variant="outline" onClick={() => setRanges(prev => [...prev, blankRange()])}>
                    <Plus className="h-4 w-4" />
                    Add range
                  </Button>
                </div>

                {ranges.length === 0 ? (
                  <div className="rounded-lg bg-muted/20 px-4 py-8 text-center text-sm text-muted-foreground">
                    No ranges yet. Add a range to use selected history.
                  </div>
                ) : (
                  <div className="space-y-2">
                    {ranges.map(range => (
                      <div key={range._key} className="grid gap-2 rounded-lg border border-border/60 bg-background/40 p-3 md:grid-cols-[1fr_120px_1fr_120px_1.2fr_auto] md:items-end">
                        <div className="space-y-1.5">
                          <Label className="text-[10px] uppercase tracking-wider text-muted-foreground">Start year</Label>
                          <Input
                            type="number"
                            min="2000"
                            max="2100"
                            value={range.start_year}
                            onChange={e => updateRange(range._key, { start_year: e.target.value })}
                            className="h-8"
                          />
                        </div>
                        <div className="space-y-1.5">
                          <Label className="text-[10px] uppercase tracking-wider text-muted-foreground">Start month</Label>
                          <Select value={range.start_month} onValueChange={v => updateRange(range._key, { start_month: v })}>
                            <SelectTrigger className="h-8">
                              <SelectValue />
                            </SelectTrigger>
                            <SelectContent>
                              {MONTH_OPTIONS.map(([value, label]) => <SelectItem key={value} value={value}>{label}</SelectItem>)}
                            </SelectContent>
                          </Select>
                        </div>
                        <div className="space-y-1.5">
                          <Label className="text-[10px] uppercase tracking-wider text-muted-foreground">End year</Label>
                          <Input
                            type="number"
                            min="2000"
                            max="2100"
                            placeholder="Open"
                            value={range.end_year}
                            onChange={e => updateRange(range._key, { end_year: e.target.value })}
                            className="h-8"
                          />
                        </div>
                        <div className="space-y-1.5">
                          <Label className="text-[10px] uppercase tracking-wider text-muted-foreground">End month</Label>
                          <Select value={range.end_month || 'open'} onValueChange={v => updateRange(range._key, { end_month: v === 'open' ? '' : v })}>
                            <SelectTrigger className="h-8">
                              <SelectValue />
                            </SelectTrigger>
                            <SelectContent>
                              <SelectItem value="open">Open</SelectItem>
                              {MONTH_OPTIONS.map(([value, label]) => <SelectItem key={value} value={value}>{label}</SelectItem>)}
                            </SelectContent>
                          </Select>
                        </div>
                        <div className="space-y-1.5">
                          <Label className="text-[10px] uppercase tracking-wider text-muted-foreground">Label</Label>
                          <Input
                            value={range.label}
                            onChange={e => updateRange(range._key, { label: e.target.value })}
                            placeholder="Optional"
                            className="h-8"
                          />
                        </div>
                        <Button
                          type="button"
                          variant="ghost"
                          size="icon"
                          className="h-8 w-8 text-destructive hover:bg-destructive/10 hover:text-destructive"
                          onClick={() => deleteRange(range)}
                          aria-label="Delete history range"
                        >
                          <Trash2 className="h-4 w-4" />
                        </Button>
                      </div>
                    ))}
                  </div>
                )}
              </div>
            )}
          </div>
        )}

        <DialogFooter className="mt-2">
          <Button type="button" variant="ghost" disabled={saving} onClick={onClose} className="text-xs">
            Cancel
          </Button>
          <Button type="button" disabled={saving || loading} onClick={save} className="text-xs">
            {saving ? 'Saving...' : 'Save History Visibility'}
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

interface ModalState { bill: Bill | null; initialBill?: Partial<Bill> }
type BoolFilterKey = 'autopay' | 'firstBucket' | 'fifteenthBucket' | 'debt' | 'inactive';
interface BillFilters {
  category: string;
  cycle: string;
  autopay: boolean;
  firstBucket: boolean;
  fifteenthBucket: boolean;
  debt: boolean;
  inactive: boolean;
}

// ── Bills Page ─────────────────────────────────────────────────────────────
export default function BillsPage() {
  const [searchParams] = useSearchParams();
  const queryClient = useQueryClient();
  // React Query is the source of truth (reuses the shared ['bills']/['categories']
  // caches, so bill mutations here also refresh the Tracker/badge). Optimistic
  // list edits below write to the cache via setQueryData.
  const { data: bills = [], isPending: billsPending } = useBills();
  const { data: categories = [] } = useCategories();
  const { data: savedTemplatesRaw = [] } = useBillTemplates();
  const savedTemplates = savedTemplatesRaw as BillTemplate[];
  const { data: deletedBills = [] } = useDeletedBills();
  const loading = billsPending;
  const setBills = useCallback(
    (updater: Bill[] | ((prev: Bill[]) => Bill[])) => queryClient.setQueryData<Bill[]>(['bills'], prev => (
      typeof updater === 'function' ? updater(prev || []) : updater
    )),
    [queryClient],
  );
  const refresh = useCallback(() => Promise.all([
    queryClient.invalidateQueries({ queryKey: ['bills'] }),
    queryClient.invalidateQueries({ queryKey: ['categories'] }),
    queryClient.invalidateQueries({ queryKey: ['bill-templates'] }),
    queryClient.invalidateQueries({ queryKey: ['deleted-bills'] }),
  ]), [queryClient]);
  const [showDeleted, setShowDeleted] = useState(false);
  const [showInactive, setShowInactive] = useState(false);
  const [search, setSearch] = useState('');
  // Keep typing responsive: the input updates `search` instantly while the
  // (whole-list) filter below runs against the deferred value (React 19).
  const deferredSearch = useDeferredValue(search);
  const [draggingId, setDraggingId] = useState<number | null>(null);
  const [dropTargetId, setDropTargetId] = useState<number | null>(null);
  const [movingBillId, setMovingBillId] = useState<number | string | null>(null);
  const [filters, setFilters] = useState<BillFilters>({
    category: FILTER_ALL,
    cycle: FILTER_ALL,
    autopay: false,
    firstBucket: false,
    fifteenthBucket: false,
    debt: false,
    inactive: false,
  });

  // modal state: null = closed, { bill: null } = add new, { bill: {...} } = edit
  const [modal,      setModal]      = useState<ModalState | null>(null);
  // Bill pending deactivation confirmation (AlertDialog replaces window.confirm)
  const [deactivate, setDeactivate] = useState<Bill | null>(null);
  const [deactivateReason, setDeactivateReason] = useState('');
  const [deleteTarget, setDeleteTarget] = useState<Bill | null>(null);
  const [deleteConfirmed, setDeleteConfirmed] = useState(false);
  const [deleteBusy, setDeleteBusy] = useState(false);
  const [historyTarget, setHistoryTarget] = useState<Bill | null>(null);

  const { prefs, toggle: togglePref } = useDisplayPrefs();

  const [billsSort, setBillsSort] = useState<'custom' | 'cadence'>(() => (
    localStorage.getItem(BILLS_SORT_KEY) === 'cadence' ? 'cadence' : 'custom'
  ));
  const [searchPanelCollapsed, setSearchPanelCollapsed] = useSearchPanelPreference();

  useEffect(() => {
    localStorage.setItem(BILLS_SORT_KEY, billsSort);
  }, [billsSort]);


  useEffect(() => {
    const querySearch = searchParams.get('search') || '';
    const includeInactive = searchParams.get('inactive') === '1' || searchParams.get('inactive') === 'true';
    if (querySearch) setSearch(querySearch);
    if (includeInactive) {
      setShowInactive(true);
      setFilters(prev => ({ ...prev, inactive: true }));
    }
  }, [searchParams]);

  useEffect(() => {
    if (filters.inactive) setShowInactive(true);
  }, [filters.inactive]);

  const toggleFilter = (key: BoolFilterKey) => setFilters(prev => ({ ...prev, [key]: !prev[key] }));
  const setFilterValue = (key: 'category' | 'cycle', value: string) => setFilters(prev => ({ ...prev, [key]: value }));
  const hasFilters = !!(
    search.trim()
    || filters.category !== FILTER_ALL
    || filters.cycle !== FILTER_ALL
    || filters.autopay
    || filters.firstBucket
    || filters.fifteenthBucket
    || filters.debt
    || filters.inactive
  );

  const resetFilters = () => {
    setSearch('');
    setFilters({
      category: FILTER_ALL,
      cycle: FILTER_ALL,
      autopay: false,
      firstBucket: false,
      fifteenthBucket: false,
      debt: false,
      inactive: false,
    });
  };

  async function handleEdit(billId: number) {
    try {
      const bill = await api.bill(billId);
      setModal({ bill });
    } catch (err) {
      toast.error(errMessage(err, 'Failed to load bill'));
    }
  }

  function handleDuplicateBill(bill: Bill) {
    setModal({ bill: null, initialBill: makeBillDraft(bill, { copy: true, categories }) as Partial<Bill> });
  }

  function handleTemplateSelect(value: string) {
    if (!value || value === 'placeholder') return;
    const builtIn = BUILT_IN_TEMPLATES.find(template => template.id === value);
    if (builtIn) {
      setModal({ bill: null, initialBill: makeBillDraft(builtIn.data, { template: builtIn, categories }) as Partial<Bill> });
      return;
    }

    const saved = savedTemplates.find(template => `saved-${template.id}` === value);
    if (saved) {
      setModal({ bill: null, initialBill: makeBillDraft(saved.data, { template: saved, categories }) as Partial<Bill> });
    }
  }

  function handleToggle(bill: Bill) {
    if (bill.active) {
      // Prompt confirmation before deactivating
      setDeactivate(bill);
    } else {
      doToggle(bill);
    }
  }

  async function doToggle(bill: Bill | null, reason?: string) {
    if (!bill) return;
    try {
      const payload: { active: number; inactive_reason?: string } = { active: bill.active ? 0 : 1 };
      if (bill.active && reason) payload.inactive_reason = reason;
      await api.updateBill(bill.id, payload);
      toast.success(bill.active ? 'Bill deactivated' : 'Bill activated');
      refresh();
    } catch (err) {
      toast.error(errMessage(err, 'Failed to update bill'));
    }
  }

  function handleDeleteRequest(bill: Bill) {
    setDeleteConfirmed(false);
    setDeleteTarget(bill);
  }

  async function handleDeleteConfirmed() {
    if (!deleteTarget || !deleteConfirmed) return;
    setDeleteBusy(true);
    try {
      const bill = deleteTarget;
      await api.deleteBill(bill.id);
      setBills(prev => prev.filter(b => b.id !== bill.id));
      toast.success(`"${bill.name}" moved to recovery`, {
        description: 'It will be permanently purged after 30 days.',
        action: {
          label: 'Undo',
          onClick: async () => {
            try {
              await api.restoreBill(bill.id);
              toast.success(`"${bill.name}" restored`);
              refresh();
            } catch (err) {
              toast.error(errMessage(err, 'Failed to restore bill'));
            }
          },
        },
      });
      setDeleteTarget(null);
      setDeleteConfirmed(false);
      refresh();
    } catch (err) {
      toast.error(errMessage(err, 'Failed to delete bill'));
    } finally {
      setDeleteBusy(false);
    }
  }

  async function handleRestoreDeleted(bill: Bill) {
    try {
      await api.restoreBill(bill.id);
      toast.success(`"${bill.name}" restored`);
      await refresh();
    } catch (err) {
      toast.error(errMessage(err, 'Failed to restore bill'));
    }
  }

  async function handleDeleteAlternative() {
    if (!deleteTarget) return;
    const bill = deleteTarget;
    setDeleteTarget(null);
    setDeleteConfirmed(false);
    await doToggle(bill);
  }

  const cycleOptions = useMemo(() => (
    Array.from(new Set(bills.map(bill => scheduleValue(bill)))).sort()
  ), [bills]);

  const filteredBills = useMemo(() => {
    const q = deferredSearch.trim().toLowerCase();
    return bills.filter(bill => {
      if (filters.inactive && bill.active) return false;
      if (filters.category !== FILTER_ALL && String(bill.category_id ?? '') !== filters.category) return false;
      if (filters.cycle !== FILTER_ALL && scheduleValue(bill) !== filters.cycle) return false;
      if (filters.autopay && !bill.autopay_enabled) return false;
      if (filters.debt && !billIsDebt(bill)) return false;
      if (filters.firstBucket && !filters.fifteenthBucket && bill.bucket !== '1st') return false;
      if (filters.fifteenthBucket && !filters.firstBucket && bill.bucket !== '15th') return false;

      if (!q) return true;
      const haystack = [
        bill.name,
        bill.category_name,
        bill.notes,
        scheduleValue(bill),
        scheduleLabel(bill),
        bill.bucket,
        amountSearchText(bill.expected_amount, bill.current_balance, bill.minimum_payment, bill.interest_rate),
      ].filter(Boolean).join(' ').toLowerCase();
      return haystack.includes(q);
    });
  }, [bills, filters, deferredSearch]);

  const active   = filteredBills.filter(b => b.active);
  const inactive = filteredBills.filter(b => !b.active);
  const totalActive = bills.filter(b => b.active).length;
  const totalInactive = bills.filter(b => !b.active).length;
  const reorderEnabled = !hasFilters && !loading && billsSort === 'custom';

  const sortedActive   = billsSort === 'cadence' ? sortBillsByCadence(active)   : active;
  const sortedInactive = billsSort === 'cadence' ? sortBillsByCadence(inactive) : inactive;

  async function persistBillOrder(nextBills: Bill[], movedId: string | number | null) {
    setBills(nextBills);
    setMovingBillId(movedId);
    try {
      await api.reorderBills(reorderPayload(nextBills));
      toast.success('Bill order saved');
      refresh();
    } catch (err) {
      toast.error(errMessage(err, 'Failed to save bill order'));
      refresh();
    } finally {
      setMovingBillId(null);
    }
  }

  function reorderBillGroup(activeState: boolean, orderedGroup: Bill[]) {
    const sourceGroup = bills.filter(bill => !!bill.active === activeState);
    const replacements = [...orderedGroup];
    const nextBills = bills.map(bill => (
      !!bill.active === activeState ? replacements.shift()! : bill
    ));
    persistBillOrder(nextBills, movedItemId(sourceGroup, orderedGroup));
  }

  function moveControlsForGroup(group: Bill[], activeState: boolean): (bill: Bill, index: number) => MoveControls {
    return (bill, index) => ({
      enabled: reorderEnabled,
      moving: movingBillId === bill.id,
      canMoveUp: index > 0,
      canMoveDown: index < group.length - 1,
      onMoveUp: () => reorderBillGroup(activeState, moveInArray(group, index, index - 1)),
      onMoveDown: () => reorderBillGroup(activeState, moveInArray(group, index, index + 1)),
    });
  }

  function dragPropsForGroup(group: Bill[], activeState: boolean): (bill: Bill, index: number) => DragProps {
    return (bill, index) => {
      if (!reorderEnabled) return { draggable: false };
      return {
        draggable: true,
        isDragging: draggingId === bill.id,
        isDropTarget: dropTargetId === bill.id && draggingId !== bill.id,
        onDragStart: (event) => {
          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) => {
          event.preventDefault();
          event.dataTransfer.dropEffect = 'move';
          if (draggingId && draggingId !== bill.id) setDropTargetId(bill.id);
        },
        onDrop: (event) => {
          event.preventDefault();
          const sourceId = Number(event.dataTransfer.getData('text/plain') || draggingId);
          const fromIndex = group.findIndex(item => item.id === sourceId);
          if (fromIndex >= 0) reorderBillGroup(activeState, moveInArray(group, fromIndex, index));
          setDraggingId(null);
          setDropTargetId(null);
        },
        onDragEnd: () => {
          setDraggingId(null);
          setDropTargetId(null);
        },
      };
    };
  }

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

      {/* ── 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">
            Manage
          </p>
          <h1 className="text-2xl font-bold tracking-tight">Bills</h1>
          <p className="text-xs text-muted-foreground mt-0.5">
            {totalActive} active
            {totalInactive > 0 && (
              <span className="text-muted-foreground/50"> · {totalInactive} inactive</span>
            )}
          </p>
        </div>

        <div className="flex w-full flex-wrap items-center gap-2 sm:w-auto sm:justify-end">
          <DisplayPrefsPanel prefs={prefs} onToggle={togglePref} />
          <Select value="placeholder" onValueChange={handleTemplateSelect}>
            <SelectTrigger className="h-9 min-w-[160px] flex-1 bg-card sm:w-[180px] sm:flex-none" aria-label="Use a bill template">
              <SelectValue placeholder="Use template" />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="placeholder">Use template...</SelectItem>
              {BUILT_IN_TEMPLATES.map(template => (
                <SelectItem key={template.id} value={template.id}>{template.name}</SelectItem>
              ))}
              {savedTemplates.length > 0 && (
                <>
                  {savedTemplates.map(template => (
                    <SelectItem key={template.id} value={`saved-${template.id}`}>
                      {template.name}
                    </SelectItem>
                  ))}
                </>
              )}
            </SelectContent>
          </Select>
          {deletedBills.length > 0 && (
            <Button
              type="button"
              variant="outline"
              onClick={() => setShowDeleted(true)}
              className="h-9 flex-1 gap-2 px-3 text-sm sm:flex-none"
            >
              <Trash2 className="h-4 w-4" />
              Recently deleted ({deletedBills.length})
            </Button>
          )}
          <Button
            onClick={() => setModal({ bill: null })}
            className="h-9 flex-1 gap-2 px-4 text-sm font-medium sm:flex-none"
          >
            <Plus className="h-4 w-4" />
            Add Bill
          </Button>
        </div>
      </div>

      <SearchFilterPanel
        title="Search & filters"
        collapsed={searchPanelCollapsed}
        onCollapsedChange={setSearchPanelCollapsed}
        hasFilters={hasFilters}
        resultLabel={`${filteredBills.length} of ${bills.length} shown`}
        onClear={resetFilters}
        className="surface-elevated"
      >
        <div className="grid gap-3 lg:grid-cols-[minmax(220px,1fr)_220px_180px] lg: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 => setSearch(e.target.value)}
              placeholder="Search bills by name, category, notes, or amount"
              className="h-10 pl-9"
            />
          </label>
          <Select value={filters.category} onValueChange={value => setFilterValue('category', value)}>
            <SelectTrigger className="h-10" aria-label="Filter by category">
              <SelectValue placeholder="Category" />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value={FILTER_ALL}>All categories</SelectItem>
              {categories.map(category => (
                <SelectItem key={category.id} value={String(category.id)}>{category.name}</SelectItem>
              ))}
            </SelectContent>
          </Select>
          <Select value={filters.cycle} onValueChange={value => setFilterValue('cycle', value)}>
            <SelectTrigger className="h-10 capitalize" aria-label="Filter by billing schedule">
              <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>
        </div>
        <div className="flex flex-wrap items-center gap-2">
          <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>
          <FilterChip active={filters.inactive} onClick={() => toggleFilter('inactive')}>Inactive</FilterChip>
          <span className="ml-auto text-xs text-muted-foreground tabular-nums">
            {filteredBills.length} of {bills.length} shown
          </span>
        </div>
      </SearchFilterPanel>

      {/* ── Active Bills ── */}
      {!filters.inactive && (
      <div className="surface-elevated rounded-xl overflow-hidden">
        <div className="flex items-center justify-between gap-3 px-5 py-3 border-b border-border/40">
          <span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
            Active
          </span>
          <div className="flex items-center gap-3">
            {!reorderEnabled && sortedActive.length > 1 && (
              <span className="text-xs text-muted-foreground hidden sm:inline">
                {billsSort === 'cadence' ? 'Switch to Custom to reorder' : 'Clear filters to reorder'}
              </span>
            )}
            <div className="grid grid-cols-2 rounded-lg border border-border/60 bg-muted/30 p-0.5">
              <SortModeButton
                active={billsSort === 'custom'}
                onClick={() => setBillsSort('custom')}
              >
                Custom
              </SortModeButton>
              <SortModeButton
                active={billsSort === 'cadence'}
                onClick={() => setBillsSort('cadence')}
              >
                Cadence
              </SortModeButton>
            </div>
          </div>
        </div>

        {loading ? (
          <div className="space-y-px p-4">
            {[...Array(4)].map((_, i) => (
              <div key={i} className="h-16 rounded-lg bg-muted/40 animate-pulse mb-2" />
            ))}
          </div>
        ) : active.length === 0 ? (
          <div className="py-16 text-center text-sm text-muted-foreground">
            {hasFilters ? (
              <>No active bills match your filters.</>
            ) : (
              <>
                No active bills.{' '}
                <button
                  onClick={() => setModal({ bill: null })}
                  className="underline underline-offset-4 hover:text-foreground transition-colors"
                >
                  Add your first bill
                </button>
              </>
            )}
          </div>
        ) : (
          <BillsTableInner
            bills={sortedActive}
            prefs={prefs}
            onEdit={handleEdit}
            onToggle={handleToggle}
            onDelete={handleDeleteRequest}
            onDuplicate={handleDuplicateBill}
            moveControlsFor={moveControlsForGroup(sortedActive, true)}
            dragPropsFor={dragPropsForGroup(sortedActive, true)}
          />
        )}
      </div>
      )}

      {/* ── Inactive Bills ── */}
      {!loading && sortedInactive.length > 0 && (
        <div className="space-y-3">

          <button
            onClick={() => setShowInactive(v => !v)}
            className="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors"
          >
            <ChevronRight className={cn(
              'h-3 w-3 transition-transform duration-200',
              showInactive && 'rotate-90',
            )} />
            <span className="uppercase tracking-[0.08em]">
              {sortedInactive.length} inactive {sortedInactive.length === 1 ? 'bill' : 'bills'}
            </span>
          </button>

          {(showInactive || filters.inactive) && (
            <div className="surface-elevated rounded-xl overflow-hidden">
              <div className="flex items-center justify-between gap-3 px-5 py-3 border-b border-border/40">
                <span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
                  Inactive
                </span>
                <span className="text-xs tabular-nums text-muted-foreground">
                  {sortedInactive.length}
                </span>
              </div>
              <BillsTableInner
                bills={sortedInactive}
                prefs={prefs}
                onEdit={handleEdit}
                onToggle={handleToggle}
                onDelete={handleDeleteRequest}
                onHistory={setHistoryTarget}
                onDuplicate={handleDuplicateBill}
                moveControlsFor={moveControlsForGroup(sortedInactive, false)}
                dragPropsFor={dragPropsForGroup(sortedInactive, false)}
              />
            </div>
          )}

        </div>
      )}

      {!loading && filters.inactive && inactive.length === 0 && (
        <div className="surface-elevated rounded-xl py-12 text-center text-sm text-muted-foreground">
          No inactive bills match your filters.
        </div>
      )}

      {/* ── Bill Modal ── */}
      {modal && (
        <BillModal
          key={modal.bill?.id ? `edit-${modal.bill.id}` : `new-${modal.initialBill?.name || 'blank'}`}
          bill={modal.bill}
          initialBill={modal.initialBill}
          categories={categories}
          onClose={() => setModal(null)}
          onSave={refresh}
          onDuplicate={handleDuplicateBill}
        />
      )}

      {historyTarget && (
        <HistoryVisibilityDialog
          bill={historyTarget}
          onClose={() => setHistoryTarget(null)}
          onSaved={refresh}
        />
      )}

      <RecentlyDeletedBillsDialog
        open={showDeleted}
        onOpenChange={setShowDeleted}
        bills={deletedBills}
        onRestore={handleRestoreDeleted}
      />

      {/* ── Deactivate confirmation (replaces window.confirm) ── */}
      <AlertDialog open={!!deactivate} onOpenChange={open => { if (!open) { setDeactivate(null); setDeactivateReason(''); } }}>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>Deactivate "{deactivate?.name}"?</AlertDialogTitle>
            <AlertDialogDescription>
              This bill will be hidden from the tracker. You can reactivate it at any time.
            </AlertDialogDescription>
          </AlertDialogHeader>
          <div className="space-y-1.5 py-1">
            <label className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
              Reason (optional)
            </label>
            <select
              value={deactivateReason}
              onChange={e => setDeactivateReason(e.target.value)}
              className="w-full rounded-md border border-border/60 bg-background px-3 py-2 text-sm text-foreground outline-none focus:ring-1 focus:ring-ring"
            >
              <option value="">Select a reason…</option>
              <option value="Moved to spouse">Moved to spouse</option>
              <option value="Switched providers">Switched providers</option>
              <option value="Paid off">Paid off</option>
              <option value="Cancelled">Cancelled</option>
              <option value="Other">Other</option>
            </select>
          </div>
          <AlertDialogFooter>
            <AlertDialogCancel onClick={() => { setDeactivate(null); setDeactivateReason(''); }}>Cancel</AlertDialogCancel>
            <AlertDialogAction
              className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
              onClick={() => { const b = deactivate; const r = deactivateReason; setDeactivate(null); setDeactivateReason(''); doToggle(b, r); }}
            >
              Deactivate
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>

      {/* ── Soft delete confirmation ── */}
      <AlertDialog
        open={!!deleteTarget}
        onOpenChange={open => {
          if (!open && !deleteBusy) {
            setDeleteTarget(null);
            setDeleteConfirmed(false);
          }
        }}
      >
        <AlertDialogContent className="max-w-lg">
          <AlertDialogHeader>
            <AlertDialogTitle>Move "{deleteTarget?.name}" to recovery?</AlertDialogTitle>
            <AlertDialogDescription>
              This hides the bill from normal tracking and keeps its payments, monthly history,
              notes, and history ranges recoverable for 30 days.
            </AlertDialogDescription>
          </AlertDialogHeader>

          <div className="rounded-lg border border-amber-500/25 bg-amber-500/10 px-4 py-3 text-sm text-amber-300">
            Deactivate is still the best option if you want to retain the bill long-term but hide it from active tracking.
          </div>

          <label className="flex items-start gap-3 rounded-lg border border-border bg-muted/25 px-4 py-3 text-sm">
            <input
              type="checkbox"
              checked={deleteConfirmed}
              onChange={e => setDeleteConfirmed(e.target.checked)}
              disabled={deleteBusy}
              className="mt-0.5 h-4 w-4 rounded border-input bg-input accent-destructive"
            />
            <span>I understand this removes the bill from normal views and will be purged after 30 days if not restored.</span>
          </label>

          <AlertDialogFooter className="sm:justify-between">
            <AlertDialogCancel
              disabled={deleteBusy}
              className="border border-input bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground"
            >
              Cancel
            </AlertDialogCancel>
            <div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
              <Button
                type="button"
                variant="outline"
                disabled={deleteBusy}
                onClick={handleDeleteAlternative}
              >
                {deleteTarget?.active ? 'Deactivate instead' : 'Activate instead'}
              </Button>
              <Button
                type="button"
                variant="destructive"
                disabled={!deleteConfirmed || deleteBusy}
                onClick={handleDeleteConfirmed}
              >
                {deleteBusy ? 'Moving…' : 'Move to recovery'}
              </Button>
            </div>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>

    </div>
  );
}
