import { useState, useEffect, useRef, useMemo } from 'react';
import { toast } from 'sonner';
import {
  Upload, FileSpreadsheet, AlertTriangle, CheckCircle2, CheckCheck,
  Loader2, ChevronDown, ChevronUp, SkipForward, Plus,
  List, Building2, ChevronLeft, XCircle,
} from 'lucide-react';
import { api } from '@/api';
import { cn, errMessage } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import { SectionCard, importErrorState, type SectionCardProps, type ImportErrorState } from './dataShared';
import type { Bill, Category } from '@/types';

interface BillMatch {
  bill_id: number;
  bill_name?: string;
  expected_amount?: number | string;
  match_confidence: string;
}

interface Recommendation {
  action?: string | null;
  bill_id?: number | null;
  bill_name?: string | null;
  category_id?: number | null;
  category_name?: string | null;
  due_day?: number | null;
  actual_amount?: number | null;
  expected_amount?: number | null;
  payment_amount?: number | null;
  payment_date?: string | null;
  confidence?: string;
  reason?: string;
  warnings?: string[];
}

interface PreviewRow {
  row_id: number | string;
  sheet_name?: string | null;
  source_row_number?: number;
  proposed_action?: string | null;
  requires_user_decision?: boolean;
  recommendation?: Recommendation;
  possible_bill_matches?: BillMatch[];
  raw_values?: unknown[];
  errors?: unknown[];
  warnings?: string[];
  detected_bill_name?: string | null;
  detected_name?: string | null;
  detected_amount?: number | null;
  detected_payment_amount?: number | null;
  detected_paid_date?: string | null;
  detected_notes?: string | null;
  detected_labels?: string[];
  detected_year?: number | null;
  detected_month?: number | null;
  year_month_source?: string | null;
  _match?: BillMatch;
}

interface Decision {
  action?: string | null;
  bill_id?: number | null;
  bill_name?: string | null;
  previous_match_bill_id?: number | null;
  category_id?: number | null;
  due_day?: number | null;
  expected_amount?: number | null;
  actual_amount?: number | null;
  payment_amount?: number | null;
  payment_date?: string | null;
  notes?: string | null;
  [key: string]: unknown;
}

interface WorkbookSheet {
  name: string;
  status?: string;
  row_count?: number;
  detected_year?: number | null;
  detected_month?: number | null;
  is_non_month_sheet?: boolean;
}

interface Workbook {
  parse_mode?: string;
  total_row_count?: number;
  row_count?: number;
  selected_sheet?: string;
  sheet_names?: string[];
  sheets?: WorkbookSheet[];
}

interface PreviewData {
  import_session_id?: string;
  rows: PreviewRow[];
  workbook: Workbook;
}

interface ImportDetail {
  row_id?: number | string;
  result?: string;
  payment?: string;
  existing_created_at?: string;
  field?: string;
  message?: string;
}

interface ApplyResult {
  rows_created?: number;
  rows_updated?: number;
  rows_skipped?: number;
  rows_errored?: number;
  rows_duplicates?: number;
  details?: ImportDetail[];
}

interface BillImportResult {
  created?: number;
  updated?: number;
  errored?: number;
  duplicates?: number;
  earliestDup?: Date | null;
  latestDup?: Date | null;
  completedRowIds?: Set<number | string>;
  erroredRowIds?: Set<number | string>;
}

interface BillGroup {
  bill: Bill;
  rows: PreviewRow[];
  counts: { high: number; medium: number; low: number };
}

interface PreviewState {
  status: 'idle' | 'loading' | 'ready' | 'error';
  data: PreviewData | null;
  error: ImportErrorState | null;
}

interface ApplyState {
  status: 'idle' | 'loading' | 'done' | 'error';
  result: ApplyResult | null;
  error: ImportErrorState | null;
}

interface Options {
  parseAllSheets: boolean;
  defaultYear: number | string;
  defaultMonth: number | string;
}

function groupRowsBySheet(rows: PreviewRow[]): { name: string; rows: PreviewRow[] }[] {
  const map = new Map<string, PreviewRow[]>();
  for (const row of rows) {
    const key = row.sheet_name || '(unknown sheet)';
    const list = map.get(key);
    if (list) list.push(row);
    else map.set(key, [row]);
  }
  return Array.from(map.entries()).map(([name, groupRows]) => ({ name, rows: groupRows }));
}

function initialDecisionFromRecommendation(row: PreviewRow): Decision {
  const rec = row.recommendation || {};
  const action = rec.action === 'ambiguous' ? null : (rec.action || row.proposed_action || null);

  if (!action || row.requires_user_decision) return { action: null };
  if (action === 'skip_row') return { action: 'skip_row' };
  if (action === 'match_existing_bill') {
    return {
      action,
      bill_id: rec.bill_id ?? row.possible_bill_matches?.[0]?.bill_id ?? null,
      bill_name: null,
      due_day: rec.due_day ?? null,
      actual_amount: rec.actual_amount ?? row.detected_amount ?? null,
      payment_amount: rec.payment_amount ?? row.detected_payment_amount ?? null,
      payment_date: rec.payment_date ?? row.detected_paid_date ?? null,
      notes: row.detected_notes ?? null,
    };
  }
  if (action === 'create_new_bill') {
    return {
      action,
      bill_id: null,
      bill_name: rec.bill_name || row.detected_bill_name || '',
      category_id: rec.category_id ?? null,
      due_day: rec.due_day ?? null,
      expected_amount: rec.expected_amount ?? row.detected_amount ?? null,
      actual_amount: rec.actual_amount ?? row.detected_amount ?? null,
      payment_amount: rec.payment_amount ?? row.detected_payment_amount ?? null,
      payment_date: rec.payment_date ?? row.detected_paid_date ?? null,
      notes: row.detected_notes ?? null,
    };
  }
  return { action };
}

function safeRawBillName(row: PreviewRow): string {
  const raw = row.raw_values?.find((v) => {
    const text = String(v || '').trim();
    if (!text || text.length > 80) return false;
    if (/^(?:total|subtotal|sum|grand\s*total)$/i.test(text)) return false;
    if (/^\$?\(?\d[\d,]*(?:\.\d{1,2})?\)?$/.test(text)) return false;
    if (/^\d{1,2}\/\d{1,2}(?:\/\d{2,4})?$/.test(text)) return false;
    if (/^\d{4}-\d{2}-\d{2}$/.test(text)) return false;
    return true;
  });
  return raw ? String(raw).trim() : '';
}

function buildCreateNewDecision(row: PreviewRow, currentDecision: Decision = {}): Decision {
  const rec = row.recommendation || {};
  const billName = currentDecision.bill_name
    || row.detected_bill_name
    || rec.bill_name
    || safeRawBillName(row);

  return {
    ...currentDecision,
    action: 'create_new_bill',
    previous_match_bill_id: currentDecision.bill_id ?? currentDecision.previous_match_bill_id ?? rec.bill_id ?? row.possible_bill_matches?.[0]?.bill_id ?? null,
    bill_id: null,
    bill_name: billName,
    category_id: currentDecision.category_id ?? rec.category_id ?? null,
    due_day: currentDecision.due_day ?? rec.due_day ?? null,
    expected_amount: currentDecision.expected_amount ?? rec.expected_amount ?? row.detected_amount ?? null,
    actual_amount: currentDecision.actual_amount ?? rec.actual_amount ?? row.detected_amount ?? null,
    payment_amount: currentDecision.payment_amount ?? rec.payment_amount ?? row.detected_payment_amount ?? null,
    payment_date: currentDecision.payment_date ?? rec.payment_date ?? row.detected_paid_date ?? null,
    notes: currentDecision.notes ?? row.detected_notes ?? null,
  };
}

function buildInitialDecisions(rows: PreviewRow[]): Record<string, Decision> {
  const d: Record<string, Decision> = {};
  for (const row of rows) {
    const hasError = (row.errors?.length ?? 0) > 0;
    if (hasError || row.proposed_action === 'skip_row') {
      d[row.row_id] = { action: 'skip_row' };
    } else {
      d[row.row_id] = initialDecisionFromRecommendation(row);
    }
  }
  return d;
}

