import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { UploadCloud, DownloadCloud, Search, Loader2, ScrollText, ListChecks, Trees, ExternalLink } from 'lucide-react';
import { AllCommunityModule, ModuleRegistry, type ColDef } from 'ag-grid-community';
import { AgGridReact } from 'ag-grid-react';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import {
  Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
} from '@/components/ui/select';
import { cn } from '@/lib/utils';
import { GetLoTWQSLDetail, SetLoTWQSLDetail, GetLoTWDownloadAllCalls, SetLoTWDownloadAllCalls, OpenExternalURL, FindQSOsForUpload, UploadFullLogHamQTH, UploadQSOsManual, DownloadConfirmations, CancelConfirmations, ImportHamlogConfirmations, ExportHamlogUnmatched, OpenADIFFile, SaveADIFFile, SyncPOTAHunterLog, ListQSO, BulkUpdateQSL, UploadCallsign, GetSlotStats } from '../../wailsjs/go/main/App';
import { Input } from '@/components/ui/input';
import { RecentQSOsGrid } from '@/components/RecentQSOsGrid';
import { EventsOn } from '../../wailsjs/runtime/runtime';
import { useI18n } from '@/lib/i18n';

ModuleRegistry.registerModules([AllCommunityModule]);

type UploadRow = {
  id: number; qso_date: string; callsign: string;
  band: string; mode: string; country: string; status: string;
};

const UPLOAD_COLS: ColDef<UploadRow>[] = [
  { field: 'qso_date', headerName: 'Date UTC', valueFormatter: (p) => fmtDate(p.value), minWidth: 150 },
  { field: 'callsign', headerName: 'Callsign', cellClass: 'font-mono font-bold', width: 130 },
  { field: 'band', headerName: 'Band', width: 90 },
  { field: 'mode', headerName: 'Mode', width: 100 },
  { field: 'country', headerName: 'Country', flex: 1, minWidth: 140 },
  { field: 'status', headerName: 'Sent', width: 90, valueFormatter: (p) => p.value || '—' },
];

type Confirmation = {
  callsign: string; station?: string; qso_date: string; band: string; mode: string; country: string;
  new_dxcc: boolean; new_band: boolean; new_mode: boolean; new_slot: boolean;
};

const SERVICES = [
  { v: 'qrz',     label: 'QRZ.com' },
  { v: 'clublog', label: 'Club Log' },
  { v: 'hrdlog',  label: 'HRDLog.net' },
  { v: 'eqsl',    label: 'eQSL.cc' },
  { v: 'lotw',    label: 'LoTW' },
  { v: 'hamlog',  label: 'HAMLOG.online' },
  { v: 'hamqth',  label: 'HamQTH' },
  { v: 'pota',    label: 'POTA hunter log' },
  { v: 'paper',   label: 'Paper QSL' },
];

// label holds an i18n key (resolved with t() at render time).
const QSL_STATUSES = [
  { v: '_', label: 'qslm.leave' },
  { v: 'Y', label: 'qslm.yes' },
  { v: 'N', label: 'qslm.no' },
  { v: 'R', label: 'qslm.requested' },
  { v: 'I', label: 'qslm.ignore' },
];

// QSL routing methods for the paper-QSL route dropdowns, one per direction.
// These are the ADIF QSL Via enumeration (B/D/E) and go to QSL_SENT_VIA and
// QSL_RCVD_VIA — NOT to QSL_VIA, which holds the manager's callsign.
const QSL_VIA_OPTIONS = [
  { v: '_', label: 'qslm.leave' },
  { v: 'B', label: 'qslm.viaBureau' },
  { v: 'D', label: 'qslm.viaDirect' },
  { v: 'E', label: 'qslm.viaElectronic' },
];

// Maps a service value → its i18n label key (only for services with
// translatable names; brand names like QRZ.com stay as-is in SERVICES).
const SERVICE_LABEL_KEYS: Record<string, string> = {
  pota: 'qslm.svcPota',
  paper: 'qslm.svcPaper',
};

type LogQSO = {
  id: number; qso_date: string; callsign: string; band: string; mode: string; country?: string;
  qsl_sent?: string; qsl_rcvd?: string; qsl_via?: string; qsl_sent_date?: string; qsl_rcvd_date?: string;
};

type POTAUnmatched = { activator: string; date: string; band: string; reference: string; reason: string; qso_id: number };
type POTASync = { fetched: number; updated: number; already_tagged: number; added: number; unmatched: number; unmatched_list: POTAUnmatched[]; skipped_other_call: number; my_call: string };

// label holds an i18n key.
const SENT_STATUSES = [
  { v: 'R', label: 'qslm.sentRequested' },
  { v: 'N', label: 'qslm.sentNo' },
  { v: 'Q', label: 'qslm.sentQueued' },
  { v: 'Y', label: 'qslm.sentYes' },
  { v: 'I', label: 'qslm.sentInvalid' },
  { v: '_', label: 'qslm.sentBlank' },
];

function fmtDate(iso: string): string {
  if (!iso) return '';
  const d = new Date(iso);
  if (isNaN(d.getTime())) return iso;
  const p = (n: number) => String(n).padStart(2, '0');
  return `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())} ${p(d.getUTCHours())}:${p(d.getUTCMinutes())}`;
}

// fmtQslDate renders a QSL/LoTW/eQSL/ClubLog date (ADIF YYYYMMDD, or an ISO
// datetime) as YYYY-MM-DD — same shape as the QSO date, without the time.
export function fmtQslDate(s?: string): string {
  if (!s) return '';
  const t = s.trim();
  const m = t.match(/^(\d{4})(\d{2})(\d{2})/);
  if (m) return `${m[1]}-${m[2]}-${m[3]}`;
  const d = new Date(t);
  if (!isNaN(d.getTime())) return d.toISOString().slice(0, 10);
  return t;
}

