import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
  AlertCircle, Antenna, CheckCircle2, Clock, Compass, Hash, Loader2, Lock,
  Maximize2, Minimize2, Pencil, RadioTower, RefreshCw, Send, Settings, Square, Trash2, Unlock, X,
} from 'lucide-react';

import {
  AddQSO, ListQSO, CountQSO,
  OpenADIFFile, ImportADIF,
  GetQSO, UpdateQSO, DeleteQSO, DeleteAllQSO,
  LookupCallsign, GetStationSettings, GetListsSettings,
  GetStartupStatus,
  WorkedBefore,
  SetCompactMode,
  GetCATState, SetCATFrequency, SetCATMode, SwitchCATRig,
  RefreshCtyDat,
  RotatorGoTo, RotatorStop,
  GetCATSettings,
} from '../wailsjs/go/main/App';
import { EventsOn, EventsOff } from '../wailsjs/runtime/runtime';
import type { adif as adifModels, lookup as lookupModels, cat as catModels } from '../wailsjs/go/models';
import type { QSOForm, WorkedBeforeView, StationSettingsForm, ListsSettingsForm, ModePresetForm } from '@/types';

import { Menubar, type Menu } from '@/components/Menubar';
import { ConfirmDialog } from '@/components/ConfirmDialog';
import { SettingsModal } from '@/components/SettingsModal';
import { QSOEditModal } from '@/components/QSOEditModal';
import { BandSlotGrid } from '@/components/BandSlotGrid';
import { CallHistoryPanel } from '@/components/CallHistoryPanel';
import { DetailsPanel, type DetailsState } from '@/components/DetailsPanel';

import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import { Checkbox } from '@/components/ui/checkbox';
import {
  Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
} from '@/components/ui/dialog';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import {
  Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
} from '@/components/ui/select';
import { cn } from '@/lib/utils';
import { pathBetween } from '@/lib/maidenhead';

type QSO = QSOForm;
type ImportResult = adifModels.ImportResult;
type LookupResult = lookupModels.Result;
type StationSettings = StationSettingsForm;
type ListsSettings = ListsSettingsForm;
type ModePreset = ModePresetForm;
type WB = WorkedBeforeView;
type CATState = Omit<catModels.RigState, 'convertValues'>;

const DEFAULT_BANDS = ['160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m','2m','70cm','23cm'];
const DEFAULT_MODES = ['SSB','CW','FT8','FT4','RTTY','PSK31','AM','FM','DIGITALVOICE'];

const emptyDetails: DetailsState = {
  state: '', cnty: '', address: '',
  lat: undefined, lon: undefined,
  dxcc: undefined, cqz: undefined, ituz: undefined, cont: '',
  qsl_msg: '', qsl_via: '',
  ant_az: undefined, ant_el: undefined, ant_path: '',
  prop_mode: '', my_rig: '', my_antenna: '',
  tx_pwr: undefined,
  sat_name: '', sat_mode: '',
  contest_id: '', srx: undefined, stx: undefined,
  email: '',
};