function isDecisionComplete(action: string | null | undefined, decision: Decision | undefined): boolean {
  if (!action) return false;
  if (action === 'skip_row') return true;
  if (action === 'create_new_bill') return !!(decision?.bill_name?.trim());
  if (['match_existing_bill', 'update_monthly_state', 'add_monthly_note', 'create_payment'].includes(action)) {
    return !!decision?.bill_id;
  }
  return true;
}

// ─── Badges ───────────────────────────────────────────────────────────────────

function SourceBadge({ source }: { source?: string }) {
  const MAP: Record<string, string> = {
    row_date:   'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400',
    sheet_name: 'bg-blue-500/15 text-blue-600 dark:text-blue-400',
    default:    'bg-amber-500/15 text-amber-600 dark:text-amber-500',
    ambiguous:  'bg-red-500/15 text-red-600 dark:text-red-400',
  };
  const LABELS: Record<string, string> = { row_date: 'date cell', sheet_name: 'tab name', default: 'default', ambiguous: 'ambiguous' };
  const key = source ?? '';
  return (
    <span className={cn('inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] font-medium', MAP[key] ?? MAP.ambiguous)}>
      {LABELS[key] ?? source}
    </span>
  );
}

function ConfidenceBadge({ confidence }: { confidence?: string }) {
  const MAP: Record<string, string> = { high: 'text-emerald-600 dark:text-emerald-400', medium: 'text-amber-600 dark:text-amber-500', low: 'text-red-500' };
  return <span className={cn('text-[10px] font-semibold uppercase', MAP[confidence ?? ''] ?? '')}>{confidence}</span>;
}

function actionLabel(action?: string | null): string {
  const MAP: Record<string, string> = {
    match_existing_bill: 'Match existing bill',
    create_new_bill: 'Create new bill',
    skip_row: 'Skip row',
    ambiguous: 'Needs decision',
    update_monthly_state: 'Update monthly record',
    add_monthly_note: 'Add monthly note',
    create_payment: 'Record as payment',
  };
  return MAP[action ?? ''] || (action ? action.replace(/_/g, ' ') : 'Needs decision');
}

function SheetStatusBadge({ status }: { status?: string }) {
  const MAP: Record<string, string> = {
    parsed:            'bg-emerald-500/15 text-emerald-600',
    parsed_month_only: 'bg-amber-500/15 text-amber-600',
    ambiguous:         'bg-orange-500/15 text-orange-600',
    skipped:           'bg-muted text-muted-foreground',
  };
  const LABELS: Record<string, string> = {
    parsed: 'parsed', parsed_month_only: 'month only', ambiguous: 'ambiguous', skipped: 'skipped',
  };
  const key = status ?? '';
  return (
    <span className={cn('inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium', MAP[key] ?? MAP.ambiguous)}>
      {LABELS[key] ?? status}
    </span>
  );
}

