import { useEffect, useMemo, useRef, useState } from 'react';
import { Trash2, Search, Loader2, CalendarDays } from 'lucide-react';
import { LookupCallsign, LookupCallsignFresh, DXCCForCountry, GetAwardDefs, ComputeQSOAwardRefs, GetListsSettings, OpenExternalURL, SetOpsLogQSLReceived } from '../../wailsjs/go/main/App';
import { rstOptions, stepRST, type RSTLists } from '@/lib/rst';
import { AwardRefSelector } from '@/components/AwardRefSelector';
import { AdifExtrasEditor } from '@/components/AdifExtrasEditor';
import { applyAwardRefs } from '@/lib/awardRefs';
import {
  Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import { Badge } from '@/components/ui/badge';
import {
  Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
} from '@/components/ui/select';
import { Checkbox } from '@/components/ui/checkbox';
import { Combobox } from '@/components/ui/combobox';
import { cn } from '@/lib/utils';
import { flagURL } from '@/lib/flags';
import { useI18n } from '@/lib/i18n';
import { bandForMHz } from '@/lib/bandplan';
import { titleCase, sentenceCase } from '@/lib/textCase';
import type { QSOForm } from '@/types';

type QSO = QSOForm;

// Quick prefix from a callsign (drops portable suffixes, keeps a slashed
// prefix). Read-only display, mirrors Log4OM's PFX box.
function pfxOf(call: string): string {
  const c = (call || '').trim().toUpperCase();
  if (!c) return '';
  const base = c.includes('/') ? c.split('/')[0] : c;
  let lastDigit = -1;
  for (let i = 0; i < base.length; i++) if (base[i] >= '0' && base[i] <= '9') lastDigit = i;
  return lastDigit >= 0 ? base.slice(0, lastDigit + 1) : base;
}

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 = ['SSB','CW','FT8','FT4','RTTY','PSK31','AM','FM','DIGITALVOICE','MFSK','OLIVIA','JS8','JT65','JT9'];
// label holds an i18n key (resolved with t() at render time).
// V is in the list because the log HOLDS it: a LoTW download writes "verified"
// rather than "yes". A dropdown without it renders a verified contact as blank,
// which reads as "nothing recorded" — and the operator's next click would replace
// the strongest confirmation they have with whatever they picked instead.
const QSL_STATUSES = [
  { value: '_', label: 'qedit.qslDash' },
  { value: 'Y', label: 'qedit.qslYes' },
  { value: 'V', label: 'qedit.qslVerified' },
  { value: 'N', label: 'qedit.qslNo'  },
  { value: 'R', label: 'qedit.qslRequested' },
  { value: 'I', label: 'qedit.qslIgnore' },
];
const PROP_MODES = ['_','AS','AUE','AUR','BS','ECH','EME','ES','F2','F2M','FAI','GWAVE','INTERNET','ION','IRL','LOS','MS','RPT','RS','SAT','TEP','TR'];

// Confirmation channels — each maps to its QSO sent/received status, dates and
// (paper-only) via fields. Drives the "Manage Confirmation" editor and the
// live status grid (Log4OM style).
type ConfDef = {
  key: string; label: string;
  sent?: keyof QSOForm; rcvd?: keyof QSOForm;
  sentDate?: keyof QSOForm; rcvdDate?: keyof QSOForm;
  // How the card travelled, each way (ADIF QSL_SENT_VIA / QSL_RCVD_VIA). Paper
  // only — the electronic channels below ARE the route.
  sentVia?: keyof QSOForm; rcvdVia?: keyof QSOForm;
};
const CONFIRMATIONS: ConfDef[] = [
  { key: 'QSL',     label: 'QSL (paper)', sent: 'qsl_sent',  rcvd: 'qsl_rcvd',  sentDate: 'qsl_sent_date',  rcvdDate: 'qsl_rcvd_date',  sentVia: 'qsl_sent_via', rcvdVia: 'qsl_rcvd_via' },
  { key: 'LOTW',    label: 'LoTW',        sent: 'lotw_sent', rcvd: 'lotw_rcvd', sentDate: 'lotw_sent_date', rcvdDate: 'lotw_rcvd_date' },
  { key: 'EQSL',    label: 'eQSL',        sent: 'eqsl_sent', rcvd: 'eqsl_rcvd', sentDate: 'eqsl_sent_date', rcvdDate: 'eqsl_rcvd_date' },
  { key: 'QRZCOM',  label: 'QRZ.com',     sent: 'qrzcom_qso_upload_status' as any, sentDate: 'qrzcom_qso_upload_date' as any, rcvd: 'qrzcom_qso_download_status' as any, rcvdDate: 'qrzcom_qso_download_date' as any },
  { key: 'CLUBLOG', label: 'Club Log',    sent: 'clublog_qso_upload_status' as any, sentDate: 'clublog_qso_upload_date' as any, rcvd: 'clublog_qso_download_status' as any, rcvdDate: 'clublog_qso_download_date' as any },
  { key: 'HRDLOG',  label: 'HRDLog',      sent: 'hrdlog_qso_upload_status' as any, sentDate: 'hrdlog_qso_upload_date' as any },
];
// i18n label keys for confirmation channels whose label has translatable words
// (brand names like LoTW/eQSL stay as their literal label in CONFIRMATIONS).
const CONF_LABEL_KEYS: Record<string, string> = {
  QSL: 'qedit.confQslPaper',
};

// Reading order for the channel list: the two that carry an ARRL award first —
// paper QSL and LoTW — then everything else alphabetically, so a channel is
// found by its name rather than by remembering the order it was added in.
const CONF_FIRST: Record<string, number> = { QSL: 0, LOTW: 1 };
function confOrder<T extends { key: string; label: string }>(rows: T[]): T[] {
  return rows.slice().sort((a, b) => {
    const ra = CONF_FIRST[a.key] ?? 2;
    const rb = CONF_FIRST[b.key] ?? 2;
    if (ra !== rb) return ra - rb;
    return a.label.localeCompare(b.label);
  });
}

// OpsLog's own card. Kept out of CONFIRMATIONS on purpose — that list maps QSO
// columns and this channel is backed by ADIF extras — but it still belongs in
// the channel picker and the status table alongside the rest.
const OPSLOG_CONF = 'OPSLOG';

// HAMLOG.online. Out of CONFIRMATIONS for the same reason as the OpsLog card —
// it is stored in the ADIF extras, not in QSO columns — but it is an upload
// service like QRZ.com or Club Log, so it gets the ordinary sent/received/date
// editor rather than a special one. The received key is THEIR field name, the
// one their ADIF export writes; see award.HamlogQSLKey.
const HAMLOG_CONF = 'HAMLOG';
const HAMLOG_KEYS = {
  sent: 'APP_OPSLOG_HAMLOG_SENT',
  sentDate: 'APP_OPSLOG_HAMLOG_SENT_DATE',
  rcvd: 'APP_HAMLOG_QSO_CFM',
  rcvdDate: 'APP_OPSLOG_HAMLOG_QSL_DATE',
};

// HamQTH — extras again (ADIF names no HamQTH field), and SENT only: the site
// publishes no confirmation feed, so a received column here would be a promise
// nothing can keep.
const HAMQTH_CONF = 'HAMQTH';
const HAMQTH_KEYS = {
  sent: 'APP_OPSLOG_HAMQTH_SENT',
  sentDate: 'APP_OPSLOG_HAMQTH_SENT_DATE',
};

// Colour-coded status cell for the confirmation grid.
function StatusCell({ value }: { value?: string }) {
  const { t } = useI18n();
  const v = (value || '').toUpperCase();
  // Empty = no value set yet → show a neutral dash, NOT "No" (which is the
  // explicit "N" status). Mirrors the dropdown, which shows "—" for empty.
  if (v === '') {
    return <span className="block text-center text-[11px] text-muted-foreground">—</span>;
  }
  // One colour per state, and each colour means something:
  //   Yes       green   — confirmed, the thing you wanted
  //   Requested blue    — in flight, waiting on the other end. Not a problem.
  //   Modified  orange  — uploaded, then the QSO changed: it needs re-uploading.
  //                       This is the ONLY state asking for action, so it gets the
  //                       only alarming colour.
  //   No        neutral — nothing done yet. Every freshly logged QSO is "No" on
  //                       every row; painting that orange (as it used to be, in the
  //                       same orange as Requested) made the table shout about a
  //                       non-problem and told you nothing apart.
  //   Verified  green   — ADIF's V: confirmed AND validated by the awarding body.
  //                       It is what a LoTW download writes, and this table used
  //                       to fall through to "No" for it — reporting a verified
  //                       contact as unconfirmed, in the one place an operator
  //                       goes to check.
  //   Ignore    dashed  — deliberately excluded, on purpose.
  const label = v === 'Y' ? t('qedit.qslYes') : v === 'V' ? t('qedit.qslVerified') : v === 'R' ? t('qedit.qslRequested') : v === 'I' ? t('qedit.qslIgnore') : v === 'M' ? t('qedit.statusModified') : t('qedit.qslNo');
  const cls = v === 'Y' || v === 'V' ? 'bg-success text-success-foreground border border-success'
    : v === 'R' ? 'bg-info-muted text-info-muted-foreground border border-info-border'
    : v === 'M' ? 'bg-warning text-warning-foreground border border-warning'
    : v === 'I' ? 'bg-muted text-muted-foreground border border-dashed border-border italic'
    : 'bg-muted text-muted-foreground border border-border';
  return <span className={cn('block text-center text-[11px] font-semibold rounded px-1 py-0.5', cls)}>{label}</span>;
}

interface Props {
  qso: QSO;
  onSave: (q: QSO) => void;
  onDelete: (id: number) => void;
  onClose: () => void;
  countries?: string[];
  bands?: string[];
  modes?: string[];
}

function toLocalISO(d: any): string {
  if (!d) return '';
  const date = new Date(d);
  if (isNaN(date.getTime())) return '';
  // Go's zero time.Time serialises as "0001-01-01T00:00:00Z" (json omitempty
  // doesn't apply to a time struct), so a QSO with no end time arrives as a
  // year-1 date. Treat anything that old as unset — otherwise the datetime
  // field shows a garbage value and fights the user's typing.
  if (date.getUTCFullYear() <= 1) return '';
  const p = (n: number) => String(n).padStart(2, '0');
  return `${date.getUTCFullYear()}-${p(date.getUTCMonth()+1)}-${p(date.getUTCDate())}T${p(date.getUTCHours())}:${p(date.getUTCMinutes())}`;
}
function parseLocalISO(s: string): string | null {
  if (!s) return null;
  const m = s.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/);
  if (!m) return null;
  return `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:00.000Z`;
}
function numOrUndef(v: any): number | undefined {
  if (v === '' || v === null || v === undefined) return undefined;
  const n = typeof v === 'number' ? v : parseFloat(String(v));
  return isNaN(n) ? undefined : n;
}
function intOrUndef(v: any): number | undefined {
  const n = numOrUndef(v);
  return n === undefined ? undefined : Math.trunc(n);
}

