import React, { useState, useEffect, useCallback, useRef, useMemo, useTransition } from 'react';
import { useSearchParams } from 'react-router-dom';
import { ChevronLeft, ChevronRight, Pencil, TrendingUp, AlertCircle, Clock, CheckCircle2, Settings2, Loader2, Plus, Search, X, RefreshCw } from 'lucide-react';
import { toast } from 'sonner';
import { api } from '@/api.js';
import { useTracker } from '@/hooks/useQueries';
import BillModal from '@/components/BillModal';
import { makeBillDraft } from '@/lib/billDrafts';
import { cn, fmt, fmtDate, todayStr } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Skeleton } from '@/components/ui/Skeleton';
import {
  Table, TableHeader, TableBody, TableHead, TableRow, TableCell,
} from '@/components/ui/table';
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 { Label } from '@/components/ui/label';

const MONTHS = [
  'January','February','March','April','May','June',
  'July','August','September','October','November','December',
];
const FILTER_ALL = 'all';

// Sentinel for the "no method" select option — empty string crashes Radix Select
const METHOD_NONE = 'none';

function paymentDateForTrackerMonth(year, month, dueDay) {
  const now = new Date();
  if (year === now.getFullYear() && month === now.getMonth() + 1) {
    return todayStr();
  }

  const daysInMonth = new Date(year, month, 0).getDate();
  const day = Number.isInteger(Number(dueDay))
    ? Math.min(Math.max(Number(dueDay), 1), daysInMonth)
    : 1;

  return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
}

const ROW_STATUS_CLS = {
  paid:      'bg-emerald-500/[0.04] dark:bg-emerald-400/[0.02]',
  autodraft: 'bg-sky-500/[0.04] dark:bg-sky-400/[0.018]',
  upcoming:  '',
  due_soon:  'bg-amber-400/[0.07] dark:bg-amber-300/[0.016]',
  late:      'bg-orange-400/[0.08] dark:bg-orange-300/[0.014]',
  missed:    'bg-red-400/[0.08] dark:bg-rose-300/[0.01]',
};

const STATUS_META = {
  paid:      { label: 'Paid',      cls: 'bg-emerald-500/15 text-emerald-500 border border-emerald-500/30 dark:bg-emerald-300/10 dark:text-emerald-200 dark:border-emerald-300/30' },
  upcoming:  { label: 'Upcoming',  cls: 'bg-secondary text-muted-foreground border border-border' },
  due_soon:  { label: 'Due Soon',  cls: 'bg-amber-400/15 text-amber-500 border border-amber-400/30 dark:bg-amber-300/10 dark:text-amber-200 dark:border-amber-300/28' },
  late:      { label: 'Late',      cls: 'bg-orange-400/15 text-orange-500 border border-orange-400/30 dark:bg-orange-300/10 dark:text-orange-200 dark:border-orange-300/26' },
  missed:    { label: 'Missed',    cls: 'bg-red-400/15 text-red-500 border border-red-400/30 dark:bg-rose-300/10 dark:text-rose-200 dark:border-rose-300/26' },
  autodraft: { label: 'Autodraft', cls: 'bg-sky-400/15 text-sky-500 border border-sky-400/30 dark:bg-sky-300/10 dark:text-sky-200 dark:border-sky-300/28' },
  skipped:   { label: 'Skipped',   cls: 'bg-muted text-muted-foreground border border-border' },
};

function amountSearchText(...values) {
  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 rowThreshold(row) {
  return row.actual_amount != null ? row.actual_amount : row.expected_amount;
}

function rowEffectiveStatus(row) {
  if (row.is_skipped) return 'skipped';
  const threshold = rowThreshold(row);
  const isPaidByThreshold = row.total_paid > 0 && row.total_paid >= threshold;
  return (isPaidByThreshold && row.status !== 'paid' && row.status !== 'autodraft') ? 'paid' : row.status;
}

function rowIsPaid(row) {
  const status = rowEffectiveStatus(row);
  if (row.autopay_suggestion && status === 'autodraft') return false;
  return status === 'paid' || status === 'autodraft';
}

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

function FilterChip({ active, children, onClick }) {
  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>
  );
}

// ── Summary cards ──────────────────────────────────────────────────────────
const CARD_DEFS = {
  starting:  {
    label: 'Starting',
    icon: TrendingUp,
    bar: 'from-slate-400 to-slate-300',
    glow: '',
    valueClass: 'text-foreground',
    activateWhen: () => true,
  },
  paid:      {
    label: 'Total Paid',
    icon: CheckCircle2,
    bar: 'from-emerald-500 to-emerald-300',
    glow: 'shadow-[0_4px_20px_rgba(16,185,129,0.15)]',
    borderActive: 'border-emerald-400/40',
    valueClass: 'text-emerald-600 dark:text-emerald-200',
    activateWhen: (v) => v > 0,
  },
  remaining: {
    label: 'Remaining',
    icon: Clock,
    bar: 'from-blue-400 to-indigo-300',
    glow: '',
    valueClass: 'text-foreground',
    activateWhen: () => true,
  },
  overdue:   {
    label: 'Overdue',
    icon: AlertCircle,
    bar: 'from-rose-400 to-orange-300',
    glow: 'shadow-[0_4px_20px_rgba(251,113,133,0.10)]',
    borderActive: 'border-rose-400/35',
    valueClass: 'text-red-500 dark:text-rose-200',
    activateWhen: (v) => v > 0,
  },
};

function TrendIndicator({ trend }) {
  if (!trend) return null;
  
  const { direction, percent_change } = trend;
  
  let icon, color, text;
  switch (direction) {
    case 'up':
      icon = '↑';
      color = 'text-emerald-500';
      text = `${icon} ${percent_change}%`;
      break;
    case 'down':
      icon = '↓';
      color = 'text-red-500';
      text = `${icon} ${Math.abs(percent_change)}%`;
      break;
    default:
      icon = '→';
      color = 'text-muted-foreground';
      text = `${icon} ${percent_change}%`;
  }
  
  return (
    <div className="flex items-center gap-1.5">
      <span className={`text-lg font-bold ${color}`}>
        {text}
      </span>
      <span className="text-[10px] text-muted-foreground whitespace-nowrap">
        vs 3-mo avg
      </span>
    </div>
  );
}

function SummaryCard({ type, value, onEdit, hint, label }) {
  const def      = CARD_DEFS[type];
  const isActive = def.activateWhen(value || 0);
  const Icon     = def.icon;
  const displayLabel = label || def.label;

  return (
    <div className={cn(
      'flex-1 min-w-0 relative overflow-hidden rounded-xl border border-border/80',
      'bg-card/95 px-5 py-4 shadow-sm shadow-black/15 transition-all duration-300',
      isActive && def.glow,
      isActive && def.borderActive,
    )}>
      <div className={cn(
        'absolute top-0 left-0 right-0 h-[3px] bg-gradient-to-r',
        def.bar,
        !isActive && (type === 'paid' || type === 'overdue') && 'opacity-30',
      )} />
      <div className="flex items-center gap-2 mb-3">
        <Icon className={cn('h-4 w-4', isActive ? def.valueClass : 'text-muted-foreground')} />
        <p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
          {displayLabel}
        </p>
        {type === 'starting' && onEdit && (
          <button
            onClick={onEdit}
            className="ml-auto h-4 w-4 text-muted-foreground hover:text-foreground transition-colors"
            title="Edit monthly starting amounts"
            aria-label="Edit monthly starting amounts"
          >
            <Settings2 className="h-4 w-4" />
          </button>
        )}
      </div>
      <p className={cn(
        'text-[1.75rem] font-bold tracking-tight font-mono leading-none',
        isActive ? def.valueClass : 'text-foreground',
      )}>
        {fmt(value)}
      </p>
      {hint && <p className="mt-2 text-[11px] text-muted-foreground">{hint}</p>}
    </div>
  );
}

function TrendCard({ trend }) {
  if (!trend) return null;
  
  return (
    <div className="flex-1 min-w-0 relative overflow-hidden rounded-xl border border-border/80 bg-card/95 px-5 py-4 shadow-sm shadow-black/15 transition-all duration-300">
      <div className="absolute top-0 left-0 right-0 h-[3px] bg-gradient-to-r from-purple-500 to-indigo-400" />
      <div className="flex items-center gap-2 mb-3">
        <TrendingUp className="h-4 w-4 text-foreground" />
        <p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
          3-Month Trend
        </p>
      </div>
      <div className="flex items-center justify-center h-10">
        <TrendIndicator trend={trend} />
      </div>
    </div>
  );
}

// ── Status badge ───────────────────────────────────────────────────────────
const StatusBadge = React.memo(function StatusBadge({ status, clickable, onClick, loading }) {
  const meta = useMemo(() => STATUS_META[status] || STATUS_META.upcoming, [status]);
  
  const isSkipped = status === 'skipped';
  const canClick = clickable && !isSkipped && !loading;
  
  return (
    <button
      type="button"
      disabled={!canClick || loading}
      onClick={onClick}
      className={cn(
        'inline-flex items-center px-2.5 py-0.5 text-[11px] rounded-md font-semibold',
        'uppercase tracking-wide whitespace-nowrap',
        'transition-all duration-150',
        canClick && 'cursor-pointer hover:scale-105 hover:shadow-sm',
        canClick && status === 'paid' && 'hover:bg-red-500/20 hover:text-red-600 hover:border-red-500/40',
        canClick && status !== 'paid' && 'hover:bg-emerald-500/20 hover:text-emerald-600 hover:border-emerald-500/40',
        loading && 'opacity-60 cursor-wait',
        meta.cls,
      )}
      title={canClick ? (status === 'paid' || status === 'autodraft' ? 'Click to mark unpaid' : 'Click to mark paid') : undefined}
    >
      {loading ? (
        <>
          <Loader2 className="h-3 w-3 mr-1 animate-spin" />
          {meta.label}
        </>
      ) : (
        meta.label
      )}
    </button>
  );
});

