import { useCallback, useEffect, useMemo, useState } from 'react';
import { Plus, Trash2, RotateCcw, Save, Download, Upload, Loader2, Search, FolderOpen, ArrowUpCircle, Share2 } from 'lucide-react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import { Textarea } from '@/components/ui/textarea';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Combobox } from '@/components/ui/combobox';
import { cn } from '@/lib/utils';
import { useI18n } from '@/lib/i18n';
import {
  GetAwardDefs, SaveAwardDefs, ResetAwardDefs, AwardFields,
  GetAwardReferenceMeta, UpdateAwardReferenceList,
  ListAwardReferences, SearchAwardReferences, SaveAwardReference, DeleteAwardReference, RenameAwardReference,
  ImportAwardReferencesText, GetAwardPresets, ApplyAwardPreset,
  ListCountries, DXCCForCountry, DXCCName,
  PopulateBuiltinReferences, HasBuiltinReferences,
  ExportAwards, ImportAwards, InspectAwardImport, ApplyAwardImport, GetCatalogCodes, OpenAwardsFolder,
  ExportAwardForCatalog, ExportAward,
  GetAwardUpdates, ApplyAwardUpdate, DismissAwardUpdate, ExplainAward,
} from '../../wailsjs/go/main/App';

// Above this many references the editor stops loading the whole list and
// switches to search-only (mirrors Log4OM's "Too many items" behaviour).
const REF_LIST_CAP = 1000;

type RefMeta = { code: string; count: number; updated_at: string; can_update: boolean };

export type AwardDef = {
  code: string; name: string; description?: string; valid?: boolean; protected?: boolean;
  url?: string; download_url?: string; ref_url?: string; valid_from?: string; valid_to?: string; alias?: string;
  ref_display?: string; // grid column shows: ref | name | both
  type?: string; field: string; match_by?: string; exact_match?: boolean; one_ref_per_qso?: boolean; pattern: string;
  leading_str?: string; trailing_str?: string; prefix?: string; dynamic?: boolean;
  or_rules?: AwardOrRule[];
  dxcc_filter: number[] | null; valid_bands?: string[]; valid_modes?: string[]; emission?: string[];
  confirm: string[] | null; validate?: string[] | null; grant_codes?: string; export_credit_granted?: boolean;
  // The "custom" confirmation source: the field it reads and, optionally, the
  // value(s) that count. Empty value = any non-empty value confirms.
  confirm_field?: string; confirm_value?: string;
  total: number; builtin?: boolean; version?: number;
};

type AwardOrRule = {
  field: string; match_by?: string; exact_match?: boolean; pattern?: string;
  leading_str?: string; trailing_str?: string; prefix?: string;
};

type AwardRef = {
  code: string; name: string; dxcc: number; group: string; subgrp: string;
  dxcc_list?: number[]; pattern?: string; valid: boolean; valid_from?: string; valid_to?: string;
  score?: number; bonus?: number; gridsquare?: string; alias?: string;
};

type Preset = { key: string; name: string; field: string; dxcc: number; refs: AwardRef[] };

// Award types mirror Log4OM: REFERENCE (we supply a list), QSOFIELDS (scan any
// QSO field — handles state/grid4/zones/etc.), CALLSIGN. The type is
// organizational only; matching is driven by the field/pattern/dynamic options,
// so there's no need for separate GRID/DXCC types (use QSOFIELDS + the field).
const AWARD_TYPES = ['REFERENCE', 'QSOFIELDS', 'CALLSIGN'];
// Sentinel for "no QSO field at all". The definition stores an empty field, and
// the matcher already treats that as "nothing to scan" — this only gives the
// operator a way to ASK for it.
const NO_FIELD = '__none__';
const CONFIRM_SRC = [
  { id: 'lotw', label: 'LoTW' }, { id: 'qsl', label: 'QSL' }, { id: 'eqsl', label: 'eQSL' },
  { id: 'qrzcom', label: 'QRZ.com' }, { id: 'hamlog', label: 'HAMLOG.online' },
  { id: 'custom', label: 'Custom' },
];
const BANDS = ['2190m','630m','160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m','4m','2m','1.25m','70cm','33cm','23cm','13cm','9cm','6cm','3cm','1.25cm','6mm','4mm','2.5mm','2mm','1mm'];
const MODES = ['CW','SSB','USB','LSB','AM','FM','RTTY','PSK31','FT8','FT4','JT65','JT9','MFSK','OLIVIA','DIGITALVOICE'];
const EMISSIONS = ['CW', 'PHONE', 'DIGITAL'];

const emptyAward = (): AwardDef => ({
  code: 'NEW', name: 'New award', description: '', valid: true,
  type: 'QSOFIELDS', field: 'note', match_by: 'code', exact_match: true, pattern: '',
  dxcc_filter: null, confirm: ['lotw', 'qsl'], validate: ['lotw', 'qsl'], total: 0,
});

interface Props { open: boolean; onClose: () => void; onSaved: () => void; }

// Small reusable multi-toggle chip group.
function Chips({ all, value, onToggle }: { all: string[]; value: string[]; onToggle: (v: string) => void }) {
  return (
    <div className="flex flex-wrap gap-1">
      {all.map((v) => {
        const on = value.includes(v);
        return (
          <button key={v} type="button" onClick={() => onToggle(v)}
            className={cn('px-2 py-0.5 rounded text-[11px] border', on
              ? 'bg-primary text-primary-foreground border-primary'
              : 'bg-background border-border text-muted-foreground hover:bg-accent')}>
            {v}
          </button>
        );
      })}
    </div>
  );
}

// Awards use a far-future date as "no end" (RDA carries 9999-12-31). Printing it
// back at the operator reads like a real deadline, so show it as open-ended.
function openEnded(d?: string): string {
  if (!d) return '—';
  return /^9\d{3}-/.test(d) ? '—' : d;
}

function Field2({ label, children }: { label: string; children: React.ReactNode }) {
  return (
    <div className="grid grid-cols-[120px_1fr] items-center gap-2">
      <Label className="text-xs text-muted-foreground">{label}</Label>
      {children}
    </div>
  );
}

// DxccFilter — pick the entities an award is scoped to by country name (like the
// QSO editor), resolving each to its ADIF DXCC number. Stored as number[].
function DxccFilter({ value, onChange, countries }: { value: number[]; onChange: (v: number[]) => void; countries: string[] }) {
  const { t } = useI18n();
  const [names, setNames] = useState<Record<number, string>>({});
  useEffect(() => {
    let live = true;
    (async () => {
      const miss = value.filter((n) => names[n] === undefined);
      if (miss.length === 0) return;
      const got: Record<number, string> = {};
      for (const n of miss) { try { got[n] = await DXCCName(n); } catch { got[n] = ''; } }
      if (live) setNames((m) => ({ ...m, ...got }));
    })();
    return () => { live = false; };
  }, [value]); // eslint-disable-line react-hooks/exhaustive-deps

  async function addCountry(name: string) {
    const n = await DXCCForCountry(name);
    if (n && n > 0 && !value.includes(n)) {
      setNames((m) => ({ ...m, [n]: name }));
      onChange([...value, n]);
    }
  }
  return (
    <div className="space-y-1.5">
      <Combobox value="" options={countries} placeholder={t('awed.addCountry')} onChange={addCountry} className="h-8 w-full" />
      {value.length > 0 && (
        <div className="flex flex-wrap gap-1">
          {value.map((n) => (
            <span key={n} className="flex items-center gap-1 px-1.5 py-0.5 rounded text-[11px] bg-accent border border-border">
              <span className="font-mono">#{n}</span>
              <span className="text-muted-foreground">{names[n] || '…'}</span>
              <button className="hover:text-destructive" onClick={() => onChange(value.filter((x) => x !== n))}>×</button>
            </span>
          ))}
        </div>
      )}
    </div>
  );
}

