import { useCallback, useDeferredValue, useEffect, useMemo, useOptimistic, useRef, useState, type ComponentType, type ComponentProps, type ReactNode } from 'react';
import { Link } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { useSubscriptions, useSubscriptionRecommendations, useCategories, useBills } from '@/hooks/useQueries';
import { toast } from 'sonner';
import {
  Bell,
  CalendarDays,
  CheckCircle2,
  CheckCircle,
  ChevronDown,
  Cloud,
  ArrowDown,
  ArrowUp,
  GripVertical,
  AlertTriangle,
  Info,
  Link2,
  Loader2,
  MoreHorizontal,
  Pause,
  Plus,
  RefreshCw,
  Repeat,
  Search,
  Sparkles,
  X,
} from 'lucide-react';
import { api } from '@/api';
import { cn, fmtDate, localDateString, errMessage } from '@/lib/utils';
import { formatUSD, asDollars } from '@/lib/money';
import { scheduleLabel } from '@/lib/billingSchedule';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import SearchFilterPanel from '@/components/SearchFilterPanel';
import {
  Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle,
} from '@/components/ui/dialog';
import {
  Tooltip, TooltipContent, TooltipProvider, TooltipTrigger,
} from '@/components/ui/tooltip';
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import BillModal from '@/components/BillModal';
import BillHistoricalImportDialog from '@/components/BillHistoricalImportDialog';
import { getLinkImportPref } from '@/pages/SettingsPage';
import { useSearchPanelPreference } from '@/hooks/useSearchPanelPreference';
import { useVirtualizer } from '@tanstack/react-virtual';
import { moveInArray, movedItemId, reorderPayload } from '@/lib/reorder';
import type {
  Bill, Category,
  Subscription,
  SubscriptionsResponse as SubData,
  SubscriptionSummary as SubSummary,
  ExistingBillMatch,
  Recommendation,
  SubscriptionTx as SubTx,
} from '@/types';
import type { DragProps, MoveControls } from '@/components/tracker/MobileTrackerRow';

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

const TYPE_LABELS: Record<string, string> = {
  streaming: 'Streaming',
  software: 'Software',
  cloud: 'Cloud',
  music: 'Music',
  news: 'News',
  fitness: 'Fitness',
  gaming: 'Gaming',
  utilities: 'Utilities',
  insurance: 'Insurance',
  food: 'Food',
  education: 'Education',
  shopping: 'Shopping',
  security: 'Security',
  other: 'Other',
};

function typeLabel(t: string | null | undefined): string {
  return (t ? TYPE_LABELS[t] : undefined) || 'Other';
}

// ── Domain types ─────────────────────────────────────────────────────────────

interface CadenceLike {
  cycle_type?: string | null;
  billing_cycle?: string | null;
  due_day?: number | null;
  cycle_day?: number | string | null;
  expected_amount?: number | null;
  name?: string | null;
  [key: string]: unknown;
}

interface SubDraft {
  name?: string;
  category_id?: number | null;
  due_day?: number;
  expected_amount?: string;
  billing_cycle?: string;
  cycle_type?: string;
  cycle_day?: string;
  is_subscription?: number;
  subscription_type?: string;
  website?: string;
  notes?: string;
  reminder_days_before?: number;
}
interface ModalState { bill: Subscription | null; initialBill?: SubDraft }

type RecAction = (rec: Recommendation) => void | Promise<void>;
type QuickLink = (rec: Recommendation, billId: number) => void | Promise<void>;

// ── Helpers ──────────────────────────────────────────────────────────────────

const SUBSCRIPTION_SORT_KEY = 'subscriptions_sort_mode';
const CADENCE_ORDER = ['weekly', 'biweekly', 'monthly', 'quarterly', 'annual', 'other'];
const CADENCE_LABELS: Record<string, string> = {
  weekly: 'Weekly',
  biweekly: 'Biweekly',
  monthly: 'Monthly',
  quarterly: 'Quarterly',
  annual: 'Yearly',
  other: 'Other',
};

const SUBSCRIPTION_MONTHLY_FACTORS: Record<string, number> = {
  weekly: 52 / 12,
  biweekly: 26 / 12,
  monthly: 1,
  quarterly: 1 / 3,
  annual: 1 / 12,
  annually: 1 / 12,
};