function WorkbookSummaryCard({ workbook }: { workbook: Workbook }) {
  const isMulti = workbook.parse_mode === 'all_sheets';

  return (
    <div className="rounded-lg border border-border bg-muted/30 p-4 mb-4 space-y-3">
      <div className="flex items-center justify-between flex-wrap gap-2">
        <p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Workbook Summary</p>
        <span className="text-xs text-muted-foreground">
          {isMulti ? `${workbook.total_row_count} rows across ${workbook.sheet_names?.length ?? 0} tabs` : `${workbook.row_count} rows · ${workbook.selected_sheet}`}
        </span>
      </div>
      {isMulti && (workbook.sheets?.length ?? 0) > 0 && (
        <div className="grid grid-cols-1 sm:grid-cols-2 gap-1.5">
          {workbook.sheets?.map(s => (
            <div key={s.name} className={cn(
              'flex items-center justify-between rounded px-3 py-1.5 text-xs',
              s.is_non_month_sheet || s.status === 'skipped' ? 'bg-muted/50 text-muted-foreground/60' : 'bg-background border border-border/60'
            )}>
              <span className="font-medium truncate mr-2">{s.name}</span>
              <div className="flex items-center gap-2 shrink-0">
                {s.detected_year && s.detected_month && (
                  <span className="tabular-nums text-muted-foreground">
                    {String(s.detected_month).padStart(2,'0')}/{s.detected_year}
                  </span>
                )}
                <SheetStatusBadge status={s.status} />
                {s.status !== 'skipped' && <span className="text-muted-foreground">{s.row_count} rows</span>}
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// ─── XLSX Import: Row Decision Controls ──────────────────────────────────────

const ACTIONS_NEEDING_BILL = new Set(['match_existing_bill','update_monthly_state','add_monthly_note','create_payment']);

interface RowDecisionRowProps {
  row: PreviewRow;
  decision?: Decision;
  onDecisionChange: (rowId: number | string, decision: Decision) => void;
  allBills: Bill[];
  categories: Category[];
  selected: boolean;
  onSelectedChange: (rowId: number | string, selected: boolean) => void;
}

function RowDecisionRow({ row, decision, onDecisionChange, allBills, categories, selected, onSelectedChange }: RowDecisionRowProps) {
  const [expanded, setExpanded] = useState(row.requires_user_decision || !decision?.action);

  const action   = decision?.action ?? null;
  const isSkip   = action === 'skip_row';
  const hasError = (row.errors?.length ?? 0) > 0;
  const complete = isDecisionComplete(action, decision);
  const rec = row.recommendation || {};

  const suggestedBills = row.possible_bill_matches ?? [];
  const suggestedIds   = new Set(suggestedBills.map(b => b.bill_id));
  const otherBills     = allBills.filter(b => !suggestedIds.has(b.id));

  const handleAction = (val: string | null) => {
    const next: Decision = { ...decision, action: val };
    if (val === 'create_new_bill') {
      Object.assign(next, buildCreateNewDecision(row, decision));
    } else if (val != null && ACTIONS_NEEDING_BILL.has(val)) {
      next.bill_name = null;
      next.bill_id = decision?.bill_id ?? decision?.previous_match_bill_id ?? rec.bill_id ?? suggestedBills[0]?.bill_id ?? null;
      next.actual_amount = decision?.actual_amount ?? rec.actual_amount ?? row.detected_amount ?? null;
      next.payment_amount = decision?.payment_amount ?? rec.payment_amount ?? row.detected_payment_amount ?? null;
      next.payment_date = decision?.payment_date ?? rec.payment_date ?? row.detected_paid_date ?? null;
    } else {
      next.bill_id = null;
      next.bill_name = null;
    }
    onDecisionChange(row.row_id, next);
    if (val === 'skip_row') setExpanded(false);
  };

  const handleBill = (e: React.ChangeEvent<HTMLSelectElement>) => {
    onDecisionChange(row.row_id, { ...decision, bill_id: parseInt(e.target.value, 10) || null });
  };

  const handleBillName = (e: React.ChangeEvent<HTMLInputElement>) => {
    onDecisionChange(row.row_id, { ...decision, bill_name: e.target.value });
  };

  const handleDecisionField = (field: string, value: unknown) => {
    onDecisionChange(row.row_id, { ...decision, [field]: value });
  };

  return (
    <div className={cn(
      'border-b border-border/40 last:border-0',
      isSkip && 'opacity-50',
      !complete && action !== null && 'bg-amber-500/5',
    )}>
      {/* Main row */}
      <div
        className="flex items-start gap-3 px-4 py-3 cursor-pointer hover:bg-muted/20 transition-colors"
        onClick={() => setExpanded(e => !e)}
      >
        {/* Selection */}
        <div className="mt-0.5 shrink-0" onClick={e => e.stopPropagation()}>
          <input
            type="checkbox"
            checked={selected}
            onChange={e => onSelectedChange(row.row_id, e.target.checked)}
            aria-label={`Select row ${row.source_row_number}`}
            className="h-4 w-4 rounded border-border accent-primary"
          />
        </div>

        {/* Status icon */}
        <div className="mt-0.5 shrink-0">
          {hasError      ? <XCircle   className="h-4 w-4 text-muted-foreground" /> :
           isSkip        ? <SkipForward className="h-4 w-4 text-muted-foreground" /> :
           complete      ? <CheckCircle2 className="h-4 w-4 text-emerald-500" /> :
           action !== null ? <AlertTriangle className="h-4 w-4 text-amber-500" /> :
                            <AlertTriangle className="h-4 w-4 text-orange-500" />}
        </div>

        {/* Content */}
        <div className="flex-1 min-w-0">
          <div className="flex items-center gap-2 flex-wrap">
            <span className="text-xs text-muted-foreground tabular-nums">#{row.source_row_number}</span>
            {row.sheet_name && <span className="text-xs text-muted-foreground">{row.sheet_name}</span>}
            {row.detected_year && row.detected_month && (
              <span className="text-xs text-muted-foreground tabular-nums">
                {String(row.detected_month).padStart(2,'0')}/{row.detected_year}
              </span>
            )}
            {row.year_month_source && <SourceBadge source={row.year_month_source} />}
          </div>
          <div className="flex items-baseline gap-3 mt-0.5 flex-wrap">
            <span className={cn('text-sm font-medium', !row.detected_bill_name && 'italic text-muted-foreground/60')}>
              {row.detected_bill_name || '(no bill name)'}
            </span>
            {row.detected_amount != null && (
              <span className="text-xs text-muted-foreground tabular-nums">
                ${row.detected_amount.toFixed(2)}
              </span>
            )}
            {row.detected_paid_date && (
              <span className="text-xs text-emerald-500 tabular-nums">
                paid {row.detected_paid_date}
              </span>
            )}
            {(row.detected_labels?.length ?? 0) > 0 && (
              <span className="text-xs text-muted-foreground/70">{row.detected_labels?.join(', ')}</span>
            )}
            {row.detected_notes && (
              <span className="text-xs text-muted-foreground/70 italic truncate max-w-[200px]">{row.detected_notes}</span>
            )}
          </div>
        </div>

        {/* Right: action status + expand */}
        <div className="flex items-center gap-2 shrink-0">
          {action === null ? (
            <span className="text-xs font-medium text-orange-500">Needs decision</span>
          ) : isSkip ? (
            <span className="text-xs text-muted-foreground">Skipped</span>
          ) : (
            <span className="text-xs text-muted-foreground capitalize">{action.replace(/_/g,' ')}</span>
          )}
          {action !== 'skip_row' && (
            expanded ? <ChevronUp className="h-3.5 w-3.5 text-muted-foreground" /> : <ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
          )}
        </div>
      </div>

      {/* Expanded decision controls */}
      {expanded && !hasError && (
        <div className="px-11 pb-4 space-y-3">
          {/* Recommendation */}
          {rec.action && (
            <div className="rounded-md border border-border bg-muted/30 px-3 py-2 space-y-1.5">
              <div className="flex items-center gap-2 flex-wrap">
                <span className="text-xs font-medium">Recommended: {actionLabel(rec.action)}</span>
                {rec.bill_name && rec.action === 'match_existing_bill' && (
                  <span className="text-xs text-muted-foreground">→ {rec.bill_name}</span>
                )}
                {rec.category_name && (
                  <span className="text-xs text-muted-foreground">Category: {rec.category_name}</span>
                )}
                {rec.due_day && <span className="text-xs text-muted-foreground">Due day: {rec.due_day}</span>}
                {rec.actual_amount != null && <span className="text-xs text-muted-foreground">${Number(rec.actual_amount).toFixed(2)}</span>}
                <ConfidenceBadge confidence={rec.confidence} />
              </div>
              {rec.reason && <p className="text-xs text-muted-foreground">Reason: {rec.reason}</p>}
            </div>
          )}

          {/* Warnings */}
          {((rec.warnings?.length ?? 0) > 0 || (row.warnings?.length ?? 0) > 0) && (
            <div className="space-y-1">
              {Array.from(new Set([...(rec.warnings || []), ...(row.warnings || [])])).map((w, i) => (
                <p key={i} className="text-xs text-amber-600 flex items-start gap-1.5">
                  <AlertTriangle className="h-3.5 w-3.5 shrink-0 mt-0.5" />{w}
                </p>
              ))}
            </div>
          )}

          {/* Possible matches hint */}
          {suggestedBills.length > 0 && (
            <div className="flex flex-wrap gap-1.5">
              <span className="text-xs text-muted-foreground self-center">Suggested:</span>
              {suggestedBills.slice(0, 3).map(b => (
                <button key={b.bill_id} type="button"
                  onClick={() => {
                    onDecisionChange(row.row_id, {
                      ...decision,
                      action: 'match_existing_bill',
                      bill_id: b.bill_id,
                      previous_match_bill_id: b.bill_id,
                      bill_name: null,
                    });
                    setExpanded(false);
                  }}
                  className="text-xs px-2 py-0.5 rounded-full border border-border bg-background hover:bg-muted transition-colors">
                  {b.bill_name}
                  {b.match_confidence === 'high' && <CheckCircle2 className="inline h-2.5 w-2.5 ml-1 text-emerald-500" />}
                </button>
              ))}
            </div>
          )}

          {/* Action selector */}
          <div className="flex items-center gap-3 flex-wrap">
            <label className="text-xs text-muted-foreground w-14 shrink-0">Action</label>
            <select
              value={action ?? ''}
              onChange={e => handleAction(e.target.value || null)}
              className="text-sm border border-input bg-background rounded-md px-2 py-1 focus:outline-none focus:ring-1 focus:ring-ring"
            >
              <option value="">— choose action —</option>
              <option value="match_existing_bill">Match existing bill</option>
              <option value="create_new_bill">Create new bill</option>
              <option value="update_monthly_state">Update monthly record</option>
              <option value="add_monthly_note">Add monthly note</option>
              <option value="create_payment">Record as payment</option>
              <option value="skip_row">Skip this row</option>
            </select>
          </div>

          {/* Bill selector (for actions that need a bill) */}
          {action != null && ACTIONS_NEEDING_BILL.has(action) && (
            <div className="flex items-center gap-3 flex-wrap">
              <label className="text-xs text-muted-foreground w-14 shrink-0">Bill</label>
              <select
                value={decision?.bill_id ?? ''}
                onChange={handleBill}
                className="text-sm border border-input bg-background rounded-md px-2 py-1 flex-1 min-w-[200px] focus:outline-none focus:ring-1 focus:ring-ring"
              >
                <option value="">— select bill —</option>
                {suggestedBills.length > 0 && (
                  <optgroup label="Suggested matches">
                    {suggestedBills.map(b => (
                      <option key={b.bill_id} value={b.bill_id}>
                        {b.bill_name}{b.expected_amount ? ` ($${b.expected_amount})` : ''} · {b.match_confidence}
                      </option>
                    ))}
                  </optgroup>
                )}
                {otherBills.length > 0 && (
                  <optgroup label={suggestedBills.length ? 'All other bills' : 'All bills'}>
                    {otherBills.map(b => (
                      <option key={b.id} value={b.id}>{b.name}</option>
                    ))}
                  </optgroup>
                )}
              </select>
            </div>
          )}

          {/* Bill name input for create_new_bill */}
          {action === 'create_new_bill' && (
            <div className="space-y-2">
              <div className="flex items-center gap-3">
                <label className="text-xs text-muted-foreground w-14 shrink-0">Name</label>
                <Input
                  value={decision?.bill_name ?? ''}
                  onChange={handleBillName}
                  placeholder="Bill name"
                  className="h-8 text-sm flex-1 max-w-sm"
                />
              </div>
              <div className="flex items-center gap-3 flex-wrap">
                <label className="text-xs text-muted-foreground w-14 shrink-0">Category</label>
                <select
                  value={decision?.category_id ?? ''}
                  onChange={e => handleDecisionField('category_id', parseInt(e.target.value, 10) || null)}
                  className="text-sm border border-input bg-background rounded-md px-2 py-1 min-w-[180px] focus:outline-none focus:ring-1 focus:ring-ring"
                >
                  <option value="">No category</option>
                  {categories.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
                </select>
                {rec.category_name && <span className="text-xs text-muted-foreground">Suggested: {rec.category_name}</span>}
              </div>
              <div className="flex items-center gap-3 flex-wrap">
                <label className="text-xs text-muted-foreground w-14 shrink-0">Details</label>
                <Input
                  type="number"
                  min="1"
                  max="31"
                  value={decision?.due_day ?? ''}
                  onChange={e => handleDecisionField('due_day', e.target.value ? parseInt(e.target.value, 10) : null)}
                  placeholder="Due day"
                  className="h-8 text-sm w-24"
                />
                <Input
                  type="number"
                  min="0"
                  step="0.01"
                  value={decision?.expected_amount ?? ''}
                  onChange={e => handleDecisionField('expected_amount', e.target.value === '' ? null : parseFloat(e.target.value))}
                  placeholder="Expected amount"
                  className="h-8 text-sm w-40"
                />
              </div>
            </div>
          )}

          {action && action !== 'skip_row' && (
            <div className="flex items-center gap-3 flex-wrap">
              <label className="text-xs text-muted-foreground w-14 shrink-0">Paid</label>
              <Input
                type="date"
                value={decision?.payment_date ?? ''}
                onChange={e => handleDecisionField('payment_date', e.target.value || null)}
                className="h-8 text-sm w-40"
              />
              <Input
                type="number"
                min="0"
                step="0.01"
                value={decision?.payment_amount ?? ''}
                onChange={e => handleDecisionField('payment_amount', e.target.value === '' ? null : parseFloat(e.target.value))}
                placeholder="Paid amount"
                className="h-8 text-sm w-36"
              />
            </div>
          )}

          {/* Quick skip */}
          {action !== 'skip_row' && (
            <button type="button"
              onClick={() => { handleAction('skip_row'); setExpanded(false); }}
              className="text-xs text-muted-foreground hover:text-foreground flex items-center gap-1 transition-colors">
              <SkipForward className="h-3 w-3" />Skip this row
            </button>
          )}
        </div>
      )}
    </div>
  );
}

// ─── XLSX Import: Preview Table ───────────────────────────────────────────────

interface PreviewTableProps {
  rows: PreviewRow[];
  decisions: Record<string, Decision>;
  onDecisionChange: (rowId: number | string, decision: Decision) => void;
  allBills: Bill[];
  categories: Category[];
  selectedRows: Set<number | string>;
  onSelectedChange: (rowId: number | string, selected: boolean) => void;
}

function PreviewTable({ rows, decisions, onDecisionChange, allBills, categories, selectedRows, onSelectedChange }: PreviewTableProps) {
  const groups   = groupRowsBySheet(rows);
  const multiTab = groups.length > 1;

  return (
    <div>
      {groups.map(({ name, rows: groupRows }) => (
        <div key={name}>
          {multiTab && (
            <div className="px-4 py-2 bg-muted/40 border-b border-border/60 flex items-center gap-2">
              <FileSpreadsheet className="h-3.5 w-3.5 text-muted-foreground" />
              <span className="text-xs font-semibold text-muted-foreground">{name}</span>
              <span className="text-xs text-muted-foreground/60">· {groupRows.length} rows</span>
            </div>
          )}
          {groupRows.map(row => (
            <RowDecisionRow
              key={row.row_id}
              row={row}
              decision={decisions[row.row_id]}
              onDecisionChange={onDecisionChange}
              allBills={allBills}
              categories={categories}
              selected={selectedRows.has(row.row_id)}
              onSelectedChange={onSelectedChange}
            />
          ))}
        </div>
      ))}
    </div>
  );
}

interface BulkActionBarProps {
  rows: PreviewRow[];
  selectedRows: Set<number | string>;
  onSelectAll: () => void;
  onClearSelection: () => void;
  onBulkSkip: () => void;
  onBulkCreateNew: () => void;
  onBulkReset: () => void;
}

function BulkActionBar({ rows, selectedRows, onSelectAll, onClearSelection, onBulkSkip, onBulkCreateNew, onBulkReset }: BulkActionBarProps) {
  const allSelected = rows.length > 0 && rows.every(r => selectedRows.has(r.row_id));
  const selectedCount = selectedRows.size;

  return (
    <div className="border-b border-border bg-muted/25 px-4 py-3">
      <div className="flex items-center justify-between gap-3 flex-wrap">
        <label className="flex items-center gap-2 text-xs text-muted-foreground cursor-pointer">
          <input
            type="checkbox"
            checked={allSelected}
            onChange={e => e.target.checked ? onSelectAll() : onClearSelection()}
            className="h-4 w-4 rounded border-border accent-primary"
          />
          Select all visible
        </label>

        <div className="flex items-center gap-2 flex-wrap">
          {selectedCount > 0 && (
            <span className="text-xs font-medium text-foreground">{selectedCount} row{selectedCount === 1 ? '' : 's'} selected</span>
          )}
          {selectedCount > 0 && (
            <>
              <Button size="sm" variant="outline" onClick={onBulkSkip} className="h-8 text-xs">
                Skip selected
              </Button>
              <Button size="sm" variant="outline" onClick={onBulkCreateNew} className="h-8 text-xs">
                Create new bills
              </Button>
              <Button size="sm" variant="ghost" onClick={onBulkReset} className="h-8 text-xs">
                Reset to recommendation
              </Button>
              <Button size="sm" variant="ghost" onClick={onClearSelection} className="h-8 text-xs text-muted-foreground">
                Clear selection
              </Button>
            </>
          )}
        </div>
      </div>
    </div>
  );
}

// ─── Section 1: Import Spreadsheet History ────────────────────────────────────

const INITIAL_OPTIONS: Options = {
  parseAllSheets: true,
  defaultYear: new Date().getFullYear(),
  defaultMonth: '',
};

// ─── Bill History Import helpers ──────────────────────────────────────────────

function ConfidenceDot({ level }: { level?: string }) {
  const cls = level === 'high'   ? 'bg-emerald-500'
            : level === 'medium' ? 'bg-amber-500'
            :                      'bg-muted-foreground/30';
  return <span className={cn('h-2 w-2 rounded-full shrink-0 inline-block', cls)} />;
}

function useBillGroups(previewRows: PreviewRow[], allBills: Bill[]): BillGroup[] {
  return useMemo(() => {
    const billMap = new Map<number, Bill>(allBills.map(b => [b.id, b]));
    const groups  = new Map<number, BillGroup>();

    for (const row of previewRows) {
      for (const match of (row.possible_bill_matches ?? [])) {
        if (!billMap.has(match.bill_id)) continue;
        if (!groups.has(match.bill_id)) {
          groups.set(match.bill_id, {
            bill:   billMap.get(match.bill_id)!,
            rows:   [],
            counts: { high: 0, medium: 0, low: 0 },
          });
        }
        const g = groups.get(match.bill_id)!;
        if (!g.rows.find(r => r.row_id === row.row_id)) {
          g.rows.push({ ...row, _match: match });
          const key = match.match_confidence as 'high' | 'medium' | 'low';
          g.counts[key] = (g.counts[key] || 0) + 1;
        }
      }
    }
    return [...groups.values()].sort((a, b) =>
      b.rows.length !== a.rows.length ? b.rows.length - a.rows.length : b.counts.high - a.counts.high
    );
  }, [previewRows, allBills]);
}

function rowDateLabel(row: PreviewRow): string {
  if (row.detected_year && row.detected_month)
    return `${row.detected_year}-${String(row.detected_month).padStart(2, '0')}`;
  return row.detected_paid_date ?? '—';
}

function billImportProgress(rows: PreviewRow[], importResult: BillImportResult | null) {
  const completedRowIds = importResult?.completedRowIds ?? new Set<number | string>();
  const remainingRows = rows.filter(row => !completedRowIds.has(row.row_id));
  return {
    completedCount: rows.length - remainingRows.length,
    remainingRows,
    remainingCount: remainingRows.length,
  };
}

function detailImportedAnything(detail: ImportDetail | undefined): boolean {
  return ['created', 'updated', 'overwritten', 'imported'].includes(detail?.result ?? '')
    || detail?.payment === 'created';
}

function detailCompletesImport(detail: ImportDetail | undefined): boolean {
  if (!detail?.row_id) return false;
  if (['error', 'ambiguous', 'skipped_conflict'].includes(detail.result ?? '')) return false;
  if (detail.result === 'skipped') return false;
  return detailImportedAnything(detail)
    || detail.result === 'skipped_duplicate'
    || detail.payment === 'skipped_duplicate';
}

interface BillDetailViewProps {
  group: BillGroup;
  onBack: () => void;
  onImport: () => void;
  isImporting: boolean;
  importResult: BillImportResult | null;
}

function BillDetailView({ group, onBack, onImport, isImporting, importResult }: BillDetailViewProps) {
  const { bill, rows } = group;
  const { completedCount, remainingCount } = billImportProgress(rows, importResult);
  const sorted = [...rows].sort((a, b) => {
    const da = (a.detected_year ?? 0) * 100 + (a.detected_month ?? 0);
    const db = (b.detected_year ?? 0) * 100 + (b.detected_month ?? 0);
    return da - db;
  });

  return (
    <div>
      <div className="px-4 py-2.5 border-b border-border/50 flex items-center justify-between gap-3">
        <button type="button" onClick={onBack}
          className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors">
          <ChevronLeft className="h-3.5 w-3.5" /> All bills
        </button>
        <span className="text-sm font-medium truncate">{bill.name}</span>
        <div className="flex items-center gap-2 shrink-0">
          {importResult && (
            <div className="text-right">
              <span className={`text-xs font-medium ${
                completedCount === rows.length ? 'text-emerald-500' : 'text-amber-400'
              }`}>
                {completedCount === rows.length
                  ? 'All imported'
                  : `${completedCount} imported · ${remainingCount} remaining`}
                {(importResult.duplicates ?? 0) > 0
                  && ` · ${importResult.duplicates} dupes`}
              </span>
              {(importResult.duplicates ?? 0) > 0 && importResult.earliestDup && (
                <p className="text-[10px] text-muted-foreground/70 mt-0.5">
                  {importResult.duplicates} dup{(importResult.duplicates ?? 0) > 1 ? 's' : ''} · recorded{' '}
                  {importResult.earliestDup.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}
                </p>
              )}
            </div>
          )}
          <Button size="sm" onClick={onImport} disabled={isImporting || remainingCount === 0} className="h-7 text-xs px-3 shrink-0 gap-1.5">
            {isImporting && <Loader2 className="h-3 w-3 animate-spin" />}
            {remainingCount === 0 ? 'All imported' : `Import all ${remainingCount}`}
          </Button>
        </div>
      </div>
      <div className="divide-y divide-border/30 max-h-80 overflow-y-auto">
        {sorted.map(row => (
          <div key={row.row_id} className="px-4 py-2 flex items-center gap-3">
            <ConfidenceDot level={row._match?.match_confidence} />
            <span className="text-xs tabular-nums text-muted-foreground w-16 shrink-0">{rowDateLabel(row)}</span>
            <span className="text-xs font-mono tabular-nums w-16 shrink-0">
              {row.detected_amount != null ? `$${Number(row.detected_amount).toFixed(2)}` : '—'}
            </span>
            <span className="text-xs text-muted-foreground truncate flex-1">{row.detected_name ?? '—'}</span>
            <span className={cn('text-[10px] shrink-0',
              row._match?.match_confidence === 'high'   ? 'text-emerald-500' :
              row._match?.match_confidence === 'medium' ? 'text-amber-500'   : 'text-muted-foreground')}>
              {row._match?.match_confidence}
            </span>
          </div>
        ))}
      </div>
    </div>
  );
}

interface BillHistoryViewProps {
  previewRows: PreviewRow[];
  allBills: Bill[];
  importingBillId: number | string | null;
  billImportResults: Map<number | string, BillImportResult>;
  onImportBill: (group: BillGroup) => void;
}

function BillHistoryView({ previewRows, allBills, importingBillId, billImportResults, onImportBill }: BillHistoryViewProps) {
  const [selectedBillId, setSelectedBillId] = useState<number | string | null>(null);
  const billGroups = useBillGroups(previewRows, allBills);

  if (billGroups.length === 0) {
    return (
      <div className="px-4 py-10 text-center text-sm text-muted-foreground">
        No existing bills matched rows in this file.
      </div>
    );
  }

  if (selectedBillId) {
    const group = billGroups.find(g => g.bill.id === selectedBillId);
    return group
      ? <BillDetailView group={group}
          isImporting={importingBillId === group.bill.id}
          importResult={billImportResults.get(group.bill.id) ?? null}
          onBack={() => setSelectedBillId(null)}
          onImport={() => onImportBill(group)} />
      : null;
  }

  return (
    <div className="divide-y divide-border/50">
      {billGroups.map(g => {
        const { bill, rows, counts } = g;
        const isImporting  = importingBillId === bill.id;
        const importResult = billImportResults.get(bill.id) ?? null;
        const { completedCount, remainingCount } = billImportProgress(rows, importResult);

        const sorted3 = [...rows]
          .sort((a, b) => {
            const da = (a.detected_year ?? 0) * 100 + (a.detected_month ?? 0);
            const db = (b.detected_year ?? 0) * 100 + (b.detected_month ?? 0);
            return da - db;
          })
          .slice(0, 3);

        return (
          <div key={bill.id} className="px-4 py-3 flex items-start gap-3">
            <div className="flex-1 min-w-0">
              <div className="flex items-center gap-2 flex-wrap">
                <span className="text-sm font-medium truncate">{bill.name}</span>
                <span className="text-[10px] text-muted-foreground border border-border/50 rounded px-1.5 py-0.5 shrink-0">
                  {rows.length} row{rows.length !== 1 ? 's' : ''}
                </span>
                {counts.high   > 0 && <span className="text-[10px] text-emerald-500">{counts.high} high</span>}
                {counts.medium > 0 && <span className="text-[10px] text-amber-500">{counts.medium} med</span>}
                {counts.low    > 0 && <span className="text-[10px] text-muted-foreground/50">{counts.low} low</span>}
                {importResult && (() => {
                  const allImported = completedCount === rows.length;
                  const fmtDate   = (d?: Date | null) => d?.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
                  const dupDate   = importResult.earliestDup ? ` · ${fmtDate(importResult.earliestDup)}` : '';
                  return (
                    <div className="space-y-0.5">
                      <span className={`text-[10px] font-medium ${allImported ? 'text-emerald-500' : 'text-amber-400'}`}>
                        {allImported ? 'All imported' : `${completedCount} imported · ${remainingCount} remaining`}
                        {(importResult.duplicates ?? 0) > 0 && ` · ${importResult.duplicates} dupes`}
                        {(importResult.errored ?? 0) > 0 && ` · ${importResult.errored} errors`}
                      </span>
                      {(importResult.duplicates ?? 0) > 0 && (
                        <p className="text-[10px] text-muted-foreground/70">
                          {importResult.duplicates} duplicate{(importResult.duplicates ?? 0) > 1 ? 's' : ''}{dupDate}
                        </p>
                      )}
                    </div>
                  );
                })()}
              </div>
              <div className="mt-1.5 space-y-0.5">
                {sorted3.map(row => (
                  <div key={row.row_id} className="flex items-center gap-2 text-xs text-muted-foreground">
                    <ConfidenceDot level={row._match?.match_confidence} />
                    <span className="tabular-nums w-16 shrink-0">{rowDateLabel(row)}</span>
                    {row.detected_amount != null && (
                      <span className="tabular-nums">${Number(row.detected_amount).toFixed(2)}</span>
                    )}
                    {row.detected_name &&
                      row.detected_name.toLowerCase() !== bill.name.toLowerCase() && (
                      <span className="truncate italic opacity-60">"{row.detected_name}"</span>
                    )}
                  </div>
                ))}
                {rows.length > 3 && (
                  <button type="button" onClick={() => setSelectedBillId(bill.id)}
                    className="text-[10px] text-primary/70 hover:text-primary transition-colors">
                    +{rows.length - 3} more — view all
                  </button>
                )}
              </div>
            </div>
            <div className="flex flex-col gap-1 shrink-0 pt-0.5">
              {importResult ? (
                <Button size="sm" variant="outline" onClick={() => onImportBill(g)}
                  disabled={!!importingBillId || remainingCount === 0} className="h-7 text-xs px-3 gap-1.5">
                  {remainingCount === 0 ? 'All imported' : `Import ${remainingCount}`}
                </Button>
              ) : (
                <Button size="sm" onClick={() => onImportBill(g)}
                  disabled={!!importingBillId} className="h-7 text-xs px-3 gap-1.5">
                  {isImporting && <Loader2 className="h-3 w-3 animate-spin" />}
                  Import {rows.length}
                </Button>
              )}
              <Button size="sm" variant="ghost" onClick={() => setSelectedBillId(bill.id)}
                disabled={!!importingBillId} className="h-7 text-xs px-3">
                Review
              </Button>
            </div>
          </div>
        );
      })}
    </div>
  );
}

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

export default function ImportSpreadsheetSection({ onHistoryRefresh, cardProps = {} }: {
  onHistoryRefresh?: () => void;
  cardProps?: Partial<SectionCardProps>;
}) {
  const fileRef = useRef<HTMLInputElement>(null);
  const [file,    setFile]    = useState<File | null>(null);
  const [options, setOptions] = useState<Options>(INITIAL_OPTIONS);
  const [preview, setPreview] = useState<PreviewState>({ status: 'idle', data: null, error: null });
  const [decisions, setDecisions]  = useState<Record<string, Decision>>({});
  const [applyState, setApplyState] = useState<ApplyState>({ status: 'idle', result: null, error: null });
  const [allBills, setAllBills]    = useState<Bill[]>([]);
  const [categories, setCategories] = useState<Category[]>([]);
  const [selectedRows, setSelectedRows] = useState<Set<number | string>>(new Set());
  const [viewMode, setViewMode]    = useState<'rows' | 'bills'>('rows');
  const [importingBillId,  setImportingBillId]  = useState<number | string | null>(null);
  const [billImportResults, setBillImportResults] = useState<Map<number | string, BillImportResult>>(new Map());

  // Load bills/categories for the decision controls
  useEffect(() => {
    api.bills().then(b => setAllBills(b as Bill[])).catch(err => console.error('[ImportSpreadsheetSection] failed to load bills', err));
    api.categories().then(c => setCategories(c as Category[])).catch(err => console.error('[ImportSpreadsheetSection] failed to load categories', err));
  }, []);

  const opt = (k: keyof Options, v: boolean | string) => setOptions(prev => ({ ...prev, [k]: v }));

  // ── Preview ──────────────────────────────────────────────────────────────────
  const handlePreview = async () => {
    if (!file) return;
    setPreview({ status: 'loading', data: null, error: null });
    setDecisions({});
    setSelectedRows(new Set());
    setApplyState({ status: 'idle', result: null, error: null });
    setViewMode('rows');
    setImportingBillId(null);
    setBillImportResults(new Map());
    try {
      const data = await api.previewSpreadsheetImport(file, {
        parseAllSheets: options.parseAllSheets,
        defaultYear:    options.defaultYear ? parseInt(String(options.defaultYear), 10) : undefined,
        defaultMonth:   options.defaultMonth ? parseInt(String(options.defaultMonth), 10) : undefined,
      }) as PreviewData;
      setPreview({ status: 'ready', data, error: null });
      setDecisions(buildInitialDecisions(data.rows));
    } catch (err) {
      setPreview({ status: 'error', data: null, error: importErrorState(err, 'Preview failed.') });
    }
  };

  // ── Decision update ──────────────────────────────────────────────────────────
  const handleDecisionChange = (rowId: number | string, decision: Decision) => {
    setDecisions(prev => ({ ...prev, [rowId]: decision }));
  };

  const handleSelectedChange = (rowId: number | string, selected: boolean) => {
    setSelectedRows(prev => {
      const next = new Set(prev);
      if (selected) next.add(rowId);
      else next.delete(rowId);
      return next;
    });
  };

  const clearSelection = () => setSelectedRows(new Set());

  // ── Bill-history direct import ────────────────────────────────────────────
  // Applies all matching rows for a bill immediately — no queue, no review step.
  const handleDirectImportBill = async (group: BillGroup) => {
    const sessionId = preview.data?.import_session_id;
    if (!sessionId || importingBillId) return;

    const previousResult = billImportResults.get(group.bill.id) ?? null;
    const rowsToImport = billImportProgress(group.rows, previousResult).remainingRows;
    if (rowsToImport.length === 0) {
      toast.info(`All rows for "${group.bill.name}" have already been imported.`);
      return;
    }

    setImportingBillId(group.bill.id);
    try {
      const decisionsList = rowsToImport.map(row => ({
        row_id:         row.row_id,
        action:         'match_existing_bill',
        bill_id:        group.bill.id,
        actual_amount:  row.detected_amount ?? null,
        payment_amount: row.detected_payment_amount ?? row.detected_amount ?? null,
        payment_date:   row.detected_paid_date ?? null,
      }));

      const result = await api.applySpreadsheetImport({
        import_session_id: sessionId,
        decisions:         decisionsList,
        options:           {},
      }) as ApplyResult;

      const created    = result.rows_created    ?? 0;
      const updated    = result.rows_updated    ?? 0;
      const errored    = result.rows_errored    ?? 0;
      const details    = result.details ?? [];
      const duplicateRowIds = new Set(
        details
          .filter(d => d.result === 'skipped_duplicate' || d.payment === 'skipped_duplicate')
          .map(d => d.row_id)
          .filter(Boolean),
      );
      const duplicates = duplicateRowIds.size || (result.rows_duplicates ?? 0);

      // Collect created_at dates from duplicate detail entries so we can show
      // when the existing payments were originally recorded.
      const dupDates = details
        .filter(d => (d.result === 'skipped_duplicate' || d.payment === 'skipped_duplicate') && d.existing_created_at)
        .map(d => new Date(d.existing_created_at as string))
        .filter(d => !isNaN(d.getTime()))
        .sort((a, b) => a.getTime() - b.getTime());

      const earliestDup = dupDates[0]   ?? null;
      const latestDup   = dupDates.at(-1) ?? null;
      const completedRowIds = new Set<number | string>(previousResult?.completedRowIds ?? []);
      const erroredRowIds = new Set<number | string>(previousResult?.erroredRowIds ?? []);

      for (const detail of details) {
        if (detailCompletesImport(detail)) {
          if (detail.row_id != null) { completedRowIds.add(detail.row_id); erroredRowIds.delete(detail.row_id); }
        } else if (['error', 'ambiguous', 'skipped_conflict'].includes(detail?.result ?? '')) {
          if (detail.row_id != null) erroredRowIds.add(detail.row_id);
        }
      }

      const mergedResult: BillImportResult = {
        created:    (previousResult?.created ?? 0) + created,
        updated:    (previousResult?.updated ?? 0) + updated,
        errored:    erroredRowIds.size,
        duplicates: (previousResult?.duplicates ?? 0) + duplicates,
        earliestDup: previousResult?.earliestDup && earliestDup
          ? (previousResult.earliestDup < earliestDup ? previousResult.earliestDup : earliestDup)
          : (previousResult?.earliestDup ?? earliestDup),
        latestDup: previousResult?.latestDup && latestDup
          ? (previousResult.latestDup > latestDup ? previousResult.latestDup : latestDup)
          : (previousResult?.latestDup ?? latestDup),
        completedRowIds,
        erroredRowIds,
      };

      setBillImportResults(prev => new Map(prev).set(group.bill.id, mergedResult));

      const imported = created + updated;
      const fmtDate  = (d?: Date | null) => d?.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
      const remainingCount = billImportProgress(group.rows, mergedResult).remainingCount;

      if (imported === 0 && duplicates > 0) {
        const dateHint = earliestDup
          ? ` (first recorded ${fmtDate(earliestDup)})`
          : '';
        toast.warning(
          remainingCount === 0
            ? `All rows for "${group.bill.name}" are now imported${dateHint}.`
            : `${duplicates} row${duplicates > 1 ? 's' : ''} for "${group.bill.name}" already exist${dateHint}. ${remainingCount} remaining.`,
        );
      } else {
        const parts = [`${imported} entr${imported === 1 ? 'y' : 'ies'} imported`];
        if (duplicates > 0) {
          const dateHint = earliestDup ? ` (recorded ${fmtDate(earliestDup)})` : '';
          parts.push(`${duplicates} already existed${dateHint}`);
        }
        if (errored > 0) parts.push(`${errored} error${errored > 1 ? 's' : ''}`);
        if (remainingCount > 0) parts.push(`${remainingCount} remaining`);
        toast.success(`${group.bill.name} — ${parts.join(' · ')}`);
      }
      onHistoryRefresh?.();
    } catch (err) {
      toast.error(errMessage(err, `Import failed for "${group.bill.name}"`));
    } finally {
      setImportingBillId(null);
    }
  };

  const selectAllVisibleRows = () => {
    setSelectedRows(new Set((preview.data?.rows || []).map(r => r.row_id)));
  };

  const selectedPreviewRows = (): PreviewRow[] => {
    const selected = selectedRows;
    return (preview.data?.rows || []).filter(r => selected.has(r.row_id));
  };

  const handleBulkSkip = () => {
    const rows = selectedPreviewRows();
    setDecisions(prev => {
      const next = { ...prev };
      rows.forEach(row => {
        next[row.row_id] = { ...(next[row.row_id] || {}), action: 'skip_row', bill_id: null, bill_name: null };
      });
      return next;
    });
    toast.success(`${rows.length} row${rows.length === 1 ? '' : 's'} marked skipped.`);
  };

  const handleBulkCreateNew = () => {
    const rows = selectedPreviewRows();
    let missingNames = 0;
    setDecisions(prev => {
      const next = { ...prev };
      rows.forEach(row => {
        const decision = buildCreateNewDecision(row, next[row.row_id] || {});
        if (!decision.bill_name?.trim()) missingNames++;
        next[row.row_id] = decision;
      });
      return next;
    });
    if (missingNames > 0) {
      toast.warning(`${missingNames} selected row${missingNames === 1 ? '' : 's'} still need a bill name.`);
    } else {
      toast.success(`${rows.length} row${rows.length === 1 ? '' : 's'} set to create new bills.`);
    }
  };

  const handleBulkReset = () => {
    const rows = selectedPreviewRows();
    setDecisions(prev => {
      const next = { ...prev };
      rows.forEach(row => {
        next[row.row_id] = initialDecisionFromRecommendation(row);
      });
      return next;
    });
    toast.success(`${rows.length} row${rows.length === 1 ? '' : 's'} reset to recommendation.`);
  };

  const buildApplyDecision = (row: PreviewRow, d: Decision | undefined) => {
    if (!d?.action) return null;

    const base = {
      row_id:        row.row_id,
      action:        d.action,
      actual_amount: d.actual_amount ?? row.detected_amount ?? undefined,
      year:          row.detected_year ?? undefined,
      month:         row.detected_month ?? undefined,
      notes:         d.notes ?? row.detected_notes ?? undefined,
      payment_amount: d.payment_amount ?? row.detected_payment_amount ?? undefined,
      payment_date:   d.payment_date ?? row.detected_paid_date ?? undefined,
    };

    if (d.action === 'create_new_bill') {
      return {
        ...base,
        bill_name:       d.bill_name?.trim() || undefined,
        category_id:     d.category_id ?? undefined,
        due_day:         d.due_day ?? undefined,
        expected_amount: d.expected_amount ?? undefined,
      };
    }

    if (ACTIONS_NEEDING_BILL.has(d.action)) {
      return {
        ...base,
        bill_id: d.bill_id ?? undefined,
      };
    }

    return base;
  };

  // ── Apply ────────────────────────────────────────────────────────────────────
  const handleApply = async () => {
    if (!preview.data) return;
    setApplyState({ status: 'loading', result: null, error: null });
    try {
      const decisionsList = preview.data.rows
        .map(row => {
          const d = decisions[row.row_id];
          if (d?.action === 'skip_row') return null;
          return buildApplyDecision(row, d);
        })
        .filter(Boolean);

      if (decisionsList.length === 0) {
        throw new Error('No rows are selected to import. Choose at least one row to match or create, or keep the preview for review.');
      }

      const result = await api.applySpreadsheetImport({
        import_session_id: preview.data.import_session_id,
        decisions:         decisionsList,
        options:           { reviewed_skipped_count: skipRows.length },
      }) as ApplyResult;
      setApplyState({ status: 'done', result, error: null });
      setSelectedRows(new Set());
      toast.success(`Import applied — ${result.rows_created} created, ${result.rows_updated} updated.`);
      onHistoryRefresh?.();
    } catch (err) {
      const errorState = importErrorState(err, 'Apply failed.');
      setApplyState({ status: 'error', result: null, error: errorState });
      toast.error(errorState.message || 'Apply failed.');
    }
  };

  // ── Reset ────────────────────────────────────────────────────────────────────
  const handleReset = () => {
    setFile(null);
    setOptions(INITIAL_OPTIONS);
    setPreview({ status: 'idle', data: null, error: null });
    setDecisions({});
    setSelectedRows(new Set());
    setApplyState({ status: 'idle', result: null, error: null });
    setViewMode('rows');
    setImportingBillId(null);
    setBillImportResults(new Map());
    if (fileRef.current) fileRef.current.value = '';
  };

  // ── Derived state ────────────────────────────────────────────────────────────
  const previewRows    = preview.data?.rows ?? [];
  const unresolvedRows = previewRows.filter(r => {
    const d = decisions[r.row_id];
    return !d?.action || !isDecisionComplete(d.action, d);
  });
  const pendingRows    = previewRows.filter(r => decisions[r.row_id]?.action && decisions[r.row_id]?.action !== 'skip_row');
  const skipRows       = previewRows.filter(r => decisions[r.row_id]?.action === 'skip_row');
  const canApply       = unresolvedRows.length === 0 && pendingRows.length > 0 && applyState.status !== 'loading';

  // ── Render ────────────────────────────────────────────────────────────────────
  return (
    <SectionCard
      title="Import Spreadsheet History"
      subtitle='Export your Google Sheet as .xlsx, upload it here, review the matches, then apply only the rows you approve.'
      {...cardProps}
    >

      {/* ── Upload panel ──────────────────────────────────────────────────────── */}
      <div className="px-6 py-5 space-y-5">

        {/* File picker */}
        <div className="space-y-1.5">
          <label className="text-xs font-medium text-muted-foreground">XLSX File</label>
          <div className="flex items-center gap-3">
            <input ref={fileRef} type="file" accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
              className="hidden" id="xlsx-upload"
              onChange={e => setFile(e.target.files?.[0] ?? null)} />
            <label htmlFor="xlsx-upload">
              <Button size="sm" variant="outline" asChild>
                <span className="cursor-pointer gap-2">
                  <Upload className="h-3.5 w-3.5" />
                  {file ? 'Change file' : 'Choose .xlsx file'}
                </span>
              </Button>
            </label>
            {file && (
              <span className="text-sm text-muted-foreground truncate max-w-[260px]">
                <FileSpreadsheet className="inline h-3.5 w-3.5 mr-1" />{file.name}
              </span>
            )}
          </div>
        </div>

        {/* Options */}
        <div className="flex flex-wrap items-center gap-x-8 gap-y-3">
          <div className="flex items-center gap-2.5">
            <Switch checked={options.parseAllSheets} onCheckedChange={v => opt('parseAllSheets', v)}
              id="parse-all" />
            <label htmlFor="parse-all" className="text-sm cursor-pointer">Parse all sheet tabs</label>
          </div>
          <div className="flex items-center gap-2">
            <label className="text-xs text-muted-foreground">Default year</label>
            <Input type="number" min="2000" max="2100" placeholder="2026"
              value={options.defaultYear}
              onChange={e => opt('defaultYear', e.target.value)}
              className="w-24 h-8 text-sm" />
          </div>
          {!options.parseAllSheets && (
            <div className="flex items-center gap-2">
              <label className="text-xs text-muted-foreground">Default month</label>
              <Input type="number" min="1" max="12" placeholder="1–12"
                value={options.defaultMonth}
                onChange={e => opt('defaultMonth', e.target.value)}
                className="w-20 h-8 text-sm" />
            </div>
          )}
        </div>

        {/* Preview button */}
        <div className="flex items-center gap-3">
          <Button size="sm" onClick={handlePreview}
            disabled={!file || preview.status === 'loading'}
            className="gap-2">
            {preview.status === 'loading'
              ? <><Loader2 className="h-3.5 w-3.5 animate-spin" />Parsing…</>
              : <><FileSpreadsheet className="h-3.5 w-3.5" />Preview Import</>}
          </Button>
          {(preview.status === 'ready' || preview.status === 'error' || applyState.status !== 'idle') && (
            <Button size="sm" variant="ghost" onClick={handleReset} className="text-muted-foreground">
              New import
            </Button>
          )}
        </div>

        {/* Error from preview */}
        {preview.status === 'error' && (
          <div className="rounded-lg bg-destructive/10 border border-destructive/30 p-3 text-sm text-destructive">
            <AlertTriangle className="inline h-4 w-4 mr-1.5" />{preview.error?.message || 'Preview failed.'}
            {(preview.error?.details?.length ?? 0) > 0 && (
              <ul className="mt-2 space-y-1 text-xs">
                {preview.error?.details.map((d, i) => {
                  const dd = d as ImportDetail;
                  return <li key={i}>{dd.row_id ? `${dd.row_id}: ` : ''}{dd.message}</li>;
                })}
              </ul>
            )}
          </div>
        )}
      </div>

      {/* ── Preview results ────────────────────────────────────────────────────── */}
      {preview.status === 'ready' && preview.data && !['loading', 'done'].includes(applyState.status) && (
        <div className="px-6 pb-5 space-y-4">

          {/* Workbook summary */}
          <WorkbookSummaryCard workbook={preview.data.workbook} />

          {/* Row decision table */}
          {previewRows.length > 0 ? (
            <div className="rounded-lg border border-border overflow-hidden bg-background">
              {/* Tab header */}
              <div className="px-4 py-3 border-b border-border bg-muted/40 flex items-center justify-between gap-3 flex-wrap">
                <div className="flex items-center gap-1 bg-background rounded-md border border-border p-0.5">
                  <button type="button" onClick={() => setViewMode('rows')}
                    className={cn('inline-flex items-center gap-1.5 rounded px-3 py-1.5 text-xs font-medium transition-colors',
                      viewMode === 'rows'
                        ? 'bg-primary text-primary-foreground shadow-sm'
                        : 'text-muted-foreground hover:text-foreground')}>
                    <List className="h-3.5 w-3.5" />
                    Rows ({previewRows.length})
                  </button>
                  <button type="button" onClick={() => setViewMode('bills')}
                    className={cn('inline-flex items-center gap-1.5 rounded px-3 py-1.5 text-xs font-medium transition-colors',
                      viewMode === 'bills'
                        ? 'bg-primary text-primary-foreground shadow-sm'
                        : 'text-muted-foreground hover:text-foreground')}>
                    <Building2 className="h-3.5 w-3.5" />
                    Bills
                  </button>
                </div>
                <span className="text-xs text-muted-foreground">
                  {viewMode === 'rows'
                    ? 'Select rows, apply bulk decisions, then import.'
                    : 'Click a bill to queue its entire history from this file.'}
                </span>
              </div>

              {/* Rows view */}
              {viewMode === 'rows' && (
                <>
                  <BulkActionBar
                    rows={previewRows}
                    selectedRows={selectedRows}
                    onSelectAll={selectAllVisibleRows}
                    onClearSelection={clearSelection}
                    onBulkSkip={handleBulkSkip}
                    onBulkCreateNew={handleBulkCreateNew}
                    onBulkReset={handleBulkReset}
                  />
                  <PreviewTable
                    rows={previewRows}
                    decisions={decisions}
                    onDecisionChange={handleDecisionChange}
                    allBills={allBills}
                    categories={categories}
                    selectedRows={selectedRows}
                    onSelectedChange={handleSelectedChange}
                  />
                </>
              )}

              {/* Bills view */}
              {viewMode === 'bills' && (
                <BillHistoryView
                  previewRows={previewRows}
                  allBills={allBills}
                  importingBillId={importingBillId}
                  billImportResults={billImportResults}
                  onImportBill={handleDirectImportBill}
                />
              )}
            </div>
          ) : (
            <p className="text-sm text-muted-foreground text-center py-4">No data rows found in this file.</p>
          )}

          {/* Apply bar */}
          {previewRows.length > 0 && (
            <div className="rounded-lg border border-border bg-muted/30 px-4 py-3 flex items-center justify-between flex-wrap gap-3">
              <div className="flex items-center gap-4 text-xs text-muted-foreground flex-wrap">
                <span className="text-foreground font-medium">{previewRows.length} rows reviewed</span>
                <span className="text-emerald-600">{pendingRows.length} to apply</span>
                <span>{skipRows.length} skipped</span>
                {unresolvedRows.length > 0 && (
                  <span className="text-orange-500 font-medium">{unresolvedRows.length} need a decision</span>
                )}
              </div>
              <Button size="sm" onClick={handleApply} disabled={!canApply} className="gap-2">
                <CheckCheck className="h-3.5 w-3.5" />
                Apply {pendingRows.length} rows
              </Button>
            </div>
          )}
        </div>
      )}

      {/* ── Applying ──────────────────────────────────────────────────────────── */}
      {applyState.status === 'loading' && (
        <div className="px-6 py-8 flex items-center justify-center gap-3 text-muted-foreground">
          <Loader2 className="h-5 w-5 animate-spin" />
          <span className="text-sm">Applying import…</span>
        </div>
      )}

      {/* ── Apply result ──────────────────────────────────────────────────────── */}
      {applyState.status === 'done' && applyState.result && (
        <div className="px-6 pb-6 space-y-4">
          <div className="rounded-lg border border-emerald-500/30 bg-emerald-500/5 p-4">
            <div className="flex items-center gap-2 mb-3">
              <CheckCircle2 className="h-5 w-5 text-emerald-500" />
              <p className="text-sm font-medium text-emerald-600">Import applied successfully</p>
            </div>
            <div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
              {([
                { label: 'Created',   value: applyState.result.rows_created,  color: 'text-emerald-600' },
                { label: 'Updated',   value: applyState.result.rows_updated,  color: 'text-blue-600'    },
                { label: 'Skipped',   value: applyState.result.rows_skipped,  color: 'text-muted-foreground' },
                { label: 'Errors',    value: applyState.result.rows_errored,  color: 'text-red-500'     },
              ]).map(({ label, value, color }) => (
                <div key={label} className="text-center">
                  <p className={cn('text-xl font-bold tabular-nums', color)}>{value}</p>
                  <p className="text-xs text-muted-foreground">{label}</p>
                </div>
              ))}
            </div>
          </div>
          <Button size="sm" variant="outline" onClick={handleReset} className="gap-2">
            <Plus className="h-3.5 w-3.5" />New Import
          </Button>
        </div>
      )}

      {/* ── Apply error ───────────────────────────────────────────────────────── */}
      {applyState.status === 'error' && (
        <div className="px-6 pb-5">
          <div className="rounded-lg bg-destructive/10 border border-destructive/30 p-3 text-sm text-destructive">
            <AlertTriangle className="inline h-4 w-4 mr-1.5" />{applyState.error?.message || 'Apply failed.'}
            {(applyState.error?.details?.length ?? 0) > 0 && (
              <ul className="mt-2 space-y-1 text-xs">
                {applyState.error?.details.map((d, i) => {
                  const dd = d as ImportDetail;
                  return (
                    <li key={i}>
                      {dd.row_id ? `${dd.row_id}: ` : ''}
                      {dd.field ? `${dd.field} - ` : ''}
                      {dd.message}
                    </li>
                  );
                })}
              </ul>
            )}
            {applyState.error?.error_id && (
              <p className="mt-2 text-xs opacity-80">Error ID: {applyState.error.error_id}</p>
            )}
          </div>
        </div>
      )}
    </SectionCard>
  );
}