function F({ label, span = 1, children }: { label: string; span?: 1 | 2 | 3 | 6; children: React.ReactNode }) {
  return (
    <div className={cn('flex flex-col gap-1 min-w-0', span === 2 && 'col-span-2', span === 3 && 'col-span-3', span === 6 && 'col-span-6')}>
      <Label>{label}</Label>
      {children}
    </div>
  );
}

// AdifDateInput — a real date picker over an ADIF date.
//
// ADIF stores YYYYMMDD; the browser's date input speaks YYYY-MM-DD, so the two
// are converted at the edge and the log keeps its ADIF form. The native control
// is used on purpose: it brings the OS calendar, the locale's day/month order
// and keyboard entry for free, which a hand-rolled popover would have to
// reimplement and get wrong.
//
// A value that is NOT a valid 8-digit date (an old hand-typed entry) is shown in
// a plain text box instead, so it stays visible and correctable rather than
// silently disappearing behind an empty picker.
function AdifDateInput({ value, onChange, disabled }: { value?: string; onChange: (v: string) => void; disabled?: boolean }) {
  const raw = (value ?? '').trim();
  const valid = /^\d{8}$/.test(raw);
  const iso = valid ? `${raw.slice(0, 4)}-${raw.slice(4, 6)}-${raw.slice(6, 8)}` : '';
  const today = () => {
    const d = new Date();
    const p = (n: number) => String(n).padStart(2, '0');
    // UTC: every date in the log is UTC, and near midnight the local day is the
    // wrong one.
    onChange(`${d.getUTCFullYear()}${p(d.getUTCMonth() + 1)}${p(d.getUTCDate())}`);
  };
  if (raw !== '' && !valid) {
    return (
      <div className="flex gap-1">
        <Input value={raw} onChange={(e) => onChange(e.target.value)} disabled={disabled} className="font-mono" />
        <Button type="button" variant="outline" size="sm" className="shrink-0 px-2" onClick={() => onChange('')} title="Clear">×</Button>
      </div>
    );
  }
  return (
    <div className="flex gap-1">
      <Input
        type="date"
        value={iso}
        disabled={disabled}
        onChange={(e) => {
          const v = e.target.value; // "" when cleared
          onChange(v ? v.replace(/-/g, '') : '');
        }}
        className="font-mono"
      />
      <Button type="button" variant="outline" size="sm" className="shrink-0 px-2" disabled={disabled}
        onClick={today} title="Today (UTC)">
        <CalendarDays className="size-3.5" />
      </Button>
    </div>
  );
}

function QslSelect({ value, onChange }: { value?: string; onChange: (v: string) => void }) {
  const { t } = useI18n();
  return (
    <Select value={value || '_'} onValueChange={(v) => onChange(v === '_' ? '' : v)}>
      <SelectTrigger><SelectValue /></SelectTrigger>
      <SelectContent>
        {QSL_STATUSES.map((s) => <SelectItem key={s.value} value={s.value}>{t(s.label)}</SelectItem>)}
      </SelectContent>
    </Select>
  );
}

// The ADIF QSL Via enumeration. M (manager) is import-only in the standard, so
// it is not offered here — a file that arrives carrying it keeps it, and this
// list is what an operator may choose.
const QSL_VIA_CHOICES = [
  { value: '_', label: 'qedit.qslDash' },
  { value: 'B', label: 'qedit.viaBureau' },
  { value: 'D', label: 'qedit.viaDirect' },
  { value: 'E', label: 'qedit.viaElectronic' },
];

function QslViaSelect({ value, onChange }: { value?: string; onChange: (v: string) => void }) {
  const { t } = useI18n();
  return (
    <Select value={value || '_'} onValueChange={(v) => onChange(v === '_' ? '' : v)}>
      <SelectTrigger><SelectValue /></SelectTrigger>
      <SelectContent>
        {QSL_VIA_CHOICES.map((s) => <SelectItem key={s.value} value={s.value}>{t(s.label)}</SelectItem>)}
      </SelectContent>
    </Select>
  );
}