function AutopaySuggestionActions({ row, loading, onConfirm, onDismiss, compact = false }) {
  const suggestion = row.autopay_suggestion;
  if (!suggestion) return null;

  const title = `${fmt(suggestion.amount)} due ${fmtDate(suggestion.paid_date)}`;

  return (
    <div className={cn(
      'flex items-center gap-1.5',
      compact ? 'w-full flex-wrap justify-end sm:w-auto' : 'justify-end',
    )}>
      <span
        className={cn(
          'inline-flex min-w-0 items-center gap-1.5 rounded-md border border-sky-500/20 bg-sky-500/10',
          'px-2 py-1 text-xs font-medium text-sky-600 dark:text-sky-300',
          compact ? 'mr-auto' : 'max-w-32',
        )}
        title={`Autopay suggested: ${title}`}
      >
        <Clock className="h-3 w-3 shrink-0" />
        <span className="truncate">{compact ? `Suggested ${fmt(suggestion.amount)}` : 'Suggested'}</span>
      </span>
      <Button
        size="icon"
        variant="ghost"
        disabled={loading}
        onClick={onDismiss}
        className="h-8 w-8 text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
        title={`Dismiss suggested autopay payment for ${title}`}
        aria-label={`Dismiss suggested autopay payment for ${row.name}`}
      >
        <X className="h-3.5 w-3.5" />
      </Button>
      <Button
        size="icon"
        variant="default"
        disabled={loading}
        onClick={onConfirm}
        className="h-8 w-8"
        title={`Confirm suggested autopay payment for ${title}`}
        aria-label={`Confirm suggested autopay payment for ${row.name}`}
      >
        {loading ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <CheckCircle2 className="h-3.5 w-3.5" />}
      </Button>
    </div>
  );
}

// ── Inline-editable payment cell ───────────────────────────────────────────
// `threshold` = actual_amount ?? expected_amount for this bill/month
function EditableCell({ row, field, threshold, defaultPaymentDate, refresh }) {
  const [editing, setEditing] = useState(false);
  const [value, setValue]     = useState('');
  const inputRef              = useRef(null);

  const displayVal = field === 'amount'
    ? (row.total_paid > 0 ? fmt(row.total_paid) : '—')
    : (row.last_paid_date ? fmtDate(row.last_paid_date) : '—');

  const isEmpty  = field === 'amount' ? row.total_paid <= 0 : !row.last_paid_date;
  // Mismatch when paid amount differs from the effective threshold for this month
  const mismatch = field === 'amount' && row.total_paid > 0 && row.total_paid !== threshold;

  function startEdit() {
    if (editing) return;
    setValue(field === 'amount'
      ? (row.total_paid > 0 ? String(row.total_paid) : '')
      : (row.last_paid_date || ''));
    setEditing(true);
    setTimeout(() => { inputRef.current?.focus(); inputRef.current?.select(); }, 0);
  }

  async function commit() {
    setEditing(false);
    const val = value.trim();
    if (!val) return;
    try {
      if (row.payments && row.payments.length > 0) {
        const update = {};
        if (field === 'amount') update.amount    = parseFloat(val);
        if (field === 'date')   update.paid_date = val;
        await api.updatePayment(row.payments[0].id, update);
      } else {
        await api.createPayment({
          bill_id:   row.id,
          amount:    field === 'amount' ? parseFloat(val) : threshold,
          paid_date: field === 'date'   ? val             : defaultPaymentDate,
        });
      }
      toast.success('Saved');
      refresh();
    } catch (err) {
      toast.error(err.message);
    }
  }

  function onKeyDown(e) {
    if (e.key === 'Enter')  inputRef.current?.blur();
    if (e.key === 'Escape') { setValue(''); setEditing(false); }
  }

  if (editing) {
    return (
      <Input
        ref={inputRef}
        type={field === 'date' ? 'date' : 'number'}
        step={field === 'amount' ? '0.01' : undefined}
        min={field === 'amount' ? '0' : undefined}
        value={value}
        onChange={e => setValue(e.target.value)}
        onBlur={commit}
        onKeyDown={onKeyDown}
        className="h-7 w-28 text-right font-mono text-sm bg-background/80 border-border/60"
      />
    );
  }

  return (
    <span
      onClick={startEdit}
      title={`Click to edit ${field === 'amount' ? 'payment amount' : 'paid date'}`}
      className={cn(
        'cursor-pointer rounded-md px-1.5 py-0.5 text-sm font-mono',
        'transition-all duration-150 hover:bg-accent hover:ring-1 hover:ring-border',
        isEmpty   && 'text-muted-foreground',
        mismatch  && 'text-amber-500',
        !isEmpty  && !mismatch && 'text-emerald-500',
      )}
    >
      {displayVal}
    </span>
  );
}

function paymentSummary(row, threshold) {
  const target = Number(threshold) || 0;
  const paid = Number(row.total_paid) || 0;
  const paidTowardDue = Number.isFinite(Number(row.paid_toward_due))
    ? Number(row.paid_toward_due)
    : Math.min(paid, target);
  const overpaid = Number.isFinite(Number(row.overpaid_amount))
    ? Number(row.overpaid_amount)
    : Math.max(paid - target, 0);
  const remaining = Math.max(target - paidTowardDue, 0);
  const percent = target > 0 ? Math.min(100, Math.round((paidTowardDue / target) * 100)) : 0;
  return {
    target,
    paid,
    paidTowardDue,
    overpaid,
    remaining,
    percent,
    count: Array.isArray(row.payments) ? row.payments.length : 0,
    partial: paid > 0 && remaining > 0,
  };
}

function PaymentProgress({ row, threshold, onOpen, onMarkFullAmount, compact = false }) {
  const summary = paymentSummary(row, threshold);
  const barTone = summary.remaining === 0
    ? 'bg-emerald-500'
    : summary.paid > 0
      ? 'bg-amber-500'
      : 'bg-muted-foreground/40';

  const amountLabel = (() => {
    if (summary.paid === 0) return '—';
    if (summary.overpaid > 0) return `${fmt(summary.paidTowardDue)} · overpaid`;
    if (summary.remaining > 0) return `${fmt(summary.paidTowardDue)} paid`;
    return fmt(summary.paidTowardDue);
  })();

  const showQuickFix = onMarkFullAmount && summary.partial && summary.paid > 0;

  return (
    <div>
      <button
        type="button"
        onClick={onOpen}
        className={cn(
          'w-full rounded-md text-left transition-colors hover:bg-accent/60 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50',
          compact ? 'p-2' : 'px-2 py-1.5',
        )}
        title="View payment history"
      >
        <div className="flex items-center justify-between gap-2 text-xs">
          <span className={cn('font-mono', summary.paid > 0 ? 'text-emerald-500' : 'text-muted-foreground')}>
            {amountLabel}
          </span>
          {summary.count > 1 && (
            <span className="shrink-0 rounded-full bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
              {summary.count}×
            </span>
          )}
        </div>
        <div className="mt-1.5 h-1.5 overflow-hidden rounded-full bg-muted">
          <div
            className={cn('h-full rounded-full transition-all', barTone)}
            style={{ width: `${summary.percent}%` }}
          />
        </div>
      </button>
      {showQuickFix && (
        <button
          type="button"
          onClick={onMarkFullAmount}
          className="mt-0.5 w-full rounded px-2 py-0.5 text-left text-[10px] text-muted-foreground/60 transition-colors hover:bg-accent/50 hover:text-foreground"
          title={`Set ${fmt(summary.paidTowardDue)} as the full amount due this month`}
        >
          ✓ {fmt(summary.paidTowardDue)} is the full amount
        </button>
      )}
    </div>
  );
}

function LowerThisMonthButton({ row, year, month, refresh, compact = false }) {
  const threshold = rowThreshold(row);
  const summary = paymentSummary(row, threshold);
  const [saving, setSaving] = useState(false);

  if (row.is_skipped || !summary.partial) return null;

  async function handleClick() {
    setSaving(true);
    try {
      await api.saveBillMonthlyState(row.id, {
        year,
        month,
        actual_amount: summary.paid,
        notes: row.monthly_notes || null,
        is_skipped: row.is_skipped,
      });
      toast.success(`${MONTHS[month - 1]} amount set to ${fmt(summary.paid)}`);
      refresh?.();
    } catch (err) {
      toast.error(err.message || 'Failed to update monthly amount');
    } finally {
      setSaving(false);
    }
  }

  return (
    <Button
      type="button"
      size="sm"
      variant="ghost"
      disabled={saving}
      onClick={handleClick}
      className={cn(
        'h-7 px-2.5 text-xs font-semibold text-amber-600 hover:bg-amber-500/10 hover:text-amber-700',
        'dark:text-amber-400 dark:hover:text-amber-300',
        compact && 'h-8',
      )}
      title={`Set ${MONTHS[month - 1]} amount to ${fmt(summary.paid)} because this bill was lower this month`}
    >
      {saving ? 'Saving...' : 'Bill was lower'}
    </Button>
  );
}