// QSL Manager as an in-app tab panel: upload logged QSOs to online logbooks
// and download confirmations, while the rest of the app stays usable.
// GridActions are the row actions the context menu offers, forwarded straight to
// whichever grid this panel is showing.
//
// The FILTERED exports are deliberately absent. "Export everything the filter
// matches" means the Recent-QSOs filter, and the rows here come from a different
// query entirely — offering it would export something other than what is on the
// screen, which is the one thing an export must never do. Selection-based
// exports have no such ambiguity: the ids are the rows you ticked.
export type GridActions = {
  onUpdateFromCty?: (ids: number[]) => void;
  onUpdateFromQRZ?: (ids: number[]) => void;
  onUpdateFromClublog?: (ids: number[]) => void;
  onUpdateCountyFromULS?: (ids: number[]) => void;
  onSendTo?: (service: string, ids: number[]) => void;
  onSendRecording?: (ids: number[]) => void;
  onSendEQSL?: (ids: number[]) => void;
  onBulkEdit?: (ids: number[]) => void;
  onExportSelected?: (ids: number[]) => void;
  onExportSelectedFields?: (ids: number[]) => void;
  onExportCabrilloSelected?: (ids: number[]) => void;
  onDelete?: (ids: number[]) => void;
};

export function QSLManagerPanel({ onEditQSO, actions, paperRequest }: {
  onEditQSO?: (id: number) => void;
  actions?: GridActions;
  // A callsign to open the Paper QSL view on, sent from elsewhere in the app
  // (the awards grid). Carries a counter rather than being a bare string: the
  // panel is force-mounted and keeps its state, so asking twice for the SAME
  // callsign has to be two requests, not one unchanged prop.
  paperRequest?: { call: string; n: number };
} = {}) {
  const { t } = useI18n();
  const [service, setService] = useState('lotw');
  // The callsign this profile signs/uploads/downloads as for the selected
  // service (Force station callsign, else the profile call). Shown so the user
  // knows WHICH of their calls a download/upload targets in a mixed-call log.
  const [uploadCall, setUploadCall] = useState('');
  useEffect(() => {
    if (service === 'pota' || service === 'paper') { setUploadCall(''); return; }
    UploadCallsign(service).then((c) => setUploadCall(c || '')).catch(() => setUploadCall(''));
  }, [service]);
  // Closing the QSL Manager (the tab's ×) unmounts this panel — cancel any download
  // still running so a slow QRZ/LoTW sync doesn't keep going in the background and
  // bleed its log into the next one.
  useEffect(() => () => { CancelConfirmations().catch(() => {}); }, []);
  const [potaSyncing, setPotaSyncing] = useState(false);
  const [potaRes, setPotaRes] = useState<POTASync | null>(null);
  const [potaErr, setPotaErr] = useState('');
  const [potaAddMissing, setPotaAddMissing] = useState(false);
  // Only sync hunts made under the active profile's callsign (skip XV9Q/NQ2H…).
  const [potaOnlyMyCall, setPotaOnlyMyCall] = useState(true);

  async function syncPota() {
    setPotaSyncing(true); setPotaErr(''); setPotaRes(null);
    try { setPotaRes((await SyncPOTAHunterLog(potaAddMissing, potaOnlyMyCall)) as any as POTASync); }
    catch (e: any) { setPotaErr(String(e?.message ?? e)); }
    finally { setPotaSyncing(false); }
  }

  // ── Paper QSL: search a callsign, bulk-set sent/received + via + date ──
  const [paperCall, setPaperCall] = useState('');
  const [paperRows, setPaperRows] = useState<LogQSO[]>([]);
  const [paperSel, setPaperSel] = useState<Set<number>>(new Set());
  const [paperSelAllSig, setPaperSelAllSig] = useState(0); // bump → grid selects all
  const [paperBusy, setPaperBusy] = useState(false);
  const [paperMsg, setPaperMsg] = useState('');
  const [qslRcvd, setQslRcvd] = useState('N');
  const [qslSent, setQslSent] = useState('_');
  const [qslRcvdDate, setQslRcvdDate] = useState('');
  const [qslSentDate, setQslSentDate] = useState('');
  const [qslVia, setQslVia] = useState('_');       // how the card was SENT
  const [qslRcvdVia, setQslRcvdVia] = useState('_'); // how it CAME BACK
  const [qslNotes, setQslNotes] = useState('');
  const [qslComment, setQslComment] = useState('');

  const searchPaper = useCallback(async () => {
    const c = paperCall.trim().toUpperCase();
    if (!c) return;
    setPaperBusy(true); setPaperMsg('');
    try {
      const r: any = await ListQSO({ callsign: c, limit: 1000 } as any);
      const list = (r ?? []) as LogQSO[];
      setPaperRows(list);
      setPaperSel(new Set(list.map((x) => x.id)));
      setPaperSelAllSig((n) => n + 1); // tell the grid to select every row

    } catch (e: any) { setPaperMsg(String(e?.message ?? e)); setPaperRows([]); }
    finally { setPaperBusy(false); }
  }, [paperCall]);


  // Honour an incoming request: switch to Paper QSL, fill the callsign, search.
  // The search runs from the effect below once paperCall has actually changed —
  // searchPaper closes over paperCall, so calling it here would search the
  // PREVIOUS callsign.
  // The pending request's callsign, so the search fires for THAT callsign and
  // for nothing else: the effect below also wakes on every keystroke in the
  // field, and searching on each of them would query the log letter by letter.
  const pendingPaperCall = useRef('');
  useEffect(() => {
    const call = paperRequest?.call?.trim().toUpperCase();
    if (!call) return;
    setService('paper');
    setPaperCall(call);
    pendingPaperCall.current = call;
  }, [paperRequest?.call, paperRequest?.n]);
  useEffect(() => {
    if (!pendingPaperCall.current || paperCall !== pendingPaperCall.current) return;
    pendingPaperCall.current = '';
    searchPaper();
  }, [paperCall, searchPaper]);

  async function applyPaper() {
    const ids = paperRows.filter((r) => paperSel.has(r.id)).map((r) => r.id);
    if (ids.length === 0) return;
    setPaperBusy(true); setPaperMsg('');
    const ymd = (d: string) => d.replaceAll('-', '');
    try {
      const n = await BulkUpdateQSL(ids as any, {
        sent_status: qslSent === '_' ? '' : qslSent,
        rcvd_status: qslRcvd === '_' ? '' : qslRcvd,
        sent_date: ymd(qslSentDate),
        rcvd_date: ymd(qslRcvdDate),
        via: qslVia === '_' ? '' : qslVia,
        rcvd_via: qslRcvdVia === '_' ? '' : qslRcvdVia,
        notes: qslNotes,
        comment: qslComment,
      } as any);
      setPaperMsg(t('qslm.qsoUpdated', { n }));
      await searchPaper();
    } catch (e: any) { setPaperMsg(String(e?.message ?? e)); }
    finally { setPaperBusy(false); }
  }
  const [sent, setSent] = useState('R');
  // Full QSO rows (so the upload view uses the same rich grid as Recent QSOs).
  const [rows, setRows] = useState<any[]>([]);
  const [selectedCount, setSelectedCount] = useState(0);
  const [uploadSelIds, setUploadSelIds] = useState<number[]>([]); // selected QSO ids → upload
  const [uploadSelAllSig, setUploadSelAllSig] = useState(0);      // bump → grid selects all
  const [searching, setSearching] = useState(false);
  const [error, setError] = useState('');
  const [addNotFound, setAddNotFound] = useState(false);
  // LoTW only: pull the whole account rather than this profile's callsign.
  const [lotwAllCalls, setLotwAllCalls] = useState(false);
  // LoTW only: ask for the QSL dates and station details. Ten times slower to
  // build, so it is a choice rather than the default it used to be.
  const [lotwDetail, setLotwDetail] = useState(false);
  useEffect(() => {
    GetLoTWDownloadAllCalls().then((v: boolean) => setLotwAllCalls(!!v)).catch(() => {});
    GetLoTWQSLDetail().then((v: boolean) => setLotwDetail(!!v)).catch(() => {});
  }, []);
  // Download date window: 'last' = incremental since last pull, 'date' = from a
  // chosen date, 'all' = everything.
  const [sinceMode, setSinceMode] = useState<'last' | 'date' | 'all'>('last');
  const [sinceDate, setSinceDate] = useState('');

  const [viewMode, setViewMode] = useState<'upload' | 'confirmations'>('upload');
  const [confirmations, setConfirmations] = useState<Confirmation[]>([]);
  const [confFilter, setConfFilter] = useState('all'); // all | new | dxcc | band | slot

  const [showLog, setShowLog] = useState(false);
  const [logLines, setLogLines] = useState<string[]>([]);
  const [busy, setBusy] = useState(false);
  const [logAction, setLogAction] = useState<'upload' | 'download'>('upload');

  // Worked/confirmed tallies. Slots are counted by mode CLASS (PH/CW/DIGI) — the
  // raw-mode granularity (RTTY ≠ FT8) stays only in the cluster/matrix new-slot flag.
  const [stats, setStats] = useState<any>(
    { slots_worked: 0, slots_confirmed: 0, dxcc_worked: 0, dxcc_confirmed: 0, ph_worked: 0, ph_confirmed: 0, cw_worked: 0, cw_confirmed: 0, dig_worked: 0, dig_confirmed: 0 });
  const refreshCounts = useCallback(() => { GetSlotStats().then((c: any) => setStats(c)).catch(() => {}); }, []);
  useEffect(() => { refreshCounts(); }, [refreshCounts]);

  useEffect(() => {
    const offLog = EventsOn('qslmgr:log', (line: string) => setLogLines((p) => [...p, line]));
    const offDone = EventsOn('qslmgr:done', (d: any) => {
      setLogLines((p) => [...p, `— Done: ${d?.uploaded ?? 0}/${d?.total ?? 0} —`]);
      setBusy(false);
      refreshCounts(); // a download may have added confirmations
    });
    const offConf = EventsOn('qslmgr:confirmations', (list: any) => {
      setConfirmations((list ?? []) as Confirmation[]);
      setViewMode('confirmations');
      refreshCounts();
    });
    return () => { offLog(); offDone(); offConf(); };
  }, [refreshCounts]);

  const serviceLabel = useMemo(() => SERVICES.find((s) => s.v === service)?.label ?? service, [service]);

  // Grid selection → just track the count; ids are read from the grid at upload.
  function onUploadRowSelected(ids: number[]) {
    setUploadSelIds(ids);
    setSelectedCount(ids.length);
  }

  const shownConfs = useMemo(() => confirmations.filter((c) => {
    switch (confFilter) {
      case 'new':  return c.new_dxcc || c.new_band || c.new_mode || c.new_slot;
      case 'dxcc': return c.new_dxcc;
      case 'band': return c.new_band;
      case 'mode': return c.new_mode;
      case 'slot': return c.new_slot;
      default:     return true;
    }
  }), [confirmations, confFilter]);

  const selectRequired = useCallback(async () => {
    setSearching(true);
    setError('');
    try {
      const r: any = await FindQSOsForUpload(service, sent === '_' ? '' : sent);
      const list = (r ?? []) as any[];
      setRows(list);
      setUploadSelIds(list.map((x) => x.id));
      setSelectedCount(list.length);
      setUploadSelAllSig((n) => n + 1); // pre-select everything once the grid renders
      setViewMode('upload');
      setShowLog(false);
    } catch (e: any) {
      setError(String(e?.message ?? e));
      setRows([]);
      setSelectedCount(0);
    } finally {
      setSearching(false);
    }
  }, [service, sent]);

  async function upload() {
    const ids = uploadSelIds;
    if (ids.length === 0) return;
    setLogLines([]); setBusy(true); setLogAction('upload'); setShowLog(true);
    try { await UploadQSOsManual(service, ids); }
    catch (e: any) { setLogLines((p) => [...p, 'Error: ' + String(e?.message ?? e)]); setBusy(false); }
  }

  async function download() {
    // Resolve the date window into the backend's `since` argument:
    //   'all'  → ""           (everything)
    //   'last' → "last"       (incremental since last successful pull)
    //   'date' → "YYYY-MM-DD" (the chosen date; falls back to all if empty)
    const since = sinceMode === 'last' ? 'last' : sinceMode === 'date' ? sinceDate.trim() : '';
    setLogLines([]); setBusy(true); setLogAction('download'); setShowLog(true);
    try { await DownloadConfirmations(service, addNotFound, since); }
    catch (e: any) { setLogLines((p) => [...p, 'Error: ' + String(e?.message ?? e)]); setBusy(false); }
  }

  // HAMLOG.online has no download verb — their agent protocol only uploads —
  // so confirmations come back as an ADIF exported from their site. It is read
  // here rather than through the ordinary ADIF import because that one matches
  // on the UTC minute and INSERTS whatever fails to match: a few hundred copies
  // of contacts already in the log. This path only ever stamps QSOs it finds.
  async function importHamlogCfm() {
    const path = await OpenADIFFile();
    if (!path) return;
    setLogLines([]); setBusy(true); setLogAction('download'); setShowLog(true);
    try { await ImportHamlogConfirmations(path); }
    catch (e: any) { setLogLines((p) => [...p, 'Error: ' + String(e?.message ?? e)]); setBusy(false); }
  }

  // The unmatched half of an import. Offered as a file because a few hundred
  // discrepancies are a list to work through, not something to read in a log
  // window.
  async function exportHamlogUnmatched() {
    try {
      const path = await SaveADIFFile();
      if (!path) return;
      const n = await ExportHamlogUnmatched(path);
      setLogLines((p) => [...p, `Exported ${n} unmatched confirmation(s) → ${path}`]);
      setShowLog(true);
    } catch (e: any) {
      setLogLines((p) => [...p, 'Error: ' + String(e?.message ?? e)]); setShowLog(true);
    }
  }

  function viewResults() {
    setShowLog(false);
    if (logAction === 'upload') selectRequired();
  }

  return (
    <div className="flex flex-col min-h-0 flex-1">
      {/* Search toolbar */}
      <div className="flex items-end gap-3 px-3 py-2 border-b border-border bg-muted/20 shrink-0">
        <div className="flex flex-col gap-1">
          <label className="text-[10px] uppercase tracking-wider text-muted-foreground">{t('qslm.service')}</label>
          <Select value={service} onValueChange={setService}>
            <SelectTrigger className="h-8 w-36"><SelectValue /></SelectTrigger>
            <SelectContent>{[...SERVICES].sort((a, b) => a.label.localeCompare(b.label)).map((s) => <SelectItem key={s.v} value={s.v}>{SERVICE_LABEL_KEYS[s.v] ? t(SERVICE_LABEL_KEYS[s.v]) : s.label}</SelectItem>)}</SelectContent>
          </Select>
        </div>
        {uploadCall && (
          <div className="flex flex-col gap-1">
            <label className="text-[10px] uppercase tracking-wider text-muted-foreground">{t('qslm.callsign')}</label>
            <span
              className="h-8 inline-flex items-center rounded border border-border bg-muted/40 px-2 font-mono text-sm font-semibold"
              title={t('qslm.callsignScopeTitle')}
            >
              {uploadCall}
            </span>
          </div>
        )}
        {service === 'pota' ? (
          <>
            <Button size="sm" className="h-8" onClick={syncPota} disabled={potaSyncing}>
              {potaSyncing ? <Loader2 className="size-3.5 animate-spin" /> : <Trees className="size-3.5" />}
              {t('qslm.syncHunterLog')}
            </Button>
            <label className="flex items-center gap-1.5 text-[11px] text-muted-foreground cursor-pointer self-center" title={t('qslm.onlyMyCallTitle')}>
              <Checkbox checked={potaOnlyMyCall} onCheckedChange={(c) => setPotaOnlyMyCall(!!c)} />
              {t('qslm.onlyMyCall')}
            </label>
            <label className="flex items-center gap-1.5 text-[11px] text-muted-foreground cursor-pointer self-center" title={t('qslm.addMissingTitle')}>
              <Checkbox checked={potaAddMissing} onCheckedChange={(c) => setPotaAddMissing(!!c)} />
              {t('qslm.addMissing')}
            </label>
            <span className="text-[11px] text-muted-foreground self-center">{t('qslm.potaToken')}</span>
          </>
        ) : service === 'paper' ? (
          <>
            <div className="flex flex-col gap-1">
              <label className="text-[10px] uppercase tracking-wider text-muted-foreground">{t('qslm.callsign')}</label>
              <Input className="h-8 w-40 font-mono uppercase" value={paperCall}
                onChange={(e) => setPaperCall(e.target.value)}
                onKeyDown={(e) => { if (e.key === 'Enter') searchPaper(); }}
                placeholder={t('qslm.callsignPlaceholder')} />
            </div>
            <Button size="sm" className="h-8" onClick={searchPaper} disabled={paperBusy || !paperCall.trim()}>
              {paperBusy ? <Loader2 className="size-3.5 animate-spin" /> : <Search className="size-3.5" />}
              {t('qslm.search')}
            </Button>
            {/* The station's QRZ page, one click away: writing a card means
                reading the address, the manager and whether they even want
                paper, and all three are on that page. */}
            <Button size="sm" variant="outline" className="h-8" disabled={!paperCall.trim()}
              title={t('qslm.qrzTitle')}
              onClick={() => {
                const c = paperCall.trim().toUpperCase();
                if (c) OpenExternalURL(`https://www.qrz.com/db/${c}`).catch(() => {});
              }}>
              <ExternalLink className="size-3.5" />
              QRZ
            </Button>
            <span className="text-[11px] text-muted-foreground self-center">{t('qslm.paperHint')}</span>
          </>
        ) : (
          <>
            <div className="flex flex-col gap-1">
              <label className="text-[10px] uppercase tracking-wider text-muted-foreground">{t('qslm.sentStatus')}</label>
              <Select value={sent} onValueChange={setSent}>
                <SelectTrigger className="h-8 w-44"><SelectValue /></SelectTrigger>
                <SelectContent>{SENT_STATUSES.map((s) => <SelectItem key={s.v} value={s.v}>{t(s.label)}</SelectItem>)}</SelectContent>
              </Select>
            </div>
            <Button size="sm" className="h-8" onClick={selectRequired} disabled={searching || busy}>
              {searching ? <Loader2 className="size-3.5 animate-spin" /> : <Search className="size-3.5" />}
              {t('qslm.selectRequired')}
            </Button>
          </>
        )}
        <div className="flex-1" />
        {/* Worked / confirmed tallies. Slots by mode class (PH/CW/DIGI), with the
            per-class breakdown so the totals are checkable. */}
        <div className="self-center flex items-center gap-3 text-[11px] font-mono" title={t('qslm.countsTip')}>
          <span className="flex items-center gap-1">
            <span className="text-muted-foreground uppercase text-[9px] tracking-wider">{t('qslm.slots')}</span>
            <span className="font-semibold">{stats.slots_worked}</span>
            <span className="text-muted-foreground">/</span>
            <span className="text-success font-semibold">{stats.slots_confirmed}</span>
          </span>
          <span className="flex items-center gap-1">
            <span className="text-muted-foreground uppercase text-[9px] tracking-wider">DXCC</span>
            <span className="font-semibold">{stats.dxcc_worked}</span>
            <span className="text-muted-foreground">/</span>
            <span className="text-success font-semibold">{stats.dxcc_confirmed}</span>
          </span>
        </div>
        {service === 'pota' && potaRes && (
          <span className="text-xs text-muted-foreground">
            {t('qslm.potaSummaryShort', { updated: potaRes.updated, added: potaRes.added, already: potaRes.already_tagged, unmatched: potaRes.unmatched })}{potaRes.skipped_other_call > 0 ? t('qslm.potaOtherCall', { n: potaRes.skipped_other_call }) : ''} / {potaRes.fetched}
          </span>
        )}
        {service === 'paper' && paperRows.length > 0 && (
          <span className="text-xs text-muted-foreground">{t('qslm.paperCount', { total: paperRows.length, selected: paperSel.size })}</span>
        )}
        {!showLog && viewMode === 'confirmations' && (
          <div className="flex flex-col gap-1">
            <label className="text-[10px] uppercase tracking-wider text-muted-foreground">{t('qslm.filter')}</label>
            <Select value={confFilter} onValueChange={setConfFilter}>
              <SelectTrigger className="h-8 w-36"><SelectValue /></SelectTrigger>
              <SelectContent>
                <SelectItem value="all">{t('qslm.filterAll')}</SelectItem>
                <SelectItem value="new">{t('qslm.filterNew')}</SelectItem>
                <SelectItem value="dxcc">{t('qslm.filterNewDxcc')}</SelectItem>
                <SelectItem value="band">{t('qslm.filterNewBand')}</SelectItem>
                <SelectItem value="mode">{t('qslm.filterNewMode')}</SelectItem>
                <SelectItem value="slot">{t('qslm.filterNewSlot')}</SelectItem>
              </SelectContent>
            </Select>
          </div>
        )}
        {logLines.length > 0 && (
          <Button variant="ghost" size="sm" className="h-8" onClick={() => (showLog ? viewResults() : setShowLog(true))}>
            {showLog ? <><ListChecks className="size-3.5" /> {t('qslm.results')}</> : <><ScrollText className="size-3.5" /> {t('qslm.log')}</>}
          </Button>
        )}
        <span className="text-xs text-muted-foreground">
          {viewMode === 'confirmations'
            ? t('qslm.confCount', { shown: shownConfs.length, total: confirmations.length })
            : t('qslm.foundCount', { found: rows.length, selected: selectedCount })}
        </span>
      </div>

      {/* Content: log OR results grid */}
      <div className="flex-1 overflow-auto px-3 py-2 min-h-0">
        {error && <div className="text-xs text-danger mb-2">{error}</div>}

        {service === 'paper' ? (
          paperRows.length === 0 ? (
            <div className="text-sm text-muted-foreground py-10 text-center">{t('qslm.paperEmpty')}</div>
          ) : (
            // Same grid as Recent QSOs (full columns + column picker). Selection
            // drives which QSOs the apply-form below updates; a search selects all.
            <div className="flex flex-col h-full min-h-0 -mx-3 -my-2">
              <RecentQSOsGrid
                // Its OWN column layout. Without a storageKey this fell back to
                // the main log's key, so every resize or hidden column here
                // silently rewrote the Recent QSOs layout, and vice versa.
                storageKey="qslmgr.paper"
                rows={paperRows as any}
                total={paperRows.length}
                selectAllSignal={paperSelAllSig}
                onRowSelected={(ids) => setPaperSel(new Set(ids))}
                {...actions}
                onRowDoubleClicked={onEditQSO ? (q) => onEditQSO(q.id as number) : undefined}
              />
            </div>
          )
        ) : service === 'pota' ? (
          <div className="space-y-3">
            {potaErr && <div className="text-xs rounded-md px-3 py-2 border border-destructive/30 bg-destructive/10 text-destructive">{potaErr}</div>}
            {!potaRes && !potaErr && !potaSyncing && (
              <div className="text-sm text-muted-foreground py-10 text-center">{t('qslm.potaEmpty')}</div>
            )}
            {potaSyncing && <div className="text-sm text-muted-foreground py-10 text-center flex items-center justify-center gap-2"><Loader2 className="size-4 animate-spin" /> {t('qslm.potaSyncing')}</div>}
            {potaRes && (
              <>
                <div className="text-xs rounded-md px-3 py-2 border border-success-border bg-success-muted text-success-muted-foreground">
                  {t('qslm.potaSummary', { updated: potaRes.updated, added: potaRes.added, already: potaRes.already_tagged, unmatched: potaRes.unmatched, fetched: potaRes.fetched })}
                  {potaRes.skipped_other_call > 0 && (
                    <>{t('qslm.potaSkipped', { n: potaRes.skipped_other_call })}{potaRes.my_call ? t('qslm.potaKeptOnly', { call: potaRes.my_call }) : ''}.</>
                  )}
                  {' '}{t('qslm.potaRescan')}
                </div>
                {potaRes.unmatched_list?.length > 0 && (
                  <table className="w-full text-xs border-collapse">
                    <thead className="sticky top-0 bg-card">
                      <tr className="text-left text-muted-foreground border-b border-border">
                        <th className="py-1.5 px-2">{t('qslm.thActivator')}</th><th className="py-1.5 px-2">{t('qslm.thDateUtc')}</th>
                        <th className="py-1.5 px-2">{t('qslm.thBand')}</th><th className="py-1.5 px-2">{t('qslm.thPark')}</th>
                        <th className="py-1.5 px-2">{t('qslm.thWhyUnmatched')}</th>
                      </tr>
                    </thead>
                    <tbody>
                      {potaRes.unmatched_list.map((u, i) => (
                        <tr key={i}
                          className={cn('border-b border-border/40', u.qso_id > 0 && 'cursor-pointer hover:bg-accent/30')}
                          onClick={() => u.qso_id > 0 && onEditQSO?.(u.qso_id)}
                          title={u.qso_id > 0 ? t('qslm.openToFix') : ''}>
                          <td className="py-1 px-2 font-mono font-bold">{u.activator}</td>
                          <td className="py-1 px-2 font-mono">{u.date}</td>
                          <td className="py-1 px-2">{u.band}</td>
                          <td className="py-1 px-2 font-mono">{u.reference}</td>
                          <td className="py-1 px-2 text-muted-foreground">
                            {u.reason}
                            {u.qso_id > 0 && <ExternalLink className="inline size-3 ml-1 opacity-60" />}
                          </td>
                        </tr>
                      ))}
                    </tbody>
                  </table>
                )}
              </>
            )}
          </div>
        ) : showLog ? (
          <div className="font-mono text-[11px] space-y-0.5 py-1">
            {logLines.length === 0 ? (
              <div className="text-muted-foreground flex items-center gap-2"><Loader2 className="size-3 animate-spin" /> {t('qslm.starting')}</div>
            ) : logLines.map((l, i) => (
              <div key={i} className={cn(
                /FAIL|failed|error/i.test(l) ? 'text-danger'
                  : /\bOK\b|UPDATED|ADDED|uploaded/i.test(l) ? 'text-success'
                  : 'text-foreground/90')}>{l}</div>
            ))}
            {busy && <div className="text-muted-foreground flex items-center gap-2 pt-1"><Loader2 className="size-3 animate-spin" /> {t('qslm.working')}</div>}
          </div>
        ) : viewMode === 'confirmations' ? (
          shownConfs.length === 0 ? (
            <div className="text-sm text-muted-foreground py-10 text-center">
              {confirmations.length === 0 ? t('qslm.noNewConf') : t('qslm.noConfMatch')}
            </div>
          ) : (
            <table className="w-full text-xs border-collapse">
              <thead className="sticky top-0 bg-card">
                <tr className="text-left text-muted-foreground border-b border-border">
                  <th className="py-1.5 px-2">{t('qslm.thDateUtc')}</th><th className="py-1.5 px-2">{t('qslm.thCallsign')}</th>
                  {/* Which of the operator's callsigns the contact was made
                      under. Only worth a column when the list can hold more than
                      one — see "All my callsigns" on the download. */}
                  {shownConfs.some((c) => c.station) && <th className="py-1.5 px-2">{t('qslm.thStation')}</th>}
                  <th className="py-1.5 px-2">{t('qslm.thBand')}</th><th className="py-1.5 px-2">{t('qslm.thMode')}</th>
                  <th className="py-1.5 px-2">{t('qslm.thCountry')}</th><th className="py-1.5 px-2">{t('qslm.thNew')}</th>
                </tr>
              </thead>
              <tbody>
                {shownConfs.map((c, i) => (
                  <tr key={i} className="border-b border-border/40">
                    <td className="py-1 px-2 font-mono">{fmtDate(c.qso_date)}</td>
                    <td className="py-1 px-2 font-mono font-bold">{c.callsign}</td>
                    {shownConfs.some((x) => x.station) && (
                      <td className="py-1 px-2 font-mono text-muted-foreground">{c.station ?? ''}</td>
                    )}
                    <td className="py-1 px-2">{c.band}</td>
                    <td className="py-1 px-2">{c.mode}</td>
                    <td className="py-1 px-2 text-muted-foreground">{c.country}</td>
                    <td className="py-1 px-2">
                      {c.new_dxcc ? <span className="inline-block px-1.5 py-px rounded text-[9px] font-bold bg-danger-muted text-danger-muted-foreground border border-danger-border">{t('qslm.newDxcc')}</span>
                        : c.new_band ? <span className="inline-block px-1.5 py-px rounded text-[9px] font-bold bg-warning-muted text-warning-muted-foreground border border-warning-border">{t('qslm.newBand')}</span>
                        : c.new_mode ? <span className="inline-block px-1.5 py-px rounded text-[9px] font-bold bg-info-muted text-info-muted-foreground border border-info-border">{t('qslm.newMode')}</span>
                        : c.new_slot ? <span className="inline-block px-1.5 py-px rounded text-[9px] font-bold bg-caution-muted text-caution-muted-foreground border border-caution-border">{t('qslm.newSlot')}</span>
                        : <span className="text-muted-foreground/50">—</span>}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          )
        ) : rows.length === 0 ? (
          <div className="text-sm text-muted-foreground py-10 text-center">{t('qslm.uploadEmpty')}</div>
        ) : (
          <div className="flex flex-col h-full min-h-0 -mx-3 -my-2">
            <RecentQSOsGrid
              storageKey="qslmgr.upload"
              rows={rows as any}
              total={rows.length}
              selectAllSignal={uploadSelAllSig}
              onRowSelected={onUploadRowSelected}
              {...actions}
              onRowDoubleClicked={onEditQSO ? (q) => onEditQSO(q.id as number) : undefined}
            />
          </div>
        )}
      </div>

      {/* Paper-QSL apply form */}
      {service === 'paper' && (
        <div className="flex items-end flex-wrap gap-3 px-3 py-2 border-t border-border bg-muted/20 shrink-0">
          {/* Each direction carries its own route, and each sits with the status
              and date it belongs to. A stack confirmed in one go usually arrived
              the same way — a bureau delivery is exactly that — which is why the
              route belongs on this form and not one QSO at a time in the editor. */}
          <div className="flex flex-col gap-1">
            <label className="text-[10px] uppercase tracking-wider text-muted-foreground">{t('qslm.qslReceived')}</label>
            <div className="flex gap-1.5">
              <Select value={qslRcvd} onValueChange={setQslRcvd}>
                <SelectTrigger className="h-8 w-28"><SelectValue /></SelectTrigger>
                <SelectContent>{QSL_STATUSES.map((s) => <SelectItem key={s.v} value={s.v}>{t(s.label)}</SelectItem>)}</SelectContent>
              </Select>
              <Input type="date" className="h-8 w-36" value={qslRcvdDate} onChange={(e) => setQslRcvdDate(e.target.value)} title={t('qslm.qslRcvdDateTitle')} />
              <Select value={qslRcvdVia} onValueChange={setQslRcvdVia}>
                <SelectTrigger className="h-8 w-32" title={t('qslm.rcvdViaTitle')}><SelectValue /></SelectTrigger>
                <SelectContent>{QSL_VIA_OPTIONS.map((o) => <SelectItem key={o.v} value={o.v}>{t(o.label)}</SelectItem>)}</SelectContent>
              </Select>
            </div>
          </div>
          <div className="flex flex-col gap-1">
            <label className="text-[10px] uppercase tracking-wider text-muted-foreground">{t('qslm.qslSent')}</label>
            <div className="flex gap-1.5">
              <Select value={qslSent} onValueChange={setQslSent}>
                <SelectTrigger className="h-8 w-28"><SelectValue /></SelectTrigger>
                <SelectContent>{QSL_STATUSES.map((s) => <SelectItem key={s.v} value={s.v}>{t(s.label)}</SelectItem>)}</SelectContent>
              </Select>
              <Input type="date" className="h-8 w-36" value={qslSentDate} onChange={(e) => setQslSentDate(e.target.value)} title={t('qslm.qslSentDateTitle')} />
              <Select value={qslVia} onValueChange={setQslVia}>
                <SelectTrigger className="h-8 w-32" title={t('qslm.sentViaTitle')}><SelectValue /></SelectTrigger>
                <SelectContent>{QSL_VIA_OPTIONS.map((o) => <SelectItem key={o.v} value={o.v}>{t(o.label)}</SelectItem>)}</SelectContent>
              </Select>
            </div>
          </div>
          <div className="flex flex-col gap-1">
            <label className="text-[10px] uppercase tracking-wider text-muted-foreground">{t('qslm.notes')}</label>
            <Input className="h-8 w-40" value={qslNotes} onChange={(e) => setQslNotes(e.target.value)} placeholder={t('qslm.notesPlaceholder')} />
          </div>
          <div className="flex flex-col gap-1">
            <label className="text-[10px] uppercase tracking-wider text-muted-foreground">{t('qslm.comment')}</label>
            <Input className="h-8 w-40" value={qslComment} onChange={(e) => setQslComment(e.target.value)} placeholder={t('qslm.commentPlaceholder')} />
          </div>
          <div className="flex-1" />
          {paperMsg && <span className="text-[11px] text-muted-foreground self-center">{paperMsg}</span>}
          <Button size="sm" onClick={applyPaper} disabled={paperBusy || paperSel.size === 0}>
            {t('qslm.applyToSelected', { n: paperSel.size })}
          </Button>
        </div>
      )}

      {/* Action bar (upload/download — not for POTA / Paper QSL) */}
      {service !== 'pota' && service !== 'paper' && (
      <div className="flex items-center justify-between gap-2 px-3 py-2 border-t border-border bg-muted/20 shrink-0">
        <div className="flex items-center gap-2 flex-wrap">
          {service === 'hamqth' ? (
            // HamQTH's file endpoint REPLACES the remote log — its own
            // documentation is explicit that partial uploads do not exist. So
            // it is offered as its own deliberate act, never as the batch path
            // behind "send these": that would delete everything not selected.
            <Button variant="outline" size="sm" disabled={busy}
              title={t('qslm.hqFullTitle')}
              onClick={async () => {
                if (!window.confirm(t('qslm.hqFullConfirm'))) return;
                // Same three lines every other action here runs: without
                // setShowLog the whole upload reported itself into a panel
                // nobody was showing, and the tab sat on "Pick a service".
                setLogLines([]); setBusy(true); setLogAction('upload'); setShowLog(true);
                try { await UploadFullLogHamQTH(); }
                catch (e: any) { setBusy(false); setLogLines((l) => [...l, String(e?.message ?? e)]); }
              }}>
              <UploadCloud className="size-3.5" /> {t('qslm.hqFull')}
            </Button>
          ) : service === 'hamlog' ? (
            <>
            <Button variant="outline" size="sm" onClick={importHamlogCfm} disabled={busy}
              title={t('qslm.hamlogImportTitle')}>
              <DownloadCloud className="size-3.5" /> {t('qslm.hamlogImportCfm')}
            </Button>
            <Button variant="outline" size="sm" onClick={exportHamlogUnmatched} disabled={busy}
              title={t('qslm.hamlogUnmatchedTitle')}>
              <UploadCloud className="size-3.5 rotate-180" /> {t('qslm.hamlogUnmatched')}
            </Button>
            </>
          ) : (
          <Button variant="outline" size="sm" onClick={download} disabled={busy}
            title={t('qslm.downloadTitle')}>
            <DownloadCloud className="size-3.5" /> {t('qslm.downloadConf')}
          </Button>
          )}
          {/* Date window */}
          {service !== 'hamlog' && (<>
          <Select value={sinceMode} onValueChange={(v) => setSinceMode(v as any)}>
            <SelectTrigger className="h-8 w-[150px] text-xs" title={t('qslm.downloadRangeTitle')}>
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="last">{t('qslm.sinceLast')}</SelectItem>
              <SelectItem value="date">{t('qslm.sinceDate')}</SelectItem>
              <SelectItem value="all">{t('qslm.sinceAll')}</SelectItem>
            </SelectContent>
          </Select>
          {sinceMode === 'date' && (
            <input
              type="date"
              value={sinceDate}
              onChange={(e) => setSinceDate(e.target.value)}
              className="h-8 rounded-md border border-input bg-background px-2 text-xs"
              title={service === 'qrz' ? t('qslm.sinceDateTitleQrz') : t('qslm.sinceDateTitleLotw')}
            />
          )}
          <label className="flex items-center gap-1.5 text-[11px] text-muted-foreground cursor-pointer" title={t('qslm.addNotFoundTitle')}>
            <Checkbox checked={addNotFound} onCheckedChange={(c) => setAddNotFound(!!c)} />
            {t('qslm.addNotFound')}
          </label>
          {service === 'lotw' && (
            <label className="flex items-center gap-1.5 text-[11px] text-muted-foreground cursor-pointer" title={t('qslm.lotwDetailTitle')}>
              <Checkbox checked={lotwDetail || addNotFound} disabled={addNotFound}
                onCheckedChange={(c) => { setLotwDetail(!!c); SetLoTWQSLDetail(!!c); }} />
              {t('qslm.lotwDetail')}
            </label>
          )}
          {service === 'lotw' && (
            <label className="flex items-center gap-1.5 text-[11px] text-muted-foreground cursor-pointer" title={t('qslm.lotwAllCallsTitle')}>
              <Checkbox checked={lotwAllCalls} onCheckedChange={(c) => { setLotwAllCalls(!!c); SetLoTWDownloadAllCalls(!!c); }} />
              {t('qslm.lotwAllCalls')}
            </label>
          )}
          </>)}
        </div>
        <Button size="sm" onClick={upload} disabled={selectedCount === 0 || busy}>
          <UploadCloud className="size-3.5" /> {t('qslm.uploadTo', { n: selectedCount, service: serviceLabel })}
        </Button>
      </div>
      )}
    </div>
  );
}