export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [], bands, modes }: Props) {
  const { t } = useI18n();
  // Use the operator's configured band/mode lists (incl. custom ones like 13cm);
  // fall back to the built-in sets. Always include the QSO's own band/mode so an
  // imported/legacy value is never silently dropped from the dropdown.
  const bandList = useMemo(() => {
    const base = (bands && bands.length ? bands : BANDS).slice();
    if (qso.band && !base.includes(qso.band)) base.unshift(qso.band);
    return base;
  }, [bands, qso.band]);
  const modeList = useMemo(() => {
    const base = (modes && modes.length ? modes : MODES).slice();
    if (qso.mode && !base.includes(qso.mode)) base.unshift(qso.mode);
    return base;
  }, [modes, qso.mode]);
  const [draft, setDraft] = useState<QSO>(() => JSON.parse(JSON.stringify(qso)));
  // Per-mode RST dropdown choices, loaded from the same settings as the entry form.
  const [rstLists, setRstLists] = useState<RSTLists>({ phone: [], cw: [], digital: [] });
  useEffect(() => {
    GetListsSettings()
      .then((l: any) => setRstLists({ phone: l?.rst_phone ?? [], cw: l?.rst_cw ?? [], digital: l?.rst_digital ?? [] }))
      .catch(() => {});
  }, []);
  // Frequencies are edited as kHz + Hz (Log4OM style) and recombined on save.
  const splitHz = (hz?: number) => hz
    ? { khz: String(Math.floor(hz / 1000)), hz: String(hz % 1000).padStart(3, '0') }
    : { khz: '', hz: '' };
  // Correcting a frequency corrects its band. The pair has to agree — the log,
  // every award and every upload are read on the BAND — and an operator fixing
  // a wrong frequency is not also expecting to fix the band by hand, which is
  // exactly how a QSO ends up filed on 20m at 7 MHz.
  //
  // Only when the number lands in a known allocation: half a frequency is typed
  // on the way to all of it, and a band must never be blanked by that.
  const syncBand = (khz: string, hz: string, field: 'band' | 'band_rx') => {
    if (!khz.trim()) return;
    const b = bandForMHz((parseInt(khz, 10) * 1000 + (parseInt(hz, 10) || 0)) / 1_000_000);
    if (b) set(field, b as any);
  };

  const f0 = splitHz(draft.freq_hz);
  const fr0 = splitHz(draft.freq_rx_hz);
  const [freqKHz, setFreqKHz] = useState(f0.khz);
  const [freqHz, setFreqHz] = useState(f0.hz);
  const [freqRxKHz, setFreqRxKHz] = useState(fr0.khz);
  const [freqRxHz, setFreqRxHz] = useState(fr0.hz);
  const [dateOn, setDateOn] = useState(toLocalISO(draft.qso_date));
  const dateOffISO = toLocalISO(draft.qso_date_off); // '' when unset / Go zero time
  const [dateOff, setDateOff] = useState(dateOffISO);
  const [endEnabled, setEndEnabled] = useState(!!dateOffISO);
  const [confSel, setConfSel] = useState('QSL'); // selected confirmation channel
  const [localErr, setLocalErr] = useState('');
  const [saving, setSaving] = useState(false);
  const [looking, setLooking] = useState(false);

  // === Award references (Log4OM-style tab) ===
  // Manual refs are edited as a "CODE@REF;…" string; computed refs (DXCC, WAZ,
  // WPX, …) are derived from the QSO by the backend and shown read-only.
  const awardFieldRef = useRef<Record<string, string>>({});
  const [awardRefs, setAwardRefs] = useState('');
  // The refs present when the editor opened, so on save we can tell which ones
  // were REMOVED and strip their in-field tokens (see applyAwardRefs).
  const seedAwardRefsRef = useRef('');
  const [computedRefs, setComputedRefs] = useState<Array<{ code: string; ref: string; name?: string }>>([]);

  // Load award definitions once, then seed the editable manual refs from the QSO.
  useEffect(() => {
    GetAwardDefs()
      .then(async (defs) => {
        const list = (defs ?? []) as any[];
        const fieldOf: Record<string, string> = {};
        for (const d of list) fieldOf[String(d.code).toUpperCase()] = String(d.field || '').toLowerCase();
        awardFieldRef.current = fieldOf;
        // Seed the editable manual refs from the backend, which already matched
        // each reference against its award's own list. Seeding from the raw QSO
        // field instead would wrongly seed every state-award (WAS/RAC/WAJA) from
        // the same `state` value — e.g. a US "CA" would seed RAC@CA too.
        try {
          const all = (await ComputeQSOAwardRefs(draft as any)) ?? [];
          const seed = all
            .filter((r: any) => r.pickable)
            .map((r: any) => `${String(r.code).toUpperCase()}@${String(r.ref).toUpperCase()}`)
            .join(';');
          seedAwardRefsRef.current = seed;
          setAwardRefs(seed);
        } catch { /* leave manual refs empty on failure */ }
      })
      .catch(() => {});
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // Recompute the read-only computed refs whenever a source field changes.
  useEffect(() => {
    const t = window.setTimeout(async () => {
      try {
        const all = (await ComputeQSOAwardRefs(draft as any)) ?? [];
        setComputedRefs(all.filter((r: any) => !r.pickable).map((r: any) => ({ code: r.code, ref: r.ref, name: r.name })));
      } catch { setComputedRefs([]); }
    }, 250);
    return () => window.clearTimeout(t);
  }, [draft.dxcc, draft.cqz, draft.ituz, draft.cont, draft.state, draft.callsign, draft.notes, draft.band]);

  // Contest exchange typed as free text → numeric SRX/STX when all-digits, else
  // the SRX_STRING/STX_STRING field. Mirrors the entry strip (F5) so the field
  // accepts letters (sections/zones), not just numbers.
  function setExchange(which: 'srx' | 'stx', raw: string) {
    const t = raw.trim();
    const num = /^\d+$/.test(t) ? parseInt(t, 10) : undefined;
    setDraft((d) => ({ ...d, [which]: num, [`${which}_string`]: num != null ? '' : t } as any));
  }
  function set<K extends keyof QSO>(key: K, value: QSO[K]) {
    setDraft((d) => ({ ...d, [key]: value }));
  }

  // Country drives the DXCC entity number (ADIF). The DXCC field is read-only;
  // picking a Country resolves and stamps its DXCC# so they can't diverge.
  async function onCountryChange(v: string) {
    set('country', v);
    try {
      const n = await DXCCForCountry(v);
      set('dxcc', (n && n > 0 ? n : undefined) as any);
    } catch { /* leave DXCC as-is if resolution fails */ }
  }

  // Re-run a callsign lookup (QRZ/HamQTH + cty.dat) and merge the result into
  // the draft — handy after correcting the callsign. Only overwrites the
  // lookup-derived fields; leaves call/band/mode/RST/dates alone.
  async function fetchLookup() {
    const call = (draft.callsign ?? '').trim().toUpperCase();
    if (!call) { setLocalErr(t('qedit.callsignRequired')); return; }
    setLooking(true);
    setLocalErr('');
    try {
      // FRESH: this fetch is a deliberate click, so it bypasses the cache and
      // refreshes it. A cached answer from a thinner QRZ subscription (or any
      // stale row) otherwise stayed for its whole 30-day life and the button
      // appeared to do nothing.
      // The QSO's OWN date: a ClubLog exception is resolved as of when the
      // contact happened, not as of today. Re-looking-up a DXpedition contact
      // months later must not move it to whatever the prefix means now.
      const r: any = await LookupCallsignFresh(call, (dateOn || '').slice(0, 10));
      // The lookup WINS over what is in the record — that is the point of asking
      // for it. But an EMPTY result must never blank a good value: `??` only
      // guards against null, and Go marshals an unset string as "", so a QRZ
      // record with no grid used to wipe the grid that was already there.
      const keep = (found: any, cur: any) => (found === undefined || found === null || found === '' ? cur : found);
      setDraft((d) => ({
        ...d,
        name: keep(r.name, d.name),
        qth: keep(r.qth, d.qth),
        address: keep(r.address, (d as any).address),
        email: keep(r.email, (d as any).email),
        country: keep(r.country, d.country),
        grid: keep(r.grid, d.grid),
        state: keep(r.state, d.state),
        cnty: keep(r.cnty, d.cnty),
        cont: keep(r.cont, d.cont),
        qsl_via: keep(r.qsl_via, d.qsl_via),
        dxcc: r.dxcc || d.dxcc,
        cqz: r.cqz || d.cqz,
        ituz: r.ituz || d.ituz,
        lat: r.lat || d.lat,
        lon: r.lon || d.lon,
      }));
    } catch (e: any) {
      setLocalErr(t('qedit.lookupError', { msg: String(e?.message ?? e) }));
    } finally {
      setLooking(false);
    }
  }

  function save() {
    if (!draft.callsign?.trim()) { setLocalErr(t('qedit.callsignRequired')); return; }
    setSaving(true);
    setLocalErr('');
    const out: any = {
      ...draft,
      callsign: draft.callsign.trim().toUpperCase(),
      grid: (draft.grid ?? '').trim().toUpperCase(),
      gridsquare_ext: (draft.gridsquare_ext ?? '').trim().toUpperCase(),
      station_callsign: (draft.station_callsign ?? '').trim().toUpperCase(),
      operator: (draft.operator ?? '').trim().toUpperCase(),
      my_grid: (draft.my_grid ?? '').trim().toUpperCase(),
      my_gridsquare_ext: (draft.my_gridsquare_ext ?? '').trim().toUpperCase(),
      // iota / sota_ref / pota_ref are set below from the Award Refs tab.
      my_iota: (draft.my_iota ?? '').trim().toUpperCase(),
      my_sota_ref: (draft.my_sota_ref ?? '').trim().toUpperCase(),
      my_pota_ref: (draft.my_pota_ref ?? '').trim().toUpperCase(),
      qso_date:     parseLocalISO(dateOn) ?? new Date().toISOString(),
      qso_date_off: endEnabled ? (parseLocalISO(dateOff) ?? undefined) : undefined,
      freq_hz:    freqKHz.trim()   ? parseInt(freqKHz, 10) * 1000   + (parseInt(freqHz, 10) || 0)   : undefined,
      freq_rx_hz: freqRxKHz.trim() ? parseInt(freqRxKHz, 10) * 1000 + (parseInt(freqRxHz, 10) || 0) : undefined,
      dxcc:       intOrUndef(draft.dxcc),
      cqz:        intOrUndef(draft.cqz),
      ituz:       intOrUndef(draft.ituz),
      age:        intOrUndef(draft.age),
      srx:        intOrUndef(draft.srx),
      stx:        intOrUndef(draft.stx),
      my_dxcc:    intOrUndef(draft.my_dxcc),
      my_cq_zone: intOrUndef(draft.my_cq_zone),
      my_itu_zone: intOrUndef(draft.my_itu_zone),
      lat: numOrUndef(draft.lat),    lon: numOrUndef(draft.lon),
      my_lat: numOrUndef(draft.my_lat), my_lon: numOrUndef(draft.my_lon),
      ant_az: numOrUndef(draft.ant_az), ant_el: numOrUndef(draft.ant_el),
      tx_pwr: numOrUndef(draft.tx_pwr),
      distance: numOrUndef(draft.distance),
      rx_pwr: numOrUndef(draft.rx_pwr),
      a_index: numOrUndef(draft.a_index),
      k_index: numOrUndef(draft.k_index),
      sfi: numOrUndef(draft.sfi),
      extras: draft.extras && Object.keys(draft.extras).length ? draft.extras : undefined,
    };
    // The Award Refs tab is authoritative for the reference-list awards. Reset
    // the dedicated columns, then route the picked refs back onto the payload
    // (POTA/SOTA/IOTA → columns, WWFF/custom → extras).
    out.iota = ''; out.sota_ref = ''; out.pota_ref = '';
    applyAwardRefs(out, awardRefs, awardFieldRef.current, seedAwardRefsRef.current);
    onSave(out);
  }

  useEffect(() => {
    function onKey(e: KeyboardEvent) {
      if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) save();
    }
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  });

  const extrasCount = useMemo(
    () => (draft.extras ? Object.keys(draft.extras).length : 0),
    [draft.extras],
  );

  // OpsLog QSL "received" marker (ADIF extra). Drives the card's PSE/TNX stamp:
  // received → TNX QSL (thanks for your card), otherwise PSE QSL (please send one).
  // Saved with the normal Save via draft.extras.
  const OPSLOG_QSL_RCVD = 'APP_OPSLOG_QSL_RCVD';
  const qslReceived = !!String(draft.extras?.[OPSLOG_QSL_RCVD] ?? '').trim();
  // Sent side, for the confirmations table. Two key names because the marker was
  // renamed once and old QSOs still carry the first one — same test the Recent
  // QSOs "OpsLog QSL" column makes.
  const opslogQslSent = !!(draft.extras?.['APP_OPSLOG_QSL_SENT'] || draft.extras?.['APP_OPSLOG_QSL_CARD_SENT']);
  const toggleQslReceived = (on: boolean) => {
    // Reflect immediately in the draft (for the PSE/TNX indicator)…
    const next = { ...(draft.extras ?? {}) };
    if (on) next[OPSLOG_QSL_RCVD] = new Date().toISOString();
    else delete next[OPSLOG_QSL_RCVD];
    set('extras', next as any);
    // …and persist right away with a targeted write that reliably sets OR clears
    // the key (the modal's extras-merge on Save wouldn't clear a removed key).
    if ((draft as any).id) SetOpsLogQSLReceived((draft as any).id, on).catch(() => {});
  };

  return (
    <Dialog open onOpenChange={(o) => { if (!o) onClose(); }}>
      <DialogContent className="max-w-5xl max-h-[92vh] grid grid-rows-[auto_1fr_auto] gap-0 p-0">
        <DialogHeader className="flex-row items-baseline gap-2">
          <DialogTitle>{t('qedit.title')}</DialogTitle>
          <span className="font-mono text-xs text-muted-foreground">#{draft.id} — {draft.callsign}</span>
          <DialogDescription className="sr-only">{t('qedit.editFieldsFor', { id: draft.id })}</DialogDescription>
        </DialogHeader>

        <Tabs defaultValue="qsoinfo" className="flex flex-col overflow-hidden min-h-0">
          <TabsList className="px-3 overflow-x-auto">
            <TabsTrigger value="qsoinfo">{t('qedit.tabQsoInfo')}</TabsTrigger>
            <TabsTrigger value="contact">{t('qedit.tabContact')}</TabsTrigger>
            <TabsTrigger value="awards">{t('qedit.tabAwards')}</TabsTrigger>
            <TabsTrigger value="qsl">{t('qedit.tabQsl')}</TabsTrigger>
            <TabsTrigger value="contest">{t('qedit.tabContest')}</TabsTrigger>
            <TabsTrigger value="sat">{t('qedit.tabSat')}</TabsTrigger>
            <TabsTrigger value="mystation">{t('qedit.tabMyStation')}</TabsTrigger>
            <TabsTrigger value="moreadif">{t('qedit.tabMoreAdif')}</TabsTrigger>
            <TabsTrigger value="extras">
              {t('qedit.tabAdifFields')}
              {extrasCount > 0 && (
                <Badge variant="accent" className="ml-1 px-1.5 py-0 text-[9px]">{extrasCount}</Badge>
              )}
            </TabsTrigger>
          </TabsList>

          {localErr && (
            <div className="mx-5 mt-3 text-xs text-destructive bg-destructive/10 border border-destructive/30 rounded-md px-3 py-2">
              {localErr}
            </div>
          )}

          <div className="overflow-y-auto px-5 py-4 flex-1">
            <TabsContent value="qsoinfo" className="mt-0">
              {/* Top: Callsign + RST + Fetch */}
              <div className="flex items-end gap-2 mb-3">
                <div className="flex flex-col flex-1 min-w-0">
                  <div className="flex items-center">
                    <Label>{t('qedit.callsign')}</Label>
                    {(draft.callsign ?? '').trim() && (
                      <button
                        type="button"
                        title={t('qrz.openTitle', { call: (draft.callsign ?? '').trim().toUpperCase() })}
                        onClick={() => {
                          const c = (draft.callsign ?? '').trim().toUpperCase().split('/').map(encodeURIComponent).join('/');
                          if (c) OpenExternalURL(`https://www.qrz.com/db/${c}`).catch((e: any) => setLocalErr(String(e?.message ?? e)));
                        }}
                        className="ml-auto shrink-0 inline-flex items-center gap-0.5 text-[9px] font-semibold normal-case tracking-wider text-primary hover:underline"
                      >
                        QRZ ↗
                      </button>
                    )}
                  </div>
                  <Input className="font-mono text-lg font-bold tracking-wider uppercase h-10"
                    value={draft.callsign ?? ''} onChange={(e) => set('callsign', e.target.value)} />
                </div>
                <div className="flex flex-col w-20"><Label>S</Label>
                  <Combobox value={draft.rst_sent ?? ''} options={rstOptions(draft.mode ?? '', rstLists)} commitOnType
                    onChange={(v) => set('rst_sent', v)}
                    onWheelStep={(d) => set('rst_sent', stepRST(draft.rst_sent ?? '', d, draft.mode ?? ''))} /></div>
                <div className="flex flex-col w-20"><Label>R</Label>
                  <Combobox value={draft.rst_rcvd ?? ''} options={rstOptions(draft.mode ?? '', rstLists)} commitOnType
                    onChange={(v) => set('rst_rcvd', v)}
                    onWheelStep={(d) => set('rst_rcvd', stepRST(draft.rst_rcvd ?? '', d, draft.mode ?? ''))} /></div>
                <Button type="button" variant="outline" className="h-10" onClick={fetchLookup} disabled={looking}
                  title={t('qedit.fetchTitle')}>
                  {looking ? <Loader2 className="size-4 animate-spin" /> : <Search className="size-4" />} {t('qedit.fetch')}
                </Button>
              </div>

              <div className="grid grid-cols-2 gap-x-6 gap-y-2.5">
                {/* ── Left column ── */}
                <div className="flex flex-col gap-2.5">
                  <div><Label>{t('qedit.name')}</Label><Input value={draft.name ?? ''} onChange={(e) => set('name', e.target.value)}
                    onBlur={() => set('name', titleCase(draft.name ?? '') as any)} /></div>
                  <div className="flex items-center gap-2">
                    <Label className="w-20 shrink-0">{t('qedit.band')}</Label>
                    <Select value={draft.band || ''} onValueChange={(v) => set('band', v)}>
                      <SelectTrigger className="h-8 flex-1"><SelectValue /></SelectTrigger>
                      <SelectContent>{bandList.map((b) => <SelectItem key={b} value={b}>{b}</SelectItem>)}</SelectContent>
                    </Select>
                  </div>
                  <div className="flex items-center gap-2">
                    <Label className="w-20 shrink-0">{t('qedit.rxBand')}</Label>
                    <Select value={draft.band_rx || '_'} onValueChange={(v) => set('band_rx', v === '_' ? '' : v)}>
                      <SelectTrigger className="h-8 flex-1"><SelectValue /></SelectTrigger>
                      <SelectContent><SelectItem value="_">—</SelectItem>{bandList.map((b) => <SelectItem key={b} value={b}>{b}</SelectItem>)}</SelectContent>
                    </Select>
                  </div>
                  <div className="flex items-center gap-2">
                    <Label className="w-20 shrink-0">{t('qedit.mode')}</Label>
                    <Select value={draft.mode || ''} onValueChange={(v) => set('mode', v)}>
                      <SelectTrigger className="h-8 flex-1"><SelectValue /></SelectTrigger>
                      <SelectContent>{modeList.map((m) => <SelectItem key={m} value={m}>{m}</SelectItem>)}</SelectContent>
                    </Select>
                  </div>
                  <div className="flex items-center gap-2">
                    <Label className="w-20 shrink-0">{t('qedit.country')}</Label>
                    <Combobox value={draft.country ?? ''} options={countries} placeholder={t('qedit.country')}
                      onChange={onCountryChange} className="flex-1" />
                  </div>
                  <div className="flex items-center gap-2">
                    <Label className="w-20 shrink-0">ITU</Label>
                    <Input type="number" value={draft.ituz ?? ''} onChange={(e) => set('ituz', intOrUndef(e.target.value) as any)} className="font-mono w-16 text-center" />
                    <Label>CQ</Label>
                    <Input type="number" value={draft.cqz ?? ''} onChange={(e) => set('cqz', intOrUndef(e.target.value) as any)} className="font-mono w-16 text-center" />
                    <Input type="number" value={draft.dxcc ?? ''} readOnly tabIndex={-1} className="font-mono w-16 text-center bg-muted/60 text-muted-foreground cursor-not-allowed" title={t('qedit.dxccTitle')} />
                    {flagURL(draft.dxcc) && <img src={flagURL(draft.dxcc)} alt="" className="h-4 rounded-[2px] border border-border/50" referrerPolicy="no-referrer" />}
                  </div>
                  <div className="flex items-center gap-2">
                    <Label className="w-20 shrink-0">{t('qedit.txFreq')}</Label>
                    <Input value={freqKHz} onChange={(e) => { const v = e.target.value.replace(/\D/g, ''); setFreqKHz(v); syncBand(v, freqHz, 'band'); }} className="font-mono w-24" placeholder="kHz" />
                    <Input value={freqHz} onChange={(e) => { const v = e.target.value.replace(/\D/g, ''); setFreqHz(v); syncBand(freqKHz, v, 'band'); }} maxLength={3} className="font-mono w-16" placeholder="Hz" />
                  </div>
                  <div className="flex items-center gap-2">
                    <Label className="w-20 shrink-0">{t('qedit.rxFreq')}</Label>
                    <Input value={freqRxKHz} onChange={(e) => { const v = e.target.value.replace(/\D/g, ''); setFreqRxKHz(v); syncBand(v, freqRxHz, 'band_rx'); }} className="font-mono w-24" placeholder="kHz" />
                    <Input value={freqRxHz} onChange={(e) => { const v = e.target.value.replace(/\D/g, ''); setFreqRxHz(v); syncBand(freqRxKHz, v, 'band_rx'); }} maxLength={3} className="font-mono w-16" placeholder="Hz" />
                  </div>
                </div>

                {/* ── Right column ── */}
                <div className="flex flex-col gap-2.5">
                  <div><Label>{t('qedit.qsoStart')}</Label><Input type="datetime-local" value={dateOn} onChange={(e) => setDateOn(e.target.value)} /></div>
                  <div>
                    <Label className="flex items-center gap-2">
                      <Checkbox checked={endEnabled} onCheckedChange={(c) => {
                        const on = !!c;
                        setEndEnabled(on);
                        // Prefill an empty end with the start time so the user
                        // only tweaks the minutes instead of typing a full date.
                        if (on && !dateOff) setDateOff(dateOn);
                      }} /> {t('qedit.qsoEnd')}
                    </Label>
                    <Input type="datetime-local" value={dateOff} disabled={!endEnabled} onChange={(e) => setDateOff(e.target.value)} />
                  </div>
                  <div className="flex items-end gap-2">
                    <div className="flex flex-col flex-1"><Label>{t('qedit.grid')}</Label><Input value={draft.grid ?? ''} onChange={(e) => set('grid', e.target.value)} className="font-mono uppercase" /></div>
                    <div className="flex flex-col w-24"><Label>{t('qedit.gridExt')}</Label><Input title={t('qedit.gridExtTip')} value={draft.gridsquare_ext ?? ''} onChange={(e) => set('gridsquare_ext', e.target.value)} className="font-mono uppercase" /></div>
                    <div className="flex flex-col w-24"><Label>PFX</Label><Input readOnly value={pfxOf(draft.callsign ?? '')} className="font-mono bg-muted/40" /></div>
                  </div>
                  <div><Label>{t('qedit.comment')}</Label><Input value={draft.comment ?? ''} onChange={(e) => set('comment', e.target.value)}
                    onBlur={() => set('comment', sentenceCase(draft.comment ?? '') as any)} /></div>
                  <div><Label>{t('qedit.note')}</Label><Textarea rows={3} value={draft.notes ?? ''} onChange={(e) => set('notes', e.target.value)}
                    onBlur={() => set('notes', sentenceCase(draft.notes ?? '') as any)} /></div>
                </div>
              </div>
            </TabsContent>

            <TabsContent value="contact" className="mt-0">
              <div className="grid grid-cols-2 gap-x-6 gap-y-2.5">
                {/* Left column */}
                <div className="flex flex-col gap-2.5">
                  <div><Label>{t('qedit.county')}</Label><Input value={draft.cnty ?? ''} onChange={(e) => set('cnty', e.target.value)} /></div>
                  <div><Label>{t('qedit.state')}</Label><Input value={draft.state ?? ''} onChange={(e) => set('state', e.target.value)} /></div>
                  <div>
                    <Label>{t('qedit.continent')}</Label>
                    <Select value={draft.cont || '_'} onValueChange={(v) => set('cont', v === '_' ? '' : v)}>
                      <SelectTrigger className="h-9"><SelectValue placeholder="—" /></SelectTrigger>
                      <SelectContent>
                        <SelectItem value="_">—</SelectItem>
                        {['NA', 'SA', 'EU', 'AF', 'AS', 'OC', 'AN'].map((c) => <SelectItem key={c} value={c}>{c}</SelectItem>)}
                      </SelectContent>
                    </Select>
                  </div>
                  <div><Label>QTH</Label><Input value={draft.qth ?? ''} onChange={(e) => set('qth', e.target.value)}
                    onBlur={() => set('qth', titleCase(draft.qth ?? '') as any)} /></div>
                  <div><Label>{t('qedit.address')}</Label><Textarea rows={4} value={draft.address ?? ''} onChange={(e) => set('address', e.target.value)} /></div>
                </div>
                {/* Right column */}
                <div className="flex flex-col gap-2.5">
                  <div><Label>{t('qedit.email')}</Label><Input value={(draft as any).email ?? ''} onChange={(e) => (set as any)('email', e.target.value)} /></div>
                  <div className="flex items-end gap-2">
                    <div className="flex flex-col flex-1"><Label>Lat</Label><Input type="number" step="0.000001" value={draft.lat ?? ''} onChange={(e) => set('lat', numOrUndef(e.target.value) as any)} className="font-mono" /></div>
                    <div className="flex flex-col flex-1"><Label>Lon</Label><Input type="number" step="0.000001" value={draft.lon ?? ''} onChange={(e) => set('lon', numOrUndef(e.target.value) as any)} className="font-mono" /></div>
                  </div>
                  {/* The OpsLog QSL marker used to sit here, under QSL Msg. It
                      belongs with the other confirmation channels — see the QSL
                      Info tab. */}
                  <div>
                    <Label>{t('qedit.qslMsg')}</Label>
                    <Input value={draft.qsl_msg ?? ''} onChange={(e) => set('qsl_msg', e.target.value)} />
                  </div>
                  {/* The manager, and only the manager — ADIF QSL_VIA. How the
                      card travelled is a separate field, on the QSL Info tab. */}
                  <div>
                    <Label>{t('qedit.qslVia')}</Label>
                    <Input value={draft.qsl_via ?? ''} onChange={(e) => set('qsl_via', e.target.value)} placeholder={t('qedit.qslViaPlaceholder')} />
                  </div>
                </div>
              </div>
            </TabsContent>

            <TabsContent value="awards" className="mt-0">
              <div className="grid grid-cols-[1fr_240px] gap-5">
                {/* Left: pick reference-list awards (POTA/SOTA/IOTA/WWFF/…) */}
                <div>
                  <AwardRefSelector dxcc={draft.dxcc} value={awardRefs} onChange={setAwardRefs} fieldValues={{ state: draft.state ?? '', cnty: draft.cnty ?? '' }} />
                </div>

                {/* Right: computed awards (read-only) derived from this QSO */}
                <div className="flex flex-col gap-1.5 min-w-0">
                  <span className="text-xs font-semibold">{t('qedit.computedAuto')}</span>
                  <p className="text-[11px] text-muted-foreground leading-snug">
                    {t('qedit.computedHint')}
                  </p>
                  <div className="flex-1 overflow-auto border rounded-md text-xs min-h-[160px] max-h-[210px]">
                    {computedRefs.length === 0 ? (
                      <div className="px-2 py-1.5 text-[11px] text-muted-foreground">{t('qedit.noneYet')}</div>
                    ) : (
                      computedRefs.map((r) => (
                        <div key={`${r.code}@${r.ref}`} className="px-2 py-1 border-b border-border/30 last:border-0">
                          <span className="font-mono font-semibold">{r.code}@{r.ref}</span>
                          {r.name && <span className="text-[10px] text-muted-foreground ml-1.5 truncate">{r.name}</span>}
                        </div>
                      ))
                    )}
                  </div>
                </div>
              </div>
            </TabsContent>

            <TabsContent value="qsl" className="mt-0">
              {(() => {
                const def = CONFIRMATIONS.find((c) => c.key === confSel) ?? CONFIRMATIONS[0];
                const val = (k?: keyof QSOForm) => (k ? ((draft as any)[k] ?? '') : '');
                const put = (k: keyof QSOForm | undefined, v: any) => { if (k) (set as any)(k, v); };
                // The extras-backed channel reads and writes the same way, one
                // level down. Save merges the extras rather than replacing them,
                // so an emptied field keeps its stored value — which is why the
                // statuses are written as explicit Y/N and never removed.
                const exVal = (k: string) => String(draft.extras?.[k] ?? '');
                const exPut = (k: string, v: any) => {
                  const next = { ...(draft.extras ?? {}) };
                  next[k] = String(v ?? '');
                  set('extras', next as any);
                };
                return (
                  <div className="flex gap-6">
                    {/* Left: edit one confirmation channel at a time */}
                    <div className="flex-1 max-w-sm space-y-3">
                      <div>
                        <Label>{t('qedit.manageConf')}</Label>
                        <Select value={confSel} onValueChange={setConfSel}>
                          <SelectTrigger><SelectValue /></SelectTrigger>
                          <SelectContent>
                            {confOrder([
                              ...CONFIRMATIONS.map((c) => ({ key: c.key, label: CONF_LABEL_KEYS[c.key] ? t(CONF_LABEL_KEYS[c.key]) : c.label })),
                              { key: OPSLOG_CONF, label: t('qedit.confOpsLog') },
                              { key: HAMLOG_CONF, label: 'HAMLOG.online' },
                              { key: HAMQTH_CONF, label: 'HamQTH' },
                            ]).map((c) => <SelectItem key={c.key} value={c.key}>{c.label}</SelectItem>)}
                            {/* The three above that are NOT in CONFIRMATIONS — the
                                QSO columns, and this channel lives in the ADIF
                                OpsLog card, HAMLOG.online and HamQTH — are backed by
                                ADIF extras rather than QSO columns, and each gets its
                                own editor below instead of the generic
                                sent/received/date grid, which has no field to bind
                                to. */}
                          </SelectContent>
                        </Select>
                      </div>

                      {confSel === HAMQTH_CONF ? (
                        <>
                          <div className="grid grid-cols-2 gap-3">
                            <div><Label>{t('qedit.sent')}</Label><QslSelect value={exVal(HAMQTH_KEYS.sent)} onChange={(v) => exPut(HAMQTH_KEYS.sent, v)} /></div>
                            <div><Label>{t('qedit.dateSent')}</Label><AdifDateInput value={exVal(HAMQTH_KEYS.sentDate)} onChange={(v) => exPut(HAMQTH_KEYS.sentDate, v)} /></div>
                          </div>
                          <p className="text-[11px] text-muted-foreground">
                            {t('qedit.qslPanelHint')} <strong>{t('qedit.saveChanges')}</strong>.
                          </p>
                        </>
                      ) : confSel === HAMLOG_CONF ? (
                        <>
                          <div className="grid grid-cols-2 gap-3">
                            <div><Label>{t('qedit.sent')}</Label><QslSelect value={exVal(HAMLOG_KEYS.sent)} onChange={(v) => exPut(HAMLOG_KEYS.sent, v)} /></div>
                            <div><Label>{t('qedit.received')}</Label><QslSelect value={exVal(HAMLOG_KEYS.rcvd)} onChange={(v) => exPut(HAMLOG_KEYS.rcvd, v)} /></div>
                            <div><Label>{t('qedit.dateSent')}</Label><AdifDateInput value={exVal(HAMLOG_KEYS.sentDate)} onChange={(v) => exPut(HAMLOG_KEYS.sentDate, v)} /></div>
                            <div><Label>{t('qedit.dateReceived')}</Label><AdifDateInput value={exVal(HAMLOG_KEYS.rcvdDate)} onChange={(v) => exPut(HAMLOG_KEYS.rcvdDate, v)} /></div>
                          </div>
                          <p className="text-[11px] text-muted-foreground">
                            {t('qedit.qslPanelHint')} <strong>{t('qedit.saveChanges')}</strong>.
                          </p>
                        </>
                      ) : confSel === OPSLOG_CONF ? (
                        /* OpsLog's own card. "Sent" is stamped when the card
                           actually goes out, so it is shown, not offered: ticking
                           it by hand would record something that never happened.
                           "Received" drives the PSE/TNX stamp printed on the card,
                           which is the whole reason the flag exists — hence the
                           live indicator next to it. It writes immediately rather
                           than on Save, because clearing an extras key would not
                           survive the merge Save does. */
                        <div className="space-y-3">
                          <div className="grid grid-cols-2 gap-3">
                            <div>
                              <Label>{t('qedit.sent')}</Label>
                              <Input disabled value={opslogQslSent ? t('qedit.qslYes') : t('qedit.qslNo')} />
                            </div>
                            <div>
                              <Label>{t('qedit.received')}</Label>
                              <label className="flex h-9 items-center gap-2 text-sm cursor-pointer">
                                <Checkbox checked={qslReceived} onCheckedChange={(c) => toggleQslReceived(!!c)} />
                                {t('qedit.qslReceived')}
                              </label>
                            </div>
                          </div>
                          <div className="flex items-center gap-2">
                            <span className="text-[11px] font-mono px-1.5 py-0.5 rounded bg-muted text-muted-foreground">
                              {qslReceived ? 'TNX QSL' : 'PSE QSL'}
                            </span>
                            <span className="text-[11px] text-muted-foreground">{t('qedit.pseTnxHint')}</span>
                          </div>
                          <p className="text-[11px] text-muted-foreground">{t('qedit.opslogSentHint')}</p>
                        </div>
                      ) : (
                        <>
                          <div className="grid grid-cols-2 gap-3">
                            <div><Label>{t('qedit.sent')}</Label><QslSelect value={val(def.sent)} onChange={(v) => put(def.sent, v)} /></div>
                            <div><Label>{t('qedit.received')}</Label>
                              {def.rcvd
                                ? <QslSelect value={val(def.rcvd)} onChange={(v) => put(def.rcvd, v)} />
                                : <Input disabled value="—" />}
                            </div>
                            <div><Label>{t('qedit.dateSent')}</Label><AdifDateInput value={val(def.sentDate)} onChange={(v) => put(def.sentDate, v)} /></div>
                            <div><Label>{t('qedit.dateReceived')}</Label><AdifDateInput value={val(def.rcvdDate)} onChange={(v) => put(def.rcvdDate, v)} disabled={!def.rcvdDate} /></div>
                            {/* How the card travelled, each way — ADIF's QSL Via
                                enumeration. Not the manager: that is a callsign
                                and lives on the Contact's details tab. */}
                            {def.sentVia && (
                              <div><Label>{t('qedit.sentVia')}</Label><QslViaSelect value={val(def.sentVia)} onChange={(v) => put(def.sentVia, v)} /></div>
                            )}
                            {def.rcvdVia && (
                              <div><Label>{t('qedit.rcvdVia')}</Label><QslViaSelect value={val(def.rcvdVia)} onChange={(v) => put(def.rcvdVia, v)} /></div>
                            )}
                          </div>
                          <p className="text-[11px] text-muted-foreground">
                            {t('qedit.qslPanelHint')} <strong>{t('qedit.saveChanges')}</strong>.
                          </p>
                        </>
                      )}
                    </div>

                    {/* Right: live status grid for every channel.
                        Sized by its content, not pinned to a width: a fixed 288px box
                        left the label column too narrow, so "QSL (paper)" wrapped onto
                        two lines and padded out the whole row. There is spare width to
                        the right — spend it on the label. */}
                    <div className="shrink-0">
                      <table className="border-separate" style={{ borderSpacing: 4 }}>
                        <thead>
                          <tr className="text-[10px] uppercase tracking-wider text-muted-foreground">
                            <th className="text-left font-semibold">{t('qedit.thType')}</th>
                            <th className="font-semibold">{t('qedit.sent')}</th>
                            <th className="font-semibold">{t('qedit.received')}</th>
                          </tr>
                        </thead>
                        <tbody>
                          {/* The extras-backed channels (OpsLog card,
                              HAMLOG.online, HamQTH) sit in the same ordered list
                              as the column-backed ones: the reader is looking for
                              a name, not for a storage detail. A dash in RECEIVED
                              means the channel has nothing to receive — Club Log
                              and HamQTH publish no confirmations — which is not
                              the same statement as "N". */}
                          {confOrder([
                            ...CONFIRMATIONS.map((c) => ({
                              key: c.key,
                              label: CONF_LABEL_KEYS[c.key] ? t(CONF_LABEL_KEYS[c.key]) : c.label,
                              sent: val(c.sent),
                              rcvd: c.rcvd ? val(c.rcvd) : null,
                            })),
                            { key: OPSLOG_CONF, label: t('qedit.confOpsLog'), sent: opslogQslSent ? 'Y' : 'N', rcvd: qslReceived ? 'Y' : 'N' },
                            { key: HAMLOG_CONF, label: 'HAMLOG.online', sent: exVal(HAMLOG_KEYS.sent), rcvd: exVal(HAMLOG_KEYS.rcvd) },
                            { key: HAMQTH_CONF, label: 'HamQTH', sent: exVal(HAMQTH_KEYS.sent), rcvd: null },
                          ]).map((c) => (
                            <tr key={c.key} className="text-xs">
                              <td className="font-medium pr-3 py-0.5 whitespace-nowrap">{c.label}</td>
                              <td className="w-24"><StatusCell value={c.sent} /></td>
                              <td className="w-24">{c.rcvd === null
                                ? <span className="block text-center text-[11px] text-muted-foreground">—</span>
                                : <StatusCell value={c.rcvd} />}</td>
                            </tr>
                          ))}
                        </tbody>
                      </table>
                    </div>
                  </div>
                );
              })()}
            </TabsContent>

            <TabsContent value="contest" className="mt-0">
              <div className="grid grid-cols-6 gap-3">
                <F label={t('qedit.contestId')} span={2}><Input value={draft.contest_id ?? ''} onChange={(e) => set('contest_id', e.target.value)} /></F>
                <F label="SRX" span={2}><Input value={draft.srx_string || (draft.srx ?? '')} placeholder={t('qedit.rcvdExchange')} onChange={(e) => setExchange('srx', e.target.value)} /></F>
                <F label="STX" span={2}><Input value={draft.stx_string || (draft.stx ?? '')} placeholder={t('qedit.sentExchange')} onChange={(e) => setExchange('stx', e.target.value)} /></F>
                <F label={t('qedit.check')}><Input value={draft.check ?? ''} onChange={(e) => set('check', e.target.value)} /></F>
                <F label={t('qedit.precedence')}><Input value={draft.precedence ?? ''} onChange={(e) => set('precedence', e.target.value)} /></F>
                <F label={t('qedit.arrlSection')}><Input value={draft.arrl_sect ?? ''} onChange={(e) => set('arrl_sect', e.target.value)} /></F>
              </div>
            </TabsContent>

            <TabsContent value="sat" className="mt-0">
              <div className="grid grid-cols-6 gap-3">
                <F label={t('qedit.propMode')}>
                  <Select value={draft.prop_mode || '_'} onValueChange={(v) => set('prop_mode', v === '_' ? '' : v)}>
                    <SelectTrigger><SelectValue /></SelectTrigger>
                    <SelectContent>{PROP_MODES.map((p) => <SelectItem key={p} value={p}>{p === '_' ? '—' : p}</SelectItem>)}</SelectContent>
                  </Select>
                </F>
                <F label={t('qedit.satName')}><Input value={draft.sat_name ?? ''} placeholder="AO-91" onChange={(e) => set('sat_name', e.target.value)} /></F>
                <F label={t('qedit.satMode')}><Input value={draft.sat_mode ?? ''} placeholder="U/V" onChange={(e) => set('sat_mode', e.target.value)} /></F>
                <F label={t('qedit.antAz')}><Input type="number" value={draft.ant_az ?? ''} onChange={(e) => set('ant_az', numOrUndef(e.target.value) as any)} /></F>
                <F label={t('qedit.antEl')}><Input type="number" value={draft.ant_el ?? ''} onChange={(e) => set('ant_el', numOrUndef(e.target.value) as any)} /></F>
                <F label={t('qedit.antPath')}><Input value={draft.ant_path ?? ''} placeholder="S, L, G" onChange={(e) => set('ant_path', e.target.value)} /></F>
              </div>
            </TabsContent>

            <TabsContent value="mystation" className="mt-0 space-y-3">
              <p className="text-xs text-muted-foreground">{t('qedit.myStationHint')}</p>
              <div className="grid grid-cols-6 gap-3">
                <F label={t('qedit.stationCallsign')} span={3}><Input value={draft.station_callsign ?? ''} onChange={(e) => set('station_callsign', e.target.value)} /></F>
                <F label={t('qedit.operator')} span={3}><Input value={draft.operator ?? ''} onChange={(e) => set('operator', e.target.value)} /></F>
                <F label={t('qedit.myGrid')}><Input value={draft.my_grid ?? ''} onChange={(e) => set('my_grid', e.target.value)} /></F>
                <F label={t('qedit.myGridExt')}><Input title={t('qedit.gridExtTip')} value={draft.my_gridsquare_ext ?? ''} onChange={(e) => set('my_gridsquare_ext', e.target.value)} /></F>
                <F label={t('qedit.country')} span={2}><Combobox value={draft.my_country ?? ''} options={countries} placeholder={t('qedit.country')} onChange={(v) => set('my_country', v)} /></F>
                <F label={t('qedit.state')}><Input value={draft.my_state ?? ''} onChange={(e) => set('my_state', e.target.value)} /></F>
                <F label={t('qedit.county')}><Input value={draft.my_cnty ?? ''} onChange={(e) => set('my_cnty', e.target.value)} /></F>
                <F label="DXCC"><Input type="number" value={draft.my_dxcc ?? ''} onChange={(e) => set('my_dxcc', intOrUndef(e.target.value) as any)} /></F>
                <F label={t('qedit.cqZone')}><Input type="number" value={draft.my_cq_zone ?? ''} onChange={(e) => set('my_cq_zone', intOrUndef(e.target.value) as any)} /></F>
                <F label={t('qedit.ituZone')}><Input type="number" value={draft.my_itu_zone ?? ''} onChange={(e) => set('my_itu_zone', intOrUndef(e.target.value) as any)} /></F>
                <F label="IOTA"><Input value={draft.my_iota ?? ''} onChange={(e) => set('my_iota', e.target.value)} /></F>
                <F label={t('qedit.sotaRef')}><Input value={draft.my_sota_ref ?? ''} onChange={(e) => set('my_sota_ref', e.target.value)} /></F>
                <F label={t('qedit.potaRef')}><Input value={draft.my_pota_ref ?? ''} onChange={(e) => set('my_pota_ref', e.target.value)} /></F>
                <F label="Lat"><Input type="number" step="0.000001" value={draft.my_lat ?? ''} onChange={(e) => set('my_lat', numOrUndef(e.target.value) as any)} /></F>
                <F label="Lon"><Input type="number" step="0.000001" value={draft.my_lon ?? ''} onChange={(e) => set('my_lon', numOrUndef(e.target.value) as any)} /></F>
                <F label={t('qedit.street')} span={2}><Input value={draft.my_street ?? ''} onChange={(e) => set('my_street', e.target.value)} /></F>
                <F label={t('qedit.city')} span={2}><Input value={draft.my_city ?? ''} onChange={(e) => set('my_city', e.target.value)} /></F>
                <F label={t('qedit.postal')} span={2}><Input value={draft.my_postal_code ?? ''} onChange={(e) => set('my_postal_code', e.target.value)} /></F>
                <F label={t('qedit.rig')} span={3}><Input value={draft.my_rig ?? ''} onChange={(e) => set('my_rig', e.target.value)} /></F>
                <F label={t('qedit.antenna')} span={3}><Input value={draft.my_antenna ?? ''} onChange={(e) => set('my_antenna', e.target.value)} /></F>
              </div>
            </TabsContent>

            <TabsContent value="moreadif" className="mt-0 space-y-4">
              {/* Special activity (POTA/SOTA/WWFF/SIG) */}
              <div>
                <p className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider mb-2">{t('qedit.specialActivity')}</p>
                <div className="grid grid-cols-6 gap-3">
                  <F label="SIG"><Input value={draft.sig ?? ''} placeholder="POTA" onChange={(e) => set('sig', e.target.value)} /></F>
                  <F label={t('qedit.sigInfo')} span={2}><Input value={draft.sig_info ?? ''} placeholder="US-0001" onChange={(e) => set('sig_info', e.target.value)} /></F>
                  <F label={t('qedit.wwffRef')} span={2}><Input value={draft.wwff_ref ?? ''} placeholder="ONFF-0001" onChange={(e) => set('wwff_ref', e.target.value)} className="font-mono uppercase" /></F>
                  <F label={t('qedit.region')}><Input value={draft.region ?? ''} onChange={(e) => set('region', e.target.value)} /></F>
                </div>
              </div>

              {/* Power & propagation */}
              <div>
                <p className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider mb-2">{t('qedit.powerWeather')}</p>
                <div className="grid grid-cols-6 gap-3">
                  {/* YOUR power first, then theirs. tx_pwr was saved by this
                      editor but had no field: a contact logged at the wrong
                      power could be read back and never corrected. */}
                  <F label={t('qedit.txPower')}><Input type="number" value={draft.tx_pwr ?? ''} onChange={(e) => set('tx_pwr', numOrUndef(e.target.value) as any)} /></F>
                  <F label={t('qedit.rxPower')}><Input type="number" value={draft.rx_pwr ?? ''} onChange={(e) => set('rx_pwr', numOrUndef(e.target.value) as any)} /></F>
                  <F label={t('qedit.distance')}><Input type="number" value={draft.distance ?? ''} onChange={(e) => set('distance', numOrUndef(e.target.value) as any)} /></F>
                  <F label={t('qedit.aIndex')}><Input type="number" value={draft.a_index ?? ''} onChange={(e) => set('a_index', numOrUndef(e.target.value) as any)} /></F>
                  <F label={t('qedit.kIndex')}><Input type="number" value={draft.k_index ?? ''} onChange={(e) => set('k_index', numOrUndef(e.target.value) as any)} /></F>
                  <F label="SFI"><Input type="number" value={draft.sfi ?? ''} onChange={(e) => set('sfi', numOrUndef(e.target.value) as any)} /></F>
                </div>
              </div>

              {/* Identity & clubs */}
              <div>
                <p className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider mb-2">{t('qedit.identityClubs')}</p>
                <div className="grid grid-cols-6 gap-3">
                  <F label={t('qedit.contactedOp')} span={2}><Input value={draft.contacted_op ?? ''} placeholder="EA8XYZ" onChange={(e) => set('contacted_op', e.target.value)} className="font-mono uppercase" /></F>
                  <F label={t('qedit.formerCall')} span={2}><Input value={draft.eq_call ?? ''} onChange={(e) => set('eq_call', e.target.value)} className="font-mono uppercase" /></F>
                  <F label={t('qedit.class')}><Input value={draft.class ?? ''} placeholder="1A" onChange={(e) => set('class', e.target.value)} /></F>
                  <F label="SKCC"><Input value={draft.skcc ?? ''} onChange={(e) => set('skcc', e.target.value)} /></F>
                  <F label="FISTS"><Input value={draft.fists ?? ''} onChange={(e) => set('fists', e.target.value)} /></F>
                  <F label="Ten-Ten"><Input value={draft.ten_ten ?? ''} onChange={(e) => set('ten_ten', e.target.value)} /></F>
                  <F label="DARC DOK"><Input value={draft.darc_dok ?? ''} onChange={(e) => set('darc_dok', e.target.value)} /></F>
                </div>
              </div>

              {/* Flags & credits */}
              <div>
                <p className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider mb-2">{t('qedit.flagsCredits')}</p>
                <div className="grid grid-cols-6 gap-3">
                  <F label={t('qedit.qsoComplete')}><Input value={draft.qso_complete ?? ''} placeholder="Y/N/NIL/?" onChange={(e) => set('qso_complete', e.target.value)} /></F>
                  <F label={t('qedit.qsoRandom')}><Input value={draft.qso_random ?? ''} placeholder="Y/N" onChange={(e) => set('qso_random', e.target.value)} /></F>
                  <F label={t('qedit.silentKey')}><Input value={draft.silent_key ?? ''} placeholder="Y/N" onChange={(e) => set('silent_key', e.target.value)} /></F>
                  <F label="SWL"><Input value={draft.swl ?? ''} placeholder="Y/N" onChange={(e) => set('swl', e.target.value)} /></F>
                  <F label={t('qedit.creditGranted')} span={3}><Input value={draft.credit_granted ?? ''} placeholder="DXCC,WAS" onChange={(e) => set('credit_granted', e.target.value)} /></F>
                  <F label={t('qedit.creditSubmitted')} span={3}><Input value={draft.credit_submitted ?? ''} onChange={(e) => set('credit_submitted', e.target.value)} /></F>
                </div>
              </div>

              {/* My station extras */}
              <div>
                <p className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider mb-2">{t('qedit.myStationAdif')}</p>
                <div className="grid grid-cols-6 gap-3">
                  <F label={t('qedit.myName')} span={2}><Input value={draft.my_name ?? ''} onChange={(e) => set('my_name', e.target.value)} /></F>
                  <F label={t('qedit.myWwffRef')} span={2}><Input value={draft.my_wwff_ref ?? ''} onChange={(e) => set('my_wwff_ref', e.target.value)} className="font-mono uppercase" /></F>
                  <F label={t('qedit.myArrlSect')} span={2}><Input value={draft.my_arrl_sect ?? ''} onChange={(e) => set('my_arrl_sect', e.target.value)} /></F>
                  <F label={t('qedit.mySig')}><Input value={draft.my_sig ?? ''} onChange={(e) => set('my_sig', e.target.value)} /></F>
                  <F label={t('qedit.mySigInfo')} span={2}><Input value={draft.my_sig_info ?? ''} onChange={(e) => set('my_sig_info', e.target.value)} /></F>
                  <F label={t('qedit.myDarcDok')}><Input value={draft.my_darc_dok ?? ''} onChange={(e) => set('my_darc_dok', e.target.value)} /></F>
                  <F label={t('qedit.myVuccGrids')} span={2}><Input value={draft.my_vucc_grids ?? ''} onChange={(e) => set('my_vucc_grids', e.target.value)} className="font-mono uppercase" /></F>
                </div>
              </div>
            </TabsContent>

            <TabsContent value="extras" className="mt-0">
              <AdifExtrasEditor value={draft.extras} onChange={(next) => set('extras', next as any)} />
            </TabsContent>
          </div>
        </Tabs>

        <DialogFooter className="!flex-row gap-2">
          <Button variant="outline" className="text-destructive hover:bg-destructive/10 hover:text-destructive" onClick={() => onDelete(draft.id)} disabled={saving}>
            <Trash2 className="size-3.5" /> {t('qedit.delete')}
          </Button>
          <div className="flex-1" />
          <Button variant="outline" onClick={onClose} disabled={saving}>{t('qedit.cancel')}</Button>
          <Button onClick={save} disabled={saving}>{saving ? t('qedit.saving') : t('qedit.saveChanges')}</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}