export function AwardEditor({ open, onClose, onSaved }: Props) {
  const { t } = useI18n();
  const [defs, setDefs] = useState<AwardDef[]>([]);
  const [fields, setFields] = useState<string[]>([]);
  const [meta, setMeta] = useState<Record<string, RefMeta>>({});
  const [presets, setPresets] = useState<Preset[]>([]);
  const [countries, setCountries] = useState<string[]>([]);
  const [sel, setSel] = useState(0);
  const [search, setSearch] = useState('');
  const [updating, setUpdating] = useState<string | null>(null);
  const [err, setErr] = useState('');
  // Version to stamp into a "publish for catalog" export — defaults to one past
  // the selected award's current version whenever the selection changes.
  const [catVer, setCatVer] = useState('1');

  // The err banner doubles as a success/notice area (export path, import counts,
  // "populated N refs"). Auto-dismiss it after a few seconds so it doesn't stay
  // forever; the longer text (export path) gets a bit more time.
  useEffect(() => {
    if (!err) return;
    const t = window.setTimeout(() => setErr(''), 8000);
    return () => window.clearTimeout(t);
  }, [err]);

  const loadMeta = () => GetAwardReferenceMeta()
    .then((m) => setMeta(Object.fromEntries(((m ?? []) as RefMeta[]).map((x) => [x.code.toUpperCase(), x]))))
    .catch(() => {});

  useEffect(() => {
    if (!open) return;
    setErr('');
    Promise.all([GetAwardDefs(), AwardFields(), GetAwardPresets(), ListCountries()])
      .then(([d, f, p, c]) => { setDefs((d ?? []) as any); setFields(((f ?? []) as string[]).slice().sort((a, b) => a.localeCompare(b))); setPresets((p ?? []) as any); setCountries((c ?? []) as any); })
      .catch((e) => setErr(String(e?.message ?? e)));
    loadMeta();
  }, [open]);

  // Codes present in the SHIPPED catalog. Anything in the database that isn't in
  // here exists only on this machine — it is yours alone, and a reinstall loses it
  // unless it has been exported. The list flags those.
  const [catalogCodes, setCatalogCodes] = useState<string[]>([]);
  useEffect(() => {
    if (!open) return;
    GetCatalogCodes().then((c: any) => setCatalogCodes(((c ?? []) as string[]).map((s) => s.toUpperCase()))).catch(() => {});
  }, [open]);

  // Shipped fixes we did NOT apply, because this award carries the operator's own
  // changes and we will not overwrite those behind their back. Offered, not forced.
  type AwardUpdate = { code: string; name: string; from: number; to: number };
  const [updates, setUpdates] = useState<AwardUpdate[]>([]);
  const loadUpdates = useCallback(() => {
    GetAwardUpdates().then((u: any) => setUpdates(Array.isArray(u) ? u : [])).catch(() => {});
  }, []);
  useEffect(() => { if (open) loadUpdates(); }, [open, loadUpdates]);

  // Pending import awaiting the operator's decision on the awards that collide.
  type ImportEntry = { code: string; name: string; references: number; exists: boolean; mine_name: string; mine_refs: number; protected: boolean };
  type ImportPreview = { path: string; awards: ImportEntry[] };
  const [importPreview, setImportPreview] = useState<ImportPreview | null>(null);
  const [decisions, setDecisions] = useState<Record<string, string>>({});
  useEffect(() => {
    if (!importPreview) return;
    // Default to the SAFE choice: keep what you have. An import must never destroy
    // an award because the operator clicked through a dialog without reading it.
    const d: Record<string, string> = {};
    for (const e of importPreview.awards) d[e.code] = e.exists ? 'skip' : 'replace';
    setDecisions(d);
  }, [importPreview]);

  const cur = defs[sel];
  const selUpdate = updates.find((u) => (u.code ?? '').toUpperCase() === (cur?.code ?? '').toUpperCase()) ?? null;
  useEffect(() => { setCatVer(String((cur?.version ?? 0) + 1)); }, [cur?.code]);

  // ── Award tester: run the award's rules against a real QSO and show every step.
  type Rejected = { candidate: string; reason: string };
  type Step = {
    rule: string; field: string; match_by?: string; exact?: boolean; pattern?: string;
    field_value?: string; candidates?: string[]; kept?: string[]; rejected?: Rejected[];
    skipped?: boolean; error?: string;
  };
  type Explanation = {
    code: string; in_scope: boolean; scope_error?: string; predefined: boolean;
    ref_count: number; steps: Step[]; manual?: string[]; ambiguous?: string[]; result: string[];
  };
  type TestRow = { qso: any; explanation: Explanation };
  const [testCall, setTestCall] = useState('');
  const [testRows, setTestRows] = useState<TestRow[] | null>(null);
  const [testErr, setTestErr] = useState('');
  const [testing, setTesting] = useState(false);
  const runTest = async () => {
    if (!cur) return;
    setTesting(true); setTestErr(''); setTestRows(null);
    try {
      const r = await ExplainAward(cur.code, testCall);
      setTestRows((Array.isArray(r) ? r : []) as TestRow[]);
    } catch (e: any) {
      setTestErr(String(e?.message ?? e));
    } finally {
      setTesting(false);
    }
  };
  // The tester reads the SAVED award, not the unsaved edits in this dialog — so say
  // so, rather than let the operator test a rule they only think they applied.
  useEffect(() => { setTestRows(null); setTestErr(''); }, [sel]);

  // Several QSOs with the same station usually trace IDENTICALLY, and twenty copies
  // of the same trace is noise. But they don't always: scope is judged per QSO
  // (band, mode, date — FFMA's 1983 cut-off), a manual override lives ON a QSO, and
  // two contacts can even hold different QTHs. That divergence is often the actual
  // answer ("why does my 2019 QSO count and my 2024 one not?"), so we group by
  // trace instead of dropping it: one card per distinct outcome, with its count.
  const testGroups = useMemo(() => {
    if (!testRows) return null;
    const groups: { key: string; rows: TestRow[] }[] = [];
    for (const row of testRows) {
      const key = JSON.stringify(row.explanation);
      const g = groups.find((x) => x.key === key);
      if (g) g.rows.push(row);
      else groups.push({ key, rows: [row] });
    }
    return groups;
  }, [testRows]);
  const noField = !String(cur?.field ?? '').trim();
  const patch = (p: Partial<AwardDef>) => setDefs((ds) => ds.map((d, j) => (j === sel ? { ...d, ...p } : d)));
  const toggleIn = (key: keyof AwardDef, v: string) => {
    const arr = ((cur?.[key] as string[]) ?? []);
    patch({ [key]: arr.includes(v) ? arr.filter((x) => x !== v) : [...arr, v] } as any);
  };

  function addAward() {
    setDefs((ds) => [...ds, emptyAward()]);
    setSel(defs.length);
  }
  function removeAward(i: number) {
    setDefs((ds) => ds.filter((_, j) => j !== i));
    setSel((s) => Math.max(0, s >= i ? s - 1 : s));
  }

  async function save() {
    setErr('');
    try {
      const clean = defs.filter((d) => d.code.trim())
        .map((d) => ({ ...d, code: d.code.trim().toUpperCase(), confirm: d.confirm ?? [], validate: d.validate ?? [] }));
      await SaveAwardDefs(clean as any);
      onSaved();
      onClose();
    } catch (e: any) { setErr(String(e?.message ?? e)); }
  }
  async function reset() {
    try { setDefs((await ResetAwardDefs()) as any); setSel(0); } catch (e: any) { setErr(String(e?.message ?? e)); }
  }
  // Export every award definition + reference list to a JSON bundle (backup
  // that survives a reinstall / PC change, independent of the database).
  async function exportAwards() {
    setErr('');
    try {
      const p = await ExportAwards();
      if (p) setErr(t('awed.exportedTo', { path: p }));
    } catch (e: any) { setErr(String(e?.message ?? e)); }
  }
  // Export the SELECTED award on its own — the unit you actually share. The
  // bundle above is a backup: sending it to someone hands over your whole
  // catalogue when they asked for one award.
  //
  // Distinct from the catalog publish below, which is for shipping an award INTO
  // OpsLog: that one stamps a version and clears user_edited. This one is a plain
  // share and leaves both alone, so the recipient's copy is correctly marked as
  // someone else's work rather than as a pristine built-in.
  async function exportOne() {
    setErr('');
    if (!cur) return;
    try {
      const code = cur.code.trim().toUpperCase();
      const p = await ExportAward(code);
      if (p) setErr(t('awed.exportedOneTo', { code, path: p }));
    } catch (e: any) { setErr(String(e?.message ?? e)); }
  }
  // Export the SELECTED award as a catalog-ready JSON, stamped with a version, to
  // paste over internal/award/catalog/<code>.json. A new release then ships it to
  // the whole team (unedited copies auto-upgrade; edited ones are offered it).
  async function exportForCatalog() {
    setErr('');
    if (!cur) return;
    const v = Math.trunc(Number(catVer));
    if (!Number.isFinite(v) || v < 1) { setErr(t('awed.catalogBadVersion')); return; }
    try {
      const p = await ExportAwardForCatalog(cur.code.trim().toUpperCase(), v);
      if (p) setErr(t('awed.catalogExportedTo', { path: p }));
    } catch (e: any) { setErr(String(e?.message ?? e)); }
  }
  // Import: LOOK FIRST, then ask.
  //
  // This used to merge by code with "imported wins", silently — import a WAPC
  // someone shared and YOUR WAPC (its province list, its city regexes) was
  // destroyed without a word. Sharing awards is exactly what we want people to do,
  // so it must not be a data-loss trap.
  async function importAwards() {
    setErr('');
    try {
      const p: any = await InspectAwardImport();
      if (!p?.path) return; // cancelled
      const clashes = (p.awards ?? []).filter((e: any) => e.exists);
      if (clashes.length === 0) {
        // Nothing collides — nothing to ask. This is the common case.
        const dec: Record<string, string> = {};
        for (const e of p.awards) dec[e.code] = 'replace';
        await applyImport(p.path, dec);
        return;
      }
      setImportPreview(p); // → the collision dialog decides
    } catch (e: any) { setErr(String(e?.message ?? e)); }
  }

  async function applyImport(path: string, decisions: Record<string, string>) {
    try {
      const r: any = await ApplyAwardImport(path, decisions);
      setImportPreview(null);
      const [d] = await Promise.all([GetAwardDefs(), loadMeta()]);
      setDefs((d ?? []) as any); setSel(0);
      onSaved();
      if (r?.awards || r?.references) {
        setErr(t('awed.importedMsg', { awards: r.awards, references: r.references }));
      }
    } catch (e: any) { setErr(String(e?.message ?? e)); }
  }
  async function updateList(code: string) {
    setUpdating(code); setErr('');
    try { await UpdateAwardReferenceList(code); await loadMeta(); }
    catch (e: any) { setErr(`${code}: ${String(e?.message ?? e)}`); }
    finally { setUpdating(null); }
  }

  const filtered = useMemo(() => {
    const q = search.trim().toUpperCase();
    // Keep the original index `i` (used for selection/patch) but display the
    // list sorted alphabetically by code — scales as the catalogue grows.
    return defs.map((d, i) => ({ d, i }))
      .filter(({ d }) => !q || d.code.toUpperCase().includes(q) || d.name.toUpperCase().includes(q))
      .sort((a, b) => a.d.code.localeCompare(b.d.code));
  }, [defs, search]);

  return (
    <Dialog open={open} onOpenChange={(o) => { if (!o) onClose(); }}>
      <DialogContent className="max-w-6xl w-[95vw] max-h-[92vh] grid grid-rows-[auto_1fr_auto] gap-0 p-0">
        <DialogHeader className="px-5 py-3 border-b">
          <DialogTitle>{t('awed.awardManagement')}</DialogTitle>
        </DialogHeader>

        <div className="grid grid-cols-[220px_1fr] min-h-0 overflow-hidden">
          {/* Left: award list */}
          <div className="border-r flex flex-col min-w-0 min-h-0">
            <div className="p-2 border-b">
              <div className="relative">
                <Search className="absolute left-2 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground" />
                <Input className="h-7 pl-7 text-xs" placeholder={t('awed.searchAwards')} value={search} onChange={(e) => setSearch(e.target.value)} />
              </div>
            </div>
            <div className="flex-1 overflow-auto">
              {filtered.map(({ d, i }) => {
                // An award that is NOT in the shipped catalog exists only in THIS
                // database: nobody else has it, and a reinstall loses it unless it
                // has been exported. That deserves to be visible at a glance, not
                // discovered the hard way.
                const onlyHere = !catalogCodes.includes((d.code ?? '').toUpperCase());
                // A pending update is only reachable from the award's own banner, so
                // the list has to say which award to open — otherwise the fix waits
                // behind a click nobody knows to make.
                const hasUpdate = updates.some((u) => (u.code ?? '').toUpperCase() === (d.code ?? '').toUpperCase());
                return (
                  <button key={i} onClick={() => setSel(i)}
                    className={cn('flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs border-b border-border/30',
                      i === sel ? 'bg-accent' : 'hover:bg-accent/50')}>
                    <span className={cn('size-1.5 rounded-full shrink-0', d.valid === false ? 'bg-muted-foreground/40' : 'bg-success')} />
                    <span className="font-mono font-semibold shrink-0">{d.code}</span>
                    <span className="text-muted-foreground truncate">{d.name}</span>
                    {hasUpdate && (
                      <span className="ml-auto shrink-0" title={t('awed.updateAvailable')}>
                        <ArrowUpCircle className="size-3.5 text-info" />
                      </span>
                    )}
                    {onlyHere && (
                      <span className="ml-auto shrink-0 px-1 rounded border border-warning-border bg-warning-muted text-warning-muted-foreground text-[9px] font-semibold uppercase tracking-wide"
                        title={t('awed.onlyHereTip')}>
                        {t('awed.onlyHere')}
                      </span>
                    )}
                  </button>
                );
              })}
            </div>
            <Button variant="ghost" size="sm" className="m-2 h-7 justify-start" onClick={addAward}>
              <Plus className="size-3.5 mr-1" /> {t('awed.newAward')}
            </Button>
          </div>

          {/* Right: tabbed editor for selected award.
              min-w-0 is load-bearing: this sits in a `1fr` grid track, and a 1fr
              track has min-width:auto — it refuses to shrink below its content's
              intrinsic width, so a wide row (the band/mode chip lists, a long
              translated label) grew the track and pushed the whole editor out
              past the dialog instead of scrolling inside it. Same reason on the
              Tabs below: a flex item defaults to min-width:auto too. */}
          <div className="flex flex-col min-w-0 min-h-0 overflow-hidden">
            {err && <div onClick={() => setErr('')} title={t('awed.clickToDismiss')} className="mx-4 mt-3 text-xs text-destructive bg-destructive/10 border border-destructive/30 rounded px-3 py-1.5 whitespace-pre-line break-all cursor-pointer">{err}</div>}
            {/* A fix shipped for an award this operator has customised. We did NOT
                apply it — that would destroy their work — so we offer it, and say
                plainly what accepting costs. */}
            {cur && selUpdate && (
              <div className="mx-4 mt-3 rounded border border-info/40 bg-info/10 px-3 py-2 text-xs">
                <div className="flex items-center gap-2">
                  <ArrowUpCircle className="size-4 text-info shrink-0" />
                  <div className="flex-1 min-w-0">
                    <div className="font-medium">{t('awed.updateAvailable')}</div>
                    <div className="text-muted-foreground">{t('awed.updateOverwrites')}</div>
                  </div>
                  <Button size="sm" className="h-7 px-2 text-[11px]"
                    onClick={async () => {
                      try {
                        await ApplyAwardUpdate(cur.code);
                        // The backend rewrote this award (and its references) — pull the
                        // new state back, or the editor would keep showing, and on the
                        // next Save re-persist, the definition we just replaced.
                        setDefs(((await GetAwardDefs()) ?? []) as any);
                        loadMeta();
                        loadUpdates();
                      } catch (e: any) { setErr(String(e?.message ?? e)); }
                    }}>{t('awed.updateApply')}</Button>
                  <Button size="sm" variant="ghost" className="h-7 px-2 text-[11px]"
                    onClick={async () => {
                      try { await DismissAwardUpdate(cur.code); loadUpdates(); }
                      catch (e: any) { setErr(String(e?.message ?? e)); }
                    }}>{t('awed.updateKeepMine')}</Button>
                </div>
              </div>
            )}
            {!cur ? (
              <div className="flex-1 grid place-items-center text-sm text-muted-foreground">{t('awed.selectOrCreate')}</div>
            ) : (
              <Tabs defaultValue="info" className="flex flex-col min-w-0 min-h-0 overflow-hidden">
                <TabsList className="px-3 justify-start">
                  <TabsTrigger value="info">{t('awed.tabInfo')}</TabsTrigger>
                  <TabsTrigger value="type">{t('awed.tabType')}</TabsTrigger>
                  <TabsTrigger value="conf">{t('awed.tabConfirmation')}</TabsTrigger>
                  <TabsTrigger value="refs">{t('awed.tabReferences')}</TabsTrigger>
                  <TabsTrigger value="test">{t('awed.tabTest')}</TabsTrigger>
                </TabsList>

                <div className="flex-1 overflow-auto p-4">
                  {/* ── Award info ── */}
                  <TabsContent value="info" className="mt-0 space-y-2.5">
                    <div className="flex items-center gap-2">
                      <Input className="h-8 w-28 font-mono font-semibold" value={cur.code} onChange={(e) => patch({ code: e.target.value })} placeholder="CODE" />
                      <Input className="h-8 flex-1" value={cur.name} onChange={(e) => patch({ name: e.target.value })} placeholder={t('awed.awardName')} />
                      <label className="flex items-center gap-1.5 text-xs cursor-pointer"><Checkbox checked={cur.valid !== false} onCheckedChange={(c) => patch({ valid: !!c })} /> {t('awed.valid')}</label>
                      {/* No "Built-in" checkbox: an award OpsLog ships IS built-in, and
                          the catalog derives that on load. Asking the author to tick a
                          box to declare it would be one more step nobody can guess —
                          forget it and the award silently misses every future catalog
                          correction. "Protected" stays: whether an award can be deleted
                          IS a real choice. */}
                      <label className="flex items-center gap-1.5 text-xs cursor-pointer" title={t('awed.protectedTip')}>
                        <Checkbox checked={!!cur.protected} onCheckedChange={(c) => patch({ protected: !!c })} /> {t('awed.protectedFlag')}
                      </label>
                      <button className="text-muted-foreground hover:text-destructive" title={t('awed.deleteAward')} onClick={() => removeAward(sel)}><Trash2 className="size-4" /></button>
                    </div>
                    <Field2 label={t('awed.description')}><Input className="h-8" value={cur.description ?? ''} onChange={(e) => patch({ description: e.target.value })} /></Field2>
                    <Field2 label={t('awed.awardUrl')}><Input className="h-8" value={cur.url ?? ''} onChange={(e) => patch({ url: e.target.value })} /></Field2>
                    <Field2 label={t('awed.referenceUrl')}><Input className="h-8" value={cur.ref_url ?? ''} onChange={(e) => patch({ ref_url: e.target.value })} placeholder="https://…/<REF>" /></Field2>
                    <Field2 label={t('awed.refDisplay')}>
                      <Select value={cur.ref_display || 'ref'} onValueChange={(v) => patch({ ref_display: v })}>
                        <SelectTrigger className="h-8 text-xs w-48"><SelectValue /></SelectTrigger>
                        <SelectContent>
                          <SelectItem value="ref">{t('awed.refDisplayRef')}</SelectItem>
                          <SelectItem value="name">{t('awed.refDisplayName')}</SelectItem>
                          <SelectItem value="both">{t('awed.refDisplayBoth')}</SelectItem>
                        </SelectContent>
                      </Select>
                    </Field2>
                    <div className="grid grid-cols-2 gap-3">
                      <Field2 label={t('awed.validFrom')}><Input type="date" className="h-8" value={cur.valid_from ?? ''} onChange={(e) => patch({ valid_from: e.target.value })} /></Field2>
                      <Field2 label={t('awed.validTo')}><Input type="date" className="h-8" value={cur.valid_to ?? ''} onChange={(e) => patch({ valid_to: e.target.value })} /></Field2>
                    </div>
                    <div className="grid grid-cols-[120px_1fr] gap-2">
                      <Label className="text-xs text-muted-foreground pt-1.5">{t('awed.dxccFilter')}</Label>
                      <DxccFilter value={cur.dxcc_filter ?? []} onChange={(v) => patch({ dxcc_filter: v })} countries={countries} />
                    </div>
                    <div className="space-y-1"><Label className="text-xs text-muted-foreground">{t('awed.validBands')}</Label><Chips all={BANDS} value={cur.valid_bands ?? []} onToggle={(v) => toggleIn('valid_bands', v)} /></div>
                    <div className="space-y-1"><Label className="text-xs text-muted-foreground">{t('awed.emission')}</Label><Chips all={EMISSIONS} value={cur.emission ?? []} onToggle={(v) => toggleIn('emission', v)} /></div>
                    <div className="space-y-1"><Label className="text-xs text-muted-foreground">{t('awed.validModes')}</Label><Chips all={MODES} value={cur.valid_modes ?? []} onToggle={(v) => toggleIn('valid_modes', v)} /></div>
                  </TabsContent>

                  {/* ── Award type ── */}
                  <TabsContent value="type" className="mt-0 space-y-2.5">
                    <Field2 label={t('awed.awardType')}>
                      <Select value={cur.type || 'QSOFIELDS'} onValueChange={(v) => patch({ type: v })}>
                        <SelectTrigger className="h-8 text-xs w-48"><SelectValue /></SelectTrigger>
                        <SelectContent>{
                          // Keep a legacy type (DXCC/GRID) selectable if an existing
                          // award still uses it, so it isn't silently changed.
                          (cur.type && !AWARD_TYPES.includes(cur.type) ? [cur.type, ...AWARD_TYPES] : AWARD_TYPES)
                            .map((t) => <SelectItem key={t} value={t}>{t}</SelectItem>)
                        }</SelectContent>
                      </Select>
                    </Field2>
                    {/* No "allow multiple references" switch: a QSO always counts for
                        every reference its field holds (an n-fer POTA activation, a
                        VUCC grid-line contact). The old checkbox was read by nothing. */}
                    <label className="flex items-center gap-2 text-xs cursor-pointer"><Checkbox checked={!!cur.dynamic} onCheckedChange={(c) => patch({ dynamic: !!c })} /> {t('awed.dynamicRefs')}</label>

                    <div className="border-t pt-2.5 mt-1 space-y-2.5">
                      <p className="text-[11px] text-muted-foreground">{t('awed.qsoParams')}</p>
                      <Field2 label={t('awed.searchInField')}>
                        {/* NO_FIELD is a sentinel because the stored value is the
                            empty string and a Select cannot carry one — an empty
                            item value means "show the placeholder" to Radix. */}
                        {/* Picking "no field" clears the fallbacks too: it promises that
                            nothing is matched automatically, and an OR rule left behind goes
                            on scanning its own field — worse than a visible one, because the
                            panel stops showing it. */}
                        <Select value={cur.field || NO_FIELD}
                          onValueChange={(v) => patch(v === NO_FIELD ? { field: '', or_rules: [] } : { field: v })}>
                          <SelectTrigger className="h-8 text-xs w-56"><SelectValue /></SelectTrigger>
                          <SelectContent className="max-h-72">
                            <SelectItem value={NO_FIELD}>{t('awed.fieldNone')}</SelectItem>
                            {fields.map((f) => <SelectItem key={f} value={f}>{f}</SelectItem>)}
                          </SelectContent>
                        </Select>
                      </Field2>
                      {/* An award whose references live in no ADIF field at all —
                          WWBOTA has no column anywhere — has nothing to match on,
                          and every control below would be a question with no
                          answer. Hiding them is the point: the references are the
                          ones the operator assigns to a QSO by hand. */}
                      {noField ? (
                        <p className="text-[11px] text-muted-foreground pl-[128px]">{t('awed.fieldNoneHint')}</p>
                      ) : (<>
                      <Field2 label={t('awed.matchBy')}>
                        <div className="flex items-center gap-3 text-xs">
                          {['code', 'description', 'pattern'].map((m) => (
                            <label key={m} className="flex items-center gap-1.5 cursor-pointer">
                              <input type="radio" name="matchby" checked={(cur.match_by || 'code') === m} onChange={() => patch({ match_by: m })} className="accent-primary" /> {m}
                            </label>
                          ))}
                        </div>
                      </Field2>
                      <label className="flex items-center gap-2 text-xs cursor-pointer pl-[128px]"><Checkbox checked={!!cur.exact_match} onCheckedChange={(c) => patch({ exact_match: !!c })} /> {t('awed.exactMatch')}</label>
                      <label className="flex items-center gap-2 text-xs cursor-pointer pl-[128px]" title={t('awed.oneRefHint')}><Checkbox checked={!!cur.one_ref_per_qso} onCheckedChange={(c) => patch({ one_ref_per_qso: !!c })} /> {t('awed.oneRef')}</label>
                      <Field2 label={t('awed.patternRegex')}><Input className="h-8 font-mono text-xs" value={cur.pattern} onChange={(e) => patch({ pattern: e.target.value })} placeholder={t('awed.patternPlaceholder')} /></Field2>
                      <div className="grid grid-cols-3 gap-3">
                        <Field2 label={t('awed.leadingString')}><Input className="h-8 font-mono text-xs" value={cur.leading_str ?? ''} onChange={(e) => patch({ leading_str: e.target.value })} /></Field2>
                        <Field2 label={t('awed.trailingString')}><Input className="h-8 font-mono text-xs" value={cur.trailing_str ?? ''} onChange={(e) => patch({ trailing_str: e.target.value })} /></Field2>
                        <Field2 label={t('awed.prefix')}><Input className="h-8 font-mono text-xs" value={cur.prefix ?? ''} onChange={(e) => patch({ prefix: e.target.value })} placeholder={t('awed.prefixPlaceholder')} title={t('awed.prefixTitle')} /></Field2>
                      </div>

                      </>)}
                      {/* Fallback searches: tried in order, only while nothing
                          has matched yet — the first that hits wins (short-circuit),
                          so a value already resolved by the primary rule isn't
                          re-derived differently by a later one.
                          
                          Shown even with no field when rules survive from an
                          imported definition: a rule that still runs must still
                          be visible, or the award finds references the panel
                          says it cannot. */}
                      {(!noField || (cur.or_rules?.length ?? 0) > 0) && (
                      <div className="border-t pt-2.5 space-y-2">
                        <div className="flex items-center justify-between">
                          <p className="text-[11px] text-muted-foreground">{t('awed.additionalSearches')} <span className="font-semibold">(OR)</span> {t('awed.orAlsoMatch')}</p>
                          <Button size="sm" variant="outline" className="h-7"
                            onClick={() => patch({ or_rules: [...(cur.or_rules ?? []), { field: cur.field || 'note', match_by: 'pattern', pattern: '', prefix: '' }] })}>
                            <Plus className="size-3.5" /> {t('awed.addOr')}
                          </Button>
                        </div>
                        {(cur.or_rules ?? []).map((r, ri) => {
                          const upd = (p: Partial<AwardOrRule>) => patch({ or_rules: (cur.or_rules ?? []).map((x, j) => (j === ri ? { ...x, ...p } : x)) });
                          const del = () => patch({ or_rules: (cur.or_rules ?? []).filter((_, j) => j !== ri) });
                          return (
                            <div key={ri} className="rounded-md border border-border bg-muted/20 p-2 space-y-2">
                              <div className="flex items-center gap-2">
                                <span className="text-[11px] text-muted-foreground">{t('awed.orSearchIn')}</span>
                                <Select value={r.field} onValueChange={(v) => upd({ field: v })}>
                                  <SelectTrigger className="h-7 text-xs w-44"><SelectValue /></SelectTrigger>
                                  <SelectContent className="max-h-72">{fields.map((f) => <SelectItem key={f} value={f}>{f}</SelectItem>)}</SelectContent>
                                </Select>
                                <div className="flex items-center gap-2 text-[11px]">
                                  {['code', 'description', 'pattern'].map((m) => (
                                    <label key={m} className="flex items-center gap-1 cursor-pointer">
                                      <input type="radio" name={`orby-${ri}`} checked={(r.match_by || 'code') === m} onChange={() => upd({ match_by: m })} className="accent-primary" /> {m}
                                    </label>
                                  ))}
                                </div>
                                <label className="flex items-center gap-1.5 text-[11px] cursor-pointer"><Checkbox checked={!!r.exact_match} onCheckedChange={(c) => upd({ exact_match: !!c })} /> {t('awed.exact')}</label>
                                <button className="ml-auto text-destructive hover:opacity-70" onClick={del} title={t('awed.removeOrSearch')}><Trash2 className="size-4" /></button>
                              </div>
                              <div className="grid grid-cols-[1fr_120px] gap-2">
                                <Input className="h-7 font-mono text-xs" value={r.pattern ?? ''} onChange={(e) => upd({ pattern: e.target.value })} placeholder={t('awed.orPatternPlaceholder')} />
                                <Input className="h-7 font-mono text-xs" value={r.prefix ?? ''} onChange={(e) => upd({ prefix: e.target.value })} placeholder={t('awed.prefixPlaceholder')} title={t('awed.prefixTitle')} />
                              </div>
                            </div>
                          );
                        })}
                      </div>
                      )}
                    </div>
                  </TabsContent>

                  {/* ── Confirmation ── */}
                  <TabsContent value="conf" className="mt-0 space-y-4">
                    <div className="grid grid-cols-2 gap-6">
                      <div className="space-y-1.5">
                        <Label className="text-xs font-semibold">{t('awed.confirmationLabel')}</Label>
                        {CONFIRM_SRC.map((c) => (
                          <label key={c.id} className="flex items-center gap-2 text-xs cursor-pointer"><Checkbox checked={(cur.confirm ?? []).includes(c.id)} onCheckedChange={() => toggleIn('confirm', c.id)} /> {c.label}</label>
                        ))}
                      </div>
                      <div className="space-y-1.5">
                        <Label className="text-xs font-semibold">{t('awed.validationLabel')}</Label>
                        {CONFIRM_SRC.map((c) => (
                          <label key={c.id} className="flex items-center gap-2 text-xs cursor-pointer"><Checkbox checked={(cur.validate ?? []).includes(c.id)} onCheckedChange={() => toggleIn('validate', c.id)} /> {c.label}</label>
                        ))}
                      </div>
                    </div>

                    {/* "Custom" is only meaningful once it names a field, so the
                        inputs appear as soon as it is ticked in either column —
                        and a custom source with no field confirms nothing. */}
                    {((cur.confirm ?? []).includes('custom') || (cur.validate ?? []).includes('custom')) && (
                      <div className="space-y-2 border-t border-border/60 pt-3">
                        <Label className="text-xs font-semibold">{t('awed.customLabel')}</Label>
                        <div className="grid grid-cols-2 gap-3">
                          <div className="space-y-1">
                            <Label className="text-[11px] text-muted-foreground">{t('awed.customField')}</Label>
                            <Input className="h-8 font-mono text-xs" placeholder="APP_OPSLOG_QSL_RCVD"
                              value={(cur as any).confirm_field ?? ''}
                              onChange={(e) => patch({ confirm_field: e.target.value.trim().toUpperCase() } as any)} />
                          </div>
                          <div className="space-y-1">
                            <Label className="text-[11px] text-muted-foreground">{t('awed.customValue')}</Label>
                            <Input className="h-8 font-mono text-xs" placeholder="Y,V"
                              value={(cur as any).confirm_value ?? ''}
                              onChange={(e) => patch({ confirm_value: e.target.value } as any)} />
                          </div>
                        </div>
                        <p className="text-[11px] text-muted-foreground leading-relaxed">{t('awed.customHint')}</p>
                      </div>
                    )}
                    {/* "Grant codes" and "export credit_granted" used to live here. No
                        ADIF export has ever written CREDIT_GRANTED, so both controls
                        did nothing at all. The stored values are kept (see award.Def);
                        put the controls back when the export is actually wired up. */}
                  </TabsContent>

                  {/* ── Test ──
                      Runs the SAVED award against a real QSO and shows every rule:
                      what it scanned, what it produced, and why each candidate was
                      rejected. An award that matches nothing used to be a black box. */}
                  <TabsContent value="test" className="mt-0 space-y-3">
                    <div className="flex items-end gap-2">
                      <div className="flex flex-col gap-1">
                        <Label className="text-xs text-muted-foreground">{t('awed.testCallsign')}</Label>
                        <Input className="h-8 w-40 font-mono uppercase" value={testCall} placeholder="I2IFT"
                          onChange={(e) => setTestCall(e.target.value.toUpperCase())}
                          onKeyDown={(e) => { if (e.key === 'Enter') runTest(); }} />
                      </div>
                      <Button size="sm" className="h-8" onClick={runTest} disabled={testing || !testCall.trim()}>
                        {testing ? <Loader2 className="size-3.5 mr-1 animate-spin" /> : <Search className="size-3.5 mr-1" />}
                        {t('awed.testRun')}
                      </Button>
                      <span className="text-[11px] text-muted-foreground pb-1">{t('awed.testSavedOnly')}</span>
                    </div>

                    {testErr && <div className="text-xs text-destructive bg-destructive/10 border border-destructive/30 rounded px-3 py-1.5">{testErr}</div>}

                    {testGroups?.map((g, ri) => {
                      const row = g.rows[0];
                      const ex = row.explanation;
                      const matched = (ex.result ?? []).length > 0;
                      const qsoLabel = (q: any) => `${String(q?.qso_date ?? '').slice(0, 10)} · ${q?.band} · ${q?.mode}`;
                      return (
                        <div key={ri} className="rounded border border-border overflow-hidden">
                          <div className="flex items-center gap-2 px-3 py-1.5 bg-muted/40 text-xs border-b border-border">
                            <span className="font-mono font-semibold">{row.qso?.callsign}</span>
                            <span className="text-muted-foreground font-mono">{qsoLabel(row.qso)}</span>
                            {g.rows.length > 1 && (
                              <span className="text-muted-foreground" title={g.rows.slice(1).map((r) => qsoLabel(r.qso)).join('\n')}>
                                {t('awed.testSameAs', { n: String(g.rows.length - 1) })}
                              </span>
                            )}
                            <span className={cn('ml-auto px-1.5 rounded text-[10px] font-semibold uppercase tracking-wide',
                              matched ? 'bg-success/15 text-success' : 'bg-muted-foreground/15 text-muted-foreground')}>
                              {matched ? (ex.result ?? []).join(', ') : t('awed.testNoMatch')}
                            </span>
                          </div>

                          {!ex.in_scope ? (
                            <div className="px-3 py-2 text-xs">
                              <span className="font-medium">{t('awed.testOutOfScope')}</span>{' '}
                              <span className="text-muted-foreground">{ex.scope_error}</span>
                            </div>
                          ) : (
                            <div className="divide-y divide-border/50">
                              {(ex.steps ?? []).map((s, si) => (
                                <div key={si} className={cn('px-3 py-2 text-xs', s.skipped && 'opacity-50')}>
                                  <div className="flex flex-wrap items-center gap-1.5">
                                    <span className="font-semibold uppercase text-[10px] tracking-wide">{s.rule}</span>
                                    <span className="text-muted-foreground">
                                      {s.field}{s.match_by ? ` / ${s.match_by}` : ''}{s.exact ? ` / ${t('awed.exact')}` : ''}
                                    </span>
                                    {s.skipped && <span className="text-muted-foreground italic">— {t('awed.testSkipped')}</span>}
                                    {s.error && <span className="text-destructive">— {s.error}</span>}
                                  </div>
                                  {!s.skipped && !s.error && (
                                    <div className="mt-1 space-y-0.5 pl-3 border-l-2 border-border">
                                      <div>
                                        <span className="text-muted-foreground">{t('awed.testFieldValue')}: </span>
                                        {s.field_value
                                          ? <span className="font-mono">{s.field_value}</span>
                                          : <span className="italic text-muted-foreground">{t('awed.testEmptyField')}</span>}
                                      </div>
                                      {(s.kept ?? []).map((k) => (
                                        <div key={k} className="text-success font-mono">✓ {k}</div>
                                      ))}
                                      {(s.rejected ?? []).map((r, i2) => (
                                        <div key={i2} className="font-mono text-muted-foreground">
                                          ✗ {r.candidate} <span className="font-sans">— {r.reason}</span>
                                        </div>
                                      ))}
                                      {!(s.kept ?? []).length && !(s.rejected ?? []).length && s.field_value && (
                                        <div className="italic text-muted-foreground">{t('awed.testNoCandidate')}</div>
                                      )}
                                    </div>
                                  )}
                                </div>
                              ))}
                              {(ex.ambiguous ?? []).length > 0 && (
                                <div className="px-3 py-2 text-xs">
                                  <span className="font-semibold uppercase text-[10px] tracking-wide text-warning">{t('awed.testAmbiguous')}</span>{' '}
                                  <span className="font-mono text-warning">{(ex.ambiguous ?? []).join(', ')}</span>{' '}
                                  <span className="text-muted-foreground">{t('awed.testAmbiguousHint')}</span>
                                </div>
                              )}
                              {(ex.manual ?? []).length > 0 && (
                                <div className="px-3 py-2 text-xs">
                                  <span className="font-semibold uppercase text-[10px] tracking-wide">{t('awed.testManual')}</span>{' '}
                                  <span className="font-mono text-success">{(ex.manual ?? []).join(', ')}</span>
                                </div>
                              )}
                            </div>
                          )}
                        </div>
                      );
                    })}
                  </TabsContent>

                  {/* ── References ── */}
                  <TabsContent value="refs" className="mt-0">
                    <ReferencesPanel
                      code={cur.code.trim().toUpperCase()} presets={presets} meta={meta[cur.code.toUpperCase()]}
                      awardValidFrom={cur.valid_from} awardValidTo={cur.valid_to}
                      onUpdateOnline={() => updateList(cur.code.toUpperCase())} updating={updating === cur.code.toUpperCase()}
                      onChanged={loadMeta} setErr={setErr}
                    />
                  </TabsContent>
                </div>
              </Tabs>
            )}
          </div>
        </div>

        {/* Eight buttons on one unwrappable row fitted in English and overflowed
            in French, where the labels are half again as long. Wrap instead of
            widening the dialog; gap-2 replaces the base sm:space-x-2, whose
            margin-left approach leaves no vertical gap once a row wraps. */}
        <DialogFooter className="px-5 py-3 border-t !flex-row flex-wrap sm:space-x-0 gap-2">
          <Button variant="ghost" onClick={reset}><RotateCcw className="size-3.5 mr-1" /> {t('awed.resetDefaults')}</Button>
          <Button variant="outline" onClick={exportAwards} title={t('awed.exportTitle')}>
            <Download className="size-3.5 mr-1" /> {t('awed.export')}
          </Button>
          {/* Share just the selected one. Sitting next to the whole-catalogue
              export because that is where an operator looks for it, and labelled
              with the code so the two are never confused at a glance. */}
          {cur && (
            <Button variant="outline" onClick={exportOne} title={t('awed.exportOneTitle')}>
              <Share2 className="size-3.5 mr-1" /> {t('awed.exportOne', { code: cur.code.trim().toUpperCase() })}
            </Button>
          )}
          <Button variant="outline" onClick={importAwards} title={t('awed.importTitle')}>
            <Upload className="size-3.5 mr-1" /> {t('awed.import')}
          </Button>
          {/* The drop folder: put an award JSON here and it installs on restart —
              no rebuild, nobody to ask. Opening it beats printing a path someone
              then has to retype. */}
          <Button variant="ghost" onClick={() => OpenAwardsFolder().catch((e: any) => setErr(String(e?.message ?? e)))}
            title={t('awed.awardsFolderTip')}>
            <FolderOpen className="size-3.5 mr-1" /> {t('awed.awardsFolder')}
          </Button>
          {/* Publish the selected award to the catalog: stamp a version and write a
              file to paste over internal/award/catalog/<code>.json, so a release
              ships your change to the whole team. */}
          {cur && (
            <div className="flex items-center gap-1" title={t('awed.catalogPublishTip')}>
              <input type="number" min={1} value={catVer} onChange={(e) => setCatVer(e.target.value)}
                className="h-8 w-14 rounded border border-input bg-background px-1.5 text-xs font-mono" />
              <Button variant="outline" onClick={exportForCatalog}>
                <Download className="size-3.5 mr-1" /> {t('awed.catalogPublish')}
              </Button>
            </div>
          )}
          <div className="flex-1" />
          <Button variant="outline" onClick={onClose}>{t('awed.cancel')}</Button>
          <Button onClick={save}><Save className="size-3.5 mr-1" /> {t('awed.save')}</Button>
        </DialogFooter>
      </DialogContent>

      {/* Collision dialog. The import cannot silently replace an award you already
          have — importing a shared WAPC used to destroy yours (province list, city
          regexes, band scope) without a word. You decide, per award. */}
      {importPreview && (
        <Dialog open onOpenChange={() => setImportPreview(null)}>
          <DialogContent className="max-w-2xl">
            <DialogHeader>
              <DialogTitle>{t('awed.importCollisionTitle')}</DialogTitle>
            </DialogHeader>
            <p className="text-xs text-muted-foreground -mt-2">{t('awed.importCollisionHint')}</p>
            <div className="max-h-[50vh] overflow-auto flex flex-col gap-2 mt-2">
              {importPreview.awards.map((e) => (
                <div key={e.code} className="rounded-md border border-border p-2.5">
                  <div className="flex items-baseline gap-2 min-w-0">
                    <span className="font-mono font-semibold text-sm">{e.code}</span>
                    <span className="text-xs text-muted-foreground truncate">{e.name}</span>
                    <span className="ml-auto shrink-0 text-[11px] text-muted-foreground tabular-nums">
                      {t('awed.importRefs', { n: e.references })}
                    </span>
                  </div>

                  {!e.exists ? (
                    <p className="mt-1 text-[11px] text-success-muted-foreground">{t('awed.importNew')}</p>
                  ) : (
                    <>
                      <p className="mt-1 text-[11px] text-warning-muted-foreground">
                        {t('awed.importExists', { name: e.mine_name || e.code, n: e.mine_refs })}
                        {e.protected && ` · ${t('awed.importProtected')}`}
                      </p>
                      <div className="mt-1.5 flex flex-wrap gap-1.5">
                        {([
                          ['skip', t('awed.importKeepMine')],
                          ['replace', t('awed.importReplace')],
                          ['copy', t('awed.importCopy', { code: e.code })],
                        ] as [string, string][]).map(([v, label]) => (
                          <button key={v} type="button"
                            onClick={() => setDecisions((d) => ({ ...d, [e.code]: v }))}
                            className={cn('px-2 h-7 rounded-md border text-xs',
                              decisions[e.code] === v
                                ? 'border-primary bg-primary text-primary-foreground'
                                : 'border-border hover:bg-muted')}>
                            {label}
                          </button>
                        ))}
                      </div>
                    </>
                  )}
                </div>
              ))}
            </div>
            <DialogFooter>
              <Button variant="outline" onClick={() => setImportPreview(null)}>{t('awed.cancel')}</Button>
              <Button onClick={() => applyImport(importPreview.path, decisions)}>
                <Upload className="size-3.5 mr-1" /> {t('awed.import2')}
              </Button>
            </DialogFooter>
          </DialogContent>
        </Dialog>
      )}
    </Dialog>
  );
}