function PaymentLedgerDialog({ row, year, month, threshold, defaultPaymentDate, onClose, onSaved }) {
  const summary = paymentSummary(row, threshold);
  const [amount, setAmount] = useState(String(summary.remaining || summary.target || ''));
  const [date, setDate] = useState(defaultPaymentDate);
  const [method, setMethod] = useState(METHOD_NONE);
  const [notes, setNotes] = useState('');
  const [busy, setBusy] = useState(false);
  const [editPayment, setEditPayment] = useState(null);
  const payments = [...(row.payments || [])].sort((a, b) => String(b.paid_date).localeCompare(String(a.paid_date)));

  async function handleAdd(e) {
    e.preventDefault();
    const parsedAmount = parseFloat(amount);
    if (!Number.isFinite(parsedAmount) || parsedAmount <= 0) {
      toast.error('Enter a positive payment amount');
      return;
    }
    if (!date) {
      toast.error('Choose a payment date');
      return;
    }

    setBusy(true);
    try {
      await api.createPayment({
        bill_id: row.id,
        amount: parsedAmount,
        paid_date: date,
        method: method === METHOD_NONE ? null : method,
        notes: notes || null,
      });
      toast.success('Partial payment added');
      onSaved?.();
      onClose?.();
    } catch (err) {
      toast.error(err.message || 'Failed to add payment');
    } finally {
      setBusy(false);
    }
  }

  return (
    <>
      <Dialog open onOpenChange={value => { if (!value) onClose(); }}>
        <DialogContent className="max-h-[92svh] overflow-y-auto border-border/60 bg-card/95 backdrop-blur-xl sm:max-w-2xl">
          <DialogHeader>
            <DialogTitle className="text-base font-semibold tracking-tight">{row.name} Payments</DialogTitle>
          </DialogHeader>

          <div className="space-y-4">
            <div className="rounded-lg border border-border/60 bg-muted/25 p-3">
              <PaymentProgress row={row} threshold={threshold} onOpen={() => {}} />
              <div className="mt-2 flex justify-end">
                <LowerThisMonthButton
                  row={row}
                  year={year}
                  month={month}
                  refresh={onSaved}
                />
              </div>
            </div>

            <div className="grid gap-3 md:grid-cols-[1fr_1fr]">
              <div className="rounded-lg border border-border/60 bg-background/45 p-3">
                <p className="mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">Payment History</p>
                {payments.length > 0 ? (
                  <div className="space-y-2">
                    {payments.map(payment => (
                      <div key={payment.id} className="flex items-center justify-between gap-3 rounded-md border border-border/50 bg-card/60 px-3 py-2">
                        <div className="min-w-0">
                          <p className="font-mono text-sm font-semibold text-foreground">{fmt(payment.amount)}</p>
                          <p className="truncate text-xs text-muted-foreground">
                            {fmtDate(payment.paid_date)}
                            {payment.method ? ` · ${payment.method}` : ''}
                          </p>
                          {payment.notes && (
                            <p className="mt-0.5 truncate text-xs text-muted-foreground/80">{payment.notes}</p>
                          )}
                        </div>
                        <Button type="button" size="sm" variant="ghost" className="h-8 px-2.5 text-xs" onClick={() => setEditPayment(payment)}>
                          Edit
                        </Button>
                      </div>
                    ))}
                  </div>
                ) : (
                  <p className="rounded-md border border-dashed border-border/70 px-3 py-8 text-center text-sm text-muted-foreground">
                    No payments recorded for this month.
                  </p>
                )}
              </div>

              <form onSubmit={handleAdd} className="rounded-lg border border-border/60 bg-background/45 p-3">
                <p className="mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">Add Partial Payment</p>
                <div className="space-y-3">
                  <div className="space-y-1.5">
                    <Label htmlFor={`partial-amount-${row.id}`} className="text-xs uppercase tracking-wider text-muted-foreground">Amount</Label>
                    <Input
                      id={`partial-amount-${row.id}`}
                      type="number"
                      min="0"
                      step="0.01"
                      value={amount}
                      onChange={e => setAmount(e.target.value)}
                      className="font-mono bg-background/70 border-border/60"
                    />
                  </div>
                  <div className="space-y-1.5">
                    <Label htmlFor={`partial-date-${row.id}`} className="text-xs uppercase tracking-wider text-muted-foreground">Paid Date</Label>
                    <Input
                      id={`partial-date-${row.id}`}
                      type="date"
                      value={date}
                      onChange={e => setDate(e.target.value)}
                      className="font-mono bg-background/70 border-border/60"
                    />
                  </div>
                  <div className="space-y-1.5">
                    <Label className="text-xs uppercase tracking-wider text-muted-foreground">Method</Label>
                    <Select value={method} onValueChange={setMethod}>
                      <SelectTrigger className="bg-background/70 border-border/60">
                        <SelectValue placeholder="—" />
                      </SelectTrigger>
                      <SelectContent>
                        <SelectItem value={METHOD_NONE}>—</SelectItem>
                        <SelectItem value="bank">Bank Transfer</SelectItem>
                        <SelectItem value="card">Card</SelectItem>
                        <SelectItem value="autopay">Autopay</SelectItem>
                        <SelectItem value="check">Check</SelectItem>
                        <SelectItem value="cash">Cash</SelectItem>
                      </SelectContent>
                    </Select>
                  </div>
                  <div className="space-y-1.5">
                    <Label htmlFor={`partial-notes-${row.id}`} className="text-xs uppercase tracking-wider text-muted-foreground">Notes</Label>
                    <Input
                      id={`partial-notes-${row.id}`}
                      value={notes}
                      onChange={e => setNotes(e.target.value)}
                      className="bg-background/70 border-border/60"
                    />
                  </div>
                  <Button type="submit" disabled={busy} className="w-full gap-2">
                    <Plus className="h-4 w-4" />
                    {busy ? 'Adding...' : 'Add Payment'}
                  </Button>
                </div>
              </form>
            </div>
          </div>
        </DialogContent>
      </Dialog>

      {editPayment && (
        <PaymentModal
          payment={editPayment}
          onClose={() => setEditPayment(null)}
          onSave={() => {
            onSaved?.();
            setEditPayment(null);
          }}
        />
      )}
    </>
  );
}

// ── Notes cell (monthly state notes) ─────────────────────────────────────
// Shows the monthly state notes for this bill in the current month.
// Notes are per-month, not per-bill - each month has its own notes field.
function NotesCell({ row, refresh }) {
  // Monthly notes - the per-month notes stored in monthly_bill_state
  const savedNote = row.monthly_notes || '';
  const [value, setValue] = useState(savedNote);
  const [saving, setSaving] = useState(false);

  async function handleBlur() {
    const trimmed = value.trim();
    if (trimmed === savedNote) return;
    
    // Need year and month to save to monthly_bill_state
    // These should be passed via row props from the parent
    const year = row.year;
    const month = row.month;
    
    if (!year || !month) {
      toast.error('Cannot save notes without year/month context');
      setValue(savedNote);
      return;
    }
    
    setSaving(true);
    try {
      await api.saveBillMonthlyState(row.id, {
        year,
        month,
        notes: trimmed || null,
        is_skipped: row.is_skipped,
        actual_amount: row.actual_amount,
      });
      refresh();
    } catch (err) {
      toast.error(err.message);
      setValue(savedNote);
    } finally { setSaving(false); }
  }

  return (
    <input
      type="text"
      value={value}
      onChange={e => setValue(e.target.value)}
      onBlur={handleBlur}
      onKeyDown={e => { if (e.key === 'Enter') e.currentTarget.blur(); }}
      placeholder='Add monthly notes…'
      disabled={saving}
      className={cn(
        'w-full bg-transparent text-sm placeholder:text-muted-foreground/40',
        'border-0 outline-none ring-0',
        'text-muted-foreground focus:text-foreground',
        'transition-colors duration-150',
        'disabled:cursor-not-allowed disabled:opacity-40',
        value && 'text-foreground/80',
      )}
    />
  );
}