function normalizedCadence(item: CadenceLike): string {
  const raw = String(item?.cycle_type || item?.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 subscriptionMonthlyEquivalent(item: CadenceLike): number {
  const key = String(item?.cycle_type || item?.billing_cycle || 'monthly').toLowerCase();
  const fallback = String(item?.billing_cycle || '').toLowerCase() === 'quarterly'
    ? 'quarterly'
    : String(item?.billing_cycle || '').toLowerCase() === 'annually'
    ? 'annual'
    : key;
  const factor = SUBSCRIPTION_MONTHLY_FACTORS[key] ?? SUBSCRIPTION_MONTHLY_FACTORS[fallback] ?? 1;
  return Math.round(Number(item?.expected_amount || 0) * factor * 100) / 100;
}

function subscriptionNextDueDate(item: CadenceLike, now = new Date()): string {
  const dueDay = Math.min(Math.max(Number(item?.due_day) || 1, 1), 31);
  const cycle = String(item?.cycle_type || item?.billing_cycle || 'monthly').toLowerCase();
  let date = new Date(now.getFullYear(), now.getMonth(), dueDay);
  if (date < new Date(now.getFullYear(), now.getMonth(), now.getDate())) {
    date = new Date(now.getFullYear(), now.getMonth() + 1, dueDay);
  }
  if (cycle === 'quarterly' || cycle === 'annual' || cycle === 'annually') {
    const startMonth = Math.min(Math.max(Number(item?.cycle_day) || 1, 1), 12) - 1;
    const step = cycle === 'quarterly' ? 3 : 12;
    date = new Date(now.getFullYear(), startMonth, dueDay);
    while (date < new Date(now.getFullYear(), now.getMonth(), now.getDate())) {
      date = new Date(date.getFullYear(), date.getMonth() + step, dueDay);
    }
  }
  return localDateString(date);
}

function decorateSavedSubscriptionBill(bill: Bill, categories: Category[]): Subscription {
  const monthly = subscriptionMonthlyEquivalent(bill);
  const category = categories.find(item => Number(item.id) === Number(bill.category_id));
  return {
    ...bill,
    active: !!bill.active,
    is_subscription: !!bill.is_subscription,
    category_name: bill.category_name || category?.name || null,
    monthly_equivalent: monthly,
    yearly_equivalent: Math.round(monthly * 12 * 100) / 100,
    next_due_date: subscriptionNextDueDate(bill),
    subscription_type: (bill.subscription_type as string | undefined) || 'other',
  };
}

function subscriptionSummaryFromList(subscriptions: Subscription[]): SubSummary {
  const active = subscriptions.filter(item => item.active);
  const monthlyTotal = active.reduce((sum, item) => sum + Number(item.monthly_equivalent || 0), 0);
  const typeTotals = new Map<string, number>();
  for (const item of active) {
    const type = item.subscription_type || 'other';
    typeTotals.set(type, (typeTotals.get(type) || 0) + Number(item.monthly_equivalent || 0));
  }
  const topType = [...typeTotals.entries()].sort((a, b) => b[1] - a[1])[0] || null;
  return {
    active_count: active.length,
    paused_count: subscriptions.length - active.length,
    monthly_total: Math.round(monthlyTotal * 100) / 100,
    yearly_total: Math.round(monthlyTotal * 12 * 100) / 100,
    top_type: topType ? { type: topType[0], monthly_total: Math.round(topType[1] * 100) / 100 } : null,
  };
}

// Extended group order: monthly bills split by 1st/15th pay bucket
const GROUP_ORDER = ['weekly', 'biweekly', 'monthly-1st', 'monthly-15th', 'quarterly', 'annual', 'other'];
const GROUP_LABELS: Record<string, string> = {
  'weekly':       'Weekly',
  'biweekly':     'Biweekly',
  'monthly-1st':  '1st  ·  Due days 1–14',
  'monthly-15th': '15th  ·  Due days 15–31',
  'quarterly':    'Quarterly',
  'annual':       'Annual',
  'other':        'Other',
};

function subscriptionGroup(item: Subscription): string {
  const cadence = normalizedCadence(item);
  if (cadence === 'monthly') {
    return (Number(item.due_day) || 1) <= 14 ? 'monthly-1st' : 'monthly-15th';
  }
  return cadence;
}

function groupIndex(item: Subscription): number {
  const idx = GROUP_ORDER.indexOf(subscriptionGroup(item));
  return idx >= 0 ? idx : GROUP_ORDER.length - 1;
}

function daysUntil(dateStr: string | null | undefined): number | null {
  if (!dateStr) return null;
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  return Math.round((new Date(dateStr + 'T00:00:00').getTime() - today.getTime()) / 86400000);
}

function sortSubscriptionsByCadence(items: Subscription[]): Subscription[] {
  return [...items].sort((a, b) => (
    groupIndex(a) - groupIndex(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: ReactNode; onClick: () => void }) {
  return (
    <button
      type="button"
      onClick={onClick}
      className={cn(
        'h-8 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 cycleLabel(item: Subscription): string {
  return scheduleLabel(item);
}

function StatCard({ icon: Icon, label, value, hint }: {
  icon: ComponentType<{ className?: string }>;
  label: ReactNode;
  value: ReactNode;
  hint?: ReactNode;
}) {
  return (
    <div className="min-w-0 rounded-lg border border-border/70 bg-card/80 p-4">
      <div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-foreground/70">
        <Icon className="h-4 w-4 text-primary" />
        <span className="truncate">{label}</span>
      </div>
      <p className="tracker-number mt-2 truncate text-2xl font-bold tracking-tight text-foreground">{value}</p>
      {hint && <p className="mt-1 text-xs font-medium text-muted-foreground">{hint}</p>}
    </div>
  );
}

function SubscriptionRowActions({ item, onEdit, onToggle, busy }: {
  item: Subscription;
  onEdit: (item: Subscription) => void;
  onToggle: (item: Subscription) => void;
  busy?: boolean;
}) {
  return (
    <div className="grid grid-cols-[minmax(0,1fr)_auto] gap-2 md:flex md:justify-end">
      <Button
        type="button"
        variant="outline"
        size="sm"
        className="h-8 min-w-0 px-3"
        onClick={() => onEdit(item)}
      >
        Edit
      </Button>
      <DropdownMenu>
        <DropdownMenuTrigger asChild>
          <Button
            type="button"
            variant="outline"
            size="sm"
            className="h-8 w-8 p-0"
            disabled={busy}
            aria-label={`More actions for ${item.name}`}
          >
            {busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <MoreHorizontal className="h-4 w-4" />}
          </Button>
        </DropdownMenuTrigger>
        <DropdownMenuContent align="end" className="w-40">
          <DropdownMenuItem onSelect={() => onToggle(item)}>
            {item.active ? <Pause className="h-4 w-4" /> : <CheckCircle2 className="h-4 w-4" />}
            {item.active ? 'Pause' : 'Resume'}
          </DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
    </div>
  );
}

function SubscriptionRow({ item, onEdit, onToggle, moveControls, dragProps, busy }: {
  item: Subscription;
  onEdit: (item: Subscription) => void;
  onToggle: (item: Subscription) => void;
  moveControls?: MoveControls;
  dragProps?: DragProps;
  busy?: boolean;
}) {
  return (
    <div
      draggable={dragProps?.draggable}
      onDragStart={dragProps?.onDragStart}
      onDragEnter={dragProps?.onDragEnter}
      onDragOver={dragProps?.onDragOver}
      onDragEnd={dragProps?.onDragEnd}
      onDrop={dragProps?.onDrop}
      className={cn(
      'group',
      'grid min-w-0 gap-3 border-b border-border/40 px-4 py-4 last:border-b-0',
      'md:grid-cols-[auto_minmax(0,1fr)_14rem_auto] md:items-center',
      !item.active && 'opacity-60',
      dragProps?.isDragging && 'opacity-45',
      dragProps?.isDropTarget && 'ring-2 ring-inset ring-primary/35',
    )}>
      <div className="flex shrink-0 items-center gap-0.5 self-start md:self-center">
        <GripVertical
          className={cn(
            'h-4 w-4 text-muted-foreground/55',
            moveControls?.enabled && 'cursor-grab group-active:cursor-grabbing',
            !moveControls?.enabled && 'opacity-30',
          )}
          aria-hidden="true"
        />
        <div className="flex flex-col">
          <button
            type="button"
            onClick={moveControls?.onMoveUp}
            disabled={!moveControls?.enabled || !moveControls?.canMoveUp || moveControls?.moving}
            className="rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-25"
            title="Move subscription up"
            aria-label={`Move ${item.name} up`}
          >
            <ArrowUp className="h-3 w-3" />
          </button>
          <button
            type="button"
            onClick={moveControls?.onMoveDown}
            disabled={!moveControls?.enabled || !moveControls?.canMoveDown || moveControls?.moving}
            className="rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-25"
            title="Move subscription down"
            aria-label={`Move ${item.name} down`}
          >
            <ArrowDown className="h-3 w-3" />
          </button>
        </div>
      </div>
      <div className="min-w-0 flex-1">
        <div className="flex flex-wrap items-center gap-2">
          <button
            type="button"
            onClick={() => onEdit(item)}
            className="min-w-0 max-w-full truncate text-left text-[15px] font-semibold text-foreground hover:text-primary"
          >
            {item.name}
          </button>
          <TooltipProvider delayDuration={300}>
            <Tooltip>
              <TooltipTrigger asChild>
                <Badge variant="outline" className="capitalize border-primary/25 bg-primary/10 text-primary cursor-default">
                  {typeLabel(item.subscription_type)}
                </Badge>
              </TooltipTrigger>
              <TooltipContent>Service category</TooltipContent>
            </Tooltip>
            {!item.active && (
              <Tooltip>
                <TooltipTrigger asChild>
                  <Badge variant="outline" className="border-amber-500/25 bg-amber-500/10 text-amber-300 cursor-default">
                    Paused
                  </Badge>
                </TooltipTrigger>
                <TooltipContent>Paused — not actively tracked</TooltipContent>
              </Tooltip>
            )}
            {!!item.autopay_enabled && (
              <Tooltip>
                <TooltipTrigger asChild>
                  <span className="shrink-0 rounded border border-sky-500/25 bg-sky-500/10 px-1.5 py-0.5 text-[11px] font-semibold text-sky-600 dark:text-sky-300 cursor-default">AP</span>
                </TooltipTrigger>
                <TooltipContent>Autopay enabled</TooltipContent>
              </Tooltip>
            )}
            {!!item.has_2fa && (
              <Tooltip>
                <TooltipTrigger asChild>
                  <span className="shrink-0 rounded bg-violet-500/20 px-1.5 py-0.5 text-[11px] font-semibold text-violet-300 cursor-default">2FA</span>
                </TooltipTrigger>
                <TooltipContent>Two-factor authentication configured</TooltipContent>
              </Tooltip>
            )}
            {(!!item.has_merchant_rule || !!item.has_linked_transactions) && (
              <Tooltip>
                <TooltipTrigger asChild>
                  <span className="shrink-0 rounded border border-emerald-500/25 bg-emerald-500/10 px-1.5 py-0.5 text-[11px] font-semibold text-emerald-600 dark:text-emerald-400 cursor-default">L</span>
                </TooltipTrigger>
                <TooltipContent>Linked to bank transactions</TooltipContent>
              </Tooltip>
            )}
          </TooltipProvider>
        </div>
        <div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs font-medium text-muted-foreground">
          <span>{item.category_name || 'Uncategorized'}</span>
          {(() => {
            const days = daysUntil(item.next_due_date);
            const label = days === null ? null
              : days < 0  ? `${Math.abs(days)}d overdue`
              : days === 0 ? 'Due today'
              : days === 1 ? 'Due tomorrow'
              : `Due in ${days}d`;
            const cls = days === null ? 'text-muted-foreground'
              : days <= 1  ? 'text-rose-500'
              : days <= 7  ? 'text-amber-500'
              : 'text-emerald-600 dark:text-emerald-400';
            return label ? (
              <span className={cls}>{label} · {fmtDate(item.next_due_date)}</span>
            ) : null;
          })()}
          <span className="capitalize">{cycleLabel(item)}</span>
          <TooltipProvider delayDuration={300}>
            <Tooltip>
              <TooltipTrigger asChild>
                <span className="cursor-default">{item.reminder_days_before ?? 3}d reminder</span>
              </TooltipTrigger>
              <TooltipContent>Reminder sent {item.reminder_days_before ?? 3} days before the due date</TooltipContent>
            </Tooltip>
          </TooltipProvider>
        </div>
      </div>

      <div className="grid grid-cols-2 gap-3 md:w-56">
        <div>
          <p className="text-xs text-muted-foreground">Per cycle</p>
          <p className="tracker-number text-sm font-semibold text-foreground">{fmt(item.expected_amount)}</p>
        </div>
        <div>
          <TooltipProvider delayDuration={300}>
            <Tooltip>
              <TooltipTrigger asChild>
                <p className="text-xs text-muted-foreground cursor-default w-fit">Monthly</p>
              </TooltipTrigger>
              <TooltipContent>Normalized monthly equivalent regardless of billing frequency</TooltipContent>
            </Tooltip>
          </TooltipProvider>
          <p className="tracker-number text-sm font-semibold text-emerald-600 dark:text-emerald-300">{fmt(item.monthly_equivalent)}</p>
        </div>
      </div>

      <SubscriptionRowActions item={item} onEdit={onEdit} onToggle={onToggle} busy={busy} />
    </div>
  );
}

function BillPickerDialog({ open, onClose, recommendation, bills, onConfirm, busy }: {
  open: boolean;
  onClose: () => void;
  recommendation: Recommendation | null;
  bills: Bill[];
  onConfirm: (billId: number | null) => void;
  busy: boolean;
}) {
  const [search, setSearch] = useState('');
  const [selectedId, setSelectedId] = useState<number | null>(null);

  useEffect(() => { if (open) { setSearch(''); setSelectedId(null); } }, [open]);

  const filtered = useMemo(() => {
    const q = search.toLowerCase();
    return (bills || []).filter(b => !q || b.name.toLowerCase().includes(q));
  }, [bills, search]);

  return (
    <Dialog open={open} onOpenChange={v => { if (!v) onClose(); }}>
      <DialogContent className="max-w-md">
        <DialogHeader>
          <DialogTitle>Link to existing bill</DialogTitle>
          <p className="text-sm text-muted-foreground mt-1">
            <span className="font-medium text-foreground">{recommendation?.name}</span>
            {recommendation && (
              <span className="ml-2 text-xs">{recommendation.occurrence_count} charge{recommendation.occurrence_count !== 1 ? 's' : ''} · {fmt(recommendation.expected_amount)}</span>
            )}
          </p>
          <p className="text-xs text-muted-foreground">
            The matching transactions will be marked as paid under the selected bill.
          </p>
        </DialogHeader>

        <Input
          autoFocus
          placeholder="Search bills…"
          value={search}
          onChange={e => setSearch(e.target.value)}
          className="text-sm"
        />

        <div className="max-h-56 overflow-y-auto rounded-lg border border-border/60 divide-y divide-border/40">
          {filtered.length === 0 ? (
            <p className="px-3 py-4 text-sm text-muted-foreground text-center">No bills found.</p>
          ) : filtered.map(bill => (
            <button
              key={bill.id}
              type="button"
              onClick={() => setSelectedId(bill.id)}
              className={cn(
                'w-full flex items-center justify-between px-3 py-2.5 text-left text-sm transition-colors hover:bg-muted/40',
                selectedId === bill.id && 'bg-primary/10 text-primary',
              )}
            >
              <span className="truncate font-medium">{bill.name}</span>
              <span className="shrink-0 ml-3 text-xs text-muted-foreground tabular-nums">
                ${(bill.expected_amount ?? 0).toFixed(2)}/mo
              </span>
            </button>
          ))}
        </div>

        <DialogFooter className="gap-2">
          <Button type="button" variant="ghost" onClick={onClose} disabled={busy}>
            Cancel
          </Button>
          <Button type="button" className="gap-2" onClick={() => onConfirm(selectedId)} disabled={!selectedId || busy}>
            {busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Link2 className="h-3.5 w-3.5" />}
            Link Bill
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

function TxResultRow({ tx, onTrack }: { tx: SubTx; onTrack: (tx: SubTx) => void }) {
  const dollars = fmt(Math.abs(tx.amount) / 100);
  const label   = tx.payee || tx.description || tx.memo || '—';
  const account = tx.account_name || tx.data_source_name || null;
  const isMatched = tx.match_status === 'matched';
  const catalogMatch = tx.catalog_match;

  return (
    <div className="flex items-center gap-3 px-4 py-2.5 border-b border-border/30 last:border-0 hover:bg-muted/20 transition-colors">
      <div className="flex-1 min-w-0">
        <div className="flex flex-wrap items-center gap-1.5">
          <span className="text-sm font-medium truncate max-w-[200px]">{label}</span>
          <TooltipProvider delayDuration={300}>
            {isMatched ? (
              <Tooltip>
                <TooltipTrigger asChild>
                  <Badge className="shrink-0 border-emerald-400/25 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 gap-1 text-[10px] cursor-default">
                    <CheckCircle className="h-3 w-3" />
                    {tx.matched_bill_name || 'Matched'}
                  </Badge>
                </TooltipTrigger>
                <TooltipContent>Already linked to this bill</TooltipContent>
              </Tooltip>
            ) : (
              <Tooltip>
                <TooltipTrigger asChild>
                  <Badge variant="outline" className="shrink-0 text-[10px] text-muted-foreground cursor-default">Unmatched</Badge>
                </TooltipTrigger>
                <TooltipContent>Not yet linked to any bill</TooltipContent>
              </Tooltip>
            )}
            {catalogMatch && (
              <Tooltip>
                <TooltipTrigger asChild>
                  <Badge variant="outline" className="shrink-0 border-primary/25 bg-primary/10 text-primary text-[10px] cursor-default">
                    Known: {catalogMatch.name}
                  </Badge>
                </TooltipTrigger>
                <TooltipContent>Recognized in the subscription catalog as {catalogMatch.name}</TooltipContent>
              </Tooltip>
            )}
          </TooltipProvider>
        </div>
        <p className="text-[11px] text-muted-foreground mt-0.5">
          {tx.posted_date}{account ? ` · ${account}` : ''}
          {catalogMatch ? ` · ${typeLabel(catalogMatch.subscription_type)}` : ''}
        </p>
      </div>
      <span className="font-mono text-sm font-semibold tabular-nums shrink-0">{dollars}</span>
      {!isMatched && (
        <Button size="sm" variant="outline" onClick={() => onTrack(tx)}
          className="h-8 shrink-0 gap-1.5 px-2.5 text-xs">
          <Plus className="h-3 w-3" /> Track
        </Button>
      )}
    </div>
  );
}

function RecommendationIconButton({ label, icon: Icon, onClick, disabled, className, variant = 'outline' }: {
  label: string;
  icon: ComponentType<{ className?: string }>;
  onClick: () => void;
  disabled?: boolean;
  className?: string;
  variant?: ComponentProps<typeof Button>['variant'];
}) {
  return (
    <Tooltip>
      <TooltipTrigger asChild>
        <Button
          type="button"
          size="sm"
          variant={variant}
          className={cn('h-9 w-9 shrink-0 p-0', className)}
          disabled={disabled}
          onClick={onClick}
          aria-label={label}
        >
          <Icon className="h-3.5 w-3.5" />
        </Button>
      </TooltipTrigger>
      <TooltipContent>{label}</TooltipContent>
    </Tooltip>
  );
}

function RecommendationMoreMenu({ recommendation, existingBill, busy, onAccept, onDecline, onMatch, categoryId }: {
  recommendation: Recommendation;
  existingBill?: ExistingBillMatch | null;
  busy?: boolean;
  onAccept: RecAction;
  onDecline: RecAction;
  onMatch: RecAction;
  categoryId: number | null;
}) {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button
          type="button"
          size="sm"
          variant="outline"
          className="h-9 w-9 shrink-0 p-0"
          disabled={busy}
          aria-label="More recommendation actions"
        >
          <MoreHorizontal className="h-4 w-4" />
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end" className="w-48">
        {existingBill && (
          <DropdownMenuItem onSelect={() => onAccept({ ...recommendation, category_id: categoryId })}>
            <CheckCircle2 className="h-4 w-4" />
            Track as new
          </DropdownMenuItem>
        )}
        <DropdownMenuItem onSelect={() => onMatch(recommendation)}>
          <Link2 className="h-4 w-4" />
          {existingBill ? 'Choose different bill' : 'Link existing bill'}
        </DropdownMenuItem>
        <DropdownMenuItem destructive onSelect={() => onDecline(recommendation)}>
          <X className="h-4 w-4" />
          Dismiss
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

function RecommendationCard({ recommendation, categoryId, onAccept, onDecline, onMatch, onQuickLink, onDetails, busy }: {
  recommendation: Recommendation;
  categoryId: number | null;
  onAccept: RecAction;
  onDecline: RecAction;
  onMatch: RecAction;
  onQuickLink: QuickLink;
  onDetails: (rec: Recommendation) => void;
  busy?: boolean;
}) {
  const identity = recommendation.evidence?.identity;
  const amount = recommendation.evidence?.amount;
  const cadence = recommendation.evidence?.cadence;
  const amountRange = recommendation.evidence?.amount_range;
  const ambiguity = recommendation.evidence?.ambiguity;
  const existingBill = recommendation.existing_bill_match;

  return (
    <TooltipProvider delayDuration={180}>
      <div className="rounded-lg border border-primary/20 bg-primary/[0.05] p-4">
      <div className="space-y-3">
        <div className="flex min-w-0 items-start justify-between gap-3">
          <div className="min-w-0 flex-1">
            <p className="truncate text-sm font-semibold text-foreground">{recommendation.name}</p>
            <p className="mt-1 text-xs font-medium text-muted-foreground">
              {typeLabel(recommendation.subscription_type)} · {recommendation.occurrence_count} charges · last seen {fmtDate(recommendation.last_seen_date)}
            </p>
            {recommendation.catalog_match?.starting_monthly_usd && (
              <p className="mt-1 text-xs font-medium text-muted-foreground">
                Catalog starts at {fmt(recommendation.catalog_match?.starting_monthly_usd)} / mo
                {recommendation.catalog_match?.starting_annual_usd
                  ? ` or ${fmt(recommendation.catalog_match?.starting_annual_usd)} / yr`
                  : ''}
              </p>
            )}
            {!!recommendation.accounts?.length && (
              <p className="mt-1 truncate text-xs font-medium text-muted-foreground">
                {(recommendation.accounts?.length ?? 0) > 1 ? 'Accounts' : 'Account'}: {recommendation.accounts?.join(', ')}
              </p>
            )}
            {existingBill && (
              <p className="mt-1 truncate text-xs font-semibold text-primary">
                Link to existing bill: {existingBill.name}
              </p>
            )}
          </div>
          <div className="shrink-0 text-right">
            <p className="tracker-number text-base font-semibold text-foreground">{fmt(recommendation.expected_amount)}</p>
            <p className="tracker-number text-xs font-semibold text-emerald-600 dark:text-emerald-300">
              {fmt(recommendation.monthly_equivalent)} / mo
            </p>
          </div>
        </div>

        <div className="flex min-w-0 flex-wrap items-center gap-2">
          <Tooltip>
            <TooltipTrigger asChild>
              <Badge variant="outline" className="border-primary/25 bg-primary/10 text-primary cursor-default">
                {recommendation.confidence}% match
              </Badge>
            </TooltipTrigger>
            <TooltipContent>Match confidence — how likely this is a real recurring subscription</TooltipContent>
          </Tooltip>
          {identity?.label && (
            <Tooltip>
              <TooltipTrigger asChild>
                <Badge variant="outline" className="border-emerald-500/25 bg-emerald-500/10 text-emerald-600 dark:text-emerald-300 cursor-default">
                  {identity.label}
                </Badge>
              </TooltipTrigger>
              <TooltipContent>Merchant identity evidence</TooltipContent>
            </Tooltip>
          )}
          {amount?.label && (
            <Tooltip>
              <TooltipTrigger asChild>
                <Badge
                  variant="outline"
                  className={cn(
                    'text-[11px] cursor-default',
                    amount.match === 'unusual'
                      ? 'border-amber-500/25 bg-amber-500/10 text-amber-500'
                      : 'border-sky-500/25 bg-sky-500/10 text-sky-600 dark:text-sky-300',
                  )}
                >
                  {amount.match === 'unusual' ? 'Unusual amount' : 'Price checked'}
                </Badge>
              </TooltipTrigger>
              <TooltipContent>{amount.label}</TooltipContent>
            </Tooltip>
          )}
          {cadence?.recurring && (
            <Tooltip>
              <TooltipTrigger asChild>
                <Badge variant="outline" className="border-violet-500/25 bg-violet-500/10 text-violet-600 dark:text-violet-300 cursor-default">
                  Recurring
                </Badge>
              </TooltipTrigger>
              <TooltipContent>{cadence.label || 'Regular recurring payment pattern detected'}</TooltipContent>
            </Tooltip>
          )}
          {ambiguity?.ambiguous && (
            <Tooltip>
              <TooltipTrigger asChild>
                <Badge variant="outline" className="gap-1 border-amber-500/25 bg-amber-500/10 text-amber-600 dark:text-amber-300 cursor-default">
                  <AlertTriangle className="h-3 w-3" />
                  Review
                </Badge>
              </TooltipTrigger>
              <TooltipContent>{ambiguity.label || 'Multiple patterns detected — verify before tracking'}</TooltipContent>
            </Tooltip>
          )}
          {existingBill && (
            <Tooltip>
              <TooltipTrigger asChild>
                <Badge variant="outline" className="border-primary/25 bg-primary/10 text-primary cursor-default">
                  Existing bill
                </Badge>
              </TooltipTrigger>
              <TooltipContent>May match your tracked bill: {existingBill.name}</TooltipContent>
            </Tooltip>
          )}
          {amountRange && amountRange.min !== amountRange.max && (
            <span className="rounded-md border border-border/60 bg-background/60 px-2 py-1 text-[11px] font-medium text-muted-foreground">
              Range {fmt(amountRange.min)}-{fmt(amountRange.max)}
            </span>
          )}
        </div>

        <div className="grid grid-cols-[minmax(0,1fr)_auto_auto] items-center gap-2">
          <Button
            type="button"
            size="sm"
            variant="default"
            className="h-9 min-w-0 gap-2 px-3"
            disabled={busy}
            onClick={() => existingBill
              ? onQuickLink(recommendation, existingBill.id)
              : onAccept({ ...recommendation, category_id: categoryId })}
          >
            {busy ? (
              <Loader2 className="h-3.5 w-3.5 animate-spin" />
            ) : existingBill ? (
              <Link2 className="h-3.5 w-3.5" />
            ) : (
              <CheckCircle2 className="h-3.5 w-3.5" />
            )}
            <span className="min-w-0 truncate">
              {existingBill ? 'Link Existing Bill' : 'Track Subscription'}
            </span>
          </Button>
          <RecommendationIconButton
            label="View recommendation details"
            icon={Info}
            disabled={busy}
            onClick={() => onDetails(recommendation)}
          />
          <RecommendationMoreMenu
            recommendation={recommendation}
            existingBill={existingBill}
            busy={busy}
            onAccept={onAccept}
            onDecline={onDecline}
            onMatch={onMatch}
            categoryId={categoryId}
          />
        </div>
      </div>
      </div>
    </TooltipProvider>
  );
}

function EvidenceItem({ label, value, tone = 'default' }: { label: string; value?: ReactNode; tone?: string }) {
  if (!value) return null;
  return (
    <div className={cn(
      'rounded-lg border px-3 py-2',
      tone === 'warn'
        ? 'border-amber-500/25 bg-amber-500/10'
        : 'border-border/60 bg-muted/20',
    )}>
      <p className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">{label}</p>
      <p className="mt-1 text-sm font-medium text-foreground">{value}</p>
    </div>
  );
}

function RecommendationDetailsDialog({ open, recommendation, categoryId, onClose, onAccept, onDecline, onMatch, onQuickLink, busy }: {
  open: boolean;
  recommendation: Recommendation | null;
  categoryId: number | null;
  onClose: () => void;
  onAccept: RecAction;
  onDecline: RecAction;
  onMatch: RecAction;
  onQuickLink: QuickLink;
  busy?: boolean;
}) {
  if (!recommendation) return null;

  const identity = recommendation.evidence?.identity;
  const amount = recommendation.evidence?.amount;
  const cadence = recommendation.evidence?.cadence;
  const amountRange = recommendation.evidence?.amount_range;
  const ambiguity = recommendation.evidence?.ambiguity;
  const existingBill = recommendation.existing_bill_match;
  const transactions = recommendation.transactions || [];
  const handleAccept = async () => {
    await onAccept({ ...recommendation, category_id: categoryId });
    onClose();
  };
  const handleDecline = async () => {
    await onDecline(recommendation);
    onClose();
  };
  const handleMatch = () => {
    onMatch(recommendation);
    onClose();
  };
  const handleQuickLink = async () => {
    if (!existingBill) return;
    await onQuickLink(recommendation, existingBill.id);
    onClose();
  };

  return (
    <Dialog open={open} onOpenChange={value => { if (!value) onClose(); }}>
      <DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-2xl">
        <DialogHeader>
          <DialogTitle className="flex flex-wrap items-center gap-2">
            <span>{recommendation.name}</span>
            <TooltipProvider delayDuration={180}>
              <Tooltip>
                <TooltipTrigger asChild>
                  <Badge variant="outline" className="border-primary/25 bg-primary/10 text-primary cursor-default">
                    {recommendation.confidence}% match
                  </Badge>
                </TooltipTrigger>
                <TooltipContent>Match confidence — how likely this is a real recurring subscription</TooltipContent>
              </Tooltip>
            </TooltipProvider>
            {ambiguity?.ambiguous && (
              <TooltipProvider delayDuration={180}>
                <Tooltip>
                  <TooltipTrigger asChild>
                    <Badge variant="outline" className="gap-1 border-amber-500/25 bg-amber-500/10 text-amber-600 dark:text-amber-300 cursor-default">
                      <AlertTriangle className="h-3 w-3" />
                      Review
                    </Badge>
                  </TooltipTrigger>
                  <TooltipContent>{ambiguity.label || 'Multiple patterns detected — verify before tracking'}</TooltipContent>
                </Tooltip>
              </TooltipProvider>
            )}
            {existingBill && (
              <TooltipProvider delayDuration={180}>
                <Tooltip>
                  <TooltipTrigger asChild>
                    <Badge variant="outline" className="border-primary/25 bg-primary/10 text-primary cursor-default">
                      Existing bill
                    </Badge>
                  </TooltipTrigger>
                  <TooltipContent>May match your tracked bill: {existingBill.name}</TooltipContent>
                </Tooltip>
              </TooltipProvider>
            )}
          </DialogTitle>
          <p className="text-sm text-muted-foreground">
            {typeLabel(recommendation.subscription_type)} · {recommendation.occurrence_count} charge{recommendation.occurrence_count !== 1 ? 's' : ''} · last seen {fmtDate(recommendation.last_seen_date)}
          </p>
        </DialogHeader>

        <div className="grid gap-3 sm:grid-cols-2">
          <EvidenceItem label="Expected" value={`${fmt(recommendation.expected_amount)} ${recommendation.cycle_type || 'monthly'}`} />
          <EvidenceItem label="Monthly" value={`${fmt(recommendation.monthly_equivalent)} / mo`} />
          <EvidenceItem label="Identity" value={identity?.label} />
          <EvidenceItem label="Price" value={amount?.label} tone={amount?.match === 'unusual' ? 'warn' : 'default'} />
          <EvidenceItem label="Cadence" value={cadence?.label} />
          <EvidenceItem
            label="Catalog"
            value={recommendation.catalog_match
              ? [
                  recommendation.catalog_match.name,
                  recommendation.catalog_match.starting_monthly_usd ? `from ${fmt(recommendation.catalog_match.starting_monthly_usd)} / mo` : null,
                ].filter(Boolean).join(' · ')
              : null}
          />
        </div>

        {amountRange && (
          <div className="rounded-lg border border-border/60 bg-muted/20 px-3 py-2">
            <p className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">Amount Range</p>
            <p className="mt-1 text-sm font-medium text-foreground">
              {fmt(amountRange.min)}-{fmt(amountRange.max)} across matching transactions
            </p>
          </div>
        )}

        {ambiguity?.ambiguous && (
          <div className="rounded-lg border border-amber-500/25 bg-amber-500/10 px-3 py-2">
            <p className="flex items-center gap-1.5 text-sm font-semibold text-amber-700 dark:text-amber-300">
              <AlertTriangle className="h-4 w-4" />
              {ambiguity.label || 'Review before tracking'}
            </p>
            {!!ambiguity.reasons?.length && (
              <div className="mt-2 space-y-1 text-sm text-muted-foreground">
                {ambiguity.reasons?.map(reason => (
                  <p key={reason}>{reason}</p>
                ))}
              </div>
            )}
          </div>
        )}

        {existingBill && (
          <div className="rounded-lg border border-primary/25 bg-primary/10 px-3 py-2">
            <p className="text-sm font-semibold text-primary">Recommended action: link existing bill</p>
            <p className="mt-1 text-sm text-muted-foreground">
              {existingBill.name} · {existingBill.expected_amount ? fmt(existingBill.expected_amount) : 'No amount'} · due day {existingBill.due_day || 'not set'}
            </p>
            {!!existingBill.reasons?.length && (
              <div className="mt-2 space-y-1 text-sm text-muted-foreground">
                {existingBill.reasons?.map(reason => (
                  <p key={reason}>{reason}</p>
                ))}
              </div>
            )}
          </div>
        )}

        {!!recommendation.reasons?.length && (
          <div className="flex flex-wrap gap-2">
            {recommendation.reasons?.map(reason => (
              <span key={reason} className="rounded-md border border-border/60 bg-background/60 px-2 py-1 text-[11px] font-medium text-muted-foreground">
                {reason}
              </span>
            ))}
          </div>
        )}

        {transactions.length > 0 && (
          <div className="overflow-hidden rounded-lg border border-border/60">
            <div className="border-b border-border/50 bg-muted/20 px-3 py-2">
              <p className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
                Bank Transactions
              </p>
            </div>
            <div className="divide-y divide-border/40">
              {transactions.map(tx => (
                <div key={tx.id} className="grid gap-1 px-3 py-2.5 sm:grid-cols-[1fr_auto] sm:items-center">
                  <div className="min-w-0">
                    <p className="truncate text-sm font-medium text-foreground">
                      {tx.payee || tx.description || tx.memo || 'Transaction'}
                    </p>
                    <p className="truncate text-xs text-muted-foreground">
                      {fmtDate(tx.date)}{tx.account ? ` · ${tx.account}` : ''}
                    </p>
                  </div>
                  <p className="tracker-number text-sm font-semibold text-foreground">{fmt(tx.amount)}</p>
                </div>
              ))}
            </div>
          </div>
        )}

        <DialogFooter className="flex-col-reverse gap-2 sm:flex-row sm:items-center sm:justify-between">
          <Button variant="ghost" onClick={handleDecline} disabled={busy} className="gap-1.5 text-muted-foreground hover:text-destructive">
            {busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <X className="h-3.5 w-3.5" />}
            Dismiss
          </Button>
          <div className="flex flex-col-reverse gap-2 sm:flex-row">
            {existingBill && (
              <Button variant="outline" onClick={handleAccept} disabled={busy} className="gap-1.5">
                <CheckCircle2 className="h-3.5 w-3.5" />
                Track New
              </Button>
            )}
            <Button variant="outline" onClick={handleMatch} disabled={busy} className="gap-1.5">
              <Link2 className="h-3.5 w-3.5" />
              {existingBill ? 'Choose Bill' : 'Link Existing'}
            </Button>
            <Button variant="default" onClick={existingBill ? handleQuickLink : handleAccept} disabled={busy} className="gap-2">
              {busy ? (
                <Loader2 className="h-3.5 w-3.5 animate-spin" />
              ) : existingBill ? (
                <Link2 className="h-3.5 w-3.5" />
              ) : (
                <CheckCircle2 className="h-3.5 w-3.5" />
              )}
              {existingBill ? 'Link Existing' : 'Track Subscription'}
            </Button>
          </div>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

type FlatItem =
  | { type: 'group-header'; sectionKey: string; groupKey: string; activeState: boolean; groupItems: Subscription[]; monthlySum: number }
  | { type: 'row'; item: Subscription; groupItems: Subscription[]; activeState: boolean; i: number }
  | { type: 'paused-divider'; count: number };

export default function SubscriptionsPage() {
  const queryClient = useQueryClient();
  const { data: subData, isPending: loading } = useSubscriptions();
  const data: SubData = subData || { summary: {}, subscriptions: [] };
  const { data: recommendationsRaw = [], isPending: recommendationsLoading } = useSubscriptionRecommendations();
  const recommendations = recommendationsRaw;
  const { data: categories = [] } = useCategories();
  const { data: bills = [] } = useBills();
  // Optimistic updates write through the query cache so every existing call site
  // (setData(prev => …), setBills(…), setRecommendations(…)) works unchanged.
  const setData = useCallback((u: SubData | ((prev: SubData) => SubData)) => queryClient.setQueryData<SubData>(['subscriptions'],
    prev => (typeof u === 'function' ? u(prev || { summary: {}, subscriptions: [] }) : u)), [queryClient]);
  const setBills = useCallback((u: Bill[] | ((prev: Bill[]) => Bill[])) => queryClient.setQueryData<Bill[]>(['bills'],
    prev => (typeof u === 'function' ? u(prev || []) : u)), [queryClient]);
  const setRecommendations = useCallback((u: Recommendation[] | ((prev: Recommendation[]) => Recommendation[])) => queryClient.setQueryData<Recommendation[]>(['subscription-recommendations'],
    prev => (typeof u === 'function' ? u(prev || []) : u)), [queryClient]);
  const [busyId, setBusyId] = useState<string | null>(null);
  const [modal, setModal] = useState<ModalState | null>(null);
  const [matchTarget, setMatchTarget] = useState<Recommendation | null>(null);
  const [detailsTarget, setDetailsTarget] = useState<Recommendation | null>(null);
  const [recSearch, setRecSearch] = useState('');
  const [subSearch, setSubSearch] = useState('');
  const [subscriptionSort, setSubscriptionSort] = useState<'custom' | 'cadence'>(() => (
    localStorage.getItem(SUBSCRIPTION_SORT_KEY) === 'cadence' ? 'cadence' : 'custom'
  ));
  const [draggingId, setDraggingId] = useState<number | null>(null);
  const [dropTargetId, setDropTargetId] = useState<number | null>(null);
  const [movingBillId, setMovingBillId] = useState<number | string | null>(null);
  const [searchPanelCollapsed, setSearchPanelCollapsed] = useSearchPanelPreference();
  const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(new Set());
  const cardHeaderRef = useRef<HTMLDivElement>(null);
  const [cardHeaderHeight, setCardHeaderHeight] = useState(0);

  const [txQuery, setTxQuery]         = useState('');
  const [txResults, setTxResults]     = useState<SubTx[]>([]);
  const [txSearching, setTxSearching] = useState(false);
  const [importDialog, setImportDialog] = useState<{ billId: number; billName: string } | null>(null);
  const txDebounce = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);

  const subscriptionCategoryId = useMemo(() => {
    const match = categories.find(category => /subscrip/i.test(category.name));
    return match?.id || null;
  }, [categories]);

  const load = useCallback(() => Promise.all([
    queryClient.invalidateQueries({ queryKey: ['subscriptions'] }),
    queryClient.invalidateQueries({ queryKey: ['categories'] }),
    queryClient.invalidateQueries({ queryKey: ['bills'] }),
  ]), [queryClient]);
  const loadRecommendations = useCallback(
    () => queryClient.invalidateQueries({ queryKey: ['subscription-recommendations'] }),
    [queryClient],
  );

  useEffect(() => {
    localStorage.setItem(SUBSCRIPTION_SORT_KEY, subscriptionSort);
  }, [subscriptionSort]);

  useEffect(() => {
    const el = cardHeaderRef.current;
    if (!el) return;
    const obs = new ResizeObserver(() => setCardHeaderHeight(el.offsetHeight));
    obs.observe(el);
    return () => obs.disconnect();
  }, []);

  useEffect(() => {
    clearTimeout(txDebounce.current);
    const q = txQuery.trim();
    if (!q) { setTxResults([]); return; }
    txDebounce.current = setTimeout(async () => {
      setTxSearching(true);
      try {
        const result = await api.subscriptionTransactionMatches({ q, limit: 50 });
        setTxResults(Array.isArray(result) ? result : (result?.transactions ?? []));
      } catch (err) {
        setTxResults([]);
        toast.error(errMessage(err, 'Transaction search failed.'));
      }
      finally { setTxSearching(false); }
    }, 300);
    return () => clearTimeout(txDebounce.current);
  }, [txQuery]);

  async function refreshAll() {
    await Promise.all([load(), loadRecommendations()]);
  }

  async function toggleSubscription(item: Subscription) {
    const newActive = !item.active;
    addOptimisticSub({ id: item.id, active: newActive }); // instant — no spinner
    try {
      await api.updateSubscription(item.id, { active: newActive });
      await load(); // reconciles optimistic state
    } catch (err) {
      toast.error(errMessage(err, 'Subscription could not be updated.'));
      await load(); // revert via useOptimistic reconciliation
    }
  }

  async function acceptRecommendation(recommendation: Recommendation) {
    setBusyId(`rec-${recommendation.id}`);
    try {
      const created = await api.createSubscriptionFromRecommendation(recommendation);
      toast.success(`${recommendation.name} is now tracked.`);
      await refreshAll();
      if (getLinkImportPref() && recommendation.merchant && created?.id) {
        setImportDialog({ billId: created.id, billName: created.name || recommendation.name });
      }
    } catch (err) {
      toast.error(errMessage(err, 'Could not create subscription.'));
    } finally {
      setBusyId(null);
    }
  }

  async function declineRecommendation(recommendation: Recommendation) {
    if (!recommendation.decline_key) return;
    setBusyId(`dec-${recommendation.id}`);
    try {
      await api.declineRecommendation(recommendation);
      setRecommendations(prev => prev.filter(r => r.id !== recommendation.id));
    } catch (err) {
      toast.error(errMessage(err, 'Could not dismiss recommendation.'));
    } finally {
      setBusyId(null);
    }
  }

  async function linkRecommendationToBill(recommendation: Recommendation | null, billId: number | null) {
    if (!recommendation || !billId) return;
    setBusyId(`match-${recommendation.id}`);
    try {
      const result = await api.matchRecommendationToBill(
        recommendation.transaction_ids,
        billId,
        recommendation.merchant,
        recommendation.catalog_match?.id,
        recommendation.confidence,
      );
      toast.success(`Linked ${result.matched_count} transaction${result.matched_count !== 1 ? 's' : ''} to "${result.bill_name}".`);
      setMatchTarget(null);
      setRecommendations(prev => prev.filter(r => r.id !== recommendation.id));
    } catch (err) {
      toast.error(errMessage(err, 'Could not link recommendation to bill.'));
    } finally {
      setBusyId(null);
    }
  }

  async function matchRecommendationToBill(billId: number | null) {
    await linkRecommendationToBill(matchTarget, billId);
  }

  function applySavedBillToPage(savedBill: Bill) {
    if (!savedBill?.id) return;
    const decorated = decorateSavedSubscriptionBill(savedBill, categories);

    setBills(prev => {
      const current = Array.isArray(prev) ? prev : [];
      const exists = current.some(item => Number(item.id) === Number(savedBill.id));
      return exists
        ? current.map(item => Number(item.id) === Number(savedBill.id) ? { ...item, ...savedBill } : item)
        : [...current, savedBill];
    });

    setData(prev => {
      const current = prev.subscriptions || [];
      const exists = current.some(item => Number(item.id) === Number(savedBill.id));
      const nextSubscriptions = decorated.is_subscription
        ? exists
          ? current.map(item => Number(item.id) === Number(savedBill.id) ? { ...item, ...decorated } : item)
          : [...current, decorated]
        : current.filter(item => Number(item.id) !== Number(savedBill.id));

      return {
        ...prev,
        subscriptions: nextSubscriptions,
        summary: subscriptionSummaryFromList(nextSubscriptions),
      };
    });
  }

  function handleBillModalSave(savedBill?: Bill) {
    if (!savedBill?.id) return;
    applySavedBillToPage(savedBill);
  }

  function openManualSubscription() {
    setModal({
      bill: null,
      initialBill: {
        name: '',
        category_id: subscriptionCategoryId,
        due_day: new Date().getDate(),
        expected_amount: '',
        billing_cycle: 'monthly',
        cycle_type: 'monthly',
        cycle_day: String(new Date().getDate()),
        is_subscription: 1,
        subscription_type: 'other',
        reminder_days_before: 3,
      },
    });
  }

  function openFromTransaction(tx: SubTx) {
    const catalogMatch = tx.catalog_match;
    const label   = catalogMatch?.name || tx.payee || tx.description || tx.memo || '';
    const dollars = Math.abs(tx.amount ?? 0) / 100;
    const rawDay  = tx.posted_date ? new Date(tx.posted_date + 'T12:00:00').getDate() : NaN;
    const dueDay  = Number.isInteger(rawDay) && rawDay >= 1 && rawDay <= 31 ? rawDay : new Date().getDate();
    setModal({
      bill: null,
      initialBill: {
        name: label,
        category_id: subscriptionCategoryId,
        due_day: dueDay,
        expected_amount: dollars > 0 ? dollars.toFixed(2) : '',
        cycle_type: 'monthly',
        is_subscription: 1,
        subscription_type: catalogMatch?.subscription_type || 'other',
        website: catalogMatch?.website || undefined,
        notes: catalogMatch
          ? `Matched known subscription catalog entry: ${catalogMatch.name}`
          : undefined,
        reminder_days_before: 3,
      },
    });
  }

  const summary = data.summary || {};
  const subscriptions = data.subscriptions || [];

  const [optimisticSubs, addOptimisticSub] = useOptimistic(
    subscriptions,
    useCallback((state: Subscription[], { id, active }: { id: number; active: boolean }) => state.map(s => s.id === id ? { ...s, active } : s), []),
  );

  // Deferred so typing in the search box stays snappy while the list re-filters.
  const deferredSubSearch = useDeferredValue(subSearch);
  const filteredSubscriptions = useMemo(() => {
    const q = deferredSubSearch.trim().toLowerCase();
    const base = q
      ? optimisticSubs.filter(item =>
          String(item.name || '').toLowerCase().includes(q) ||
          String(item.subscription_type || '').toLowerCase().includes(q) ||
          String(item.category_name || '').toLowerCase().includes(q)
        )
      : optimisticSubs;
    return base;
  }, [optimisticSubs, deferredSubSearch]);
  const active = filteredSubscriptions.filter(item => item.active);
  const paused = filteredSubscriptions.filter(item => !item.active);
  const sortedActive = useMemo(
    () => subscriptionSort === 'cadence' ? sortSubscriptionsByCadence(active) : active,
    [active, subscriptionSort],
  );
  const sortedPaused = useMemo(
    () => subscriptionSort === 'cadence' ? sortSubscriptionsByCadence(paused) : paused,
    [paused, subscriptionSort],
  );
  const reorderEnabled = !loading && bills.length > 0 && subscriptionSort === 'custom';

  // Flat item list for virtualizer — cadence mode only (custom mode needs DOM order for drag-reorder)
  const flatVirtualItems = useMemo<FlatItem[] | null>(() => {
    if (subscriptionSort !== 'cadence') return null;
    const items: FlatItem[] = [];
    const buildGroup = (group: Subscription[], activeState: boolean) => {
      GROUP_ORDER.forEach(groupKey => {
        const groupItems = group.filter(item => subscriptionGroup(item) === groupKey);
        if (!groupItems.length) return;
        const sectionKey = `${activeState ? 'active' : 'paused'}-${groupKey}`;
        const monthlySum = groupItems.reduce((s, i) => s + (Number(i.monthly_equivalent) || 0), 0);
        items.push({ type: 'group-header', sectionKey, groupKey, activeState, groupItems, monthlySum });
        if (!collapsedGroups.has(sectionKey)) {
          groupItems.forEach((item, i) => items.push({ type: 'row', item, groupItems, activeState, i }));
        }
      });
    };
    buildGroup(sortedActive, true);
    if (sortedPaused.length > 0) {
      items.push({ type: 'paused-divider', count: sortedPaused.length });
      buildGroup(sortedPaused, false);
    }
    return items;
  }, [sortedActive, sortedPaused, subscriptionSort, collapsedGroups]);

  const listRef = useRef<HTMLDivElement>(null);
  const virtualizer = useVirtualizer({
    count: flatVirtualItems?.length ?? 0,
    getScrollElement: () => listRef.current,
    estimateSize: i => {
      if (!flatVirtualItems) return 96;
      const it = flatVirtualItems[i];
      if (it?.type === 'group-header') return 40;
      if (it?.type === 'paused-divider') return 32;
      return 96;
    },
    overscan: 5,
    enabled: !!flatVirtualItems,
  });

  async function persistSubscriptionOrder(nextSubscriptions: Subscription[], nextBills: Bill[], movedId: string | number | null) {
    const prevData = data;
    const prevBills = bills;
    setData(prev => ({ ...prev, subscriptions: nextSubscriptions }));
    setBills(nextBills);
    setMovingBillId(movedId);
    try {
      await api.reorderBills(reorderPayload(nextBills));
      await load();
      api.allBills().then(b => setBills(Array.isArray(b) ? b : [])).catch(() => {});
    } catch (err) {
      toast.error(errMessage(err, 'Failed to save subscription order'));
      setData(prevData);
      setBills(prevBills);
    } finally {
      setMovingBillId(null);
    }
  }

  function reorderSubscriptionGroup(activeState: boolean, orderedGroup: Subscription[]) {
    const sourceGroup = subscriptions.filter(item => !!item.active === activeState);
    const replacements = [...orderedGroup];
    const nextSubscriptions = subscriptions.map(item => (
      !!item.active === activeState ? replacements.shift()! : item
    ));

    const sourceBills: Bill[] = bills.length ? bills : (subscriptions as unknown as Bill[]);
    const affectedIds = new Set(sourceGroup.map(item => item.id));
    const billById = new Map(sourceBills.map(item => [item.id, item]));
    const orderedBills = orderedGroup.map(item => ({ ...(billById.get(item.id) || item), ...item }) as unknown as Bill);
    const nextBills = sourceBills.map(bill => (
      affectedIds.has(bill.id) ? orderedBills.shift()! : bill
    ));
    persistSubscriptionOrder(nextSubscriptions, nextBills, movedItemId(sourceGroup, orderedGroup));
  }

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

  function dragPropsForGroup(group: Subscription[], activeState: boolean): (item: Subscription, index: number) => DragProps {
    return (item, index) => {
      if (!reorderEnabled) return { draggable: false };
      return {
        draggable: true,
        isDragging: draggingId === item.id,
        isDropTarget: dropTargetId === item.id && draggingId !== item.id,
        onDragStart: (event) => {
          setDraggingId(item.id);
          event.dataTransfer.effectAllowed = 'move';
          event.dataTransfer.setData('text/plain', String(item.id));
        },
        onDragEnter: () => {
          if (draggingId && draggingId !== item.id) setDropTargetId(item.id);
        },
        onDragOver: (event) => {
          event.preventDefault();
          event.dataTransfer.dropEffect = 'move';
          if (draggingId && draggingId !== item.id) setDropTargetId(item.id);
        },
        onDrop: (event) => {
          event.preventDefault();
          const sourceId = Number(event.dataTransfer.getData('text/plain') || draggingId);
          const fromIndex = group.findIndex(row => row.id === sourceId);
          if (fromIndex >= 0) reorderSubscriptionGroup(activeState, moveInArray(group, fromIndex, index));
          setDraggingId(null);
          setDropTargetId(null);
        },
        onDragEnd: () => {
          setDraggingId(null);
          setDropTargetId(null);
        },
      };
    };
  }

  const MIN_CONFIDENCE = 90;
  const highConfidenceRecs = useMemo(
    () => recommendations.filter(r => r.catalog_match && (r.confidence ?? 0) >= MIN_CONFIDENCE),
    [recommendations],
  );
  const filteredRecs = useMemo(() => {
    const q = recSearch.trim().toLowerCase();
    return q ? highConfidenceRecs.filter(r => r.name?.toLowerCase().includes(q)) : highConfidenceRecs;
  }, [highConfidenceRecs, recSearch]);

  function renderGroupHeader(sectionKey: string, groupKey: string, groupItems: Subscription[], monthlySum: number) {
    const isCollapsed = collapsedGroups.has(sectionKey);
    return (
      <button
        type="button"
        onClick={() => setCollapsedGroups(prev => {
          const next = new Set(prev);
          if (next.has(sectionKey)) next.delete(sectionKey); else next.add(sectionKey);
          return next;
        })}
        style={{ top: cardHeaderHeight }}
        className="sticky z-10 w-full border-b border-t border-border/40 bg-card/95 backdrop-blur-sm px-4 py-2 text-left transition-colors hover:bg-muted/30"
      >
        <div className="flex items-center justify-between gap-3">
          <p className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
            {GROUP_LABELS[groupKey]}
          </p>
          <div className="flex items-center gap-3">
            <span className="text-[11px] font-medium text-muted-foreground tabular-nums">
              {fmt(monthlySum)}/mo · {groupItems.length}
            </span>
            <ChevronDown className={cn('h-3.5 w-3.5 text-muted-foreground/60 transition-transform duration-150', isCollapsed && '-rotate-90')} />
          </div>
        </div>
      </button>
    );
  }

  // Custom sort: normal DOM rendering (drag-reorder needs DOM order)
  function renderCustomRows(group: Subscription[], activeState: boolean) {
    return group.map((item, index) => (
      <SubscriptionRow
        key={item.id}
        item={item}
        onEdit={bill => setModal({ bill })}
        onToggle={toggleSubscription}
        busy={false}
        moveControls={moveControlsForGroup(group, activeState)(item, index)}
        dragProps={dragPropsForGroup(group, activeState)(item, index)}
      />
    ));
  }

  // Cadence sort: virtualizer renders from flatVirtualItems
  function renderVirtualItem(vRow: { index: number }) {
    const it = flatVirtualItems?.[vRow.index];
    if (!it) return null;
    if (it.type === 'paused-divider') {
      return (
        <div className="border-t-2 border-border/60 bg-muted/20 px-4 py-1.5">
          <p className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground/60">
            Paused · {it.count}
          </p>
        </div>
      );
    }
    if (it.type === 'group-header') {
      return renderGroupHeader(it.sectionKey, it.groupKey, it.groupItems, it.monthlySum);
    }
    return (
      <SubscriptionRow
        item={it.item}
        onEdit={bill => setModal({ bill })}
        onToggle={toggleSubscription}
        busy={false}
        moveControls={moveControlsForGroup(it.groupItems, it.activeState)(it.item, it.i)}
        dragProps={dragPropsForGroup(it.groupItems, it.activeState)(it.item, it.i)}
      />
    );
  }

  return (
    <div className="mx-auto w-full max-w-6xl space-y-6">
      <div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
        <div className="min-w-0">
          <p className="mb-1 text-[11px] font-semibold uppercase tracking-[0.15em] text-muted-foreground">
            Recurring Services
          </p>
          <h1 className="text-3xl font-semibold tracking-tight">Subscriptions</h1>
          <p className="mt-1 text-sm text-muted-foreground">
            Track manual subscriptions and review recurring SimpleFIN charges.
          </p>
        </div>
        <div className="grid grid-cols-2 gap-2 sm:flex sm:flex-wrap">
          <Button type="button" variant="outline" className="h-9 gap-2" onClick={refreshAll} disabled={loading || recommendationsLoading}>
            <RefreshCw className={cn('h-4 w-4', (loading || recommendationsLoading) && 'animate-spin')} />
            Refresh
          </Button>
          <Button type="button" className="h-9 gap-2" onClick={openManualSubscription}>
            <Plus className="h-4 w-4" />
            Add Subscription
          </Button>
        </div>
      </div>

      <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
        <StatCard icon={Repeat} label="Monthly" value={fmt(summary.monthly_total)} hint={`${summary.active_count || 0} active subscriptions`} />
        <StatCard icon={CalendarDays} label="Yearly" value={fmt(summary.yearly_total)} hint="Normalized yearly impact" />
        <StatCard icon={Pause} label="Paused" value={summary.paused_count || 0} hint="Kept but not active" />
        <StatCard icon={Cloud} label="Top Type" value={summary.top_type ? TYPE_LABELS[summary.top_type.type] || summary.top_type.type : '—'} hint={summary.top_type ? `${fmt(summary.top_type.monthly_total)} / mo` : 'No active subscriptions'} />
      </div>

      <div className="grid min-w-0 gap-5 2xl:grid-cols-[minmax(0,1fr)_minmax(360px,420px)]">
        <Card>
          <CardHeader ref={cardHeaderRef} className="sticky top-0 z-20 rounded-t-xl border-b border-border/50 bg-card/95 px-4 pb-3 backdrop-blur-sm sm:px-6">
            <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
              <div className="min-w-0">
                <CardTitle className="text-base">Tracked Subscriptions</CardTitle>
                <CardDescription>Subscriptions are bills with recurring-service metadata.</CardDescription>
              </div>
              <TooltipProvider delayDuration={300}>
                <div className="grid grid-cols-2 rounded-lg border border-border/60 bg-muted/30 p-1">
                  <Tooltip>
                    <TooltipTrigger asChild>
                      <SortModeButton
                        active={subscriptionSort === 'custom'}
                        onClick={() => setSubscriptionSort('custom')}
                      >
                        Custom
                      </SortModeButton>
                    </TooltipTrigger>
                    <TooltipContent>Drag to reorder manually</TooltipContent>
                  </Tooltip>
                  <Tooltip>
                    <TooltipTrigger asChild>
                      <SortModeButton
                        active={subscriptionSort === 'cadence'}
                        onClick={() => setSubscriptionSort('cadence')}
                      >
                        Cadence
                      </SortModeButton>
                    </TooltipTrigger>
                    <TooltipContent>Sort by billing frequency: weekly → biweekly → monthly → quarterly → annual</TooltipContent>
                  </Tooltip>
                </div>
              </TooltipProvider>
            </div>
            <SearchFilterPanel
              title="Search subscriptions"
              collapsed={searchPanelCollapsed}
              onCollapsedChange={setSearchPanelCollapsed}
              hasFilters={!!subSearch.trim()}
              resultLabel={`${filteredSubscriptions.length} of ${subscriptions.length} shown`}
              onClear={() => setSubSearch('')}
              variant="embedded"
            >
              <div className="relative">
                <Search className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
                <Input
                  value={subSearch}
                  onChange={e => setSubSearch(e.target.value)}
                  placeholder="Search subscriptions…"
                  className="h-8 pl-8 pr-8 text-sm"
                />
                {subSearch && (
                  <button
                    type="button"
                    onClick={() => setSubSearch('')}
                    className="absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
                    aria-label="Clear search"
                  >
                    <X className="h-3.5 w-3.5" />
                  </button>
                )}
              </div>
            </SearchFilterPanel>
          </CardHeader>
          <CardContent className={cn('p-0', subscriptionSort === 'cadence' && 'max-h-[70vh] overflow-y-auto')}>
            {loading ? (
              <div className="p-4">
                {Array.from({ length: 4 }).map((_, index) => (
                  <div key={index} className="mb-2 h-20 animate-pulse rounded-lg bg-muted/40" />
                ))}
              </div>
            ) : subscriptions.length === 0 ? (
              <div className="px-4 py-14 text-center">
                <Repeat className="mx-auto h-8 w-8 text-muted-foreground" />
                <p className="mt-3 text-sm font-medium">No subscriptions tracked yet.</p>
                <p className="mt-1 text-sm text-muted-foreground">Add one manually or accept a SimpleFIN recommendation.</p>
              </div>
            ) : subscriptionSort === 'cadence' ? (
              // Cadence mode: virtualised from flatVirtualItems
              <div
                ref={listRef}
                style={{ height: virtualizer.getTotalSize(), position: 'relative' }}
              >
                {virtualizer.getVirtualItems().map(vRow => (
                  <div
                    key={vRow.key}
                    data-index={vRow.index}
                    ref={virtualizer.measureElement}
                    style={{
                      position: 'absolute',
                      top: 0,
                      width: '100%',
                      transform: `translateY(${vRow.start}px)`,
                    }}
                  >
                    {renderVirtualItem(vRow)}
                  </div>
                ))}
              </div>
            ) : (
              // Custom mode: normal DOM rendering so drag-reorder works
              <>
                {renderCustomRows(sortedActive, true)}
                {renderCustomRows(sortedPaused, false)}
              </>
            )}
          </CardContent>
        </Card>

        <Card className="overflow-hidden">
          <CardHeader className="px-4 pb-3 sm:px-6">
            <div className="flex items-center gap-2">
              <Sparkles className="h-4 w-4 text-primary" />
              <CardTitle className="text-base">Recommendations</CardTitle>
              {!recommendationsLoading && highConfidenceRecs.length > 0 && (
                <span className="ml-auto text-[11px] font-medium text-muted-foreground tabular-nums">
                  {filteredRecs.length} of {highConfidenceRecs.length}
                </span>
              )}
            </div>
            <CardDescription>Known subscription services found in your bank transactions with 90%+ confidence.</CardDescription>
            {!recommendationsLoading && highConfidenceRecs.length > 0 && (
              <SearchFilterPanel
                title="Search recommendations"
                collapsed={searchPanelCollapsed}
                onCollapsedChange={setSearchPanelCollapsed}
                hasFilters={!!recSearch.trim()}
                resultLabel={`${filteredRecs.length} of ${highConfidenceRecs.length} shown`}
                onClear={() => setRecSearch('')}
                variant="embedded"
              >
                <div className="relative">
                  <Search className="absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground pointer-events-none" />
                  <Input
                    type="search"
                    placeholder="Search recommendations…"
                    value={recSearch}
                    onChange={e => setRecSearch(e.target.value)}
                    className="pl-8 h-8 text-sm"
                  />
                </div>
              </SearchFilterPanel>
            )}
          </CardHeader>
          <CardContent className="space-y-3">
            {recommendationsLoading ? (
              <div className="rounded-lg border border-border/60 bg-muted/20 px-4 py-10 text-center text-sm text-muted-foreground">
                Scanning transactions…
              </div>
            ) : highConfidenceRecs.length === 0 ? (
              <div className="rounded-lg border border-dashed border-border/70 px-4 py-10 text-center">
                <Bell className="mx-auto h-7 w-7 text-muted-foreground" />
                <p className="mt-3 text-sm font-medium">No high-confidence recommendations.</p>
                <p className="mt-1 text-sm text-muted-foreground">
                  {recommendations.length > 0
                    ? 'More account activity or stronger descriptors will improve accuracy.'
                    : 'Sync your accounts after charges from known subscription services appear.'}
                </p>
              </div>
            ) : filteredRecs.length === 0 ? (
              <div className="rounded-lg border border-dashed border-border/70 px-4 py-8 text-center">
                <Search className="mx-auto h-6 w-6 text-muted-foreground" />
                <p className="mt-3 text-sm font-medium">No matches for "{recSearch}"</p>
                <button
                  type="button"
                  className="mt-1 text-xs text-primary hover:underline"
                  onClick={() => setRecSearch('')}
                >
                  Clear search
                </button>
              </div>
            ) : (
              filteredRecs.map(recommendation => (
                <RecommendationCard
                  key={recommendation.id}
                  recommendation={recommendation}
                  categoryId={subscriptionCategoryId}
                  busy={busyId === `rec-${recommendation.id}` || busyId === `dec-${recommendation.id}` || busyId === `match-${recommendation.id}`}
                  onAccept={acceptRecommendation}
                  onDecline={declineRecommendation}
                  onMatch={rec => setMatchTarget(rec)}
                  onQuickLink={linkRecommendationToBill}
                  onDetails={setDetailsTarget}
                />
              ))
            )}
          </CardContent>
        </Card>
      </div>

      {/* Transaction search */}
      <Card className="overflow-hidden">
        <CardHeader className="px-4 pb-3 sm:px-6">
          <div className="flex items-center gap-2">
            <Search className="h-4 w-4 text-primary" />
            <CardTitle className="text-base">Search Bank Transactions</CardTitle>
          </div>
          <CardDescription>
            Search all account charges — matched and unmatched — to find subscriptions the algorithm may have missed.
          </CardDescription>
          <SearchFilterPanel
            title="Search transactions"
            collapsed={searchPanelCollapsed}
            onCollapsedChange={setSearchPanelCollapsed}
            hasFilters={!!txQuery.trim()}
            resultLabel={txQuery.trim() ? `${txResults.length} result${txResults.length === 1 ? '' : 's'}` : 'all bank transactions'}
            onClear={() => setTxQuery('')}
            variant="embedded"
          >
            <div className="relative">
              <Search className="absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground pointer-events-none" />
              <Input
                type="search"
                placeholder="Search by merchant, description, or payee…"
                value={txQuery}
                onChange={e => setTxQuery(e.target.value)}
                className="pl-8 h-9 text-sm"
                autoComplete="off"
              />
            </div>
          </SearchFilterPanel>
        </CardHeader>

        {(txQuery.trim() || txSearching) && (
          <CardContent className="p-0">
            {txSearching ? (
              <div className="flex items-center justify-center gap-2 py-8 text-sm text-muted-foreground">
                <Loader2 className="h-4 w-4 animate-spin" />
                Searching transactions…
              </div>
            ) : txResults.length === 0 ? (
              <div className="px-4 py-8 text-center">
                <Search className="mx-auto h-7 w-7 text-muted-foreground" />
                <p className="mt-3 text-sm font-medium">No transactions found for "{txQuery}"</p>
                <p className="mt-1 text-xs text-muted-foreground">Try a different merchant name or description.</p>
              </div>
            ) : (
              <div>
                <div className="px-4 py-2 border-b border-border/40 bg-muted/20">
                  <p className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
                    {txResults.length} result{txResults.length !== 1 ? 's' : ''}
                    {txResults.length === 50 ? ' (showing first 50)' : ''}
                  </p>
                </div>
                {txResults.map(tx => (
                  <TxResultRow key={tx.id} tx={tx} onTrack={openFromTransaction} />
                ))}
              </div>
            )}
          </CardContent>
        )}
      </Card>

      <Card className="overflow-hidden">
        <CardHeader className="px-4 pb-3 sm:px-6">
          <div className="flex items-center gap-2">
            <Link2 className="h-4 w-4 text-primary" />
            <CardTitle className="text-base">Improve Matching</CardTitle>
          </div>
          <CardDescription>
            Manage known services, catalog links, and custom bank descriptors on a dedicated page.
          </CardDescription>
        </CardHeader>
        <CardContent className="flex flex-col gap-3 px-4 pb-4 sm:flex-row sm:items-center sm:justify-between sm:px-6">
          <p className="text-sm text-muted-foreground">
            Use the service catalog when a recommendation names the wrong service, a bill needs a catalog link, or your bank uses a custom descriptor.
          </p>
          <Button asChild type="button" variant="outline" className="h-9 shrink-0 gap-2">
            <Link to="/subscriptions/catalog">
              <Link2 className="h-4 w-4" />
              Service Catalog
            </Link>
          </Button>
        </CardContent>
      </Card>

      {modal && (
        <BillModal
          key={modal.bill?.id ? `subscription-edit-${modal.bill.id}` : 'subscription-new'}
          bill={modal.bill as Bill | null}
          initialBill={modal.initialBill as unknown as Partial<Bill> | undefined}
          categories={categories}
          onClose={() => setModal(null)}
          onSave={handleBillModalSave}
        />
      )}

      <RecommendationDetailsDialog
        open={!!detailsTarget}
        recommendation={detailsTarget}
        categoryId={subscriptionCategoryId}
        onClose={() => setDetailsTarget(null)}
        onAccept={acceptRecommendation}
        onDecline={declineRecommendation}
        onMatch={rec => setMatchTarget(rec)}
        onQuickLink={linkRecommendationToBill}
        busy={detailsTarget ? (
          busyId === `rec-${detailsTarget.id}`
          || busyId === `dec-${detailsTarget.id}`
          || busyId === `match-${detailsTarget.id}`
        ) : false}
      />

      <BillPickerDialog
        open={!!matchTarget}
        onClose={() => setMatchTarget(null)}
        recommendation={matchTarget}
        bills={bills}
        onConfirm={matchRecommendationToBill}
        busy={!!busyId?.startsWith('match-')}
      />

      <BillHistoricalImportDialog
        billId={importDialog?.billId ?? ''}
        billName={importDialog?.billName ?? ''}
        open={!!importDialog}
        onClose={() => setImportDialog(null)}
        onImported={() => { setImportDialog(null); refreshAll(); }}
      />
    </div>
  );
}
