import { useActionState, useEffect, useState } from 'react';
import { Copy, Loader2 } from 'lucide-react';
import { validateNonNegativeMoney } from '@/lib/money';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
  Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
} from '@/components/ui/dialog';
import {
  AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
  AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import {
  Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
} from '@/components/ui/select';
import { api } from '@/api';
import { createPaymentOrConfirm } from '@/lib/paymentActions';
import { cn, todayStr, errMessage } from '@/lib/utils';
import DebtDetailsSection from '@/components/bill-modal/DebtDetailsSection';
import AutopayTrustIndicator from '@/components/bill-modal/AutopayTrustIndicator';
import PaymentHistoryList from '@/components/bill-modal/PaymentHistoryList';
import PaymentFormFields from '@/components/bill-modal/PaymentFormFields';
import UnmatchDialogs, { type BulkUnmatchState } from '@/components/bill-modal/UnmatchDialogs';
import LinkedTransactionsSection from '@/components/bill-modal/LinkedTransactionsSection';
import TemplateSection from '@/components/bill-modal/TemplateSection';
import { transactionTitle, isSimilarPayee } from '@/components/bill-modal/transactionDisplay';
import {
  BILLING_SCHEDULE_OPTIONS,
  billingCycleForSchedule,
  defaultCycleDayForSchedule,
  scheduleValue,
} from '@/lib/billingSchedule';
import type { AutopayStats, BankTransaction, Bill, Category, Payment } from '@/types';

type FormErrors = Record<string, string>;

function getOrdinalSuffix(day: number): string {
  if (day > 3 && day < 21) return 'th';
  switch (day % 10) {
    case 1: return 'st';
    case 2: return 'nd';
    case 3: return 'rd';
    default: return 'th';
  }
}

// Radix Select crashes on empty string value
const CAT_NONE = 'none';
const DEBT_KEYWORDS = ['credit', 'loan', 'mortgage', 'housing', 'debt'];
const SNOWBALL_KEYWORDS = ['credit', 'loan', 'debt', 'mortgage', 'housing'];
const SUBSCRIPTION_TYPES: [string, string][] = [
  ['streaming', 'Streaming'],
  ['software', 'Software'],
  ['cloud', 'Cloud'],
  ['music', 'Music'],
  ['news', 'News'],
  ['fitness', 'Fitness'],
  ['gaming', 'Gaming'],
  ['utilities', 'Utilities'],
  ['insurance', 'Insurance'],
  ['other', 'Other'],
];

function isDebtCat(categories: Category[], catId: string | null | undefined): boolean {
  if (!catId || catId === CAT_NONE) return false;
  const cat = categories.find(c => String(c.id) === catId);
  return cat ? DEBT_KEYWORDS.some(kw => String(cat.name).toLowerCase().includes(kw)) : false;
}
function isSnowballCat(categories: Category[], catId: string | null | undefined): boolean {
  if (!catId || catId === CAT_NONE) return false;
  const cat = categories.find(c => String(c.id) === catId);
  return cat ? SNOWBALL_KEYWORDS.some(kw => String(cat.name).toLowerCase().includes(kw)) : false;
}

interface BillModalProps {
  bill?: Bill | null;
  initialBill?: Partial<Bill> | null;   // a pre-filled draft (from a transaction/subscription/template); lacks id + full Bill fields
  categories: Category[];
  onClose: () => void;
  onSave: (savedBill?: Bill) => void;
  onDuplicate?: (bill: Bill) => void;
}

export default function BillModal({ bill, initialBill, categories, onClose, onSave, onDuplicate }: BillModalProps) {
  const isNew = !bill;
  const sourceBill = bill || initialBill || null;

  const [name,           setName]         = useState(sourceBill?.name            || '');
  const [categoryId,     setCategoryId]   = useState(sourceBill?.category_id ? String(sourceBill.category_id) : CAT_NONE);
  const [dueDay,         setDueDay]       = useState(String(sourceBill?.due_day   || ''));
  const [expectedAmount, setExpected]     = useState(String(sourceBill?.expected_amount || ''));
  const [interestRate,   setInterestRate] = useState(sourceBill?.interest_rate == null ? '' : String(sourceBill.interest_rate));
  const initialCycleType = scheduleValue(sourceBill || {});
  const [cycleType,      setCycleType]    = useState(initialCycleType);
  const [cycleDay,       setCycleDay]     = useState(String(sourceBill?.cycle_day || defaultCycleDayForSchedule(initialCycleType)));
  const [autopay,        setAutopay]      = useState(!!sourceBill?.autopay_enabled);
  const [autodraftStatus, setAutodraftStatus] = useState(sourceBill?.autodraft_status || (sourceBill?.autopay_enabled ? 'assumed_paid' : 'none'));
  const [autoMarkPaid,   setAutoMarkPaid] = useState(!!sourceBill?.auto_mark_paid);
  const [isSubscription, setIsSubscription] = useState(!!sourceBill?.is_subscription);
  const [subscriptionType, setSubscriptionType] = useState(sourceBill?.subscription_type || 'other');
  const [reminderDaysBefore, setReminderDaysBefore] = useState(String(sourceBill?.reminder_days_before ?? 3));
  const [has2fa,         setHas2fa]       = useState(!!sourceBill?.has_2fa);
  const [website,        setWebsite]      = useState(sourceBill?.website          || '');
  const [username,       setUsername]     = useState(sourceBill?.username          || '');
  const [accountInfo,    setAccountInfo]  = useState(sourceBill?.account_info     || '');
  const [notes,          setNotes]        = useState(sourceBill?.notes            || '');
  const [currentBalance, setCurrentBalance] = useState(sourceBill?.current_balance == null ? '' : String(sourceBill.current_balance));
  const [minimumPayment, setMinimumPayment] = useState(sourceBill?.minimum_payment == null ? '' : String(sourceBill.minimum_payment));
  const [snowballInclude, setSnowballInclude] = useState(!!sourceBill?.snowball_include);
  const [snowballExempt, setSnowballExempt] = useState(!!sourceBill?.snowball_exempt);
  const [syncingPayments, setSyncingPayments] = useState(false);
  // Track whether rules exist locally so the Sync button appears immediately
  // after the first rule is added without waiting for sourceBill to refetch.
  const [localHasRules, setLocalHasRules] = useState(!!sourceBill?.has_merchant_rule);
  const [showDebtSection, setShowDebtSection] = useState(
    () => isDebtCat(categories, sourceBill?.category_id ? String(sourceBill.category_id) : CAT_NONE)
      || !!sourceBill?.snowball_include
      || !!sourceBill?.snowball_exempt
      || sourceBill?.current_balance != null
      || sourceBill?.minimum_payment != null
  );
  const [saveTemplate, setSaveTemplate] = useState(false);
  const [templateName, setTemplateName] = useState('');
  const [errors,         setErrors]       = useState<FormErrors>({});
  const [payments,       setPayments]     = useState<Payment[]>([]);
  const [paymentsLoading, setPaymentsLoading] = useState(false);
  const [linkedTransactions, setLinkedTransactions] = useState<BankTransaction[]>([]);
  const [linkedTransactionsLoading, setLinkedTransactionsLoading] = useState(false);
  const [transactionBusyId, setTransactionBusyId] = useState<number | null>(null);
  const [paymentBusy,    setPaymentBusy]  = useState(false);
  const [paymentFormOpen, setPaymentFormOpen] = useState(false);
  const [editingPayment, setEditingPayment] = useState<Payment | null>(null);
  const [deletePaymentTarget, setDeletePaymentTarget] = useState<Payment | null>(null);
  const [paymentAmount,  setPaymentAmount] = useState('');
  const [paymentDate,    setPaymentDate]   = useState(todayStr());
  const [paymentMethod,  setPaymentMethod] = useState('manual');
  const [paymentNotes,   setPaymentNotes]  = useState('');
  const [localVerifiedAt, setLocalVerifiedAt] = useState<Date | null>(
    bill?.autopay_verified_at ? new Date(bill.autopay_verified_at as string) : null
  );

  // Controls the outer Dialog's open state so it closes via its own animation
  // rather than being abruptly unmounted, which can leave Radix cleanup in a broken state.
  const [dialogOpen, setDialogOpen] = useState(true);

  // Deactivate dialog state
  const [deactivateOpen,   setDeactivateOpen]   = useState(false);
  const [deactivateReason, setDeactivateReason] = useState('');

  // Unmatch dialog state
  const [unmatchTarget,      setUnmatchTarget]      = useState<BankTransaction | null>(null);
  const [unmatchConfirmOpen, setUnmatchConfirmOpen] = useState(false);
  const [bulkUnmatch,        setBulkUnmatch]        = useState<BulkUnmatchState | null>(null);
  const [bulkBusy,           setBulkBusy]           = useState(false);

  const isSnowballCategory = isSnowballCat(categories, categoryId);
  const showOnSnowball = snowballInclude || (isSnowballCategory && !snowballExempt);
  const canAutoMarkPaid = autopay && autodraftStatus === 'assumed_paid';

  async function loadPayments() {
    if (isNew || !bill?.id) return;
    setPaymentsLoading(true);
    try {
      const data = await api.billPayments(bill.id, 1, 100) as { payments?: Payment[] };
      setPayments(data.payments || []);
    } catch (err) {
      toast.error(errMessage(err, 'Failed to load payment history.'));
    } finally {
      setPaymentsLoading(false);
    }
  }

  async function loadLinkedTransactions() {
    if (isNew || !bill?.id) return;
    setLinkedTransactionsLoading(true);
    try {
      const data = await api.billTransactions(bill.id) as { transactions?: BankTransaction[] };
      setLinkedTransactions(data.transactions || []);
    } catch (err) {
      toast.error(errMessage(err, 'Failed to load linked transactions.'));
    } finally {
      setLinkedTransactionsLoading(false);
    }
  }

  useEffect(() => {
    loadPayments();
    loadLinkedTransactions();
    // Intentional: reload only when the bill identity changes. The loaders are
    // recreated each render, so listing them would reload on every render.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [bill?.id]);

  // Imported payments (via sync or a merchant-rule historical import) must
  // refresh the payment list AND the Tracker behind the modal, not just the
  // linked transactions — matching the unmatch handlers.
  async function refreshAfterImport() {
    await Promise.all([loadPayments(), loadLinkedTransactions()]);
    onSave?.();
  }

  async function handleSyncBillPayments() {
    if (!sourceBill?.id) return;
    setSyncingPayments(true);
    const promise = api.syncBillSimplefinPayments(sourceBill.id);
    toast.promise(promise, {
      loading: 'Scanning bank history…',
      success: (r) => {
        const added = (r as { added?: number }).added ?? 0;
        return added > 0
          ? `${added} payment${added !== 1 ? 's' : ''} imported from bank history.`
          : 'No new matching transactions found.';
      },
      error: (err) => errMessage(err, 'Sync failed.'),
    });
    try {
      const result = await promise as { added?: number; late_attributions?: unknown[] };
      if ((result.added ?? 0) > 0) await refreshAfterImport();
      if (result.late_attributions?.length) {
        window.dispatchEvent(new CustomEvent('tracker:late-attributions', {
          detail: { attributions: result.late_attributions },
        }));
      }
    } catch {
      // toast.promise already surfaced the error
    } finally {
      setSyncingPayments(false);
    }
  }

  async function handleRulesChanged() {
    setLocalHasRules(true);
    await refreshAfterImport();
  }

  const validateName = (val: string): string => {
    if (!val || val.trim() === '') return 'Name is required';
    if (val.trim().length < 2) return 'Name must be at least 2 characters';
    return '';
  };

  const validateDueDay = (val: string): string => {
    if (!val || val.trim() === '') return 'Due day is required';
    const num = parseInt(val, 10);
    if (isNaN(num) || num < 1 || num > 31) return 'Due day must be between 1 and 31';
    return '';
  };

  // Money fields share one non-negative validator (blank allowed, 0 allowed).
  const validateExpectedAmount = (val: string): string => validateNonNegativeMoney(val, 'Amount');
  const validateCurrentBalance = (val: string): string => validateNonNegativeMoney(val, 'Balance');
  const validateMinimumPayment = (val: string): string => validateNonNegativeMoney(val, 'Min payment');

  const validateInterestRate = (val: string): string => {
    if (val === '' || val === null) return '';
    const num = parseFloat(val);
    if (isNaN(num)) return 'Invalid number';
    if (num < 0 || num > 100) return 'Interest rate must be between 0 and 100';
    return '';
  };

  const validateForm = (): boolean => {
    const newErrors: FormErrors = {
      name: validateName(name),
      dueDay: validateDueDay(dueDay),
      expectedAmount: validateExpectedAmount(expectedAmount),
      interestRate: validateInterestRate(interestRate),
      currentBalance: validateCurrentBalance(currentBalance),
      minimumPayment: validateMinimumPayment(minimumPayment),
    };
    setErrors(newErrors);
    return Object.values(newErrors).every(err => err === '');
  };

  // Value passed explicitly so this never falls through to the wrong field's
  // state (the old positional guessing defaulted every unmapped field to
  // interestRate).
  const handleBlur = (field: string, value: string, validator: (v: string) => string) => {
    setErrors(prev => ({ ...prev, [field]: validator(value) }));
  };

  const handleCategoryChange = (val: string) => {
    setCategoryId(val);
    if (isDebtCat(categories, val)) {
      setShowDebtSection(true);
    } else {
      setSnowballExempt(false);
    }
  };

  const handleSnowballVisibilityChange = (checked: boolean) => {
    if (checked) {
      setSnowballExempt(false);
      setSnowballInclude(!isSnowballCategory);
    } else {
      setSnowballInclude(false);
      setSnowballExempt(isSnowballCategory);
    }
  };

  async function handleVerifyAutopay() {
    if (!bill?.id) return;
    try {
      const res = await api.verifyAutopay(bill.id) as { autopay_verified_at?: string };
      setLocalVerifiedAt(new Date(res.autopay_verified_at ?? Date.now()));
      toast.success('Autopay marked as verified.');
    } catch (err) {
      toast.error(errMessage(err, 'Failed to verify autopay.'));
    }
  }

  const handleAutopayChange = (checked: boolean) => {
    setAutopay(checked);
    if (checked) {
      setAutodraftStatus(prev => (prev && prev !== 'none' ? prev : 'assumed_paid'));
    } else {
      setAutodraftStatus('none');
      setAutoMarkPaid(false);
    }
  };

  const handleCycleTypeChange = (value: string) => {
    setCycleType(value);
    setCycleDay(defaultCycleDayForSchedule(value));
  };

  function resetPaymentForm() {
    setPaymentAmount('');
    setPaymentDate(todayStr());
    setPaymentMethod('manual');
    setPaymentNotes('');
    setEditingPayment(null);
  }

  function startAddPayment() {
    resetPaymentForm();
    setPaymentFormOpen(true);
  }

  function startEditPayment(payment: Payment) {
    setEditingPayment(payment);
    setPaymentAmount(String(payment.amount ?? ''));
    setPaymentDate(payment.paid_date || todayStr());
    setPaymentMethod(payment.method || 'manual');
    setPaymentNotes(payment.notes || '');
    setPaymentFormOpen(true);
  }

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

    setPaymentBusy(true);
    try {
      const payload = {
        amount: parsedAmount,
        paid_date: paymentDate,
        method: paymentMethod,
        notes: paymentNotes || null,
        payment_source: 'manual',
      };
      const finishPaymentSave = async (successMessage: string) => {
        toast.success(successMessage);
        resetPaymentForm();
        setPaymentFormOpen(false);
        await loadPayments();
        onSave?.();
      };
      if (editingPayment) {
        await api.updatePayment(editingPayment.id, payload);
        await finishPaymentSave('Payment updated');
      } else {
        await createPaymentOrConfirm(
          { ...payload, bill_id: bill?.id },
          () => finishPaymentSave('Payment added'),
        );
      }
    } catch (err) {
      toast.error(errMessage(err, 'Payment could not be saved.'));
    } finally {
      setPaymentBusy(false);
    }
  }

  async function handleDeletePayment() {
    if (!deletePaymentTarget) return;
    const payment = deletePaymentTarget;
    setPaymentBusy(true);
    try {
      await api.deletePayment(payment.id);
      setDeletePaymentTarget(null);
      toast.success('Payment moved to recovery.', {
        action: {
          label: 'Undo',
          onClick: async () => {
            try {
              await api.restorePayment(payment.id);
              toast.success('Payment restored');
              await loadPayments();
              onSave?.();
            } catch (err) {
              toast.error(errMessage(err, 'Failed to restore payment.'));
            }
          },
        },
      });
      if (editingPayment?.id === payment.id) {
        resetPaymentForm();
        setPaymentFormOpen(false);
      }
      await loadPayments();
      onSave?.();
    } catch (err) {
      toast.error(errMessage(err, 'Payment could not be removed.'));
    } finally {
      setPaymentBusy(false);
    }
  }

  function openUnmatch(transaction: BankTransaction) {
    setUnmatchTarget(transaction);
    setBulkUnmatch(null);
    setUnmatchConfirmOpen(false);
  }

  function closeUnmatch() {
    setUnmatchTarget(null);
    setBulkUnmatch(null);
    setUnmatchConfirmOpen(false);
  }

  async function handleSingleUnmatch() {
    if (!unmatchTarget?.id) return;
    setTransactionBusyId(unmatchTarget.id);
    try {
      await api.unmatchTransaction(unmatchTarget.id);
      toast.success('Transaction unmatched');
      closeUnmatch();
      await Promise.all([loadPayments(), loadLinkedTransactions()]);
      onSave?.();
    } catch (err) {
      toast.error(errMessage(err, 'Transaction could not be unmatched.'));
    } finally {
      setTransactionBusyId(null);
    }
  }

  async function handleOpenBulkUnmatch() {
    if (!unmatchTarget || !bill?.id) return;
    const targetPayee = transactionTitle(unmatchTarget);
    const similar = linkedTransactions.filter(tx =>
      isSimilarPayee(transactionTitle(tx), targetPayee)
    );
    if (!similar.find(tx => tx.id === unmatchTarget.id)) {
      similar.unshift(unmatchTarget);
    }
    let rules: { id: number; merchant: string }[] = [];
    try {
      const ruleData = await api.billMerchantRules(bill.id) as { id: number; merchant: string }[] | null;
      rules = (ruleData || []).filter(r => isSimilarPayee(r.merchant, targetPayee));
    } catch {
      // ignore — rules are optional
    }
    setBulkUnmatch({
      similar,
      rules,
      checkedIds: new Set(similar.map(tx => tx.id)),
      removeRuleId: null,
    });
  }

  async function handleBulkConfirm() {
    if (!bulkUnmatch) return;
    const { similar, checkedIds, removeRuleId } = bulkUnmatch;
    const matches = similar
      .filter(tx => checkedIds.has(tx.id) && tx.linked_payment)
      .map(tx => {
        const lp = tx.linked_payment as { id: number; payment_source?: string };
        return {
          transaction_id: tx.id,
          payment_id: lp.id,
          payment_source: lp.payment_source,
        };
      });
    if (matches.length === 0) { closeUnmatch(); return; }
    setBulkBusy(true);
    try {
      await api.unmatchTransactionBulk(matches);
      if (removeRuleId && bill?.id) {
        try {
          await api.deleteMerchantRule(bill.id, removeRuleId);
        } catch {
          toast.error('Transactions unmatched, but could not remove merchant rule.');
        }
      }
      toast.success(`${matches.length} transaction${matches.length !== 1 ? 's' : ''} unmatched`);
      closeUnmatch();
      await Promise.all([loadPayments(), loadLinkedTransactions()]);
      onSave?.();
    } catch (err) {
      toast.error(errMessage(err, 'Could not unmatch transactions.'));
    } finally {
      setBulkBusy(false);
    }
  }

  const [, submitAction, isPending] = useActionState(async () => {
    if (!validateForm()) {
      toast.error('Please fix the form errors before saving.');
      return;
    }

    // validateForm() already enforced due-day (1–31) and interest-rate (0–100)
    // ranges and blocked the save with field errors, so these are just parses.
    const parsedDueDay = Number(dueDay);
    const trimmedInterestRate = interestRate.trim();
    const parsedInterestRate = trimmedInterestRate === '' ? null : Number(trimmedInterestRate);

    const data = {
      source_bill_id:    sourceBill?.source_bill_id,
      name:              name.trim(),
      category_id:       categoryId === CAT_NONE ? null : parseInt(categoryId, 10),
      due_day:           parsedDueDay,
      override_due_date: sourceBill?.override_due_date,
      expected_amount:   parseFloat(expectedAmount) || 0,
      interest_rate:     parsedInterestRate,
      billing_cycle:     billingCycleForSchedule(cycleType),
      cycle_type:        cycleType,
      cycle_day:         cycleDay,
      autopay_enabled:   autopay,
      autodraft_status:  autopay ? autodraftStatus : 'none',
      auto_mark_paid:    canAutoMarkPaid && autoMarkPaid,
      is_subscription:   isSubscription,
      subscription_type: isSubscription ? subscriptionType : null,
      reminder_days_before: parseInt(reminderDaysBefore || '3', 10),
      subscription_source: sourceBill?.subscription_source || 'manual',
      subscription_detected_at: sourceBill?.subscription_detected_at,
      has_2fa:           has2fa,
      website:           website     || null,
      username:          username    || null,
      account_info:      accountInfo || null,
      notes:             notes       || null,
      history_visibility: sourceBill?.history_visibility,
      current_balance:   currentBalance === '' ? null : parseFloat(currentBalance),
      minimum_payment:   minimumPayment === '' ? null : parseFloat(minimumPayment),
      snowball_order:    sourceBill?.snowball_order,
      snowball_include:  snowballInclude,
      snowball_exempt:   snowballExempt,
    };
    try {
      let savedBill: Bill;
      if (isNew) {
        if (data.source_bill_id) {
          savedBill = await api.duplicateBill(data.source_bill_id as number, data) as Bill;
        } else {
          savedBill = await api.createBill(data);
        }
        toast.success(`${data.name} added`);
      } else {
        savedBill = await api.updateBill(bill.id, data);
        toast.success(`${data.name} updated`);
      }
      if (saveTemplate) {
        const safeTemplateName = templateName.trim() || data.name;
        await api.saveBillTemplate({ name: safeTemplateName, data });
        toast.success('Template saved');
      }
      onSave(savedBill);
      setDialogOpen(false);
    } catch (err) {
      toast.error(errMessage(err, 'Failed to save bill.'));
    }
  }, null);

  async function handleDeactivate() {
    if (!bill?.id) return;
    try {
      const payload: { active: number; inactive_reason?: string } = { active: bill.active ? 0 : 1 };
      if (bill.active && deactivateReason) payload.inactive_reason = deactivateReason;
      await api.updateBill(bill.id, payload);
      toast.success(bill.active ? 'Bill deactivated' : 'Bill reactivated');
      onSave?.();
      setDialogOpen(false);
    } catch (err) {
      toast.error(errMessage(err, 'Failed to update bill.'));
    }
  }

  const inp = 'bg-background/50 border-border/60 h-9 text-sm w-full';

  return (
    <Dialog open={dialogOpen} onOpenChange={v => { if (!v) 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">
            {isNew ? 'Add Bill' : 'Edit Bill'}
          </DialogTitle>
        </DialogHeader>

        <form id="bill-modal-form" action={submitAction}>
          <div className="grid gap-x-5 gap-y-4 py-2 sm:grid-cols-2">

            {/* Name */}
            <div className="col-span-2 space-y-1.5">
              <Label className="text-xs uppercase tracking-wider text-muted-foreground">Name *</Label>
              <Input
                className={cn(inp, errors.name && 'border-red-500 focus-visible:ring-red-500')}
                placeholder="e.g. Electricity"
                value={name}
                onChange={e => {
                  setName(e.target.value);
                  setTimeout(() => setErrors(prev => ({ ...prev, name: validateName(e.target.value) })), 300);
                }}
                onBlur={() => handleBlur('name', name, validateName)}
                required
              />
              {errors.name && (
                <span className="text-[10px] text-red-500 font-medium">{errors.name}</span>
              )}
            </div>

            {/* Category */}
            <div className="space-y-1.5">
              <Label className="text-xs uppercase tracking-wider text-muted-foreground">Category</Label>
              <Select value={categoryId} onValueChange={handleCategoryChange}>
                <SelectTrigger className={cn(inp, 'w-full')}>
                  <SelectValue placeholder="— none —" />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value={CAT_NONE}>— none —</SelectItem>
                  {categories.map(c => (
                    <SelectItem key={c.id} value={String(c.id)}>{c.name}</SelectItem>
                  ))}
                </SelectContent>
              </Select>
            </div>

            {/* Due Day */}
            <div className="space-y-1.5">
              <Label className="text-xs uppercase tracking-wider text-muted-foreground">Due day of month *</Label>
              <Input
                className={cn(inp, errors.dueDay && 'border-red-500 focus-visible:ring-red-500')}
                type="number" min="1" max="31" required
                value={dueDay}
                onChange={e => {
                  setDueDay(e.target.value);
                  setTimeout(() => setErrors(prev => ({ ...prev, dueDay: validateDueDay(e.target.value) })), 300);
                }}
                onBlur={() => handleBlur('dueDay', dueDay, validateDueDay)}
              />
              {errors.dueDay && (
                <span className="text-[10px] text-red-500 font-medium">{errors.dueDay}</span>
              )}
              <p className="text-[10px] text-muted-foreground/70">
                Enter the day of the month this bill is due.
              </p>
            </div>

            {/* Expected Amount */}
            <div className="space-y-1.5">
              <Label className="text-xs uppercase tracking-wider text-muted-foreground">Expected Amount ($)</Label>
              <Input
                className={cn(inp, 'font-mono', errors.expectedAmount && 'border-red-500 focus-visible:ring-red-500')}
                type="number" min="0" step="0.01" placeholder="0.00"
                value={expectedAmount}
                onChange={e => {
                  setExpected(e.target.value);
                  setTimeout(() => setErrors(prev => ({ ...prev, expectedAmount: validateExpectedAmount(e.target.value) })), 300);
                }}
                onBlur={() => handleBlur('expectedAmount', expectedAmount, validateExpectedAmount)}
              />
              {errors.expectedAmount && (
                <span className="text-[10px] text-red-500 font-medium">{errors.expectedAmount}</span>
              )}
            </div>

            {/* Billing Schedule */}
            <div className="space-y-1.5">
              <Label className="text-xs uppercase tracking-wider text-muted-foreground">Billing Schedule</Label>
              <Select value={cycleType} onValueChange={handleCycleTypeChange}>
                <SelectTrigger className={cn(inp, 'w-full')}>
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  {BILLING_SCHEDULE_OPTIONS.map(([value, label]) => (
                    <SelectItem key={value} value={value}>{label}</SelectItem>
                  ))}
                </SelectContent>
              </Select>
            </div>

            {/* Cycle Day */}
            <div className="space-y-1.5">
              <Label className="text-xs uppercase tracking-wider text-muted-foreground">Cycle Day</Label>
              {cycleType === 'monthly' ? (
                <Select value={cycleDay} onValueChange={setCycleDay}>
                  <SelectTrigger className={cn(inp, 'w-full')}>
                    <SelectValue />
                  </SelectTrigger>
                  <SelectContent>
                    {[...Array(31)].map((_, i) => (
                      <SelectItem key={i+1} value={String(i+1)}>{i+1}{getOrdinalSuffix(i+1)}</SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              ) : cycleType === 'weekly' || cycleType === 'biweekly' ? (
                <Select value={cycleDay} onValueChange={setCycleDay}>
                  <SelectTrigger className={cn(inp, 'w-full')}>
                    <SelectValue />
                  </SelectTrigger>
                  <SelectContent>
                    <SelectItem value="monday">Monday</SelectItem>
                    <SelectItem value="tuesday">Tuesday</SelectItem>
                    <SelectItem value="wednesday">Wednesday</SelectItem>
                    <SelectItem value="thursday">Thursday</SelectItem>
                    <SelectItem value="friday">Friday</SelectItem>
                    <SelectItem value="saturday">Saturday</SelectItem>
                    <SelectItem value="sunday">Sunday</SelectItem>
                  </SelectContent>
                </Select>
              ) : (
                <Select value={cycleDay} onValueChange={setCycleDay}>
                  <SelectTrigger className={cn(inp, 'w-full')}>
                    <SelectValue />
                  </SelectTrigger>
                  <SelectContent>
                    {[
                      'January', 'February', 'March', 'April', 'May', 'June',
                      'July', 'August', 'September', 'October', 'November', 'December',
                    ].map((label, index) => (
                      <SelectItem key={label} value={String(index + 1)}>{label}</SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              )}
              <p className="text-[10px] text-muted-foreground/70">
                {cycleType === 'monthly' ? 'Day of the month' :
                 cycleType === 'weekly' || cycleType === 'biweekly' ? 'Day of the week' :
                 cycleType === 'quarterly' ? 'First month of the quarterly cycle' :
                 'Month due each year'}
              </p>
            </div>

            {/* Subscription Details */}
            <div className="col-span-2 rounded-lg border border-border/60 bg-muted/20 p-3">
              <label className="flex items-start gap-2.5 cursor-pointer group">
                <input
                  type="checkbox"
                  checked={isSubscription}
                  onChange={e => setIsSubscription(e.target.checked)}
                  className="mt-0.5 h-4 w-4 rounded border-border accent-primary"
                />
                <span>
                  <span className="block text-sm font-medium text-muted-foreground group-hover:text-foreground transition-colors">
                    Track as subscription
                  </span>
                  <span className="mt-0.5 block text-[10px] text-muted-foreground/70">
                    Adds this bill to the subscription view with monthly equivalent cost and SimpleFIN recommendation matching.
                  </span>
                </span>
              </label>

              {isSubscription && (
                <div className="mt-3 border-t border-border/40 pt-3">
                  <div className="space-y-1.5 sm:max-w-[50%]">
                    <Label className="text-xs uppercase tracking-wider text-muted-foreground">Subscription Type</Label>
                    <Select value={subscriptionType} onValueChange={setSubscriptionType}>
                      <SelectTrigger className={cn(inp, 'w-full')}>
                        <SelectValue />
                      </SelectTrigger>
                      <SelectContent>
                        {SUBSCRIPTION_TYPES.map(([value, label]) => (
                          <SelectItem key={value} value={value}>{label}</SelectItem>
                        ))}
                      </SelectContent>
                    </Select>
                  </div>
                  {/* Bank sync button moved to the Transactions tab → Bank Matching Rules section */}
                </div>
              )}
            </div>

            {/* Reminder lead time — applies to every bill (the notifier honors
                reminder_days_before for the early "due soon" reminder). */}
            <div className="col-span-2 rounded-lg border border-border/60 bg-muted/20 p-3">
              <div className="space-y-1.5 sm:max-w-[50%]">
                <Label className="text-xs uppercase tracking-wider text-muted-foreground">Reminder Days Before</Label>
                <Input
                  className={cn(inp, 'tracker-number')}
                  type="number"
                  min="0"
                  max="30"
                  value={reminderDaysBefore}
                  onChange={e => setReminderDaysBefore(e.target.value)}
                />
                <p className="text-[10px] text-muted-foreground/70">
                  Get an early reminder this many days before the due date (0-30). Also needs reminders enabled in Settings.
                </p>
              </div>
            </div>

            {/* Debt / Snowball Details — collapsible */}
            <DebtDetailsSection
              inp={inp}
              errors={errors}
              setErrors={setErrors}
              showDebtSection={showDebtSection}
              setShowDebtSection={setShowDebtSection}
              isSnowballCategory={isSnowballCategory}
              showOnSnowball={showOnSnowball}
              interestRate={interestRate}
              setInterestRate={setInterestRate}
              currentBalance={currentBalance}
              setCurrentBalance={setCurrentBalance}
              minimumPayment={minimumPayment}
              setMinimumPayment={setMinimumPayment}
              validateInterestRate={validateInterestRate}
              validateCurrentBalance={validateCurrentBalance}
              validateMinimumPayment={validateMinimumPayment}
              handleBlur={handleBlur}
              onSnowballVisibilityChange={handleSnowballVisibilityChange}
            />

            {/* Website */}
            <div className="space-y-1.5">
              <Label className="text-xs uppercase tracking-wider text-muted-foreground">Website</Label>
              <Input
                className={inp}
                placeholder="https://…"
                value={website}
                onChange={e => setWebsite(e.target.value)}
              />
            </div>

            {/* Username */}
            <div className="space-y-1.5">
              <Label className="text-xs uppercase tracking-wider text-muted-foreground">Username / Email</Label>
              <Input
                className={inp}
                value={username}
                onChange={e => setUsername(e.target.value)}
              />
            </div>

            {/* Account Info */}
            <div className="space-y-1.5">
              <Label className="text-xs uppercase tracking-wider text-muted-foreground">Account Info</Label>
              <Input
                className={inp}
                placeholder="Last 4 digits, account #…"
                value={accountInfo}
                onChange={e => setAccountInfo(e.target.value)}
              />
            </div>

            {/* Checkboxes */}
            <div className="space-y-2.5 flex flex-col justify-end">
              <label className="flex items-center gap-2.5 cursor-pointer group">
                <input
                  type="checkbox"
                  checked={autopay}
                  onChange={e => handleAutopayChange(e.target.checked)}
                  className="h-4 w-4 rounded border-border accent-emerald-500"
                />
                <span className="text-sm text-muted-foreground group-hover:text-foreground transition-colors">
                  Autopay / Autodraft
                </span>
              </label>
              <label
                className={cn('flex items-center gap-2.5 group', canAutoMarkPaid ? 'cursor-pointer' : 'cursor-not-allowed opacity-50')}
                title={canAutoMarkPaid ? undefined : 'Auto-mark requires Autodraft Status set to Assumed paid'}
              >
                <input
                  type="checkbox"
                  checked={canAutoMarkPaid && autoMarkPaid}
                  onChange={e => setAutoMarkPaid(e.target.checked)}
                  disabled={!canAutoMarkPaid}
                  className="h-4 w-4 rounded border-border accent-sky-500"
                />
                <span className="text-sm text-muted-foreground group-hover:text-foreground transition-colors">
                  Auto-mark paid on due date
                </span>
              </label>
              <label className="flex items-center gap-2.5 cursor-pointer group">
                <input
                  type="checkbox"
                  checked={has2fa}
                  onChange={e => setHas2fa(e.target.checked)}
                  className="h-4 w-4 rounded border-border accent-violet-500"
                />
                <span className="text-sm text-muted-foreground group-hover:text-foreground transition-colors">
                  Has 2FA
                </span>
              </label>
            </div>

            <div className="space-y-1.5">
              <Label className="text-xs uppercase tracking-wider text-muted-foreground">Autodraft Status</Label>
              <Select value={autodraftStatus} onValueChange={setAutodraftStatus} disabled={!autopay}>
                <SelectTrigger className={cn(inp, !autopay && 'opacity-60')}>
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value="none">None</SelectItem>
                  <SelectItem value="pending">Pending</SelectItem>
                  <SelectItem value="assumed_paid">Assumed paid</SelectItem>
                  <SelectItem value="confirmed">Confirmed</SelectItem>
                </SelectContent>
              </Select>
            </div>

            {/* Autopay trust indicator — edit mode only */}
            <AutopayTrustIndicator
              isNew={isNew}
              autopay={autopay}
              stats={bill?.autopay_stats as AutopayStats | null}
              verifiedAt={localVerifiedAt}
              onVerify={handleVerifyAutopay}
            />

            {/* Notes */}
            <div className="col-span-2 space-y-1.5">
              <Label className="text-xs uppercase tracking-wider text-muted-foreground">Notes</Label>
              <textarea
                rows={2}
                value={notes}
                onChange={e => setNotes(e.target.value)}
                className={cn(
                  'w-full rounded-md border border-border/60 bg-background/50 px-3 py-2',
                  'text-sm text-foreground placeholder:text-muted-foreground/50',
                  'resize-none outline-none focus:ring-1 focus:ring-ring transition-shadow',
                )}
                placeholder="Any additional notes…"
              />
            </div>

            <TemplateSection
              inp={inp}
              saveTemplate={saveTemplate}
              setSaveTemplate={setSaveTemplate}
              templateName={templateName}
              setTemplateName={setTemplateName}
              namePlaceholder={name}
            />

          </div>
        </form>

        {!isNew && (
          <div className="mt-4 border-t border-border/50 pt-4">
            <PaymentHistoryList
              payments={payments}
              paymentsLoading={paymentsLoading}
              paymentBusy={paymentBusy}
              onAdd={startAddPayment}
              onEdit={startEditPayment}
              onDelete={setDeletePaymentTarget}
            />

            {/* Bank matching rules + linked transactions */}
            <LinkedTransactionsSection
              isNew={isNew}
              billId={sourceBill?.id}
              billName={sourceBill?.name}
              localHasRules={localHasRules}
              syncingPayments={syncingPayments}
              onSync={handleSyncBillPayments}
              onRulesChanged={handleRulesChanged}
              linkedTransactions={linkedTransactions}
              linkedTransactionsLoading={linkedTransactionsLoading}
              transactionBusyId={transactionBusyId}
              onUnmatch={openUnmatch}
            />

            {paymentFormOpen && (
              <PaymentFormFields
                inp={inp}
                editingPayment={editingPayment}
                paymentBusy={paymentBusy}
                amount={paymentAmount} setAmount={setPaymentAmount}
                date={paymentDate} setDate={setPaymentDate}
                method={paymentMethod} setMethod={setPaymentMethod}
                notes={paymentNotes} setNotes={setPaymentNotes}
                onSubmit={handlePaymentSubmit}
                onCancel={() => { resetPaymentForm(); setPaymentFormOpen(false); }}
              />
            )}
          </div>
        )}

        <DialogFooter className="mt-2 gap-2 sm:justify-between">
          <div className="flex gap-2">
            {!isNew && onDuplicate && (
              <Button
                type="button"
                variant="outline"
                disabled={isPending}
                onClick={() => onDuplicate(bill)}
                className="gap-2 text-xs"
              >
                <Copy className="h-3.5 w-3.5" />
                Duplicate
              </Button>
            )}
            {!isNew && (
              <Button
                type="button"
                variant="outline"
                disabled={isPending}
                onClick={() => bill?.active ? setDeactivateOpen(true) : handleDeactivate()}
                className={cn('gap-1.5 text-xs', bill?.active ? 'border-destructive/40 text-destructive hover:bg-destructive/10 hover:border-destructive/60' : 'border-emerald-500/40 text-emerald-600 dark:text-emerald-400 hover:bg-emerald-500/10')}
              >
                {bill?.active ? 'Deactivate' : 'Reactivate'}
              </Button>
            )}
          </div>
          <div className="flex gap-2">
            <Button type="button" variant="ghost" disabled={isPending} onClick={() => setDialogOpen(false)} className="text-xs">
              Cancel
            </Button>
            <Button type="submit" form="bill-modal-form" disabled={isPending} className="gap-1.5 text-xs">
              {isPending && <Loader2 className="h-3.5 w-3.5 animate-spin" />}
              {isNew ? 'Add Bill' : 'Save Changes'}
            </Button>
          </div>
        </DialogFooter>

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

        <AlertDialog
          open={!!deletePaymentTarget}
          onOpenChange={open => {
            if (!open && !paymentBusy) setDeletePaymentTarget(null);
          }}
        >
          <AlertDialogContent>
            <AlertDialogHeader>
              <AlertDialogTitle>Remove this payment?</AlertDialogTitle>
              <AlertDialogDescription>
                This moves the payment to recovery and removes it from bill status calculations.
              </AlertDialogDescription>
            </AlertDialogHeader>
            <AlertDialogFooter>
              <AlertDialogCancel disabled={paymentBusy}>Cancel</AlertDialogCancel>
              <AlertDialogAction
                disabled={paymentBusy}
                onClick={handleDeletePayment}
                className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
              >
                {paymentBusy ? 'Removing...' : 'Remove Payment'}
              </AlertDialogAction>
            </AlertDialogFooter>
          </AlertDialogContent>
        </AlertDialog>

        <UnmatchDialogs
          unmatchTarget={unmatchTarget}
          bulkUnmatch={bulkUnmatch}
          setBulkUnmatch={setBulkUnmatch}
          unmatchConfirmOpen={unmatchConfirmOpen}
          setUnmatchConfirmOpen={setUnmatchConfirmOpen}
          transactionBusyId={transactionBusyId}
          bulkBusy={bulkBusy}
          closeUnmatch={closeUnmatch}
          onSingleUnmatch={handleSingleUnmatch}
          onOpenBulkUnmatch={handleOpenBulkUnmatch}
          onBulkConfirm={handleBulkConfirm}
        />
      </DialogContent>
    </Dialog>
  );
}