// ── Monthly state dialog ───────────────────────────────────────────────────
// Edits actual_amount, monthly notes, and is_skipped for a specific bill+month.
// Changes are isolated to the selected month — other months are not affected.
function MonthlyStateDialog({ row, year, month, open, onOpenChange, onSaved }) {
  const [actualAmount, setActualAmount] = useState('');
  const [notes,        setNotes]        = useState('');
  const [isSkipped,    setIsSkipped]    = useState(false);
  const [saving,       setSaving]       = useState(false);

  // Populate from current row state when dialog opens
  useEffect(() => {
    if (open) {
      setActualAmount(row.actual_amount != null ? String(row.actual_amount) : '');
      setNotes(row.monthly_notes || '');
      setIsSkipped(!!row.is_skipped);
    }
  }, [open, row]);

  async function handleSave(e) {
    e.preventDefault();
    const amt = actualAmount.trim() ? parseFloat(actualAmount) : null;
    if (amt !== null && (isNaN(amt) || amt < 0)) {
      toast.error('Amount must be a positive number or empty');
      return;
    }
    setSaving(true);
    try {
      await api.saveBillMonthlyState(row.id, {
        year,
        month,
        actual_amount: amt,
        notes: notes.trim() || null,
        is_skipped: isSkipped,
      });
      toast.success(`${MONTHS[month - 1]} state saved`);
      onSaved();
      onOpenChange(false);
    } catch (err) {
      toast.error(err.message);
    } finally {
      setSaving(false);
    }
  }

  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent className="sm:max-w-sm border-border/60 bg-card/95 backdrop-blur-xl">
        <DialogHeader>
          <DialogTitle className="text-base font-semibold tracking-tight">
            {row.name}
            <span className="text-muted-foreground font-normal ml-2">
              {MONTHS[month - 1]} {year}
            </span>
          </DialogTitle>
          <p className="text-[11px] text-muted-foreground">
            Monthly overrides — changes only affect {MONTHS[month - 1]}
          </p>
        </DialogHeader>

        <form id="mbs-form" onSubmit={handleSave} className="space-y-4 py-1">
          {/* Actual amount this month */}
          <div className="space-y-1.5">
            <Label htmlFor="mbs-amount" className="text-xs uppercase tracking-wider text-muted-foreground">
              Actual Amount ($)
            </Label>
            <Input
              id="mbs-amount"
              type="number" min="0" step="0.01"
              placeholder={String(row.expected_amount)}
              value={actualAmount}
              onChange={e => setActualAmount(e.target.value)}
              className="font-mono bg-background/50 border-border/60"
            />
            <p className="text-[11px] text-muted-foreground">
              Leave blank to use the template default ({fmt(row.expected_amount)}).
            </p>
          </div>

          {/* Monthly notes */}
          <div className="space-y-1.5">
            <Label htmlFor="mbs-notes" className="text-xs uppercase tracking-wider text-muted-foreground">
              Notes (this month only)
            </Label>
            <Input
              id="mbs-notes"
              value={notes}
              onChange={e => setNotes(e.target.value)}
              placeholder="e.g. higher than usual, double-billed…"
              className="bg-background/50 border-border/60"
            />
          </div>

          {/* Skip this month */}
          <label className="flex items-start gap-3 cursor-pointer select-none">
            <input
              type="checkbox"
              checked={isSkipped}
              onChange={e => setIsSkipped(e.target.checked)}
              className="mt-0.5 h-4 w-4 rounded border-border accent-primary"
            />
            <div>
              <p className="text-sm font-medium leading-tight">Skip this month</p>
              <p className="text-[11px] text-muted-foreground mt-0.5">
                Excludes this bill from {MONTHS[month - 1]} totals. Other months are unchanged.
              </p>
            </div>
          </label>
        </form>

        <DialogFooter className="mt-2">
          <Button type="button" variant="ghost" disabled={saving} onClick={() => onOpenChange(false)} className="text-xs">
            Cancel
          </Button>
          <Button type="submit" form="mbs-form" disabled={saving} className="text-xs">
            {saving ? 'Saving…' : 'Save'}
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

function StartingAmountsEditDialog({ open, onClose, year, month, onSave }) {
  const [loading, setLoading] = useState(false);
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState('');
  const [firstAmount, setFirstAmount] = useState('0');
  const [fifteenthAmount, setFifteenthAmount] = useState('0');
  const [otherAmount, setOtherAmount] = useState('0');
  const [preview, setPreview] = useState(null);

  const monthName = `${MONTHS[month - 1]} ${year}`;
  const localFirst = Number(firstAmount) || 0;
  const localFifteenth = Number(fifteenthAmount) || 0;
  const localOther = Number(otherAmount) || 0;
  const totalStarting = localFirst + localFifteenth + localOther;
  const paidSoFar = Number(preview?.paid_total || 0);
  const firstRemaining = localFirst - Number(preview?.paid_from_first || 0);
  const fifteenthRemaining = localFifteenth - Number(preview?.paid_from_fifteenth || 0);
  const totalRemaining = totalStarting - paidSoFar;

  useEffect(() => {
    let alive = true;
    async function loadStartingAmounts() {
      if (!open) return;
      setLoading(true);
      setError('');
      try {
        const result = await api.getMonthlyStartingAmounts(year, month);
        if (!alive) return;
        setPreview(result);
        setFirstAmount(String(result.first_amount ?? 0));
        setFifteenthAmount(String(result.fifteenth_amount ?? 0));
        setOtherAmount(String(result.other_amount ?? 0));
      } catch (err) {
        if (!alive) return;
        setError(err.message || 'Monthly starting amounts could not be loaded.');
      } finally {
        if (alive) setLoading(false);
      }
    }
    loadStartingAmounts();
    return () => { alive = false; };
  }, [open, year, month]);

  async function handleSave(e) {
    e.preventDefault();
    const first = Number(firstAmount);
    const fifteenth = Number(fifteenthAmount);
    const other = Number(otherAmount);
    if (![first, fifteenth, other].every(value => Number.isFinite(value) && value >= 0)) {
      setError('Starting amounts must be non-negative numbers.');
      return;
    }

    setSaving(true);
    setError('');
    try {
      await api.updateMonthlyStartingAmounts({
        year,
        month,
        first_amount: first,
        fifteenth_amount: fifteenth,
        other_amount: other,
      });
      toast.success('Monthly starting amounts saved.');
      onSave();
    } catch (err) {
      setError(err.message || 'Monthly starting amounts could not be saved.');
    } finally {
      setSaving(false);
    }
  }

  return (
    <Dialog open={open} onOpenChange={value => { if (!value) onClose(); }}>
      <DialogContent className="max-h-[92vh] overflow-y-auto border-border/60 bg-card/95 backdrop-blur-xl sm:max-w-lg">
        <DialogHeader>
          <DialogTitle className="text-lg font-semibold tracking-tight">Monthly Starting Amounts</DialogTitle>
          <p className="text-sm text-muted-foreground">{monthName}</p>
        </DialogHeader>

        <form id="starting-amounts-form" onSubmit={handleSave} className="space-y-5">
          {error && (
            <div className="rounded-lg border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive">
              {error}
            </div>
          )}

          <div className="grid gap-3 sm:grid-cols-3">
            <label className="space-y-1.5">
              <Label htmlFor="starting-first" className="text-xs uppercase tracking-wider text-muted-foreground">
                1st
              </Label>
              <Input
                id="starting-first"
                type="number"
                min="0"
                step="0.01"
                value={firstAmount}
                disabled={loading || saving}
                onChange={e => setFirstAmount(e.target.value)}
                className="font-mono bg-background/50 border-border/60"
              />
            </label>
            <label className="space-y-1.5">
              <Label htmlFor="starting-fifteenth" className="text-xs uppercase tracking-wider text-muted-foreground">
                15th
              </Label>
              <Input
                id="starting-fifteenth"
                type="number"
                min="0"
                step="0.01"
                value={fifteenthAmount}
                disabled={loading || saving}
                onChange={e => setFifteenthAmount(e.target.value)}
                className="font-mono bg-background/50 border-border/60"
              />
            </label>
            <label className="space-y-1.5">
              <Label htmlFor="starting-other" className="text-xs uppercase tracking-wider text-muted-foreground">
                Other
              </Label>
              <Input
                id="starting-other"
                type="number"
                min="0"
                step="0.01"
                value={otherAmount}
                disabled={loading || saving}
                onChange={e => setOtherAmount(e.target.value)}
                className="font-mono bg-background/50 border-border/60"
              />
            </label>
          </div>

          <div className="rounded-xl border border-border/60 bg-muted/35 p-4">
            <div className="grid gap-3 sm:grid-cols-3">
              <div>
                <p className="text-xs font-medium text-muted-foreground">Total starting</p>
                <p className="mt-1 font-mono text-lg font-bold">{fmt(totalStarting)}</p>
              </div>
              <div>
                <p className="text-xs font-medium text-muted-foreground">Paid so far</p>
                <p className="mt-1 font-mono text-lg font-bold text-emerald-500">{fmt(paidSoFar)}</p>
              </div>
              <div>
                <p className="text-xs font-medium text-muted-foreground">Total remaining</p>
                <p className="mt-1 font-mono text-lg font-bold">{fmt(totalRemaining)}</p>
              </div>
            </div>
            <div className="mt-4 grid gap-2 border-t border-border/60 pt-3 text-sm sm:grid-cols-3">
              <div className="flex justify-between gap-3 sm:block">
                <span className="text-muted-foreground">1st remaining</span>
                <span className="font-mono font-semibold sm:block">{fmt(firstRemaining)}</span>
              </div>
              <div className="flex justify-between gap-3 sm:block">
                <span className="text-muted-foreground">15th remaining</span>
                <span className="font-mono font-semibold sm:block">{fmt(fifteenthRemaining)}</span>
              </div>
              <div className="flex justify-between gap-3 sm:block">
                <span className="text-muted-foreground">Other</span>
                <span className="font-mono font-semibold sm:block">{fmt(localOther)}</span>
              </div>
            </div>
          </div>
        </form>

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

// ── Payment modal ──────────────────────────────────────────────────────────
function PaymentModal({ payment, onClose, onSave }) {
  const [amount, setAmount] = useState(String(payment.amount));
  const [date, setDate]     = useState(payment.paid_date);
  // Use METHOD_NONE sentinel — empty string value crashes Radix Select
  const [method, setMethod] = useState(payment.method || METHOD_NONE);
  const [notes, setNotes]   = useState(payment.notes  || '');
  const [busy, setBusy]     = useState(false);
  const [confirmDelete, setConfirmDelete] = useState(false);

  async function handleSave(e) {
    e.preventDefault();
    setBusy(true);
    try {
      await api.updatePayment(payment.id, {
        amount:    parseFloat(amount),
        paid_date: date,
        method:    method === METHOD_NONE ? null : method,
        notes:     notes  || null,
      });
      toast.success('Payment saved');
      onSave(); onClose();
    } catch (err) {
      toast.error(err.message);
    } finally { setBusy(false); }
  }

  async function handleDelete() {
    setBusy(true);
    try {
      await api.deletePayment(payment.id);
      toast.success('Payment moved to recovery. Bill is now marked as unpaid.', {
        action: {
          label: 'Undo',
          onClick: async () => {
            try {
              await api.restorePayment(payment.id);
              toast.success('Payment restored');
              onSave();
            } catch (err) {
              toast.error(err.message || 'Failed to restore payment');
            }
          },
        },
      });
      onSave(); onClose();
    } catch (err) {
      toast.error(err.message);
    } finally { setBusy(false); }
  }

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

        <form id="payment-modal-form" onSubmit={handleSave} className="space-y-4">
          <div className="space-y-1.5">
            <Label htmlFor="pm-amount" className="text-xs uppercase tracking-wider text-muted-foreground">Amount ($) *</Label>
            <Input id="pm-amount" type="number" min="0" step="0.01" required
              value={amount} onChange={e => setAmount(e.target.value)}
              className="font-mono bg-background/50 border-border/60" />
          </div>
          <div className="space-y-1.5">
            <Label htmlFor="pm-date" className="text-xs uppercase tracking-wider text-muted-foreground">Paid Date *</Label>
            <Input id="pm-date" type="date" required
              value={date} onChange={e => setDate(e.target.value)}
              className="font-mono bg-background/50 border-border/60" />
          </div>
          <div className="space-y-1.5">
            <Label htmlFor="pm-method" className="text-xs uppercase tracking-wider text-muted-foreground">Method</Label>
            <Select value={method} onValueChange={setMethod}>
              <SelectTrigger id="pm-method" className="bg-background/50 border-border/60">
                <SelectValue placeholder="—" />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value={METHOD_NONE}>—</SelectItem>
                <SelectItem value="bank">Bank Transfer</SelectItem>
                <SelectItem value="card">Card</SelectItem>
                <SelectItem value="autopay">Autopay</SelectItem>
                <SelectItem value="check">Check</SelectItem>
                <SelectItem value="cash">Cash</SelectItem>
              </SelectContent>
            </Select>
          </div>
          <div className="space-y-1.5">
            <Label htmlFor="pm-notes" className="text-xs uppercase tracking-wider text-muted-foreground">Notes</Label>
            <Input id="pm-notes" value={notes} onChange={e => setNotes(e.target.value)}
              className="bg-background/50 border-border/60" />
          </div>
        </form>

        <DialogFooter className="flex-row gap-2 sm:justify-between mt-2">
          <Button
            type="button" variant="destructive" disabled={busy} onClick={() => setConfirmDelete(true)}
            className="text-xs"
            title="Removes this payment record. The bill itself is NOT deleted."
          >
            Remove Payment
          </Button>
          <div className="flex gap-2">
            <Button type="button" variant="ghost" disabled={busy} onClick={onClose} className="text-xs">Cancel</Button>
            <Button type="submit" form="payment-modal-form" disabled={busy} className="text-xs">Save</Button>
          </div>
        </DialogFooter>
        </DialogContent>
      </Dialog>

      <AlertDialog open={confirmDelete} onOpenChange={setConfirmDelete}>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>Remove this payment?</AlertDialogTitle>
            <AlertDialogDescription>
              This marks the payment as removed and reverses any debt balance update. You can undo it from the toast.
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel disabled={busy}>Cancel</AlertDialogCancel>
            <AlertDialogAction
              className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
              disabled={busy}
              onClick={handleDelete}
            >
              {busy ? 'Removing...' : 'Remove Payment'}
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </>
  );
}

// ── Table row ──────────────────────────────────────────────────────────────
function Row({ row, year, month, refresh, index, onEditBill }) {
  const amountRef                     = useRef(null);
  const [editPayment, setEditPayment] = useState(null);
  const [paymentLedgerOpen, setPaymentLedgerOpen] = useState(false);
  const [showMbs,     setShowMbs]     = useState(false);
  const [confirmUnpay, setConfirmUnpay] = useState(false);
  const [loading,     setLoading]     = useState(false);
  const [suggestionLoading, setSuggestionLoading] = useState(false);
  const [optimisticActual, setOptimisticActual] = useState(undefined);
  const [showUpdateNudge, setShowUpdateNudge] = useState(false);
  const [nudgeAmount, setNudgeAmount] = useState(null);
  const [, startTransition] = useTransition();

  const [editingExpected, setEditingExpected] = useState(false);
  const [expectedDraft, setExpectedDraft] = useState('');
  const [editingDue, setEditingDue] = useState(false);
  const [dueDraft, setDueDraft] = useState('');

  // Effective amount threshold: optimistic override → monthly override → template default.
  const effectiveActual = optimisticActual !== undefined ? optimisticActual : row.actual_amount;
  const threshold = effectiveActual != null ? effectiveActual : row.expected_amount;
  const defaultPaymentDate = paymentDateForTrackerMonth(year, month, row.due_day);

  const isSkipped = !!row.is_skipped;
  const hasAutopaySuggestion = !!row.autopay_suggestion && !isSkipped;

  // Paid when total payments >= effective threshold
  const isPaidByThreshold = row.total_paid > 0 && row.total_paid >= threshold;
  const isPaid = row.status === 'paid' || (row.status === 'autodraft' && !hasAutopaySuggestion) || isPaidByThreshold;
  const summary = paymentSummary(row, threshold);

  // Effective status to show:
  // skipped > paid (threshold-based) > backend status
  const effectiveStatus = isSkipped
    ? 'skipped'
    : (isPaidByThreshold && row.status !== 'paid' && row.status !== 'autodraft')
      ? 'paid'
      : row.status;

  const rowBg = isSkipped ? '' : (ROW_STATUS_CLS[effectiveStatus] || '');

  async function handleQuickPay() {
    const val = parseFloat(amountRef.current?.value);
    if (!val || val <= 0) { toast.error('Enter a payment amount'); return; }
    try {
      await api.quickPay({ bill_id: row.id, amount: val, paid_date: defaultPaymentDate });
      toast.success('Payment added');
      refresh();
    } catch (err) {
      toast.error(err.message);
    }
  }

  async function performTogglePaid() {
    setLoading?.(true);
    try {
      const result = await api.togglePaid(row.id, {
        amount: isPaid ? undefined : threshold,
        year: year,
        month: month,
      });
      if (isPaid && result.paymentId) {
        toast.success('Payment moved to recovery', {
          action: {
            label: 'Undo',
            onClick: async () => {
              try {
                await api.restorePayment(result.paymentId);
                toast.success('Payment restored');
                refresh?.();
              } catch (err) {
                toast.error(err.message || 'Failed to restore payment');
              }
            },
          },
        });
      } else {
        toast.success('Payment recorded');
      }
      refresh?.();
    } catch (err) {
      toast.error(err.message || 'Failed to toggle payment status');
    } finally {
      setLoading?.(false);
    }
  }

  function handleTogglePaid() {
    if (isPaid) {
      setConfirmUnpay(true);
      return;
    }
    performTogglePaid();
  }

  async function handleMarkFullAmount() {
    const newActual = summary.paidTowardDue;
    setOptimisticActual(newActual);
    try {
      await api.saveBillMonthlyState(row.id, {
        year, month,
        actual_amount: newActual,
        notes: row.monthly_notes || null,
        is_skipped: row.is_skipped,
      });
      setNudgeAmount(newActual);
      setShowUpdateNudge(true);
      refresh?.();
    } catch (err) {
      setOptimisticActual(undefined);
      toast.error(err.message || 'Failed to update amount');
    }
  }

  function handleUpdateTemplate() {
    const amount = nudgeAmount;
    setShowUpdateNudge(false);
    startTransition(async () => {
      try {
        await api.updateBill(row.id, { name: row.name, due_day: row.due_day, expected_amount: amount });
        toast.success(`Default updated to ${fmt(amount)}`);
        refresh?.();
      } catch (err) {
        toast.error(err.message || 'Failed to update default');
      }
    });
  }

  async function handleApplySuggestion(amount) {
    setOptimisticActual(amount);
    try {
      await api.saveBillMonthlyState(row.id, {
        year, month,
        actual_amount: amount,
        notes: row.monthly_notes || null,
        is_skipped: row.is_skipped,
      });
      refresh?.();
    } catch (err) {
      setOptimisticActual(undefined);
      toast.error(err.message || 'Failed to apply suggestion');
    }
  }

  async function handleSaveExpected() {
    setEditingExpected(false);
    const val = parseFloat(expectedDraft);
    if (!isFinite(val) || val < 0) return;
    const current = effectiveActual ?? row.expected_amount;
    if (val === current) return;

    if (effectiveActual != null) {
      setOptimisticActual(val);
      try {
        await api.saveBillMonthlyState(row.id, {
          year, month,
          actual_amount: val,
          notes: row.monthly_notes || null,
          is_skipped: row.is_skipped,
        });
        refresh?.();
      } catch (err) {
        setOptimisticActual(undefined);
        toast.error(err.message || 'Failed to update amount');
      }
    } else {
      try {
        await api.updateBill(row.id, { name: row.name, due_day: row.due_day, expected_amount: val });
        refresh?.();
      } catch (err) {
        toast.error(err.message || 'Failed to update expected amount');
      }
    }
  }

  async function handleSaveDue() {
    setEditingDue(false);
    const day = parseInt(dueDraft, 10);
    if (!isFinite(day) || day < 1 || day > 31) return;
    if (day === row.due_day) return;
    try {
      await api.updateBill(row.id, { name: row.name, due_day: day, expected_amount: row.expected_amount });
      refresh?.();
    } catch (err) {
      toast.error(err.message || 'Failed to update due date');
    }
  }

  async function handleConfirmSuggestion() {
    setSuggestionLoading(true);
    try {
      const result = await api.confirmAutopaySuggestion(row.id, { year, month });
      toast.success(result.created ? 'Autopay payment confirmed' : 'Autopay already recorded');
      refresh?.();
    } catch (err) {
      toast.error(err.message || 'Failed to confirm autopay suggestion');
    } finally {
      setSuggestionLoading(false);
    }
  }

  async function handleDismissSuggestion() {
    setSuggestionLoading(true);
    try {
      await api.dismissAutopaySuggestion(row.id, { year, month });
      toast.success('Autopay suggestion dismissed');
      refresh?.();
    } catch (err) {
      toast.error(err.message || 'Failed to dismiss autopay suggestion');
    } finally {
      setSuggestionLoading(false);
    }
  }

  return (
    <>
      <TableRow
        className={cn(
          'group border-border/65 transition-colors duration-150',
          isSkipped ? 'opacity-40' : 'hover:bg-accent/50',
          rowBg,
        )}
        style={{ animationDelay: `${index * 40}ms` }}
      >
        {/* Bill name + category + monthly notes (if set) */}
        <TableCell className="w-[18%] py-3">
          <div className="flex items-center gap-2.5">
            {row.autopay_enabled && (
              <span className="h-1.5 w-1.5 shrink-0 rounded-full bg-sky-500" title="Autopay" />
            )}
            <div>
              <div className="flex items-center gap-1">
                {row.website ? (
                  <a
                    href={row.website}
                    target="_blank"
                    rel="noreferrer"
                    className={cn(
                      'font-medium text-sm leading-tight transition-colors',
                      'hover:underline decoration-muted-foreground/50 underline-offset-2',
                      isSkipped && 'line-through',
                    )}
                  >
                    {row.name}
                  </a>
                ) : (
                  <span className={cn('font-medium text-sm leading-tight', isSkipped && 'line-through')}>
                    {row.name}
                  </span>
                )}
                <Button
                  size="icon" variant="ghost"
                  className="h-7 w-7 opacity-100 transition-opacity text-muted-foreground hover:text-foreground hover:bg-accent lg:opacity-0 lg:group-hover:opacity-100"
                  title="Edit bill"
                  onClick={() => onEditBill?.(row)}
                >
                  <Pencil className="h-3 w-3" />
                </Button>
              </div>
              {row.category_name && (
                <p className="text-[11px] text-muted-foreground mt-0.5">{row.category_name}</p>
              )}
              {/* Monthly notes shown inline under the bill name */}
              {row.monthly_notes && (
                <p className="text-[11px] text-amber-500/80 mt-0.5 italic truncate max-w-[140px]"
                  title={row.monthly_notes}>
                  {row.monthly_notes}
                </p>
              )}
            </div>
          </div>
        </TableCell>

        {/* Due */}
        <TableCell className="w-[10%] py-3 text-sm font-mono text-muted-foreground">
          {editingDue ? (
            <input
              type="number"
              min="1" max="31"
              value={dueDraft}
              autoFocus
              onChange={e => setDueDraft(e.target.value)}
              onBlur={handleSaveDue}
              onKeyDown={e => {
                if (e.key === 'Enter') e.currentTarget.blur();
                if (e.key === 'Escape') { setEditingDue(false); }
              }}
              className="w-12 rounded border border-border bg-transparent px-1 py-0.5 text-sm font-mono text-foreground outline-none focus:ring-[2px] focus:ring-ring/50"
              title="Day of month (1–31)"
            />
          ) : (
            <button
              type="button"
              onClick={() => { setDueDraft(String(row.due_day)); setEditingDue(true); }}
              className="rounded px-1 py-0.5 transition-colors hover:bg-accent hover:text-foreground"
              title="Click to edit due day"
            >
              {fmtDate(row.due_date)}
            </button>
          )}
        </TableCell>

        {/* Expected / Actual — shows actual_amount in amber when it overrides the template */}
        <TableCell className="w-[10%] py-3 text-right font-mono text-sm">
          {editingExpected ? (
            <input
              type="number"
              min="0" step="0.01"
              value={expectedDraft}
              autoFocus
              onChange={e => setExpectedDraft(e.target.value)}
              onBlur={handleSaveExpected}
              onKeyDown={e => {
                if (e.key === 'Enter') e.currentTarget.blur();
                if (e.key === 'Escape') { setEditingExpected(false); }
              }}
              className="w-24 rounded border border-border bg-transparent px-1 py-0.5 text-right text-sm font-mono text-foreground outline-none focus:ring-[2px] focus:ring-ring/50"
            />
          ) : effectiveActual != null ? (
            <button
              type="button"
              onClick={() => { setExpectedDraft(String(effectiveActual)); setEditingExpected(true); }}
              className="rounded px-1 py-0.5 text-amber-500 transition-colors hover:bg-accent"
              title={`Monthly override — click to edit. Template default: ${fmt(row.expected_amount)}`}
            >
              {fmt(effectiveActual)}
            </button>
          ) : (
            <div className="flex flex-col items-end gap-0.5">
              <button
                type="button"
                onClick={() => { setExpectedDraft(String(row.expected_amount)); setEditingExpected(true); }}
                className="rounded px-1 py-0.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
                title="Click to edit expected amount"
              >
                {fmt(row.expected_amount)}
              </button>
              {row.amount_suggestion?.suggestion != null &&
               Math.abs(row.amount_suggestion.suggestion - row.expected_amount) / row.expected_amount > 0.05 && (
                <button
                  type="button"
                  onClick={() => handleApplySuggestion(row.amount_suggestion.suggestion)}
                  className="text-[10px] text-muted-foreground/50 transition-colors hover:text-muted-foreground"
                  title={`Based on last ${row.amount_suggestion.months_used} months`}
                >
                  ~{fmt(row.amount_suggestion.suggestion)}
                </button>
              )}
            </div>
          )}
        </TableCell>

        {/* Previous month paid */}
        <TableCell className="w-[10%] py-3 text-right font-mono text-sm text-muted-foreground/70">
          {row.previous_month_paid > 0 ? fmt(row.previous_month_paid) : '—'}
        </TableCell>

        {/* Amount paid — mismatch now compares against threshold */}
        <TableCell className="w-[10%] py-3 text-right">
          <PaymentProgress
            row={row}
            threshold={threshold}
            onOpen={() => setPaymentLedgerOpen(true)}
            onMarkFullAmount={!isSkipped ? handleMarkFullAmount : undefined}
          />
        </TableCell>

        {/* Paid date */}
        <TableCell className="w-[10%] py-3 text-sm text-muted-foreground">
          <button
            type="button"
            onClick={() => setPaymentLedgerOpen(true)}
            className="rounded-md px-1.5 py-0.5 font-mono transition-colors hover:bg-accent hover:text-foreground"
            title="View payment history"
          >
            {row.last_paid_date ? fmtDate(row.last_paid_date) : '—'}
            {summary.count > 1 && <span className="ml-1 text-[10px] text-muted-foreground">({summary.count})</span>}
          </button>
        </TableCell>

        {/* Status — uses effectiveStatus (accounts for skipped + threshold) */}
        <TableCell className="w-[9%] py-3">
          <StatusBadge 
            status={effectiveStatus} 
            clickable
            onClick={() => {
              if (effectiveStatus === 'skipped') return;
              handleTogglePaid();
            }}
            loading={loading}
          />
        </TableCell>

        {/* Actions */}
        <TableCell className="w-[10%] py-3 text-right">
          <div className="flex items-center justify-end gap-1">
            {showUpdateNudge ? (
              <div className="flex items-center gap-1 animate-in fade-in slide-in-from-right-1 duration-200">
                <span className="text-[10px] text-muted-foreground">Update default?</span>
                <Button
                  size="sm" variant="ghost"
                  onClick={handleUpdateTemplate}
                  className="h-6 px-2 text-[10px] font-semibold text-emerald-600 hover:bg-emerald-500/10 hover:text-emerald-700 dark:text-emerald-400 dark:hover:text-emerald-300"
                >
                  {fmt(nudgeAmount)}
                </Button>
                <button
                  type="button"
                  onClick={() => setShowUpdateNudge(false)}
                  className="text-muted-foreground/40 transition-colors hover:text-muted-foreground"
                  title="Dismiss"
                >
                  <X className="h-3 w-3" />
                </button>
              </div>
            ) : (
              <>
                {hasAutopaySuggestion && (
                  <AutopaySuggestionActions
                    row={row}
                    loading={suggestionLoading}
                    onConfirm={handleConfirmSuggestion}
                    onDismiss={handleDismissSuggestion}
                  />
                )}
                {/* Quick pay — hidden for skipped/paid bills */}
                {!isPaid && !isSkipped && !hasAutopaySuggestion && (
                  <div className="flex items-center gap-1">
                    <Input
                      ref={amountRef}
                      type="number" min="0" step="0.01"
                      defaultValue={summary.remaining || threshold}
                      className="h-7 w-20 text-right font-mono text-sm bg-background/50 border-border/50"
                      title="Payment amount"
                    />
                    <Button
                      size="sm" variant="ghost"
                      onClick={handleQuickPay}
                      className="h-7 px-2.5 text-xs font-semibold text-emerald-600 hover:text-emerald-700 hover:bg-emerald-50 dark:text-emerald-400 dark:hover:text-emerald-300 dark:hover:bg-emerald-500/10"
                    >
                      Add
                    </Button>
                  </div>
                )}
              </>
            )}
          </div>
        </TableCell>

        {/* Notes cell (monthly state notes) */}
        <TableCell className="w-[23%] py-3 border-l border-border pl-4">
          <NotesCell row={{ ...row, year, month }} refresh={refresh} />
        </TableCell>
      </TableRow>

      {editPayment && (
        <PaymentModal
          payment={editPayment}
          onClose={() => setEditPayment(null)}
          onSave={refresh}
        />
      )}

      {paymentLedgerOpen && (
        <PaymentLedgerDialog
          row={row}
          year={year}
          month={month}
          threshold={threshold}
          defaultPaymentDate={defaultPaymentDate}
          onClose={() => setPaymentLedgerOpen(false)}
          onSaved={refresh}
        />
      )}

      {showMbs && (
        <MonthlyStateDialog
          row={row}
          year={year}
          month={month}
          open={showMbs}
          onOpenChange={setShowMbs}
          onSaved={refresh}
        />
      )}

      <AlertDialog open={confirmUnpay} onOpenChange={setConfirmUnpay}>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>Mark this bill unpaid?</AlertDialogTitle>
            <AlertDialogDescription>
              This removes the current payment record for this month and moves it into recovery.
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel disabled={loading}>Cancel</AlertDialogCancel>
            <AlertDialogAction
              className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
              disabled={loading}
              onClick={performTogglePaid}
            >
              {loading ? 'Removing...' : 'Remove Payment'}
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </>
  );
}

function MobileTrackerRow({ row, year, month, refresh, index, onEditBill }) {
  const amountRef                     = useRef(null);
  const [editPayment, setEditPayment] = useState(null);
  const [paymentLedgerOpen, setPaymentLedgerOpen] = useState(false);
  const [showMbs,     setShowMbs]     = useState(false);
  const [confirmUnpay, setConfirmUnpay] = useState(false);
  const [suggestionLoading, setSuggestionLoading] = useState(false);

  const threshold = row.actual_amount != null ? row.actual_amount : row.expected_amount;
  const defaultPaymentDate = paymentDateForTrackerMonth(year, month, row.due_day);
  const isSkipped = !!row.is_skipped;
  const hasAutopaySuggestion = !!row.autopay_suggestion && !isSkipped;
  const isPaidByThreshold = row.total_paid > 0 && row.total_paid >= threshold;
  const isPaid = row.status === 'paid' || (row.status === 'autodraft' && !hasAutopaySuggestion) || isPaidByThreshold;
  const effectiveStatus = isSkipped
    ? 'skipped'
    : (isPaidByThreshold && row.status !== 'paid' && row.status !== 'autodraft')
      ? 'paid'
      : row.status;
  const rowBg = isSkipped ? '' : (ROW_STATUS_CLS[effectiveStatus] || '');
  const remaining = Math.max((threshold || 0) - (row.total_paid || 0), 0);
  const summary = paymentSummary(row, threshold);

  async function handleQuickPay() {
    const val = parseFloat(amountRef.current?.value);
    if (!val || val <= 0) { toast.error('Enter a payment amount'); return; }
    try {
      await api.quickPay({ bill_id: row.id, amount: val, paid_date: defaultPaymentDate });
      toast.success('Payment added');
      refresh();
    } catch (err) {
      toast.error(err.message);
    }
  }

  async function performTogglePaid() {
    try {
      const result = await api.togglePaid(row.id, {
        amount: isPaid ? undefined : threshold,
        year: year,
        month: month,
      });
      if (isPaid && result.paymentId) {
        toast.success('Payment moved to recovery', {
          action: {
            label: 'Undo',
            onClick: async () => {
              try {
                await api.restorePayment(result.paymentId);
                toast.success('Payment restored');
                refresh();
              } catch (err) {
                toast.error(err.message || 'Failed to restore payment');
              }
            },
          },
        });
      } else {
        toast.success('Payment recorded');
      }
      refresh();
    } catch (err) {
      toast.error(err.message || 'Failed to toggle payment status');
    }
  }

  function handleTogglePaid() {
    if (isPaid) {
      setConfirmUnpay(true);
      return;
    }
    performTogglePaid();
  }

  async function handleConfirmSuggestion() {
    setSuggestionLoading(true);
    try {
      const result = await api.confirmAutopaySuggestion(row.id, { year, month });
      toast.success(result.created ? 'Autopay payment confirmed' : 'Autopay already recorded');
      refresh();
    } catch (err) {
      toast.error(err.message || 'Failed to confirm autopay suggestion');
    } finally {
      setSuggestionLoading(false);
    }
  }

  async function handleDismissSuggestion() {
    setSuggestionLoading(true);
    try {
      await api.dismissAutopaySuggestion(row.id, { year, month });
      toast.success('Autopay suggestion dismissed');
      refresh();
    } catch (err) {
      toast.error(err.message || 'Failed to dismiss autopay suggestion');
    } finally {
      setSuggestionLoading(false);
    }
  }

  return (
    <>
      <div
        className={cn(
          'rounded-lg border border-border/60 bg-background/60 p-3 shadow-sm',
          'space-y-3 transition-colors',
          isSkipped ? 'opacity-55' : rowBg,
        )}
        style={{ animationDelay: `${index * 40}ms` }}
      >
        <div className="flex min-w-0 items-start justify-between gap-3">
          <div className="min-w-0">
            <div className="flex min-w-0 items-center gap-2">
              {row.autopay_enabled && (
                <span
                  className="inline-flex shrink-0 rounded bg-sky-500/15 px-1.5 py-0.5 text-[10px] font-semibold text-sky-500"
                  title="Autopay"
                >
                  AP
                </span>
              )}
              {row.website ? (
                <a
                  href={row.website}
                  target="_blank"
                  rel="noreferrer"
                  className={cn(
                    'min-w-0 truncate text-sm font-semibold leading-tight text-foreground',
                    'underline-offset-2 transition-colors hover:text-primary hover:underline',
                    isSkipped && 'line-through',
                  )}
                >
                  {row.name}
                </a>
              ) : (
                <span className={cn('min-w-0 truncate text-sm font-semibold leading-tight text-foreground', isSkipped && 'line-through')}>
                  {row.name}
                </span>
              )}
              <Button
                size="icon" variant="ghost"
                className="h-7 w-7 opacity-100 transition-opacity text-muted-foreground hover:text-foreground hover:bg-accent lg:opacity-0 lg:group-hover:opacity-100"
                title="Edit bill"
                onClick={() => onEditBill?.(row)}
              >
                <Pencil className="h-3 w-3" />
              </Button>
            </div>
            {row.monthly_notes && (
              <p className="mt-1 line-clamp-2 text-xs italic text-amber-500/80" title={row.monthly_notes}>
                {row.monthly_notes}
              </p>
            )}
          </div>
          <StatusBadge status={effectiveStatus} clickable={!isSkipped} onClick={handleTogglePaid} />
        </div>

        <div className="grid grid-cols-2 gap-x-3 gap-y-2 text-xs text-muted-foreground sm:grid-cols-4">
          <div>
            <p className="uppercase tracking-wide text-muted-foreground/60">Due</p>
            <p className="mt-0.5 font-mono text-sm text-foreground">{fmtDate(row.due_date)}</p>
          </div>
          <div>
            <p className="uppercase tracking-wide text-muted-foreground/60">Category</p>
            <p className="mt-0.5 truncate text-sm text-foreground">{row.category_name || 'Uncategorized'}</p>
          </div>
          <div>
            <p className="uppercase tracking-wide text-muted-foreground/60">Expected</p>
            <p className={cn('mt-0.5 font-mono text-sm', row.actual_amount != null ? 'text-amber-500' : 'text-foreground')}>
              {fmt(threshold)}
            </p>
          </div>
          <div>
            <p className="uppercase tracking-wide text-muted-foreground/60">Last Month</p>
            <p className="mt-0.5 font-mono text-sm text-muted-foreground/70">
              {fmt(row.previous_month_paid)}
            </p>
          </div>
          <div>
            <p className="uppercase tracking-wide text-muted-foreground/60">Remaining</p>
            <p className={cn('mt-0.5 font-mono text-sm', remaining > 0 ? 'text-foreground' : 'text-emerald-500')}>
              {fmt(remaining)}
            </p>
          </div>
        </div>

        <div className="rounded-md border border-border/50 bg-muted/20">
          <PaymentProgress row={row} threshold={threshold} onOpen={() => setPaymentLedgerOpen(true)} compact />
        </div>

        <div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
          <div className="grid grid-cols-2 gap-2 text-xs sm:flex sm:items-center">
            <div className="rounded-md bg-muted/45 px-2 py-1.5">
              <span className="text-muted-foreground">Paid </span>
              <span className="font-mono text-emerald-500">{row.total_paid > 0 ? fmt(row.total_paid) : '—'}</span>
            </div>
            <div className="rounded-md bg-muted/45 px-2 py-1.5">
              <span className="text-muted-foreground">Date </span>
              <button
                type="button"
                onClick={() => setPaymentLedgerOpen(true)}
                className="rounded font-mono text-foreground underline-offset-2 hover:underline"
              >
                {row.last_paid_date ? fmtDate(row.last_paid_date) : '—'}
                {summary.count > 1 && <span className="ml-1 text-[10px] text-muted-foreground">({summary.count})</span>}
              </button>
            </div>
          </div>

          <div className="flex flex-wrap items-center justify-end gap-1.5">
            {hasAutopaySuggestion && (
              <AutopaySuggestionActions
                row={row}
                loading={suggestionLoading}
                onConfirm={handleConfirmSuggestion}
                onDismiss={handleDismissSuggestion}
                compact
              />
            )}
            {!isPaid && !isSkipped && !hasAutopaySuggestion && (
              <div className="flex items-center gap-1.5">
                <Input
                  ref={amountRef}
                  type="number" min="0" step="0.01"
                  defaultValue={summary.remaining || threshold}
                  className="h-8 w-24 text-right font-mono text-sm bg-background/70 border-border/60"
                  title="Payment amount"
                  aria-label={`${row.name} payment amount`}
                />
                <Button
                  size="sm" variant="default"
                  onClick={handleQuickPay}
                  className="h-8 px-3 text-xs font-semibold"
                >
                  Add
                </Button>
              </div>
            )}
            <LowerThisMonthButton
              row={row}
              year={year}
              month={month}
              refresh={refresh}
              compact
            />

          </div>
        </div>

        <div className="rounded-md border border-border/50 bg-muted/25 px-2 py-1.5">
          <NotesCell row={{ ...row, year, month }} refresh={refresh} />
        </div>
      </div>

      {editPayment && (
        <PaymentModal
          payment={editPayment}
          onClose={() => setEditPayment(null)}
          onSave={refresh}
        />
      )}

      {paymentLedgerOpen && (
        <PaymentLedgerDialog
          row={row}
          year={year}
          month={month}
          threshold={threshold}
          defaultPaymentDate={defaultPaymentDate}
          onClose={() => setPaymentLedgerOpen(false)}
          onSaved={refresh}
        />
      )}

      {showMbs && (
        <MonthlyStateDialog
          row={row}
          year={year}
          month={month}
          open={showMbs}
          onOpenChange={setShowMbs}
          onSaved={refresh}
        />
      )}

      <AlertDialog open={confirmUnpay} onOpenChange={setConfirmUnpay}>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>Mark this bill unpaid?</AlertDialogTitle>
            <AlertDialogDescription>
              This removes the current payment record for this month and moves it into recovery.
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>Cancel</AlertDialogCancel>
            <AlertDialogAction
              className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
              onClick={performTogglePaid}
            >
              Remove Payment
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </>
  );
}

// ── Bucket ─────────────────────────────────────────────────────────────────
function Bucket({ label, rows, year, month, refresh, onEditBill, loading }) {
  // Use actual_amount (if set) as the per-row threshold; exclude skipped rows from totals
  const activeRows    = rows.filter(r => !r.is_skipped);
  const totalThreshold = activeRows.reduce((s, r) => s + (r.actual_amount ?? r.expected_amount ?? 0), 0);
  const totalPaid      = activeRows.reduce((s, r) => s + (r.total_paid || 0), 0);
  const totalPaidTowardDue = activeRows.reduce((s, r) => {
    const threshold = Number(r.actual_amount ?? r.expected_amount ?? 0) || 0;
    const cappedPaid = Number(r.paid_toward_due);
    return s + (Number.isFinite(cappedPaid) ? cappedPaid : Math.min(Number(r.total_paid) || 0, threshold));
  }, 0);
  const totalOverpaid = Math.max(totalPaid - totalPaidTowardDue, 0);
  const skippedCount   = rows.length - activeRows.length;
  const pct            = totalThreshold > 0 ? Math.min((totalPaidTowardDue / totalThreshold) * 100, 100) : 0;
  const allPaid        = pct >= 100;

  return (
    <div className="rounded-xl border border-border/80 overflow-hidden bg-card/95 shadow-sm shadow-black/15">

      {/* Bucket header */}
      <div className="flex items-center justify-between px-5 py-3 bg-muted/35 border-b border-border/80">
        <div className="flex items-center gap-3">
          <span className="text-[11px] font-bold uppercase tracking-[0.12em] text-muted-foreground">
            {label}
          </span>
          {skippedCount > 0 && (
            <span className="text-[10px] text-muted-foreground/60">
              ({skippedCount} skipped)
            </span>
          )}
          <div className="flex items-center gap-2">
            <div className="h-1.5 w-24 rounded-full bg-border overflow-hidden">
              <div
                className={cn(
                  'h-full rounded-full transition-all duration-700',
                  allPaid ? 'bg-emerald-500' : 'bg-emerald-400/70',
                )}
                style={{ width: `${pct}%` }}
              />
            </div>
            <span className="text-[11px] font-mono text-muted-foreground/70">
              {Math.round(pct)}%
            </span>
          </div>
        </div>
        <span className="text-xs font-mono text-muted-foreground">
          <span className={cn(allPaid ? 'text-emerald-500' : 'text-foreground')}>
            {fmt(totalPaidTowardDue)}
          </span>
          <span className="text-muted-foreground/50 mx-1">/</span>
          {fmt(totalThreshold)}
          {totalOverpaid > 0 && (
            <span className="ml-1 text-emerald-500">+{fmt(totalOverpaid)}</span>
          )}
        </span>
      </div>

      <div className="grid gap-3 p-3 lg:hidden" aria-busy={loading ? 'true' : 'false'}>
        {loading ? (
          Array.from({ length: 3 }).map((_, i) => (
            <div key={i} className="rounded-lg border border-border/60 bg-background/60 p-3 animate-pulse">
              <div className="flex min-w-0 items-start justify-between gap-3">
                <div className="min-w-0">
                  <div className="flex min-w-0 items-center gap-2">
                    <div className="h-1.5 w-1.5 shrink-0 rounded-full bg-muted" />
                    <div className="h-4 w-32 rounded-md bg-muted" />
                  </div>
                </div>
                <div className="h-5 w-20 rounded-md bg-muted" />
              </div>
              <div className="grid grid-cols-2 gap-x-3 gap-y-2 text-xs text-muted-foreground mt-2">
                <div>
                  <p className="uppercase tracking-wide text-muted-foreground/60">Expected</p>
                  <div className="h-3 w-20 rounded-md bg-muted mt-0.5" />
                </div>
                <div>
                  <p className="uppercase tracking-wide text-muted-foreground/60">Remaining</p>
                  <div className="h-3 w-20 rounded-md bg-muted mt-0.5" />
                </div>
              </div>
            </div>
          ))
        ) : rows.length === 0 ? (
          <div className="rounded-lg border border-dashed border-border/70 bg-background/40 px-4 py-8 text-center text-sm text-muted-foreground">
            No bills match this bucket and filter set.
          </div>
        ) : (
          rows.map((r, i) => (
            <MobileTrackerRow
              key={r.id}
              row={r}
              year={year}
              month={month}
              refresh={refresh}
              index={i}
              onEditBill={onEditBill}
            />
          ))
        )}
      </div>

      <div className="hidden lg:block" aria-busy={loading ? 'true' : 'false'}>
        <div className="overflow-x-auto">
          <Table className="min-w-[1120px]">
          <TableHeader>
            <TableRow className="border-border/80 bg-background/30 hover:bg-background/30">
              <TableHead className="w-[18%] py-2.5 text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/90">Bill</TableHead>
              <TableHead className="w-[10%] py-2.5 text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/90">Due</TableHead>
              <TableHead className="w-[10%] py-2.5 text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/90 text-right">Expected</TableHead>
              <TableHead className="w-[10%] py-2.5 text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/80 text-right">Last Month</TableHead>
              <TableHead className="w-[10%] py-2.5 text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/90 text-right">Paid</TableHead>
              <TableHead className="w-[10%] py-2.5 text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/90">Paid Date</TableHead>
              <TableHead className="w-[9%] py-2.5 text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/90">Status</TableHead>
              <TableHead className="w-[10%] py-2.5" />
              <TableHead className="w-[23%] py-2.5 text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/90 border-l border-border/80 pl-4">
                Notes
              </TableHead>
            </TableRow>
          </TableHeader>
          <TableBody>
            {loading ? (
              Array.from({ length: 5 }).map((_, i) => (
                <TableRow key={i} className="border-border/50">
                  <TableCell className="w-[18%] py-3">
                    <div className="flex items-center gap-2.5">
                      <div className="h-1.5 w-1.5 rounded-full bg-muted" />
                      <div className="h-4 w-48 rounded-md bg-muted" />
                    </div>
                  </TableCell>
                  <TableCell className="w-[10%] py-3"><div className="h-3 w-20 rounded-md bg-muted" /></TableCell>
                  <TableCell className="w-[10%] py-3 text-right"><div className="h-3 w-20 ml-auto rounded-md bg-muted" /></TableCell>
                  <TableCell className="w-[10%] py-3 text-right"><div className="h-3 w-20 ml-auto rounded-md bg-muted" /></TableCell>
                  <TableCell className="w-[10%] py-3"><div className="h-7 w-24 ml-auto rounded-md bg-muted" /></TableCell>
                  <TableCell className="w-[10%] py-3"><div className="h-7 w-24 rounded-md bg-muted" /></TableCell>
                  <TableCell className="w-[9%] py-3"><div className="h-5 w-20 rounded-md bg-muted" /></TableCell>
                  <TableCell className="w-[10%] py-3 text-right">
                    <div className="flex items-center justify-end gap-1">
                      <div className="h-7 w-20 rounded-md bg-muted" />
                      <div className="h-7 w-7 rounded-md bg-muted" />
                    </div>
                  </TableCell>
                  <TableCell className="w-[23%] py-3 border-l border-border pl-4">
                    <div className="h-4 w-full rounded-md bg-muted" />
                  </TableCell>
                </TableRow>
              ))
            ) : rows.length === 0 ? (
              <TableRow className="border-border/50">
                <TableCell colSpan={9} className="py-10 text-center text-sm text-muted-foreground">
                  No bills match this bucket and filter set.
                </TableCell>
              </TableRow>
            ) : (
              rows.map((r, i) => (
                <Row
                  key={r.id}
                  row={r}
                  year={year}
                  month={month}
                  refresh={refresh}
                  index={i}
                  onEditBill={onEditBill}
                />
              ))
            )}
          </TableBody>
        </Table>
        </div>
      </div>
    </div>
  );
}

// ── Main page ──────────────────────────────────────────────────────────────
export default function TrackerPage() {
  const [searchParams] = useSearchParams();
  const now = new Date();
  const [year, setYear]   = useState(now.getFullYear());
  const [month, setMonth] = useState(now.getMonth() + 1);
  // Edit Bill modal: { bill, categories } when open, null when closed
  const [editBillData, setEditBillData] = useState(null);
  // Edit Starting Amounts modal: true when open, false when closed
  const [editStartingOpen, setEditStartingOpen] = useState(false);
  const [search, setSearch] = useState('');
  const [filters, setFilters] = useState({
    category: FILTER_ALL,
    cycle: FILTER_ALL,
    autopay: false,
    firstBucket: false,
    fifteenthBucket: false,
    unpaid: false,
    overdue: false,
    debt: false,
  });

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

  useEffect(() => {
    const querySearch = searchParams.get('search') || '';
    if (querySearch) setSearch(querySearch);
  }, [searchParams]);

  function navigate(delta) {
    setMonth(m => {
      const nm = m + delta;
      if (nm > 12) { setYear(y => y + 1); return 1; }
      if (nm < 1)  { setYear(y => y - 1); return 12; }
      return nm;
    });
  }

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

  function goToday() {
    const n = new Date();
    setYear(n.getFullYear());
    setMonth(n.getMonth() + 1);
  }


  const rows    = data?.rows    || [];
  const summary = data?.summary || {};
  const toggleFilter = (key) => setFilters(prev => ({ ...prev, [key]: !prev[key] }));
  const setFilterValue = (key, value) => setFilters(prev => ({ ...prev, [key]: value }));
  const hasFilters = !!(
    search.trim()
    || filters.category !== FILTER_ALL
    || filters.cycle !== FILTER_ALL
    || filters.autopay
    || filters.firstBucket
    || filters.fifteenthBucket
    || filters.unpaid
    || filters.overdue
    || filters.debt
  );
  const resetFilters = () => {
    setSearch('');
    setFilters({
      category: FILTER_ALL,
      cycle: FILTER_ALL,
      autopay: false,
      firstBucket: false,
      fifteenthBucket: false,
      unpaid: false,
      overdue: false,
      debt: false,
    });
  };
  const categoryOptions = useMemo(() => {
    const map = new Map();
    rows.forEach(row => {
      if (row.category_id && row.category_name) map.set(String(row.category_id), row.category_name);
    });
    return Array.from(map, ([id, name]) => ({ id, name })).sort((a, b) => a.name.localeCompare(b.name));
  }, [rows]);
  const cycleOptions = useMemo(() => (
    Array.from(new Set(rows.map(row => row.billing_cycle || 'monthly'))).sort()
  ), [rows]);
  const filteredRows = useMemo(() => {
    const q = search.trim().toLowerCase();
    return rows.filter(row => {
      const effectiveStatus = rowEffectiveStatus(row);
      if (filters.category !== FILTER_ALL && String(row.category_id ?? '') !== filters.category) return false;
      if (filters.cycle !== FILTER_ALL && String(row.billing_cycle || 'monthly') !== filters.cycle) return false;
      if (filters.autopay && !row.autopay_enabled) return false;
      if (filters.debt && !rowIsDebt(row)) return false;
      if (filters.unpaid && (row.is_skipped || rowIsPaid(row))) return false;
      if (filters.overdue && !(effectiveStatus === 'late' || effectiveStatus === 'missed')) return false;
      if (filters.firstBucket && !filters.fifteenthBucket && row.bucket !== '1st') return false;
      if (filters.fifteenthBucket && !filters.firstBucket && row.bucket !== '15th') return false;

      if (!q) return true;
      const haystack = [
        row.name,
        row.category_name,
        row.notes,
        row.monthly_notes,
        row.billing_cycle,
        row.bucket,
        row.status,
        amountSearchText(row.expected_amount, row.actual_amount, row.total_paid, row.balance, row.current_balance, row.minimum_payment),
      ].filter(Boolean).join(' ').toLowerCase();
      return haystack.includes(q);
    });
  }, [filters, rows, search]);
  const first   = filteredRows.filter(r => r.bucket === '1st');
  const second  = filteredRows.filter(r => r.bucket === '15th');

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

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

        <div className="flex items-center gap-1 bg-muted/50 border border-border rounded-lg p-1">
          <Button
            size="icon" variant="ghost"
            onClick={() => navigate(-1)}
            className="h-7 w-7 hover:bg-white/5"
          >
            <ChevronLeft className="h-4 w-4" />
          </Button>
          <Button
            size="sm" variant="ghost"
            onClick={goToday}
            className="h-7 px-3 text-xs font-medium hover:bg-white/5"
          >
            Today
          </Button>
          <Button
            size="icon" variant="ghost"
            onClick={() => navigate(1)}
            className="h-7 w-7 hover:bg-white/5"
          >
            <ChevronRight className="h-4 w-4" />
          </Button>
        </div>
      </div>

      <div className="rounded-xl border border-border/80 bg-card/95 p-4 shadow-sm shadow-black/15 space-y-3">
        <div className="grid gap-3 lg:grid-cols-[minmax(220px,1fr)_220px_180px_auto] 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 this month by bill, category, notes, or amount"
              className="h-10 pl-9"
            />
          </label>
          <Select value={filters.category} onValueChange={value => setFilterValue('category', value)}>
            <SelectTrigger className="h-10">
              <SelectValue placeholder="Category" />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value={FILTER_ALL}>All categories</SelectItem>
              {categoryOptions.map(category => (
                <SelectItem key={category.id} value={category.id}>{category.name}</SelectItem>
              ))}
            </SelectContent>
          </Select>
          <Select value={filters.cycle} onValueChange={value => setFilterValue('cycle', value)}>
            <SelectTrigger className="h-10 capitalize">
              <SelectValue placeholder="Billing cycle" />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value={FILTER_ALL}>All cycles</SelectItem>
              {cycleOptions.map(cycle => (
                <SelectItem key={cycle} value={cycle}>{cycle.replace(/_/g, ' ')}</SelectItem>
              ))}
            </SelectContent>
          </Select>
          <Button
            type="button"
            variant="ghost"
            disabled={!hasFilters}
            onClick={resetFilters}
            className="h-10 justify-center gap-2 text-xs"
          >
            <X className="h-3.5 w-3.5" />
            Clear
          </Button>
        </div>
        <div className="flex flex-wrap items-center gap-2">
          <FilterChip active={filters.unpaid} onClick={() => toggleFilter('unpaid')}>Unpaid</FilterChip>
          <FilterChip active={filters.overdue} onClick={() => toggleFilter('overdue')}>Overdue</FilterChip>
          <FilterChip active={filters.autopay} onClick={() => toggleFilter('autopay')}>Autopay</FilterChip>
          <FilterChip active={filters.firstBucket} onClick={() => toggleFilter('firstBucket')}>1st bucket</FilterChip>
          <FilterChip active={filters.fifteenthBucket} onClick={() => toggleFilter('fifteenthBucket')}>15th bucket</FilterChip>
          <FilterChip active={filters.debt} onClick={() => toggleFilter('debt')}>Debt</FilterChip>
          <span className="ml-auto text-xs text-muted-foreground tabular-nums">
            {filteredRows.length} of {rows.length} shown
          </span>
        </div>
      </div>

      {/* ── Summary cards (backend already excludes skipped from totals) ── */}
      {loading ? (
        <div className="grid grid-cols-2 gap-3 lg:flex" aria-busy="true">
          <Skeleton variant="card" className="h-32" />
          <Skeleton variant="card" className="h-32" />
          <Skeleton variant="card" className="h-32" />
          <Skeleton variant="card" className="h-32" />
          <Skeleton variant="card" className="h-32" />
          {summary.trend && <Skeleton variant="card" className="h-32" />}
        </div>
      ) : (
        <div className="grid grid-cols-2 gap-3 lg:flex">
          <SummaryCard
            type="starting"
            value={summary.total_starting}
            hint={!summary.has_starting_amounts ? 'Set monthly starting cash' : ''}
            onEdit={() => setEditStartingOpen(true)}
          />
          <SummaryCard type="paid"      value={summary.total_paid} />
          <SummaryCard
            type="remaining"
            value={summary.remaining}
            label={summary.remaining_label || 'Remaining'}
            hint={summary.remaining_hint}
          />
          <SummaryCard type="overdue"   value={summary.overdue} />
          <SummaryCard type="paid"      value={summary.previous_month_total} hint="Previous month"/>
          {summary.trend && <TrendCard trend={summary.trend} />}
        </div>
      )}

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

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

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

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

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

    </div>
  );
}
