import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
  AllCommunityModule, ModuleRegistry,
  type ColDef,
} from 'ag-grid-community';
import { hamlogGridTheme } from '@/lib/gridTheme';
import { AgGridReact } from 'ag-grid-react';
import { Plus, Trash2, Radio, PlusCircle, MinusCircle, Search, UserPlus, History, ChevronUp, ChevronDown } from 'lucide-react';
import {
  Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import { QSOEditModal } from '@/components/QSOEditModal';
import { RecentQSOsGrid } from '@/components/RecentQSOsGrid';
import { useI18n } from '@/lib/i18n';
import type { QSOForm } from '@/types';
import {
  NetList, NetCreate, NetRename, NetDelete, NetOpen, NetClose, NetOpenID,
  NetRoster, NetRosterUpsert, NetRosterRemove, NetLookup,
  NetActiveList, NetActivate, NetDeactivate, NetUpdateActive, NetDiscardActive,
  WorkedBefore,
} from '@/../wailsjs/go/main/App';
import { netctl } from '@/../wailsjs/go/models';

ModuleRegistry.registerModules([AllCommunityModule]);

const hamlogTheme = hamlogGridTheme;

type Net = netctl.Net;
type Station = netctl.Station;

function fmtTimeOn(s: any): string {
  if (!s) return '';
  const d = new Date(s);
  if (isNaN(d.getTime())) return '';
  const p = (n: number) => String(n).padStart(2, '0');
  return `${p(d.getUTCHours())}:${p(d.getUTCMinutes())}:${p(d.getUTCSeconds())}Z`;
}

const emptyStation = (): Station => netctl.Station.createFrom({ callsign: '' });

// The right-click actions of the main Recent QSOs grid (update from cty/QRZ/
// Club Log, send to services, recording e-mail, eQSL, delete) — passed down so
// the worked-before grid here offers the SAME context menu on logged QSOs.
type QSOMenuHandlers = {
  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;
  onDelete?: (ids: number[]) => void;
};

type Props = {
  onLogged?: () => void;
  countries?: string[];
  bands?: string[];
  modes?: string[];
  qsoMenuHandlers?: QSOMenuHandlers;
};

export function NetControlPanel({ onLogged, countries, bands, modes, qsoMenuHandlers }: Props) {
  const { t } = useI18n();
  const [nets, setNets] = useState<Net[]>([]);
  const [selId, setSelId] = useState<string>('');
  const [openId, setOpenId] = useState<string>('');
  const [roster, setRoster] = useState<Station[]>([]);
  const [active, setActive] = useState<QSOForm[]>([]);
  const [error, setError] = useState('');

  // Full-QSO edit modal for an on-air draft (same one Recent QSOs uses).
  const [editingDraft, setEditingDraft] = useState<QSOForm | null>(null);
  const [selectedActiveIds, setSelectedActiveIds] = useState<number[]>([]);
  // Programmatic row selection in the on-air grid (auto-advance after logging).
  const [selectRow, setSelectRow] = useState<{ id: number; seq: number }>({ id: 0, seq: 0 });

  // Add/edit-contact dialog.
  const [contactOpen, setContactOpen] = useState(false);
  const [contact, setContact] = useState<Station>(emptyStation());
  const [looking, setLooking] = useState(false);

  const rosterGrid = useRef<any>(null);
  // Cross-grid drag & drop: roster row → on-air grid puts the station on air;
  // on-air row → roster grid logs it (same as the "Log & end" button). AG-Grid
  // external drop zones need each grid's api + the OTHER grid's container div.
  const onAirApi = useRef<any>(null);
  const onAirWrap = useRef<HTMLDivElement | null>(null);
  const rosterWrap = useRef<HTMLDivElement | null>(null);
  const dropZonesDone = useRef({ roster: false, onair: false });
  // Callbacks live in refs so the drop-zone closures (registered once) always
  // call the CURRENT activate/deactivate, not a stale first-render one.
  const activateRef = useRef<(call: string) => void>(() => {});
  const deactivateRef = useRef<(id?: number) => void>(() => {});
  const wireDropZones = useCallback(() => {
    const rApi = rosterGrid.current?.api;
    if (rApi && onAirWrap.current && !dropZonesDone.current.roster) {
      dropZonesDone.current.roster = true;
      rApi.addRowDropZone({
        getContainer: () => onAirWrap.current!,
        onDragStop: (p: any) => { const call = p.node?.data?.callsign; if (call) activateRef.current(call); },
      });
    }
    if (onAirApi.current && rosterWrap.current && !dropZonesDone.current.onair) {
      dropZonesDone.current.onair = true;
      onAirApi.current.addRowDropZone({
        getContainer: () => rosterWrap.current!,
        onDragStop: (p: any) => { const id = p.node?.data?.id; if (id != null) deactivateRef.current(id); },
      });
    }
  }, []);

  // Worked-before for the clicked station (see if/when we contacted it before).
  const [wbCall, setWbCall] = useState('');
  const [wb, setWb] = useState<any>(null);
  const [wbBusy, setWbBusy] = useState(false);
  const wbReq = useRef(0);
  // Resizable height (drag the handle on top). Persisted.
  const [wbHeight, setWbHeight] = useState(() => {
    const v = parseInt(localStorage.getItem('opslog.netWbHeight') || '', 10);
    return v >= 80 && v <= 600 ? v : 176;
  });
  useEffect(() => { localStorage.setItem('opslog.netWbHeight', String(wbHeight)); }, [wbHeight]);
  const startWbResize = (e: React.MouseEvent) => {
    e.preventDefault();
    const startY = e.clientY, startH = wbHeight;
    const onMove = (ev: MouseEvent) => setWbHeight(Math.max(80, Math.min(600, startH - (ev.clientY - startY))));
    const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
    document.addEventListener('mousemove', onMove);
    document.addEventListener('mouseup', onUp);
  };
  const showWorkedBefore = useCallback(async (call: string, dxcc = 0) => {
    const c = (call ?? '').trim().toUpperCase();
    setWbCall(c);
    if (!c) { setWb(null); return; }
    const req = ++wbReq.current;
    setWbBusy(true);
    try { const r = await WorkedBefore(c, dxcc); if (wbReq.current === req) setWb(r); }
    catch { if (wbReq.current === req) setWb(null); }
    finally { if (wbReq.current === req) setWbBusy(false); }
  }, []);

  const isOpen = openId !== '' && openId === selId;

  const refreshNets = useCallback(async () => {
    try {
      const [list, oid] = await Promise.all([NetList(), NetOpenID()]);
      const arr = (list ?? []) as Net[];
      setNets(arr);
      setOpenId(oid ?? '');
      setSelId((cur) => cur || (oid ?? '') || (arr[0]?.id ?? ''));
    } catch (e: any) { setError(String(e?.message ?? e)); }
  }, []);

  const refreshRoster = useCallback(async (id: string) => {
    if (!id) { setRoster([]); return; }
    try { setRoster(((await NetRoster(id)) ?? []) as Station[]); }
    catch (e: any) { setError(String(e?.message ?? e)); }
  }, []);

  const refreshActive = useCallback(async () => {
    try { setActive(((await NetActiveList()) ?? []) as unknown as QSOForm[]); }
    catch { /* ignore */ }
  }, []);

  // ---- Mic pass order ------------------------------------------------------
  // The on-air grid shows the stations in the order the mic goes around — NOT
  // by log time. The order is owned here as a list of draft ids: new check-ins
  // join at the END of the queue, logged/cancelled ones drop out, and the ⬆⬇
  // buttons move the selected station. Persisted per net so a panel remount
  // (tab switch) keeps the round.
  const [orderIds, setOrderIds] = useState<number[]>([]);
  const orderKey = openId ? `opslog.netOrder.${openId}` : '';
  // Load the saved order when a net opens.
  useEffect(() => {
    if (!orderKey) { setOrderIds([]); return; }
    try { setOrderIds(JSON.parse(localStorage.getItem(orderKey) || '[]')); }
    catch { setOrderIds([]); }
  }, [orderKey]);
  // Reconcile with the live list: keep known ids in their order, append new
  // ones, drop the gone. Persist.
  useEffect(() => {
    setOrderIds((cur) => {
      const liveIds = active.map((a) => a.id as number).filter((id) => id != null);
      const liveSet = new Set(liveIds);
      const kept = cur.filter((id) => liveSet.has(id));
      const keptSet = new Set(kept);
      const next = [...kept, ...liveIds.filter((id) => !keptSet.has(id))];
      if (next.length === cur.length && next.every((v, i) => v === cur[i])) return cur;
      if (orderKey) { try { localStorage.setItem(orderKey, JSON.stringify(next)); } catch { /* quota */ } }
      return next;
    });
  }, [active, orderKey]);
  const activeOrdered = useMemo(() => {
    const byId = new Map(active.map((a) => [a.id as number, a]));
    return orderIds.map((id) => byId.get(id)).filter(Boolean) as QSOForm[];
  }, [active, orderIds]);
  // Move the selected on-air station up/down the queue.
  const moveSelected = useCallback((dir: -1 | 1) => {
    const id = selectedActiveIds[0];
    if (id == null) return;
    setOrderIds((cur) => {
      const i = cur.indexOf(id);
      const j = i + dir;
      if (i < 0 || j < 0 || j >= cur.length) return cur;
      const next = [...cur];
      [next[i], next[j]] = [next[j], next[i]];
      if (orderKey) { try { localStorage.setItem(orderKey, JSON.stringify(next)); } catch { /* quota */ } }
      return next;
    });
  }, [selectedActiveIds, orderKey]);

  useEffect(() => { refreshNets(); }, [refreshNets]);
  useEffect(() => { refreshRoster(selId); }, [selId, refreshRoster]);
  useEffect(() => { if (isOpen) refreshActive(); else setActive([]); }, [isOpen, refreshActive]);

  // The roster side hides callsigns currently on the air (they live in the left
  // grid until logged), mirroring Log4OM's two-list behaviour.
  const activeCalls = useMemo(
    () => new Set(active.map((a) => (a.callsign ?? '').toUpperCase())),
    [active],
  );
  const rosterShown = useMemo(
    () => roster.filter((s) => !activeCalls.has((s.callsign ?? '').toUpperCase())),
    [roster, activeCalls],
  );

  async function newNet() {
    const name = window.prompt(t('ncp.newNetPrompt'));
    if (!name?.trim()) return;
    try { const n = await NetCreate(name.trim()); await refreshNets(); setSelId(n.id); }
    catch (e: any) { setError(String(e?.message ?? e)); }
  }
  async function renameNet() {
    if (!selId) return;
    const cur = nets.find((n) => n.id === selId);
    const name = window.prompt(t('ncp.renamePrompt'), cur?.name ?? '');
    if (!name?.trim()) return;
    try { await NetRename(selId, name.trim()); await refreshNets(); }
    catch (e: any) { setError(String(e?.message ?? e)); }
  }
  async function deleteNet() {
    if (!selId) return;
    const cur = nets.find((n) => n.id === selId);
    if (!window.confirm(t('ncp.deleteConfirm', { name: cur?.name ?? '' }))) return;
    try { await NetDelete(selId); setSelId(''); await refreshNets(); }
    catch (e: any) { setError(String(e?.message ?? e)); }
  }
  async function toggleOpen() {
    try {
      if (isOpen) {
        if (active.length > 0 &&
          !window.confirm(t('ncp.closeConfirm', { n: active.length }))) return;
        await NetClose(); setOpenId('');
      } else {
        await NetOpen(selId); setOpenId(selId); await refreshActive();
      }
    } catch (e: any) { setError(String(e?.message ?? e)); }
  }

  // Roster → active (start QSO).
  async function activate(call: string) {
    if (!isOpen || !call) return;
    try { await NetActivate(call); await refreshActive(); }
    catch (e: any) { setError(String(e?.message ?? e)); }
  }
  // Active → logged (end QSO, removed from session, written to the logbook).
  // Then auto-select the NEXT station in the mic-pass order, so the operator
  // can chain "log → log → log" without re-clicking a row each time.
  async function deactivate(id?: number) {
    if (id == null) return;
    const next = activeOrdered.find((a) => a.id !== id); // computed BEFORE the list shrinks
    try {
      await NetDeactivate(id);
      await refreshActive();
      onLogged?.();
      if (next?.id != null) {
        setSelectRow((s) => ({ id: next.id as number, seq: s.seq + 1 }));
        setSelectedActiveIds([next.id as number]);
        showWorkedBefore(next.callsign, (next as any).dxcc ?? 0);
      }
    } catch (e: any) { setError(String(e?.message ?? e)); }
  }
  // Keep the drop-zone closures pointed at the CURRENT handlers.
  activateRef.current = activate;
  deactivateRef.current = deactivate;

  // Log EVERYONE still on air (end of net): each draft is written to the logbook
  // exactly as the single-log button would.
  async function logAll() {
    if (active.length === 0) return;
    if (!window.confirm(t('ncp.logAllConfirm', { n: active.length }))) return;
    try {
      for (const a of [...active]) {
        if (a.id != null) await NetDeactivate(a.id as number);
      }
      await refreshActive();
      onLogged?.();
    } catch (e: any) { setError(String(e?.message ?? e)); await refreshActive(); }
  }

  // Edit-modal handlers (operate on the in-memory draft, not the DB).
  async function saveDraft(q: QSOForm) {
    try { await NetUpdateActive(q as any); setEditingDraft(null); await refreshActive(); }
    catch (e: any) { setError(String(e?.message ?? e)); }
  }
  async function discardDraft(id: number) {
    try { await NetDiscardActive(id); setEditingDraft(null); await refreshActive(); }
    catch (e: any) { setError(String(e?.message ?? e)); }
  }

  // Add-contact dialog.
  function openAddContact() { setContact(emptyStation()); setContactOpen(true); }
  async function lookupContact() {
    const call = (contact.callsign ?? '').trim();
    if (!call) return;
    setLooking(true);
    try {
      const r = await NetLookup(call);
      setContact((c) => netctl.Station.createFrom({
        ...c, callsign: (r.callsign || call).toUpperCase(),
        name: r.name || c.name, qth: r.qth || c.qth, country: r.country || c.country,
        dxcc: r.dxcc || c.dxcc, itu: r.itu || c.itu, cq: r.cq || c.cq,
        grid: r.grid || c.grid, address: r.address || c.address, state: r.state || c.state,
        cnty: r.cnty || c.cnty, cont: r.cont || c.cont, lat: r.lat || c.lat, lon: r.lon || c.lon,
        email: r.email || c.email,
      }));
    } catch (e: any) { setError(String(e?.message ?? e)); }
    finally { setLooking(false); }
  }
  async function saveContact() {
    const call = (contact.callsign ?? '').trim().toUpperCase();
    if (!call || !selId) return;
    try {
      await NetRosterUpsert(selId, netctl.Station.createFrom({ ...contact, callsign: call }));
      setContactOpen(false);
      await refreshRoster(selId);
    } catch (e: any) { setError(String(e?.message ?? e)); }
  }
  async function removeSelectedRoster() {
    const sel = (rosterGrid.current?.api?.getSelectedRows() ?? []) as Station[];
    if (sel.length === 0) return;
    if (!window.confirm(t('ncp.removeConfirm', { n: sel.length }))) return;
    try {
      for (const s of sel) await NetRosterRemove(selId, s.callsign);
      await refreshRoster(selId);
    } catch (e: any) { setError(String(e?.message ?? e)); }
  }

  const rosterCols = useMemo<ColDef<Station>[]>(() => [
    // rowDrag: drag a roster station onto the on-air grid to start its QSO.
    { headerName: t('ncp.colCallsign'), field: 'callsign', width: 110, cellClass: 'font-mono font-semibold', rowDrag: true },
    { headerName: t('ncp.colName'), field: 'name', flex: 1 },
    { headerName: 'QTH', field: 'qth', flex: 1 },
    { headerName: t('ncp.colCountry'), field: 'country', width: 130 },
  ], [t]);

  const defaultColDef = useMemo<ColDef>(() => ({ sortable: true, resizable: true, suppressMovable: false }), []);

  return (
    <div className="flex flex-col min-h-0 flex-1">
      {/* Toolbar */}
      <div className="flex items-center gap-2 px-3 py-2 border-b border-border/60 bg-muted/30">
        <Button variant="outline" size="sm" className="h-8" onClick={newNet}>
          <Plus className="size-3.5" /> {t('ncp.newNet')}
        </Button>
        <select
          className="h-8 rounded-md border border-input bg-background px-2 text-sm min-w-[180px]"
          value={selId}
          disabled={isOpen}
          onChange={(e) => setSelId(e.target.value)}
          title={isOpen ? t('ncp.closeToSwitch') : t('ncp.selectNetTitle')}
        >
          <option value="">{t('ncp.selectNetOption')}</option>
          {nets.map((n) => <option key={n.id} value={n.id}>{n.name}</option>)}
        </select>
        <Button variant={isOpen ? 'destructive' : 'default'} size="sm" className="h-8" disabled={!selId} onClick={toggleOpen}>
          <Radio className="size-3.5" /> {isOpen ? t('ncp.closeNet') : t('ncp.openNet')}
        </Button>
        <Button variant="ghost" size="sm" className="h-8" disabled={!selId || isOpen} onClick={renameNet}>{t('ncp.rename')}</Button>
        <Button variant="ghost" size="sm" className="h-8 text-danger" disabled={!selId || isOpen} onClick={deleteNet}>
          <Trash2 className="size-3.5" /> {t('ncp.delete')}
        </Button>

        <div className="flex-1" />
        {isOpen && <Badge className="bg-success text-success-foreground tracking-wider">{t('ncp.netOpenBadge')}</Badge>}
        <span className="text-[11px] text-muted-foreground">
          {t('ncp.onAir')} <strong className="text-foreground">{active.length}</strong> ·
          {t('ncp.roster')} <strong className="text-foreground">{roster.length}</strong>
        </span>
        {error && <Badge variant="destructive" className="max-w-[280px] truncate" title={error}>{error}</Badge>}
      </div>

      <div className="flex min-h-0 flex-1">
        {/* ACTIVE USERS (left) */}
        <div className="flex flex-col min-h-0 flex-1 border-r border-border/60">
          <div className="flex items-center gap-2 px-3 py-1.5 bg-destructive text-destructive-foreground text-[11px] font-semibold uppercase tracking-wider">
            {t('ncp.onAirActive')}
            <span className="ml-auto font-normal normal-case opacity-90">{t('ncp.activeHint')}</span>
          </div>
          <div ref={onAirWrap} className="flex flex-col min-h-0 flex-1">
            <RecentQSOsGrid
              rows={activeOrdered}
              total={activeOrdered.length}
              storageKey="net.onair"
              selectRowSignal={selectRow}
              rowDragCall
              passOrder
              onGridApi={(api) => { onAirApi.current = api; wireDropZones(); }}
              onRowClicked={(q) => showWorkedBefore(q.callsign, (q as any).dxcc ?? 0)}
              onRowDoubleClicked={(q) => setEditingDraft(q)}
              onRowSelected={setSelectedActiveIds}
            />
          </div>
          {isOpen && active.length > 0 && (
            <div className="px-3 py-1.5 border-t border-border/60 bg-muted/30 flex items-center gap-2">
              <Button variant="ghost" size="sm" className="h-7 text-[11px]"
                onClick={() => { if (selectedActiveIds[0] != null) deactivate(selectedActiveIds[0]); }}>
                <MinusCircle className="size-3.5" /> {t('ncp.logEndSelected')}
              </Button>
              {/* Mic pass order: move the selected station up/down the round. */}
              <div className="flex items-center gap-0.5 pl-1 border-l border-border/50">
                <Button variant="ghost" size="icon" className="size-7" title={t('ncp.moveUp')}
                  disabled={selectedActiveIds[0] == null || orderIds.indexOf(selectedActiveIds[0]) <= 0}
                  onClick={() => moveSelected(-1)}>
                  <ChevronUp className="size-4" />
                </Button>
                <Button variant="ghost" size="icon" className="size-7" title={t('ncp.moveDown')}
                  disabled={selectedActiveIds[0] == null || orderIds.indexOf(selectedActiveIds[0]) >= orderIds.length - 1}
                  onClick={() => moveSelected(1)}>
                  <ChevronDown className="size-4" />
                </Button>
              </div>
              <Button variant="ghost" size="sm" className="h-7 text-[11px] ml-auto" onClick={logAll}>
                <MinusCircle className="size-3.5" /> {t('ncp.logAll', { n: active.length })}
              </Button>
            </div>
          )}

          {/* Worked-before for the clicked station — click a row (on air or
              roster) to see if/when we contacted it before. Lives INSIDE the
              on-air column so resizing only splits this column; the roster on
              the right keeps its full height. */}
          <div className="shrink-0 flex flex-col" style={{ height: wbHeight }}>
            <div className="h-1.5 shrink-0 cursor-ns-resize border-t border-border/60 hover:bg-primary/40 transition-colors" onMouseDown={startWbResize} title={t('ncp.wbResize')} />
            <div className="flex-1 min-h-0 bg-card flex flex-col">
              <div className="flex items-center gap-2 px-3 py-1.5 bg-muted/40 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground shrink-0">
                <History className="size-3.5" />
                {t('ncp.workedBefore')}
                {wbCall && <span className="font-mono text-foreground normal-case">{wbCall}</span>}
                {wb && wb.count > 0 && (
                  <span className="ml-auto font-normal normal-case text-foreground">
                    {wb.count} QSO · {t('ncp.wbFirst')} {String(wb.first ?? '').slice(0, 10)} · {t('ncp.wbLast')} {String(wb.last ?? '').slice(0, 10)}
                    {wb.dxcc_name ? ` · ${wb.dxcc_name} (${wb.dxcc_count})` : ''}
                  </span>
                )}
              </div>
              <div className="flex-1 min-h-0 flex flex-col">
                {!wbCall ? (
                  <div className="flex h-full items-center justify-center text-xs text-muted-foreground">{t('ncp.wbHint')}</div>
                ) : wbBusy ? (
                  <div className="flex h-full items-center justify-center text-xs text-muted-foreground">…</div>
                ) : !wb || wb.count === 0 ? (
                  <div className="flex h-full items-center justify-center text-xs text-muted-foreground">{t('ncp.wbNone')} {wbCall}</div>
                ) : (
                  <RecentQSOsGrid
                    rows={(wb.entries ?? []) as any}
                    total={wb.count}
                    storageKey="net.wb"
                    {...qsoMenuHandlers}
                  />
                )}
              </div>
            </div>
          </div>
        </div>

        {/* NET USERS / roster (right) */}
        <div className="flex flex-col min-h-0 w-[40%] max-w-[560px]">
          <div className="flex items-center gap-2 px-3 py-1.5 bg-success text-success-foreground text-[11px] font-semibold uppercase tracking-wider">
            {t('ncp.netUsersRoster')}
            <span className="ml-auto font-normal normal-case opacity-90">{t('ncp.rosterHint')}</span>
          </div>
          <div ref={rosterWrap} style={{ flex: 1, minHeight: 0, position: 'relative' }}>
            <div style={{ position: 'absolute', inset: 0 }}>
              <AgGridReact<Station>
                ref={rosterGrid}
                theme={hamlogTheme}
                rowData={rosterShown}
                columnDefs={rosterCols}
                defaultColDef={defaultColDef}
                rowSelection={{ mode: 'multiRow', checkboxes: false, headerCheckbox: false, enableClickSelection: true }}
                onGridReady={() => wireDropZones()}
                onRowClicked={(e) => e.data && showWorkedBefore(e.data.callsign, (e.data as any).dxcc ?? 0)}
                onRowDoubleClicked={(e) => e.data && activate(e.data.callsign)}
                animateRows={false}
                getRowId={(p) => String((p.data as any).callsign)}
              />
            </div>
          </div>
          <div className="px-3 py-1.5 border-t border-border/60 bg-muted/30 flex items-center gap-2">
            <Button variant="ghost" size="sm" className="h-7 text-[11px]" disabled={!selId} onClick={openAddContact}>
              <UserPlus className="size-3.5" /> {t('ncp.addContact')}
            </Button>
            <Button variant="ghost" size="sm" className="h-7 text-[11px] text-danger" disabled={!selId} onClick={removeSelectedRoster}>
              <MinusCircle className="size-3.5" /> {t('ncp.remove')}
            </Button>
            {isOpen && (
              <Button variant="ghost" size="sm" className="h-7 text-[11px] ml-auto"
                onClick={() => { const r = rosterGrid.current?.api?.getSelectedRows?.()?.[0] as Station; if (r) activate(r.callsign); }}>
                <PlusCircle className="size-3.5" /> {t('ncp.putOnAir')}
              </Button>
            )}
          </div>
        </div>
      </div>

      {/* Full-QSO edit modal for the selected on-air draft. Save writes back to
          the in-memory draft (NetUpdateActive); Delete cancels it (no log). */}
      {editingDraft && (
        <QSOEditModal
          qso={editingDraft}
          onSave={saveDraft}
          onDelete={discardDraft}
          onClose={() => setEditingDraft(null)}
          countries={countries}
          bands={bands}
          modes={modes}
        />
      )}

      {/* Add / edit contact dialog */}
      <Dialog open={contactOpen} onOpenChange={setContactOpen}>
        <DialogContent className="max-w-md">
          <DialogHeader>
            <DialogTitle>{t('ncp.addContactTitle')}</DialogTitle>
            <DialogDescription>{t('ncp.addContactDesc')}</DialogDescription>
          </DialogHeader>
          <div className="grid gap-3 px-5 py-2">
            <div className="flex items-end gap-2">
              <div className="flex-1">
                <Label className="text-[11px]">{t('ncp.callsign')}</Label>
                <Input className="font-mono uppercase" value={contact.callsign ?? ''}
                  onChange={(e) => setContact((c) => netctl.Station.createFrom({ ...c, callsign: e.target.value.toUpperCase() }))}
                  onKeyDown={(e) => { if (e.key === 'Enter') lookupContact(); }} />
              </div>
              <Button variant="outline" size="sm" className="h-9" onClick={lookupContact} disabled={looking || !(contact.callsign ?? '').trim()}>
                <Search className="size-3.5" /> {looking ? '…' : t('ncp.search')}
              </Button>
            </div>
            <div>
              <Label className="text-[11px]">{t('ncp.name')}</Label>
              <Input value={contact.name ?? ''} onChange={(e) => setContact((c) => netctl.Station.createFrom({ ...c, name: e.target.value }))} />
            </div>
            <div>
              <Label className="text-[11px]">QTH</Label>
              <Input value={contact.qth ?? ''} onChange={(e) => setContact((c) => netctl.Station.createFrom({ ...c, qth: e.target.value }))} />
            </div>
            <div>
              <Label className="text-[11px]">{t('ncp.country')}</Label>
              <Input value={contact.country ?? ''} onChange={(e) => setContact((c) => netctl.Station.createFrom({ ...c, country: e.target.value }))} />
            </div>
          </div>
          <DialogFooter>
            <Button variant="ghost" size="sm" onClick={() => setContactOpen(false)}>{t('ncp.cancel')}</Button>
            <Button size="sm" onClick={saveContact} disabled={!(contact.callsign ?? '').trim()}>
              <PlusCircle className="size-3.5" /> {t('ncp.saveInNet')}
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </div>
  );
}