function fmtDateUTC(s: any): string {
  if (!s) return '';
  const d = new Date(s);
  if (isNaN(d.getTime())) return s;
  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())}`;
}
function fmtFreq(hz?: number): string {
  if (!hz) return '';
  return (hz / 1_000_000).toFixed(4);
}
function fmtHMSUTC(d: Date): string {
  const p = (n: number) => String(n).padStart(2, '0');
  return `${p(d.getUTCHours())}:${p(d.getUTCMinutes())}:${p(d.getUTCSeconds())}`;
}
// parseHMSUTC parses "HH:MM" or "HH:MM:SS" and returns a Date with that
// UTC time on the same UTC day as base. Tolerates "HHMM" / "HHMMSS" too
// (no separators) so the user can type fast. Falls back to base on bad input.
function parseHMSUTC(s: string, base: Date): Date {
  const clean = s.replace(/[^0-9:]/g, '');
  let h = 0, m = 0, sec = 0;
  if (clean.includes(':')) {
    const parts = clean.split(':');
    h = parseInt(parts[0] ?? '0', 10) || 0;
    m = parseInt(parts[1] ?? '0', 10) || 0;
    sec = parseInt(parts[2] ?? '0', 10) || 0;
  } else if (clean.length >= 4) {
    h = parseInt(clean.slice(0, 2), 10) || 0;
    m = parseInt(clean.slice(2, 4), 10) || 0;
    sec = clean.length >= 6 ? (parseInt(clean.slice(4, 6), 10) || 0) : 0;
  } else {
    return base;
  }
  if (h > 23 || m > 59 || sec > 59) return base;
  const d = new Date(base);
  d.setUTCHours(h, m, sec, 0);
  return d;
}
// fmtFreqDots formats a MHz string for display as MHz.kHz.Hz with dot
// separators (14.266500 → "14.266.500"). Pads the fractional part to 6
// digits so partial inputs ("14.21") still render as "14.210.000".
function fmtFreqDots(mhzStr: string): string {
  if (!mhzStr) return '';
  const [intPart, fracRaw = ''] = mhzStr.split('.');
  const frac = (fracRaw + '000000').slice(0, 6);
  return `${intPart}.${frac.slice(0, 3)}.${frac.slice(3, 6)}`;
}
// shortCatError condenses a backend error into a few words for the topbar
// pill. The full message stays in the tooltip. Recognises the common cases
// (OmniRig not installed, not registered) and otherwise truncates.
function shortCatError(err?: string): string {
  if (!err) return '';
  const e = err.toLowerCase();
  if (e.includes('not registered') || e.includes('not available')) return 'OmniRig not found';
  if (e.includes('not connected')) return 'not connected';
  if (e.includes('coinitialize')) return 'COM error';
  return err.length > 24 ? err.slice(0, 22) + '…' : err;
}
function computePrefix(call: string): string {
  if (!call) return '';
  const c = call.trim().toUpperCase().split('/')[0];
  let lastDigit = -1;
  for (let i = 0; i < c.length; i++) {
    if (c[i] >= '0' && c[i] <= '9') lastDigit = i;
  }
  return lastDigit >= 0 ? c.slice(0, lastDigit + 1) : c;
}

export default function App() {
  // === Lists from settings (fallback for first paint) ===
  const [bands, setBands] = useState<string[]>(DEFAULT_BANDS);
  const [modes, setModes] = useState<string[]>(DEFAULT_MODES);
  const [modePresets, setModePresets] = useState<ModePreset[]>([]);

  // === Entry ===
  const [callsign, setCallsign] = useState('');
  // QSO start time — frozen when the operator starts typing the callsign,
  // logged as ADIF QSO_DATE. End time = save click (logged as QSO_DATE_OFF).
  const [qsoStartedAt, setQsoStartedAt] = useState<Date | null>(null);
  // Frozen end time used when the End-UTC field is locked (backdated QSO);
  // null means the field tracks the live clock.
  const [qsoEndedAt, setQsoEndedAt] = useState<Date | null>(null);
  // Local string buffers for the time inputs while the user is editing.
  // Parsing only on blur — otherwise every keystroke would re-format the
  // value and React would move the cursor back to the end of the field.
  const [startInputStr, setStartInputStr] = useState('');
  const [endInputStr, setEndInputStr] = useState('');
  const [startFocused, setStartFocused] = useState(false);
  const [endFocused, setEndFocused] = useState(false);
  const [freqFocused, setFreqFocused] = useState(false);
  // Per-field locks — like Log4OM's padlocks. When locked, CAT updates skip
  // that field and the time fields become editable so the user can log a
  // QSO from the past while the radio is parked elsewhere.
  type LockKey = 'band' | 'mode' | 'freq' | 'start' | 'end';
  const [locks, setLocks] = useState<Record<LockKey, boolean>>({
    band: false, mode: false, freq: false, start: false, end: false,
  });
  const locksRef = useRef(locks);
  useEffect(() => { locksRef.current = locks; }, [locks]);
  const toggleLock = (k: LockKey) => {
    setLocks((s) => {
      const wasLocked = s[k];
      const next = { ...s, [k]: !wasLocked };
      // Unlocking → restore automatic behavior. Without this the locked
      // value would linger forever: a stale Start time would never refresh
      // even after a new callsign is entered.
      if (wasLocked) {
        if (k === 'start') {
          // If a QSO is currently in progress (callsign typed), snap start
          // to now since we missed the auto-start moment. Otherwise clear.
          setQsoStartedAt(callsign.trim() ? new Date() : null);
        } else if (k === 'end') {
          // Drop the frozen end so the field tracks the live UTC clock.
          setQsoEndedAt(null);
        }
      }
      return next;
    });
  };
  // Small padlock toggle rendered inside each lockable field's label. Match
  // the icon to the current state so the user can tell at a glance which
  // fields are immune to CAT updates / live clock.
  function LockBtn({ k, title }: { k: LockKey; title: string }) {
    const on = locks[k];
    const Icon = on ? Lock : Unlock;
    return (
      <button
        type="button"
        tabIndex={-1}
        onClick={() => toggleLock(k)}
        title={`${on ? 'Unlock' : 'Lock'} ${title}`}
        className={cn(
          'inline-flex items-center justify-center size-3.5 rounded transition-colors',
          on ? 'text-amber-600 hover:text-amber-700' : 'text-muted-foreground/40 hover:text-muted-foreground',
        )}
      >
        <Icon className="size-3" />
      </button>
    );
  }
  const [band, setBand] = useState('20m');
  const [mode, setMode] = useState('SSB');
  const [freqMhz, setFreqMhz] = useState('');
  // RX freq for split — only set/shown when the rig is in split mode.
  const [rxFreqMhz, setRxFreqMhz] = useState('');
  const [rstSent, setRstSent] = useState('59');
  const [rstRcvd, setRstRcvd] = useState('59');
  const [grid, setGrid] = useState('');
  const [name, setName] = useState('');
  const [qth, setQth] = useState('');
  const [country, setCountry] = useState('');
  const [comment, setComment] = useState('');
  const [note, setNote] = useState('');

  // Compact (popout) mode: shrinks the window + always-on-top so the entry
  // strip can sit on top of WSJT-X / JT-Alert / the cluster.
  const [compact, setCompact] = useState(false);
  function toggleCompact() {
    const next = !compact;
    setCompact(next);
    SetCompactMode(next);
  }

  // CAT — receives live rig state via Wails events.
  const [catState, setCatState] = useState<CATState>({ enabled: false, connected: false } as any);
  // Mode HamLog shows when the rig reports generic DIG_U/DIG_L. OmniRig
  // can't tell us if it's FT8 vs FT4 vs RTTY, so the user picks the default
  // in Preferences > Hardware > CAT interface.
  const digitalDefaultRef = useRef<string>('FT8');
  // Don't override freq/band/mode the user JUST typed — track a small grace
  // window after manual edits and skip CAT updates during it.
  const catFreezeUntilRef = useRef<number>(0);
  function noteManualEdit() { catFreezeUntilRef.current = Date.now() + 1500; }

  // Suggested QSY frequency (Hz) for a given band + mode. Common phone /
  // CW / FT8 watering holes per IARU practice. Fallback = mid-band SSB freq.
  // TODO: make this a user preference per band/mode later.
  const QSY_TABLE: Record<string, Record<string, number>> = {
    '160m': { SSB: 1842000,  CW: 1820000,  FT8: 1840000,  FT4: 1840000,  RTTY: 1838000, default: 1840000  },
    '80m':  { SSB: 3750000,  CW: 3520000,  FT8: 3573000,  FT4: 3575000,  RTTY: 3580000, default: 3750000  },
    '60m':  { default: 5357000 },
    '40m':  { SSB: 7150000,  CW: 7030000,  FT8: 7074000,  FT4: 7047500,  RTTY: 7040000, default: 7150000  },
    '30m':  { CW: 10116000,  FT8: 10136000, FT4: 10140000, RTTY: 10142000, default: 10136000 },
    '20m':  { SSB: 14250000, CW: 14030000, FT8: 14074000, FT4: 14080000, RTTY: 14080000, default: 14250000 },
    '17m':  { SSB: 18140000, CW: 18080000, FT8: 18100000, FT4: 18104000, RTTY: 18105000, default: 18140000 },
    '15m':  { SSB: 21300000, CW: 21030000, FT8: 21074000, FT4: 21140000, RTTY: 21080000, default: 21300000 },
    '12m':  { SSB: 24950000, CW: 24910000, FT8: 24915000, FT4: 24919000, RTTY: 24920000, default: 24950000 },
    '10m':  { SSB: 28500000, CW: 28030000, FT8: 28074000, FT4: 28180000, RTTY: 28080000, default: 28500000 },
    '6m':   { SSB: 50150000, CW: 50080000, FT8: 50313000, FT4: 50318000,                default: 50150000 },
    '2m':   { SSB: 144300000, CW: 144050000, FT8: 144174000,                            default: 144300000 },
    '70cm': { SSB: 432300000, CW: 432050000, FT8: 432174000,                            default: 432300000 },
  };
  function qsyFreqHz(band: string, mode: string): number {
    const t = QSY_TABLE[band];
    if (!t) return 0;
    return t[mode] ?? t.default ?? 0;
  }

  // Digital watering holes (±3 kHz tolerance). When the rig is parked here
  // CAT typically reports SSB (USB-D under the hood) which is misleading —
  // auto-promote to the specific digital mode for the entry strip.
  const DIGITAL_FREQS: { freq: number; mode: string }[] = [
    { freq: 1.840,   mode: 'FT8' },
    { freq: 3.573,   mode: 'FT8' },
    { freq: 5.357,   mode: 'FT8' },
    { freq: 7.074,   mode: 'FT8' },  { freq: 7.0475, mode: 'FT4' },
    { freq: 10.136,  mode: 'FT8' },  { freq: 10.140, mode: 'FT4' },
    { freq: 14.074,  mode: 'FT8' },  { freq: 14.080, mode: 'FT4' },
    { freq: 18.100,  mode: 'FT8' },  { freq: 18.104, mode: 'FT4' },
    { freq: 21.074,  mode: 'FT8' },  { freq: 21.140, mode: 'FT4' },
    { freq: 24.915,  mode: 'FT8' },  { freq: 24.919, mode: 'FT4' },
    { freq: 28.074,  mode: 'FT8' },  { freq: 28.180, mode: 'FT4' },
    { freq: 50.313,  mode: 'FT8' },  { freq: 50.318, mode: 'FT4' },
    { freq: 144.174, mode: 'FT8' },
  ];
  function inferDigitalMode(freqHz: number): string {
    const mhz = freqHz / 1_000_000;
    for (const d of DIGITAL_FREQS) {
      if (Math.abs(mhz - d.freq) <= 0.003) return d.mode;
    }
    return '';
  }

  // User changed band/mode in the entry strip → push to the rig if CAT is up.
  // Both calls are fire-and-forget; CAT will reflect back via cat:state.
  function onBandUserChange(v: string) {
    setBand(v);
    noteManualEdit();
    if (catState.enabled && catState.connected) {
      const hz = qsyFreqHz(v, mode);
      if (hz > 0) SetCATFrequency(hz).catch(() => {});
    }
  }
  function onModeUserChange(v: string) {
    setMode(v);
    applyModePreset(v);
    noteManualEdit();
    if (catState.enabled && catState.connected) {
      SetCATMode(v).catch(() => {});
    }
  }

  const userEditedRef = useRef<Set<string>>(new Set());
  const lastLookedUpRef = useRef<string>('');
  const rstUserEditedRef = useRef<boolean>(false);

  const [details, setDetails] = useState<DetailsState>(emptyDetails);
  const updateDetails = useCallback((patch: Partial<DetailsState>) => {
    setDetails((d) => ({ ...d, ...patch }));
  }, []);
  const prefix = useMemo(() => computePrefix(callsign), [callsign]);
  // Bearing/distance from operator's home grid to the remote station —
  // shown live in the entry strip (SP azimuth) and Info tab (LP + dist).

  // === Logbook list ===
  const [qsos, setQsos] = useState<QSO[]>([]);
  const [total, setTotal] = useState<number>(0);
  const [error, setError] = useState('');
  const [saving, setSaving] = useState(false);
  const [filterCallsign, setFilterCallsign] = useState('');
  const [filterBand, setFilterBand] = useState('');
  const [filterMode, setFilterMode] = useState('');
  const [activeTab, setActiveTab] = useState('recent');

  // === Modals ===
  const [editingQSO, setEditingQSO] = useState<QSO | null>(null);
  const [deletingQSO, setDeletingQSO] = useState<QSO | null>(null);
  const [selectedId, setSelectedId] = useState<number | null>(null);
  const [showSettings, setShowSettings] = useState(false);
  // Optional deep-link: which Preferences section to open. Cleared on
  // close so the next plain "Preferences" launch reverts to default.
  const [settingsSection, setSettingsSection] = useState<string | undefined>(undefined);
  const [showDeleteAll, setShowDeleteAll] = useState(false);
  const [deletingAll, setDeletingAll] = useState(false);
  const [ctyRefreshing, setCtyRefreshing] = useState(false);

  // === ADIF ===
  const [importing, setImporting] = useState(false);
  const [importResult, setImportResult] = useState<ImportResult | null>(null);
  const [importErrorsOpen, setImportErrorsOpen] = useState(false);
  const [importDupsOpen, setImportDupsOpen] = useState(false);
  // ADIF import confirmation: after the user picks a file, hold the path
  // until they confirm the options (skip duplicates etc.).
  const [pendingImportPath, setPendingImportPath] = useState<string | null>(null);
  const [importSkipDups, setImportSkipDups] = useState(true);

  // === Lookup + WB ===
  const [lookupResult, setLookupResult] = useState<LookupResult | null>(null);
  const [lookupBusy, setLookupBusy] = useState(false);
  const [lookupError, setLookupError] = useState('');
  const lookupTimerRef = useRef<number | null>(null);
  const wbTimerRef = useRef<number | null>(null);
  const [wb, setWb] = useState<WB | null>(null);
  const [wbBusy, setWbBusy] = useState(false);

  // === Station ===
  const [station, setStation] = useState<StationSettings>({
    callsign: '', operator: '',
    my_grid: '', my_country: '', my_sota_ref: '', my_pota_ref: '',
  });

  // === Clock ===
  const [utcNow, setUtcNow] = useState('');
  useEffect(() => {
    function tick() {
      const d = new Date();
      const p = (n: number) => String(n).padStart(2, '0');
      setUtcNow(`${d.getUTCFullYear()}-${p(d.getUTCMonth()+1)}-${p(d.getUTCDate())} ${p(d.getUTCHours())}:${p(d.getUTCMinutes())}:${p(d.getUTCSeconds())}`);
    }
    tick();
    const id = window.setInterval(tick, 1000);
    return () => window.clearInterval(id);
  }, []);

  const refresh = useCallback(async () => {
    try {
      const list = await ListQSO({
        callsign: filterCallsign, band: filterBand, mode: filterMode,
        limit: 500, offset: 0,
      } as any);
      const n = await CountQSO();
      setQsos(list);
      setTotal(n);
      setError('');
    } catch (e: any) {
      setError(String(e?.message ?? e));
    }
  }, [filterCallsign, filterBand, filterMode]);

  const loadStation = useCallback(async () => {
    try { setStation(await GetStationSettings()); } catch {}
  }, []);
  const loadCATCfg = useCallback(async () => {
    try {
      const c = await GetCATSettings();
      if (c.digital_default) digitalDefaultRef.current = c.digital_default;
    } catch {}
  }, []);
  const loadLists = useCallback(async () => {
    try {
      const l: ListsSettings = await GetListsSettings();
      if (l.bands && l.bands.length) setBands(l.bands);
      if (l.modes && l.modes.length) {
        setModePresets(l.modes);
        const names = l.modes.map((m) => m.name);
        setModes(names);
        setMode((cur) => names.includes(cur) ? cur : names[0]);
        const preset = l.modes.find((m) => m.name === mode) ?? l.modes[0];
        if (preset && !rstUserEditedRef.current) {
          if (preset.default_rst_sent) setRstSent(preset.default_rst_sent);
          if (preset.default_rst_rcvd) setRstRcvd(preset.default_rst_rcvd);
        }
      }
    } catch {}
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  function applyModePreset(m: string) {
    if (rstUserEditedRef.current) return;
    const p = modePresets.find((x) => x.name === m);
    if (!p) return;
    if (p.default_rst_sent) setRstSent(p.default_rst_sent);
    if (p.default_rst_rcvd) setRstRcvd(p.default_rst_rcvd);
  }

  useEffect(() => { refresh(); }, [refresh]);
  useEffect(() => {
    (async () => {
      try {
        const st = await GetStartupStatus();
        if (!st.ok) { setError(`Startup failed: ${st.err}\nDB path: ${st.db_path}`); return; }
      } catch {}
      loadStation();
      loadLists();
      loadCATCfg();
      // Hydrate CAT state on mount (the backend may already be polling).
      try { setCatState(await GetCATState()); } catch {}
    })();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // CAT live updates. Push freq/band/mode into the entry strip when the rig
  // moves, unless the user just typed something (1.5s grace window).
  useEffect(() => {
    EventsOn('cat:state', (s: CATState) => {
      setCatState(s);
      if (!s?.connected) return;
      if (Date.now() < catFreezeUntilRef.current) return;
      const lk = locksRef.current;
      if (!lk.freq && s.freq_hz && s.freq_hz > 0) {
        setFreqMhz((s.freq_hz / 1_000_000).toFixed(5));
      }
      // RX freq (split only): backend follows ADIF — freq_hz = TX,
      // freq_rx_hz = RX. Only set when the rig is in split, otherwise the
      // field would duplicate TX for no reason. The freq lock covers both.
      if (!lk.freq) {
        if (s.split && s.freq_rx_hz && s.freq_rx_hz > 0) {
          setRxFreqMhz((s.freq_rx_hz / 1_000_000).toFixed(5));
        } else {
          setRxFreqMhz('');
        }
      }
      if (!lk.band && s.band) setBand(s.band);

      // Mode resolution priority:
      //   1. If freq matches a known digital watering hole, pick the specific
      //      mode for that hole (FT8 / FT4) — beats whatever CAT reports.
      //   2. Else if CAT reports DATA (generic), use the user's configured
      //      default digital mode (FT8 by default).
      //   3. Else trust CAT (SSB, CW, AM, FM…).
      if (!lk.mode) {
        const inferred = s.freq_hz ? inferDigitalMode(s.freq_hz) : '';
        if (inferred) {
          setMode(inferred);
        } else if (s.mode === 'DATA') {
          setMode(digitalDefaultRef.current || 'FT8');
        } else if (s.mode) {
          setMode(s.mode);
        }
      }
    });
    return () => { EventsOff('cat:state'); };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  async function save() {
    if (!callsign.trim()) { setError('Callsign required'); return; }
    setSaving(true); setError('');
    try {
      const freqHz = freqMhz.trim() ? Math.round(parseFloat(freqMhz) * 1_000_000) : undefined;
      const rxFreqHz = rxFreqMhz.trim() ? Math.round(parseFloat(rxFreqMhz) * 1_000_000) : undefined;
      const now = new Date();
      const start = qsoStartedAt ?? now;
      const end = (locks.end && qsoEndedAt) ? qsoEndedAt : now;
      const payload: any = {
        callsign: callsign.trim().toUpperCase(),
        qso_date: start.toISOString(),
        qso_date_off: end.toISOString(),
        band, mode, freq_hz: freqHz, freq_rx_hz: rxFreqHz,
        rst_sent: rstSent, rst_rcvd: rstRcvd,
        grid: grid.trim().toUpperCase(),
        name, qth, country, comment, notes: note,
        state: details.state, cnty: details.cnty, address: details.address,
        lat: details.lat, lon: details.lon,
        dxcc: details.dxcc, cqz: details.cqz, ituz: details.ituz, cont: details.cont || undefined,
        qsl_msg: details.qsl_msg, qsl_via: details.qsl_via,
        ant_az: details.ant_az, ant_el: details.ant_el, ant_path: details.ant_path,
        prop_mode: details.prop_mode,
        my_rig: details.my_rig, my_antenna: details.my_antenna,
        tx_pwr: details.tx_pwr,
        sat_name: details.sat_name, sat_mode: details.sat_mode,
        contest_id: details.contest_id,
        srx: details.srx, stx: details.stx,
        email: details.email,
      };
      await AddQSO(payload);
      resetEntry();
      await refresh();
    } catch (e: any) {
      setError(String(e?.message ?? e));
    } finally { setSaving(false); }
  }

  // resetEntry clears the form for the next QSO. Triggered after a
  // successful log AND by ESC. Locked values (band/mode/freq/start/end)
  // are preserved so backdated batches stay productive.
  function resetEntry() {
    setCallsign(''); setComment(''); setNote('');
    if (!locks.start) setQsoStartedAt(null);
    if (!locks.end) setQsoEndedAt(null);
    resetAutoFill();
    setLookupError('');
    rstUserEditedRef.current = false;
    applyModePreset(mode);
    setDetails((d) => ({
      ...d,
      state: '', cnty: '', address: '', lat: undefined, lon: undefined,
      dxcc: undefined, cqz: undefined, ituz: undefined, cont: '',
      qsl_msg: '', qsl_via: '',
      contest_id: '', srx: undefined, stx: undefined,
      email: '',
    }));
  }

  function resetAutoFill() {
    setName(''); setQth(''); setCountry(''); setGrid('');
    setDetails((d) => ({
      ...d,
      state: '', cnty: '', address: '',
      lat: undefined, lon: undefined,
      dxcc: undefined, cqz: undefined, ituz: undefined, cont: '',
      qsl_via: '',
      email: '',
    }));
    userEditedRef.current.clear();
    lastLookedUpRef.current = '';
    setLookupResult(null);
  }

  async function openEdit(id: number) {
    try { setEditingQSO(await GetQSO(id)); }
    catch (e: any) { setError(String(e?.message ?? e)); }
  }
  async function onModalSave(q: QSO) {
    try { await UpdateQSO(q as any); setEditingQSO(null); await refresh(); }
    catch (err: any) { setError(String(err?.message ?? err)); }
  }
  function onModalDelete(id: number) {
    const q = editingQSO; setEditingQSO(null);
    if (q) setDeletingQSO(q); else askDelete(id);
  }
  function askDelete(id: number) {
    const q = qsos.find((x) => x.id === id);
    if (q) setDeletingQSO(q);
  }
  async function confirmDelete() {
    if (!deletingQSO) return;
    try {
      await DeleteQSO(deletingQSO.id);
      if (selectedId === deletingQSO.id) setSelectedId(null);
      setDeletingQSO(null);
      await refresh();
    } catch (err: any) {
      setError(String(err?.message ?? err));
      setDeletingQSO(null);
    }
  }
  async function confirmDeleteAll() {
    if (deletingAll) return;
    setDeletingAll(true);
    try {
      await DeleteAllQSO();
      setSelectedId(null);
      setShowDeleteAll(false);
      await refresh();
    } catch (err: any) { setError(String(err?.message ?? err)); }
    finally { setDeletingAll(false); }
  }

  async function runWorkedBefore(call: string, dxccHint: number = 0) {
    setWbBusy(true);
    try { setWb(await WorkedBefore(call, dxccHint)); }
    catch { setWb(null); }
    finally { setWbBusy(false); }
  }
  async function runLookup(call: string) {
    if (call !== lastLookedUpRef.current) resetAutoFill();
    setLookupBusy(true);
    try {
      const r = await LookupCallsign(call);
      setLookupResult(r);
      lastLookedUpRef.current = call;
      const ue = userEditedRef.current;
      if (!ue.has('name'))    setName(r.name ?? '');
      if (!ue.has('qth'))     setQth(r.qth ?? '');
      if (!ue.has('country')) setCountry(r.country ?? '');
      if (!ue.has('grid'))    setGrid(r.grid ?? '');
      setDetails((d) => ({
        ...d,
        address: d.address || (r.address ?? ''),
        state:   d.state   || (r.state ?? ''),
        cnty:    d.cnty    || (r.cnty ?? ''),
        lat:     d.lat     ?? (r.lat || undefined),
        lon:     d.lon     ?? (r.lon || undefined),
        dxcc:    d.dxcc    ?? (r.dxcc || undefined),
        cqz:     d.cqz     ?? (r.cqz  || undefined),
        ituz:    d.ituz    ?? (r.ituz || undefined),
        cont:    d.cont    || (r.cont ?? ''),
        email:   d.email   || (r.email ?? ''),
        qsl_via: d.qsl_via || (r.qsl_via ?? ''),
      }));
      if (r.dxcc && r.dxcc > 0) runWorkedBefore(call, r.dxcc);
    } catch (e: any) {
      setLookupResult(null);
      setLookupError(String(e?.message ?? e));
    } finally { setLookupBusy(false); }
  }
  function scheduleLookup(value: string) {
    setLookupError('');
    if (lookupTimerRef.current) window.clearTimeout(lookupTimerRef.current);
    if (wbTimerRef.current)     window.clearTimeout(wbTimerRef.current);
    const call = value.trim().toUpperCase();
    if (call.length < 3) {
      setLookupResult(null); setWb(null);
      if (lastLookedUpRef.current !== '') resetAutoFill();
      return;
    }
    lookupTimerRef.current = window.setTimeout(() => runLookup(call), 400);
    wbTimerRef.current     = window.setTimeout(() => runWorkedBefore(call), 150);
  }
  function onCallsignInput(v: string) {
    const wasEmpty = callsign.trim() === '';
    const isEmpty = v.trim() === '';
    if (wasEmpty && !isEmpty && !locks.start) {
      // First keystroke of a new QSO — freeze the start time so it doesn't
      // drift even if the lookup or typing takes 30 seconds. Skip when
      // start is locked: the user is back-entering a past QSO and set a
      // specific time manually.
      setQsoStartedAt(new Date());
    } else if (isEmpty && !locks.start) {
      // Callsign wiped → user abandoned this QSO; reset the timer.
      setQsoStartedAt(null);
    }
    setCallsign(v);
    scheduleLookup(v);
  }
  function markEdited(field: string) { userEditedRef.current.add(field); }

  async function importAdif() {
    if (importing) return;
    setError('');
    try {
      const path = await OpenADIFFile();
      if (!path) return;
      // Stash the path and open the options dialog. The actual import
      // is fired from runImport() when the user clicks "Import".
      setPendingImportPath(path);
    } catch (e: any) { setError(String(e?.message ?? e)); }
  }

  async function runImport() {
    const path = pendingImportPath;
    if (!path || importing) return;
    setPendingImportPath(null);
    setImporting(true);
    setImportResult(null);
    setImportErrorsOpen(false);
    setImportDupsOpen(false);
    try {
      const res = await ImportADIF(path, importSkipDups);
      setImportResult(res);
      await refresh();
    } catch (e: any) {
      setError(String(e?.message ?? e));
    } finally {
      setImporting(false);
    }
  }

  const menus: Menu[] = useMemo(() => [
    { name: 'file', label: 'File', items: [
      { type: 'item', label: 'Import ADIF…', action: 'file.import', shortcut: 'Ctrl+O' },
      { type: 'item', label: 'Export ADIF…', action: 'file.export', shortcut: 'Ctrl+E', disabled: true },
      { type: 'separator' },
      { type: 'item', label: 'Delete all QSOs…', action: 'file.deleteall', disabled: total === 0 },
      { type: 'separator' },
      { type: 'item', label: 'Exit', action: 'file.exit', shortcut: 'Ctrl+Q', disabled: true },
    ]},
    { name: 'edit', label: 'Edit', items: [
      { type: 'item', label: 'Edit selected QSO…', action: 'edit.edit', shortcut: 'Enter', disabled: selectedId === null },
      { type: 'item', label: 'Delete selected QSO', action: 'edit.delete', shortcut: 'Del', disabled: selectedId === null },
      { type: 'separator' },
      { type: 'item', label: 'Preferences…', action: 'edit.prefs' },
    ]},
    { name: 'view', label: 'View', items: [
      { type: 'item', label: 'Refresh', action: 'view.refresh', shortcut: 'F5' },
      { type: 'item', label: 'Clear filters', action: 'view.clearfilters' },
    ]},
    { name: 'tools', label: 'Tools', items: [
      { type: 'item', label: 'Callsign lookup settings…', action: 'tools.lookup' },
      { type: 'item', label: 'DX Cluster', action: 'tools.cluster', disabled: true },
      { type: 'item', label: 'CAT control', action: 'tools.cat', disabled: true },
      { type: 'item', label: 'Rotator', action: 'tools.rotator', disabled: true },
      { type: 'separator' },
      // Maintenance — bumped here while we only have one entry. Will move
      // to a Tools → Maintenance submenu once Clublog + LoTW refresh land.
      { type: 'item', label: ctyRefreshing ? 'Refreshing cty.dat…' : 'Refresh cty.dat', action: 'tools.refreshCty', disabled: ctyRefreshing },
    ]},
    { name: 'help', label: 'Help', items: [
      { type: 'item', label: 'About HamLog', action: 'help.about', disabled: true },
    ]},
  ], [total, selectedId, ctyRefreshing]);

  function handleMenu(action: string) {
    switch (action) {
      case 'file.import':       importAdif(); break;
      case 'file.deleteall':    setShowDeleteAll(true); break;
      case 'view.refresh':      refresh(); break;
      case 'view.clearfilters': setFilterCallsign(''); setFilterBand(''); setFilterMode(''); break;
      case 'edit.edit':         if (selectedId !== null) openEdit(selectedId); break;
      case 'edit.delete':       if (selectedId !== null) askDelete(selectedId); break;
      case 'edit.prefs':        setShowSettings(true); break;
      case 'tools.lookup':      setSettingsSection('lookup'); setShowSettings(true); break;
      case 'tools.refreshCty':  refreshCtyDat(); break;
    }
  }

  async function refreshCtyDat() {
    if (ctyRefreshing) return;
    setCtyRefreshing(true);
    setError('');
    try {
      const info = await RefreshCtyDat();
      // Use the regular error banner area for a brief success note — keeps
      // us from pulling in a toast system just for one maintenance action.
      setError(`cty.dat refreshed — ${info.entities} entities loaded`);
      setTimeout(() => setError((e) => e.startsWith('cty.dat refreshed') ? '' : e), 4000);
    } catch (e: any) {
      setError(`cty.dat refresh failed: ${String(e?.message ?? e)}`);
    } finally {
      setCtyRefreshing(false);
    }
  }

  useEffect(() => {
    function onKey(e: KeyboardEvent) {
      const tag = (e.target as HTMLElement)?.tagName;
      const typing = tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA';
      if (e.key === 'F5') { e.preventDefault(); refresh(); return; }
      if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'o') {
        e.preventDefault(); importAdif(); return;
      }
      if (typing) return;
      if (selectedId !== null) {
        if (e.key === 'Delete') { e.preventDefault(); askDelete(selectedId); return; }
        if (e.key === 'Enter')  { e.preventDefault(); openEdit(selectedId); return; }
      }
    }
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [selectedId, refresh]);

  return (
    <div className="flex flex-col h-screen bg-background">
      {/* ===== TOPBAR ===== */}
      {compact ? (
        // Minimal compact topbar — brand + freq + toggle. Saves vertical space
        // so the single-row entry strip fits in a ~140px tall window.
        <header className="flex items-center gap-3 px-3 h-8 bg-card border-b border-border shrink-0">
          <div className="flex items-center gap-1.5">
            <div className="size-2 rounded-full bg-gradient-to-br from-primary to-orange-400" />
            <span className="font-bold text-xs tracking-tight">HamLog</span>
          </div>
          <div className="flex items-baseline gap-1.5 font-mono ml-2">
            <span className="text-sm font-semibold text-primary">{freqMhz ? fmtFreqDots(freqMhz) : '—.———.———'}</span>
            <span className="text-[9px] text-muted-foreground">MHz</span>
            <Badge variant="accent" className="font-mono ml-2 text-[9px] py-0">{band}</Badge>
            <Badge className="bg-emerald-100 text-emerald-700 hover:bg-emerald-100 font-mono text-[9px] py-0" variant="outline">{mode}</Badge>
          </div>
          <span className="ml-2 font-mono text-[10px] text-muted-foreground">{utcNow}<span className="text-[9px]">Z</span></span>
          <div className="flex-1" />
          {station.callsign && (
            <span className="font-mono text-[11px] font-bold text-primary mr-2">{station.callsign}</span>
          )}
          <Button variant="outline" size="icon" className="size-6" onClick={toggleCompact} title="Exit compact mode">
            <Maximize2 className="size-3" />
          </Button>
        </header>
      ) : (
        <header className="grid grid-cols-[auto_auto_1fr_auto_auto] items-center gap-4 px-4 h-12 bg-card/95 backdrop-blur border-b border-border shrink-0 shadow-sm">
          <div className="flex items-center gap-2 pr-2 border-r border-border/60">
            <div className="size-2.5 rounded-full bg-gradient-to-br from-primary to-orange-400 shadow-[0_0_0_3px_rgba(234,88,12,0.18)]" />
            <span className="font-bold text-[15px] tracking-tight">HamLog</span>
            <span className="text-[11px] text-muted-foreground">v0.1</span>
          </div>

          <Menubar menus={menus} onAction={handleMenu} />

          <div className="flex items-center justify-center gap-2 font-mono">
            <div className="flex flex-col items-end leading-none">
              <span className="text-[22px] font-semibold text-primary tracking-wide">{freqMhz ? fmtFreqDots(freqMhz) : '—.———.———'}</span>
              {catState.split && rxFreqMhz && (
                <span className="text-[10px] text-muted-foreground mt-0.5">
                  <span className="text-rose-600 font-semibold mr-1">RX</span>
                  {fmtFreqDots(rxFreqMhz)}
                </span>
              )}
            </div>
            <span className="text-[10px] text-muted-foreground uppercase">MHz</span>
            {catState.split && (
              <Badge className="bg-amber-100 text-amber-800 border-amber-300 font-mono text-[10px] py-0" variant="outline">SPLIT</Badge>
            )}
            <div className="w-px h-4 bg-border mx-2" />
            <Badge variant="accent" className="font-mono">{band}</Badge>
            <Badge className="bg-emerald-100 text-emerald-700 hover:bg-emerald-100 font-mono" variant="outline">{mode}</Badge>
            {/* Bearing controls — three separate buttons so SP and LP are
                both directly clickable, plus an always-visible Stop. The
                old Shift/Ctrl shortcuts were not discoverable enough. */}
            {(() => {
              const p = pathBetween(station.my_grid, grid);
              const disabled = !p;
              const goto = (az: number) => RotatorGoTo(Math.round(az), -1).catch((err) => setError(String(err?.message ?? err)));
              return (
                <div className="inline-flex items-center rounded-full border border-sky-300 bg-sky-100 overflow-hidden text-[11px] font-mono font-semibold">
                  <button
                    type="button"
                    disabled={disabled}
                    onClick={() => p && goto(p.bearingShort)}
                    title={p
                      ? `Rotate short-path · ${Math.round(p.distanceShort).toLocaleString()} km`
                      : (station.my_grid ? 'No remote grid' : 'Set your station grid in Preferences')}
                    className={cn(
                      'inline-flex items-center gap-1 px-2 py-0.5 transition-colors',
                      disabled
                        ? 'text-muted-foreground/60 cursor-not-allowed'
                        : 'text-sky-800 hover:bg-sky-200 cursor-pointer',
                    )}
                  >
                    <Compass className="size-3" />
                    {p ? `${Math.round(p.bearingShort)}°` : '—°'}
                  </button>
                  <button
                    type="button"
                    disabled={disabled}
                    onClick={() => p && goto(p.bearingLong)}
                    title={p ? `Rotate long-path · ${Math.round(p.distanceLong).toLocaleString()} km` : ''}
                    className={cn(
                      'px-1.5 py-0.5 border-l border-sky-300 text-[10px] transition-colors',
                      disabled
                        ? 'text-muted-foreground/60 cursor-not-allowed'
                        : 'text-sky-800 hover:bg-sky-200 cursor-pointer',
                    )}
                  >
                    LP {p ? `${Math.round(p.bearingLong)}°` : '—'}
                  </button>
                  <button
                    type="button"
                    onClick={() => RotatorStop().catch((err) => setError(String(err?.message ?? err)))}
                    title="Stop rotation"
                    className="px-1.5 py-0.5 border-l border-sky-300 text-rose-700 hover:bg-rose-100 hover:text-rose-800 cursor-pointer transition-colors"
                  >
                    <Square className="size-2.5 fill-current" />
                  </button>
                </div>
              );
            })()}
            {catState.enabled && (
              <>
                <span
                  className={cn(
                    'inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-semibold font-sans ml-2 border',
                    catState.connected
                      ? 'bg-emerald-100 text-emerald-800 border-emerald-300'
                      : 'bg-rose-100 text-rose-800 border-rose-300',
                  )}
                  title={
                    catState.connected
                      ? `CAT: ${catState.rig || catState.backend || 'connected'}`
                      : catState.error || 'CAT offline'
                  }
                >
                  <RadioTower className={cn('size-3', catState.connected && 'animate-pulse')} />
                  {catState.connected
                    ? (catState.rig || 'CAT')
                    : (shortCatError(catState.error) || 'CAT off')}
                </span>
                {catState.backend === 'omnirig' && (
                  <div className="inline-flex rounded-md border border-border overflow-hidden text-[10px] font-sans font-semibold">
                    {[1, 2].map((n) => {
                      const active = (catState.rig_num || 1) === n;
                      return (
                        <button
                          key={n}
                          type="button"
                          onClick={() => SwitchCATRig(n).catch(() => {})}
                          className={cn(
                            'px-1.5 py-0.5 transition-colors',
                            active
                              ? 'bg-primary text-primary-foreground'
                              : 'bg-card text-muted-foreground hover:bg-muted',
                          )}
                          title={`Use OmniRig Rig ${n}`}
                        >
                          R{n}
                        </button>
                      );
                    })}
                  </div>
                )}
              </>
            )}
          </div>

          <div className="flex items-center gap-1.5 font-mono text-xs text-muted-foreground px-2.5 py-1 bg-muted rounded-md border border-border/60">
            <Clock className="size-3" />
            {utcNow}<span className="text-[10px]">UTC</span>
          </div>

          <div className="flex items-center gap-3">
            {station.callsign ? (
              <button
                onClick={() => { setSettingsSection('profiles'); setShowSettings(true); }}
                className="flex items-center gap-1.5 bg-accent border border-accent-foreground/20 text-accent-foreground px-3 py-1 rounded-full font-mono text-xs font-bold hover:bg-accent/80 transition-colors"
                title="Click to switch / edit profiles"
              >
                <Antenna className="size-3" />
                {station.callsign}
                {station.my_grid && (
                  <span className="bg-white/60 px-1.5 rounded text-[11px] text-primary">{station.my_grid}</span>
                )}
              </button>
            ) : (
              <Button variant="outline" size="sm" onClick={() => setShowSettings(true)}>
                <Settings className="size-3.5" /> Set station
              </Button>
            )}
            <div className="text-right">
              <div className="font-bold text-[15px] leading-none">{total.toLocaleString('en-US')}</div>
              <div className="text-[10px] text-muted-foreground uppercase tracking-wider">Total QSOs</div>
            </div>
            <Button
              variant="outline"
              size="icon"
              onClick={toggleCompact}
              title="Compact mode (small always-on-top window)"
              className="size-8"
            >
              <Minimize2 className="size-3.5" />
            </Button>
          </div>
        </header>
      )}

      {error && (
        <div className="bg-destructive/10 text-destructive border-b border-destructive/30 px-4 py-2 flex items-start gap-3 text-xs">
          <AlertCircle className="size-4 mt-0.5 shrink-0" />
          <pre className="flex-1 font-mono whitespace-pre-wrap m-0">{error}</pre>
          <button className="hover:text-destructive/70" onClick={() => setError('')}><X className="size-4" /></button>
        </div>
      )}

      {/* ===== ENTRY STRIP =====
          Enter from any <input> inside the strip logs the QSO. Radix Selects
          render as <button> elements and are ignored by this handler — they
          keep their own keyboard behaviour. */}
      <section
        className="flex gap-2 items-end flex-wrap px-3 py-2 bg-card border-b border-border shadow-sm shrink-0"
        onKeyDown={(e) => {
          if (e.key === 'Enter' && (e.target as HTMLElement).tagName === 'INPUT') {
            e.preventDefault();
            save();
          } else if (e.key === 'Escape') {
            e.preventDefault();
            resetEntry();
          }
        }}
      >
        <div className="flex flex-col w-40">
          <Label className="mb-1 flex items-center gap-2 h-3.5">
            Callsign
            {lookupBusy && <Badge variant="secondary" className="text-[9px] py-0 px-1.5 normal-case font-medium tracking-wider"><Loader2 className="size-2.5 mr-1 animate-spin" />Looking up…</Badge>}
            {!lookupBusy && lookupResult && (
              <Badge className="bg-emerald-100 text-emerald-700 hover:bg-emerald-100 text-[9px] py-0 px-1.5 normal-case font-medium tracking-wider" variant="outline">
                <CheckCircle2 className="size-2.5 mr-1" />{lookupResult.source}
              </Badge>
            )}
            {!lookupBusy && !lookupResult && lookupError && (
              <Badge variant="destructive" className="text-[9px] py-0 px-1.5 normal-case font-medium tracking-wider">{lookupError}</Badge>
            )}
          </Label>
          <Input
            className="font-mono text-base font-bold tracking-wider uppercase h-9 bg-muted/40 focus:bg-card"
            value={callsign}
            onChange={(e) => onCallsignInput(e.target.value)}
                        placeholder="F4XYZ"
          />
        </div>
        <div className="flex flex-col w-24">
          <Label className="mb-1 h-3.5 flex items-center gap-1">Band <LockBtn k="band" title="band" /></Label>
          <Select value={band} onValueChange={onBandUserChange}>
            <SelectTrigger><SelectValue /></SelectTrigger>
            <SelectContent>{bands.map((b) => <SelectItem key={b} value={b}>{b}</SelectItem>)}</SelectContent>
          </Select>
        </div>
        <div className="flex flex-col w-28">
          <Label className="mb-1 h-3.5 flex items-center gap-1">Mode <LockBtn k="mode" title="mode" /></Label>
          <Select value={mode} onValueChange={onModeUserChange}>
            <SelectTrigger><SelectValue /></SelectTrigger>
            <SelectContent>{modes.map((m) => <SelectItem key={m} value={m}>{m}</SelectItem>)}</SelectContent>
          </Select>
        </div>
        <div className="flex flex-col w-28">
          <Label className="mb-1 h-3.5 flex items-center gap-1">
            {catState.split ? 'TX Freq (MHz)' : 'Freq (MHz)'} <LockBtn k="freq" title="frequency" />
          </Label>
          <Input
            className="font-mono"
            value={freqFocused ? freqMhz : (freqMhz ? fmtFreqDots(freqMhz) : '')}
            placeholder="14.250"
            onFocus={() => setFreqFocused(true)}
            onBlur={() => setFreqFocused(false)}
            onChange={(e) => { setFreqMhz(e.target.value); noteManualEdit(); }}
          />
        </div>
        {catState.split && (
          <div className="flex flex-col w-28">
            <Label className="mb-1 h-3.5 text-rose-600">RX Freq (MHz)</Label>
            <Input
              value={freqFocused ? rxFreqMhz : (rxFreqMhz ? fmtFreqDots(rxFreqMhz) : '')}
              placeholder="14.255"
              onFocus={() => setFreqFocused(true)}
              onBlur={() => setFreqFocused(false)}
              onChange={(e) => { setRxFreqMhz(e.target.value); noteManualEdit(); }}
              className="font-mono bg-rose-50/40 border-rose-200 focus:bg-card"
            />
          </div>
        )}
        <div className="flex flex-col w-20"><Label className="mb-1 h-3.5">RST tx</Label>
          <Input value={rstSent} onChange={(e) => { setRstSent(e.target.value); rstUserEditedRef.current = true; }} />
        </div>
        <div className="flex flex-col w-20"><Label className="mb-1 h-3.5">RST rx</Label>
          <Input value={rstRcvd} onChange={(e) => { setRstRcvd(e.target.value); rstUserEditedRef.current = true; }} />
        </div>
        <div className="flex flex-col w-24">
          <Label className="mb-1 h-3.5 flex items-center gap-1 text-emerald-700">
            Start UTC <LockBtn k="start" title="start time" />
          </Label>
          <Input
            readOnly={!locks.start}
            tabIndex={locks.start ? 0 : -1}
            value={startFocused
              ? startInputStr
              : (qsoStartedAt ? fmtHMSUTC(qsoStartedAt) : (locks.start ? '' : '—'))}
            onFocus={() => {
              setStartInputStr(qsoStartedAt ? fmtHMSUTC(qsoStartedAt) : '');
              setStartFocused(true);
            }}
            onBlur={() => {
              setStartFocused(false);
              if (startInputStr.trim() === '') {
                if (locks.start) setQsoStartedAt(null);
                return;
              }
              setQsoStartedAt(parseHMSUTC(startInputStr, qsoStartedAt ?? new Date()));
            }}
            onChange={(e) => setStartInputStr(e.target.value)}
            placeholder="HH:MM:SS"
            className={cn('font-mono', locks.start ? '' : 'bg-muted/40 cursor-default')}
          />
        </div>
        <div className="flex flex-col w-24">
          <Label className="mb-1 h-3.5 flex items-center gap-1 text-rose-700">
            End UTC <LockBtn k="end" title="end time" />
          </Label>
          <Input
            readOnly={!locks.end}
            tabIndex={locks.end ? 0 : -1}
            value={endFocused
              ? endInputStr
              : (locks.end
                ? (qsoEndedAt ? fmtHMSUTC(qsoEndedAt) : '')
                : (qsoStartedAt ? utcNow.slice(11) : '—'))}
            onFocus={() => {
              setEndInputStr(qsoEndedAt ? fmtHMSUTC(qsoEndedAt) : '');
              setEndFocused(true);
            }}
            onBlur={() => {
              setEndFocused(false);
              if (endInputStr.trim() === '') {
                if (locks.end) setQsoEndedAt(null);
                return;
              }
              setQsoEndedAt(parseHMSUTC(endInputStr, qsoEndedAt ?? new Date()));
            }}
            onChange={(e) => setEndInputStr(e.target.value)}
            placeholder="HH:MM:SS"
            className={cn('font-mono', locks.end ? '' : 'bg-muted/40 cursor-default')}
          />
        </div>
        {/* Optional ID/location fields — hidden in compact mode. */}
        {!compact && <>
          <div className="flex flex-col w-24"><Label className="mb-1 h-3.5">Grid</Label>
            <Input value={grid} placeholder="JN05" onChange={(e) => { setGrid(e.target.value); markEdited('grid'); }} />
          </div>
          <div className="flex flex-col flex-1 min-w-[100px]"><Label className="mb-1 h-3.5">Name</Label>
            <Input value={name} onChange={(e) => { setName(e.target.value); markEdited('name'); }} />
          </div>
          <div className="flex flex-col flex-1 min-w-[100px]"><Label className="mb-1 h-3.5">QTH</Label>
            <Input value={qth} onChange={(e) => { setQth(e.target.value); markEdited('qth'); }} />
          </div>
          <div className="flex flex-col flex-1 min-w-[100px]"><Label className="mb-1 h-3.5">Country</Label>
            <Input value={country} onChange={(e) => { setCountry(e.target.value); markEdited('country'); }} />
          </div>
          {/* Numeric DXCC metadata + short-path azimuth — surfaced in the
              main strip per user request (LP + distances stay in F2). */}
          <div className="flex flex-col w-16"><Label className="mb-1 h-3.5">DXCC #</Label>
            <Input
              type="number"
              className="font-mono"
              value={details.dxcc ?? ''}
              onChange={(e) => updateDetails({ dxcc: e.target.value === '' ? undefined : parseInt(e.target.value, 10) || undefined })}
              placeholder="—"
            />
          </div>
          <div className="flex flex-col w-16"><Label className="mb-1 h-3.5">CQ</Label>
            <Input
              type="number"
              className="font-mono"
              value={details.cqz ?? ''}
              onChange={(e) => updateDetails({ cqz: e.target.value === '' ? undefined : parseInt(e.target.value, 10) || undefined })}
              placeholder="—"
            />
          </div>
          <div className="flex flex-col w-16"><Label className="mb-1 h-3.5">ITU</Label>
            <Input
              type="number"
              className="font-mono"
              value={details.ituz ?? ''}
              onChange={(e) => updateDetails({ ituz: e.target.value === '' ? undefined : parseInt(e.target.value, 10) || undefined })}
              placeholder="—"
            />
          </div>
          <div className="flex flex-col w-14"><Label className="mb-1 h-3.5">Cont</Label>
            <Input
              className="font-mono uppercase"
              value={details.cont}
              onChange={(e) => updateDetails({ cont: e.target.value.toUpperCase() })}
              placeholder="—"
              maxLength={2}
            />
          </div>
        </>}
        {/* Comment stays visible in compact mode — handy for quick contest/
            portable annotations alongside the basic frequency info. */}
        <div className="flex flex-col flex-1 min-w-[120px]"><Label className="mb-1 h-3.5">Comment</Label>
          <Input value={comment} onChange={(e) => setComment(e.target.value)} />
        </div>
        {!compact && (
          <div className="flex flex-col flex-1 min-w-[120px]"><Label className="mb-1 h-3.5">Note (ADIF)</Label>
            <Input value={note} onChange={(e) => setNote(e.target.value)} />
          </div>
        )}
        <div className="flex flex-col">
          <Label className="mb-1 h-3.5">&nbsp;</Label>
          <Button onClick={save} disabled={saving} className="h-8">
            <Send className="size-3.5" />
            {saving ? '…' : 'Log QSO'}
          </Button>
        </div>
      </section>

      {/* In compact mode the entry strip is the whole app — hide everything
          else and let the user re-expand with the topbar toggle. */}
      {compact ? null : <>
      {/* ===== BAND/SLOT GRID ===== */}
      <BandSlotGrid wb={wb} busy={wbBusy} currentBand={band} currentMode={mode} />

      {/* ===== F2-F5 DETAILS ===== */}
      <DetailsPanel
        callsign={callsign}
        prefix={prefix}
        operatorGrid={station.my_grid}
        remoteGrid={grid}
        details={details}
        onChange={updateDetails}
      />

      {/* ===== LOWER: tabs+table | call history ===== */}
      <div className="grid grid-cols-[1fr_360px] gap-2.5 p-2.5 flex-1 min-h-0">
        <section className="bg-card border border-border rounded-lg shadow-sm flex flex-col min-h-0 overflow-hidden">
          <Tabs value={activeTab} onValueChange={setActiveTab} className="flex flex-col min-h-0">
            <TabsList className="px-3 shrink-0">
              <TabsTrigger value="main">Main</TabsTrigger>
              <TabsTrigger value="recent">
                Recent QSOs
                <Badge variant="secondary" className="ml-1.5 px-1.5 py-0 text-[10px]">{qsos.length}</Badge>
              </TabsTrigger>
              <TabsTrigger value="cluster">Cluster</TabsTrigger>
              <TabsTrigger value="awards">Awards</TabsTrigger>
              <TabsTrigger value="propagation">Propagation</TabsTrigger>
            </TabsList>

            <TabsContent value="recent" className="mt-0 flex flex-col min-h-0 flex-1">
              <div className="flex gap-2 p-2.5 border-b border-border/60">
                <Input
                  className="flex-1"
                  placeholder="Search callsign…"
                  value={filterCallsign}
                  onChange={(e) => setFilterCallsign(e.target.value)}
                />
                <Select value={filterBand || '_'} onValueChange={(v) => setFilterBand(v === '_' ? '' : v)}>
                  <SelectTrigger className="w-32"><SelectValue /></SelectTrigger>
                  <SelectContent>
                    <SelectItem value="_">All bands</SelectItem>
                    {bands.map((b) => <SelectItem key={b} value={b}>{b}</SelectItem>)}
                  </SelectContent>
                </Select>
                <Select value={filterMode || '_'} onValueChange={(v) => setFilterMode(v === '_' ? '' : v)}>
                  <SelectTrigger className="w-32"><SelectValue /></SelectTrigger>
                  <SelectContent>
                    <SelectItem value="_">All modes</SelectItem>
                    {modes.map((m) => <SelectItem key={m} value={m}>{m}</SelectItem>)}
                  </SelectContent>
                </Select>
                <Button variant="outline" size="sm" onClick={refresh}>
                  <RefreshCw className="size-3.5" /> Refresh
                </Button>
              </div>

              {importResult && (
                <div className={cn(
                  'mx-2.5 mt-2 px-3 py-2 rounded-md text-xs border flex flex-col gap-1.5',
                  importResult.errors && importResult.errors.length > 0
                    ? 'bg-amber-50 border-amber-300 text-amber-800'
                    : 'bg-emerald-50 border-emerald-300 text-emerald-800',
                )}>
                  <div className="flex items-center gap-3 flex-wrap">
                    <strong>Import complete.</strong>
                    <Badge variant="outline" className="bg-white/60 font-mono text-emerald-700 border-emerald-300">{importResult.imported} imported</Badge>
                    {importResult.duplicates > 0 && (
                      <Badge variant="outline" className="bg-white/60 font-mono text-sky-700 border-sky-300">{importResult.duplicates} duplicates</Badge>
                    )}
                    <Badge variant="outline" className="bg-white/60 font-mono text-amber-700 border-amber-300">{importResult.skipped} skipped</Badge>
                    <Badge variant="outline" className="bg-white/60 font-mono">{importResult.total} total</Badge>
                    {importResult.duplicates > 0 && importResult.duplicate_samples && importResult.duplicate_samples.length > 0 && (
                      <button className="underline text-xs" onClick={() => setImportDupsOpen((v) => !v)}>
                        {importDupsOpen ? 'Hide' : 'Show'} duplicates
                        {importResult.duplicates > importResult.duplicate_samples.length
                          ? ` (first ${importResult.duplicate_samples.length} of ${importResult.duplicates})`
                          : ''}
                      </button>
                    )}
                    {importResult.errors && importResult.errors.length > 0 && (
                      <button className="underline text-xs" onClick={() => setImportErrorsOpen((v) => !v)}>
                        {importErrorsOpen ? 'Hide' : 'Show'} {importResult.errors.length} error{importResult.errors.length > 1 ? 's' : ''}
                      </button>
                    )}
                    <button className="ml-auto" onClick={() => setImportResult(null)}><X className="size-4" /></button>
                  </div>
                  {importDupsOpen && importResult.duplicate_samples && (
                    <ul className="font-mono text-[11px] pl-6 max-h-32 overflow-y-auto list-disc border-t border-current/20 pt-2 mt-1">
                      {importResult.duplicate_samples.map((d, i) => <li key={i}>{d}</li>)}
                    </ul>
                  )}
                  {importErrorsOpen && importResult.errors && (
                    <ul className="font-mono text-[11px] pl-6 max-h-32 overflow-y-auto list-disc border-t border-current/20 pt-2 mt-1">
                      {importResult.errors.map((e, i) => <li key={i}>{e}</li>)}
                    </ul>
                  )}
                </div>
              )}

              <div className="flex-1 overflow-auto">
                <table className="w-full border-collapse text-[12.5px]">
                  <thead>
                    <tr>
                      {['Date UTC','Callsign','Band','Mode','MHz','RST sent','RST rcvd','Name','QTH','Country','Grid','Station','Comment',''].map((h, i) => (
                        <th
                          key={i}
                          className={cn(
                            'sticky top-0 z-10 bg-stone-200 px-2.5 py-2 text-left font-semibold text-muted-foreground text-[11px] uppercase tracking-wide border-b border-border whitespace-nowrap',
                            h === 'MHz' && 'text-right',
                            i === 13 && 'w-0',
                          )}
                        >{h}</th>
                      ))}
                    </tr>
                  </thead>
                  <tbody>
                    {qsos.length === 0 ? (
                      <tr><td colSpan={14} className="text-center py-10 text-muted-foreground italic">No QSO yet. Log your first contact above.</td></tr>
                    ) : qsos.map((q, i) => (
                      <tr
                        key={q.id}
                        className={cn(
                          'cursor-pointer hover:bg-stone-100 transition-colors',
                          i % 2 === 1 && 'bg-stone-50/60',
                          selectedId === q.id && '!bg-accent',
                        )}
                        onClick={() => setSelectedId(q.id)}
                        onDoubleClick={() => openEdit(q.id)}
                      >
                        <td className="px-2.5 py-1.5 font-mono whitespace-nowrap border-b border-border/40">{fmtDateUTC(q.qso_date)}</td>
                        <td className="px-2.5 py-1.5 font-mono font-semibold text-primary whitespace-nowrap border-b border-border/40">{q.callsign}</td>
                        <td className="px-2.5 py-1.5 whitespace-nowrap border-b border-border/40">
                          <span className="inline-block px-2 py-0.5 rounded font-mono text-[11px] font-semibold bg-accent text-accent-foreground">{q.band}</span>
                        </td>
                        <td className="px-2.5 py-1.5 whitespace-nowrap border-b border-border/40">
                          <span className="inline-block px-2 py-0.5 rounded font-mono text-[11px] font-semibold bg-emerald-100 text-emerald-700">{q.mode}</span>
                        </td>
                        <td className="px-2.5 py-1.5 font-mono text-right whitespace-nowrap border-b border-border/40">{fmtFreq(q.freq_hz)}</td>
                        <td className="px-2.5 py-1.5 font-mono whitespace-nowrap border-b border-border/40">{q.rst_sent ?? ''}</td>
                        <td className="px-2.5 py-1.5 font-mono whitespace-nowrap border-b border-border/40">{q.rst_rcvd ?? ''}</td>
                        <td className="px-2.5 py-1.5 whitespace-nowrap border-b border-border/40">{q.name ?? ''}</td>
                        <td className="px-2.5 py-1.5 whitespace-nowrap border-b border-border/40">{q.qth ?? ''}</td>
                        <td className="px-2.5 py-1.5 whitespace-nowrap border-b border-border/40">{q.country ?? ''}</td>
                        <td className="px-2.5 py-1.5 font-mono whitespace-nowrap border-b border-border/40">{q.grid ?? ''}</td>
                        <td className="px-2.5 py-1.5 font-mono whitespace-nowrap border-b border-border/40">{q.station_callsign ?? ''}</td>
                        <td className="px-2.5 py-1.5 text-muted-foreground whitespace-nowrap border-b border-border/40 max-w-[200px] overflow-hidden text-ellipsis">{q.comment ?? ''}</td>
                        <td className="px-1.5 py-0.5 text-right whitespace-nowrap border-b border-border/40 opacity-0 group-hover:opacity-100">
                          <Button size="icon" variant="ghost" className="size-6 mx-0.5"
                            onClick={(e) => { e.stopPropagation(); openEdit(q.id); }}>
                            <Pencil className="size-3" />
                          </Button>
                          <Button size="icon" variant="ghost" className="size-6 mx-0.5 hover:bg-destructive/10 hover:text-destructive"
                            onClick={(e) => { e.stopPropagation(); askDelete(q.id); }}>
                            <Trash2 className="size-3" />
                          </Button>
                        </td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
            </TabsContent>

            {(['main','cluster','awards','propagation'] as const).map((t) => (
              <TabsContent key={t} value={t} className="flex-1 flex flex-col items-center justify-center text-muted-foreground gap-2 py-12">
                <Hash className="size-10 opacity-30" />
                <div className="text-base font-semibold text-foreground/70">{t[0].toUpperCase() + t.slice(1)}</div>
                <div className="text-xs">Module coming soon.</div>
              </TabsContent>
            ))}
          </Tabs>
        </section>

        <CallHistoryPanel wb={wb} busy={wbBusy} currentCall={callsign} />
      </div>
      </>}

      {editingQSO && (
        <QSOEditModal qso={editingQSO} onSave={onModalSave} onDelete={onModalDelete} onClose={() => setEditingQSO(null)} />
      )}
      {showSettings && (
        <SettingsModal
          initialSection={settingsSection}
          onClose={() => { setShowSettings(false); setSettingsSection(undefined); }}
          onSaved={() => { loadStation(); loadLists(); loadCATCfg(); }}
        />
      )}
      {deletingQSO && (
        <ConfirmDialog
          title="Delete QSO?"
          message={`This will permanently delete the QSO with ${deletingQSO.callsign} on ${fmtDateUTC(deletingQSO.qso_date)} (${deletingQSO.band} ${deletingQSO.mode}). This cannot be undone.`}
          confirmLabel="Delete"
          danger
          onConfirm={confirmDelete}
          onCancel={() => setDeletingQSO(null)}
        />
      )}
      {showDeleteAll && (
        <ConfirmDialog
          title="Delete ALL QSOs?"
          message={`This will permanently wipe every QSO from the logbook (${total.toLocaleString('en-US')} contacts). This cannot be undone. Consider exporting an ADIF backup first.`}
          confirmLabel={deletingAll ? 'Deleting…' : `Delete ${total.toLocaleString('en-US')} QSOs`}
          confirmPhrase="DELETE ALL"
          danger
          onConfirm={confirmDeleteAll}
          onCancel={() => { if (!deletingAll) setShowDeleteAll(false); }}
        />
      )}
      {pendingImportPath && (
        <Dialog open onOpenChange={(o) => { if (!o) setPendingImportPath(null); }}>
          <DialogContent className="max-w-lg px-6">
            <DialogHeader className="px-2">
              <DialogTitle>Import ADIF</DialogTitle>
              <DialogDescription className="text-xs break-all">
                {pendingImportPath}
              </DialogDescription>
            </DialogHeader>
            <div className="py-2 px-2 space-y-3">
              <label className="flex items-start gap-2 text-sm cursor-pointer">
                <Checkbox
                  checked={importSkipDups}
                  onCheckedChange={(c) => setImportSkipDups(!!c)}
                  className="mt-0.5"
                />
                <span>
                  Skip duplicates
                  <span className="block text-xs text-muted-foreground mt-0.5">
                    Records that match an existing QSO on (callsign + UTC minute + band + mode) are not re-inserted. Uncheck to import everything as-is — useful for merging two logs that overlap intentionally.
                  </span>
                </span>
              </label>
            </div>
            <DialogFooter className="px-2">
              <Button variant="outline" onClick={() => setPendingImportPath(null)}>Cancel</Button>
              <Button onClick={runImport}>Import</Button>
            </DialogFooter>
          </DialogContent>
        </Dialog>
      )}
    </div>
  );
}