// ReferencesPanel — manage the reference list of one award: search/list on the
// left, a per-reference editor on the right, plus bulk paste/CSV, presets and
// the online updater (POTA/SOTA/WWFF).
function ReferencesPanel({ code, presets, meta, awardValidFrom, awardValidTo, onUpdateOnline, updating, onChanged, setErr }: {
  code: string; presets: Preset[]; meta?: RefMeta; awardValidFrom?: string; awardValidTo?: string;
  onUpdateOnline: () => void; updating: boolean; onChanged: () => void; setErr: (s: string) => void;
}) {
  const { t } = useI18n();
  const [refs, setRefs] = useState<AwardRef[]>([]);
  const [q, setQ] = useState('');
  const [selCode, setSelCode] = useState<string | null>(null);
  // The code as TYPED. The list and every patch key off selCode, so editing the
  // code in place would make the editor lose the reference mid-edit; the draft
  // is applied as a rename when the operator saves.
  const [codeDraft, setCodeDraft] = useState('');
  const [busy, setBusy] = useState(false);
  const [bulk, setBulk] = useState('');
  const [showBulk, setShowBulk] = useState(false);
  const [hasBuiltin, setHasBuiltin] = useState(false);

  const total = meta?.count ?? 0;
  const large = total > REF_LIST_CAP;

  // Small lists are loaded whole and filtered client-side; large lists (POTA,
  // 85k parks) are search-only to stay responsive.
  const load = () => {
    if (!code) return;
    if (total > REF_LIST_CAP) { setRefs([]); return; }
    setBusy(true);
    ListAwardReferences(code).then((r) => setRefs((r ?? []) as any)).catch(() => {}).finally(() => setBusy(false));
  };
  useEffect(load, [code, total]);
  useEffect(() => { HasBuiltinReferences(code).then(setHasBuiltin).catch(() => setHasBuiltin(false)); }, [code]);

  // Server-side search for large lists (debounced, min 2 chars).
  useEffect(() => {
    if (!large) return;
    const s = q.trim();
    if (s.length < 2) { setRefs([]); return; }
    const t = window.setTimeout(() => {
      setBusy(true);
      SearchAwardReferences(code, s, 0, 200).then((r) => setRefs((r ?? []) as any)).catch(() => setRefs([])).finally(() => setBusy(false));
    }, 200);
    return () => window.clearTimeout(t);
  }, [code, q, large]);

  async function populateBuiltin() {
    try { const n = await PopulateBuiltinReferences(code); load(); onChanged(); setErr(t('awed.populatedMsg', { n })); }
    catch (e: any) { setErr(String(e?.message ?? e)); }
  }

  const sel = refs.find((r) => r.code === selCode) || null;
  useEffect(() => { setCodeDraft(selCode ?? ''); }, [selCode]);
  // Large lists are already filtered by the server; small lists filter locally.
  const filtered = useMemo(() => {
    if (large) return refs;
    const s = q.trim().toUpperCase();
    return refs.filter((r) => !s || r.code.toUpperCase().includes(s) || (r.name ?? '').toUpperCase().includes(s));
  }, [refs, q, large]);

  const patchSel = (p: Partial<AwardRef>) => setRefs((rs) => rs.map((r) => (r.code === selCode ? { ...r, ...p } : r)));

  async function saveRef(r: AwardRef) {
    try { await SaveAwardReference(code, r as any); load(); onChanged(); }
    catch (e: any) { setErr(String(e?.message ?? e)); }
  }
  // Save the selected reference, renumbering it first when the code was edited.
  //
  // The rename has to come first and has to be a rename: saving under the new
  // code would simply create a second reference and leave the old one behind,
  // which is how a list quietly grows duplicates.
  async function saveSelected(r: AwardRef) {
    const next = codeDraft.trim().toUpperCase();
    if (!next) { setErr(t('awed.refCodeEmpty')); return; }
    if (next !== r.code) {
      try {
        await RenameAwardReference(code, r.code, next);
      } catch (e: any) {
        // Most often the number is already taken by another reference. Said
        // here rather than swallowed: the save has NOT happened.
        setErr(String(e?.message ?? e));
        return;
      }
      setSelCode(next);
    }
    await saveRef({ ...r, code: next });
  }
  async function addRef() {
    const c = prompt(t('awed.newRefCodePrompt'))?.trim().toUpperCase();
    if (!c) return;
    const r: AwardRef = { code: c, name: '', dxcc: 0, group: '', subgrp: '', valid: true };
    await saveRef(r); setSelCode(c);
  }
  async function delRef(c: string) {
    try { await DeleteAwardReference(code, c); setSelCode(null); load(); onChanged(); }
    catch (e: any) { setErr(String(e?.message ?? e)); }
  }
  async function applyPreset(key: string) {
    if (!key) return;
    try { await ApplyAwardPreset(code, key); load(); onChanged(); }
    catch (e: any) { setErr(String(e?.message ?? e)); }
  }
  async function importBulk() {
    try { const n = await ImportAwardReferencesText(code, bulk); setBulk(''); setShowBulk(false); load(); onChanged(); setErr(t('awed.importedRefsMsg', { n })); }
    catch (e: any) { setErr(String(e?.message ?? e)); }
  }

  return (
    <div className="space-y-2">
      {/* Toolbar */}
      <div className="flex items-center gap-2 flex-wrap">
        <span className="text-xs text-muted-foreground">{t('awed.referenceCount')} <span className="font-mono text-foreground">{total.toLocaleString()}</span></span>
        <div className="flex-1" />
        <Select value="" onValueChange={applyPreset}>
          <SelectTrigger className="h-7 w-44 text-xs"><SelectValue placeholder={t('awed.applyPreset')} /></SelectTrigger>
          <SelectContent>{presets.map((p) => <SelectItem key={p.key} value={p.key}>{p.name}</SelectItem>)}</SelectContent>
        </Select>
        <Button variant="outline" size="sm" className="h-7" onClick={() => setShowBulk((s) => !s)}>{t('awed.pasteCsv')}</Button>
        {hasBuiltin && (
          <Button variant="outline" size="sm" className="h-7" onClick={populateBuiltin} title={t('awed.populateBuiltinTitle')}>
            {t('awed.populateBuiltin')}
          </Button>
        )}
        {meta?.can_update && (
          <Button variant="outline" size="sm" className="h-7" disabled={updating} onClick={onUpdateOnline}>
            {updating ? <Loader2 className="size-3.5 mr-1 animate-spin" /> : <Download className="size-3.5 mr-1" />} {t('awed.updateOnline')}
          </Button>
        )}
        <Button variant="outline" size="sm" className="h-7" onClick={addRef}><Plus className="size-3.5 mr-1" /> {t('awed.add')}</Button>
      </div>

      {showBulk && (
        <div className="space-y-1.5 border rounded p-2 bg-muted/30">
          <p className="text-[11px] text-muted-foreground">{t('awed.onePerLine')} <span className="font-mono">CODE,Description,Group,Subgroup,DXCC</span> {t('awed.replacesList')}</p>
          <Textarea rows={6} className="font-mono text-xs" value={bulk} onChange={(e) => setBulk(e.target.value)} placeholder={'ON,Ontario,,,1\nQC,Quebec,,,1'} />
          <div className="flex justify-end gap-2"><Button variant="ghost" size="sm" className="h-7" onClick={() => setShowBulk(false)}>{t('awed.cancel')}</Button><Button size="sm" className="h-7" onClick={importBulk}>{t('awed.import2')}</Button></div>
        </div>
      )}

      <div className="grid grid-cols-[200px_1fr] gap-3">
        {/* List */}
        <div className="border rounded flex flex-col min-h-0 max-h-[46vh]">
          <div className="p-1.5 border-b"><Input className="h-7 text-xs" placeholder={t('awed.search')} value={q} onChange={(e) => setQ(e.target.value)} /></div>
          <div className="flex-1 overflow-auto">
            {busy && <div className="px-2 py-1.5 text-[11px] text-muted-foreground flex items-center gap-1.5"><Loader2 className="size-3 animate-spin" /> {t('awed.searching')}</div>}
            {!busy && large && q.trim().length < 2 && (
              <div className="m-2 rounded border border-warning-border bg-warning-muted px-2 py-1.5 text-[11px] text-warning-muted-foreground">
                {t('awed.tooManyItems', { total: total.toLocaleString() })}
              </div>
            )}
            {!busy && filtered.length === 0 && !(large && q.trim().length < 2) && <div className="px-2 py-1.5 text-[11px] text-muted-foreground">{t('awed.noReferences')}</div>}
            {filtered.map((r) => (
              <button key={r.code} onClick={() => setSelCode(r.code)}
                className={cn('flex w-full items-baseline gap-2 px-2 py-1 text-left text-xs border-b border-border/30', r.code === selCode ? 'bg-accent' : 'hover:bg-accent/50', !r.valid && 'opacity-50')}>
                <span className="font-mono font-semibold shrink-0">{r.code}</span>
                <span className="text-muted-foreground truncate">{r.name}</span>
              </button>
            ))}
          </div>
        </div>

        {/* Per-reference editor */}
        <div className="border rounded p-3">
          {!sel ? (
            <div className="grid place-items-center h-full text-xs text-muted-foreground">{t('awed.selectReference')}</div>
          ) : (
            <div className="space-y-2">
              <div className="flex items-center gap-2">
                {/* Editable, because a shipped list can be wrong about it: WAJA
                    went out carrying Japan's civil prefecture numbers instead of
                    the JARL's, and correcting that meant deleting all 47
                    references and importing a new list. */}
                <Input className="h-8 w-28 font-mono font-semibold" value={codeDraft}
                  title={t('awed.refCodeTip')}
                  onChange={(e) => setCodeDraft(e.target.value)} />
                <label className="flex items-center gap-1.5 text-xs cursor-pointer"><Checkbox checked={sel.valid} onCheckedChange={(c) => patchSel({ valid: !!c })} /> {t('awed.valid')}</label>
                <div className="flex-1" />
                <button className="text-muted-foreground hover:text-destructive" onClick={() => delRef(sel.code)}><Trash2 className="size-4" /></button>
              </div>
              <Field2 label={t('awed.description')}><Input className="h-8" value={sel.name ?? ''} onChange={(e) => patchSel({ name: e.target.value })} /></Field2>
              {/* One per row: side by side, each half spends 120px of its width on
                  the label column and the input is left too narrow to read a group
                  name ("Basilicata" → "Basili"). */}
              <Field2 label={t('awed.group')}><Input className="h-8" value={sel.group ?? ''} onChange={(e) => patchSel({ group: e.target.value })} /></Field2>
              <Field2 label={t('awed.subgroup')}><Input className="h-8" value={sel.subgrp ?? ''} onChange={(e) => patchSel({ subgrp: e.target.value })} /></Field2>
              <Field2 label="DXCC"><Input type="number" className="h-8 w-32 font-mono" value={sel.dxcc || ''} onChange={(e) => patchSel({ dxcc: parseInt(e.target.value, 10) || 0 })} /></Field2>
              <Field2 label={t('awed.patternRegex')}><Input className="h-8 font-mono text-xs" value={sel.pattern ?? ''} onChange={(e) => patchSel({ pattern: e.target.value })} placeholder={t('awed.perRefRegex')} /></Field2>
              {/* Score / Bonus were here. Nothing computes an award score, so both
                  boxes were pure decoration. The columns stay in the database — a
                  third-party list may carry the values — but they are not offered
                  for editing until something actually reads them. */}
              <Field2 label={t('awed.grid')}><Input className="h-8 font-mono" value={sel.gridsquare ?? ''} onChange={(e) => patchSel({ gridsquare: e.target.value })} /></Field2>
              {/* This reference's own validity window. A reference is not forever:
                  a park is delisted, a district merged. A QSO made while it
                  existed still counts — it was a valid contact on the day — and
                  one made afterwards does not.
                  Left empty the award's own dates govern, which is why they show
                  as the placeholder: the operator can see what "empty" inherits
                  instead of having to remember. */}
              <Field2 label={t('awed.refValidFrom')}>
                <Input type="date" className="h-8 w-44" value={sel.valid_from ?? ''}
                  onChange={(e) => patchSel({ valid_from: e.target.value })} />
              </Field2>
              <Field2 label={t('awed.refValidTo')}>
                <Input type="date" className="h-8 w-44" value={sel.valid_to ?? ''}
                  onChange={(e) => patchSel({ valid_to: e.target.value })} />
              </Field2>
              <p className="text-xs text-muted-foreground">
                {(awardValidFrom || awardValidTo)
                  ? t('awed.refValidHintAward', { from: openEnded(awardValidFrom), to: openEnded(awardValidTo) })
                  : t('awed.refValidHint')}
              </p>
              <div className="flex justify-end pt-1"><Button size="sm" className="h-7" onClick={() => sel && saveSelected(sel)}><Save className="size-3.5 mr-1" /> {t('awed.saveReference')}</Button></div>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}
