import { useCallback, useMemo, useRef, useState } from 'react';
import {
  AllCommunityModule, ModuleRegistry, themeQuartz,
  type ColDef, type ColumnState, type GridReadyEvent, type RowDoubleClickedEvent,
} from 'ag-grid-community';
import { AgGridReact } from 'ag-grid-react';
import { Columns3, FilterX } from 'lucide-react';
import type { QSOForm } from '@/types';
import { QSOContextMenu, type QSOMenuState } from './QSOContextMenu';
import {
  Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { loadLocal, loadRemote, saveState, seedLocal } from '@/lib/gridPrefs';

// Register every Community feature once. v32+ requires explicit registration;
// AllCommunityModule keeps it simple and pulls in sort/filter/resize/reorder/
// virtual-scroll — everything we want out of the box for a logbook table.
ModuleRegistry.registerModules([AllCommunityModule]);

// Custom Quartz theme tuned to match OpsLog's warm palette.
const hamlogTheme = themeQuartz.withParams({
  fontFamily: 'inherit',
  fontSize: 12.5,
  backgroundColor: '#faf6ea',
  foregroundColor: '#2a2419',
  headerBackgroundColor: '#e8dfc9',
  headerTextColor: '#5a4f3a',
  headerFontWeight: 600,
  oddRowBackgroundColor: '#f5efe0',
  rowHoverColor: '#ecdcb4',
  selectedRowBackgroundColor: '#f0d9a8',
  borderColor: '#c8b994',
  rowBorder: { color: '#d8c9a8', width: 1 },
  columnBorder: { color: '#d8c9a8', width: 1 },
  cellHorizontalPadding: 10,
  rowHeight: 32,
  headerHeight: 34,
  spacing: 4,
  accentColor: '#b8410c',
  iconSize: 12,
});

type Props = {
  rows: QSOForm[];
  total: number;
  onRowDoubleClicked?: (q: QSOForm) => void;
  onRowSelected?: (id: number | null) => void;
  onUpdateFromCty?: (ids: number[]) => void;
  onUpdateFromQRZ?: (ids: number[]) => void;
  onUpdateFromClublog?: (ids: number[]) => void;
  onSendTo?: (service: string, ids: number[]) => void;
  onSendRecording?: (ids: number[]) => void;
  onExportSelected?: (ids: number[]) => void;
  onExportFiltered?: () => void;
};

const COL_STATE_KEY = 'hamlog.qsoColState.v2';

function fmtMhzDots(hz?: number): string {
  if (!hz) return '';
  const mhz = (hz / 1_000_000).toFixed(6);
  const [i, f] = mhz.split('.');
  return `${i}.${f.slice(0, 3)}.${f.slice(3, 6)}`;
}

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 fmtDateOnly(s: any): string {
  if (!s) return '';
  const t = String(s).trim();
  // QSL/LoTW/eQSL/ClubLog dates are ADIF YYYYMMDD; upload dates may be ISO.
  const m = t.match(/^(\d{4})(\d{2})(\d{2})/);
  if (m) return `${m[1]}-${m[2]}-${m[3]}`;
  const d = new Date(t);
  if (isNaN(d.getTime())) return t;
  const p = (n: number) => String(n).padStart(2, '0');
  return `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())}`;
}

// Full catalog of selectable columns, grouped for the picker. `defaultVisible`
// = shown out of the box; anything else stays hidden until the user toggles
// it in the Columns dialog.
export type ColEntry = ColDef<QSOForm> & { group: string; label: string; defaultVisible?: boolean };

// Shared so the Worked-before grid (which now also shows full QSO records)
// can offer the exact same column choices without duplicating the catalog.
export const COL_CATALOG: ColEntry[] = [
  // ── QSO basics ──
  { group: 'QSO', label: 'Date UTC',  colId: 'qso_date', headerName: 'Date UTC', field: 'qso_date' as any, width: 150, cellClass: 'font-mono', valueFormatter: (p) => fmtDateUTC(p.value), sort: 'desc', defaultVisible: true },
  { group: 'QSO', label: 'Date Off',  colId: 'qso_date_off', headerName: 'Date off', field: 'qso_date_off' as any, width: 150, cellClass: 'font-mono', valueFormatter: (p) => fmtDateUTC(p.value) },
  { group: 'QSO', label: 'Callsign',  colId: 'callsign', headerName: 'Callsign', field: 'callsign' as any, width: 110, cellClass: 'font-mono font-semibold', cellStyle: { color: '#b8410c' }, defaultVisible: true },
  { group: 'QSO', label: 'Band',      colId: 'band', headerName: 'Band', field: 'band' as any, width: 75, cellClass: 'font-mono', defaultVisible: true },
  { group: 'QSO', label: 'Band RX',   colId: 'band_rx', headerName: 'Band RX', field: 'band_rx' as any, width: 75, cellClass: 'font-mono' },
  { group: 'QSO', label: 'Mode',      colId: 'mode', headerName: 'Mode', field: 'mode' as any, width: 80, cellClass: 'font-mono', defaultVisible: true },
  { group: 'QSO', label: 'Submode',   colId: 'submode', headerName: 'Submode', field: 'submode' as any, width: 80, cellClass: 'font-mono' },
  { group: 'QSO', label: 'MHz (TX)',  colId: 'freq_hz', headerName: 'MHz', field: 'freq_hz' as any, width: 110, type: 'rightAligned', cellClass: 'font-mono', valueFormatter: (p) => fmtMhzDots(p.value as number | undefined), comparator: (a, b) => (a ?? 0) - (b ?? 0), defaultVisible: true },
  { group: 'QSO', label: 'MHz (RX)',  colId: 'freq_rx_hz', headerName: 'MHz RX', field: 'freq_rx_hz' as any, width: 110, type: 'rightAligned', cellClass: 'font-mono', valueFormatter: (p) => fmtMhzDots(p.value as number | undefined), comparator: (a, b) => (a ?? 0) - (b ?? 0) },
  { group: 'QSO', label: 'RST sent',  colId: 'rst_sent', headerName: 'RST sent', field: 'rst_sent' as any, width: 85, cellClass: 'font-mono', defaultVisible: true },
  { group: 'QSO', label: 'RST rcvd',  colId: 'rst_rcvd', headerName: 'RST rcvd', field: 'rst_rcvd' as any, width: 85, cellClass: 'font-mono', defaultVisible: true },
  { group: 'QSO', label: 'TX Power',  colId: 'tx_pwr', headerName: 'TX Power', field: 'tx_pwr' as any, width: 90, type: 'rightAligned', cellClass: 'font-mono' },

  // ── Contacted station ──
  { group: 'Contacted', label: 'Name',     colId: 'name',     headerName: 'Name',     field: 'name' as any,     width: 170, defaultVisible: true },
  { group: 'Contacted', label: 'QTH',      colId: 'qth',      headerName: 'QTH',      field: 'qth' as any,      width: 200, defaultVisible: true },
  { group: 'Contacted', label: 'Address',  colId: 'address',  headerName: 'Address',  field: 'address' as any,  width: 200 },
  { group: 'Contacted', label: 'Country',  colId: 'country',  headerName: 'Country',  field: 'country' as any,  width: 150, defaultVisible: true },
  { group: 'Contacted', label: 'State',    colId: 'state',    headerName: 'State',    field: 'state' as any,    width: 80 },
  { group: 'Contacted', label: 'County',   colId: 'cnty',     headerName: 'County',   field: 'cnty' as any,     width: 130 },
  { group: 'Contacted', label: 'Continent',colId: 'cont',     headerName: 'Cont',     field: 'cont' as any,     width: 60 },
  { group: 'Contacted', label: 'Grid',     colId: 'grid',     headerName: 'Grid',     field: 'grid' as any,     width: 85, cellClass: 'font-mono', defaultVisible: true },
  { group: 'Contacted', label: 'Grid Ext', colId: 'gridsquare_ext', headerName: 'GridExt', field: 'gridsquare_ext' as any, width: 85, cellClass: 'font-mono' },
  { group: 'Contacted', label: 'VUCC grids',colId: 'vucc_grids', headerName: 'VUCC',  field: 'vucc_grids' as any, width: 130, cellClass: 'font-mono' },
  { group: 'Contacted', label: 'DXCC #',   colId: 'dxcc',     headerName: 'DXCC #',   field: 'dxcc' as any,     width: 70, type: 'rightAligned', cellClass: 'font-mono' },
  { group: 'Contacted', label: 'CQZ',      colId: 'cqz',      headerName: 'CQZ',      field: 'cqz' as any,      width: 60, type: 'rightAligned', cellClass: 'font-mono' },
  { group: 'Contacted', label: 'ITU',      colId: 'ituz',     headerName: 'ITU',      field: 'ituz' as any,     width: 60, type: 'rightAligned', cellClass: 'font-mono' },
  { group: 'Contacted', label: 'IOTA',     colId: 'iota',     headerName: 'IOTA',     field: 'iota' as any,     width: 80, cellClass: 'font-mono' },
  { group: 'Contacted', label: 'SOTA ref', colId: 'sota_ref', headerName: 'SOTA',     field: 'sota_ref' as any, width: 110, cellClass: 'font-mono' },
  { group: 'Contacted', label: 'POTA ref', colId: 'pota_ref', headerName: 'POTA',     field: 'pota_ref' as any, width: 110, cellClass: 'font-mono' },
  { group: 'Contacted', label: 'Age',      colId: 'age',      headerName: 'Age',      field: 'age' as any,      width: 60, type: 'rightAligned' },
  { group: 'Contacted', label: 'Lat',      colId: 'lat',      headerName: 'Lat',      field: 'lat' as any,      width: 90, type: 'rightAligned', cellClass: 'font-mono' },
  { group: 'Contacted', label: 'Lon',      colId: 'lon',      headerName: 'Lon',      field: 'lon' as any,      width: 90, type: 'rightAligned', cellClass: 'font-mono' },
  { group: 'Contacted', label: 'Email',    colId: 'email',    headerName: 'Email',    field: 'email' as any,    width: 180 },
  { group: 'Contacted', label: 'Web',      colId: 'web',      headerName: 'Web',      field: 'web' as any,      width: 180 },

  // ── QSL ──
  { group: 'QSL',  label: 'QSL sent',     colId: 'qsl_sent',      headerName: 'QSL sent',   field: 'qsl_sent' as any,      width: 80 },
  { group: 'QSL',  label: 'QSL rcvd',     colId: 'qsl_rcvd',      headerName: 'QSL rcvd',   field: 'qsl_rcvd' as any,      width: 80 },
  { group: 'QSL',  label: 'QSL sent date',colId: 'qsl_sent_date', headerName: 'QSL S date', field: 'qsl_sent_date' as any, width: 120, valueFormatter: (p) => fmtDateOnly(p.value) },
  { group: 'QSL',  label: 'QSL rcvd date',colId: 'qsl_rcvd_date', headerName: 'QSL R date', field: 'qsl_rcvd_date' as any, width: 120, valueFormatter: (p) => fmtDateOnly(p.value) },
  { group: 'QSL',  label: 'QSL via',      colId: 'qsl_via',       headerName: 'QSL via',    field: 'qsl_via' as any,       width: 130 },
  { group: 'QSL',  label: 'QSL msg',      colId: 'qsl_msg',       headerName: 'QSL msg',    field: 'qsl_msg' as any,       width: 200 },
  { group: 'QSL',  label: 'QSL msg rcvd', colId: 'qslmsg_rcvd',   headerName: 'QSL msg rcvd', field: 'qslmsg_rcvd' as any, width: 200 },

  // ── LoTW ──
  { group: 'LoTW', label: 'LoTW sent',      colId: 'lotw_sent',      headerName: 'LoTW sent', field: 'lotw_sent' as any,      width: 80 },
  { group: 'LoTW', label: 'LoTW rcvd',      colId: 'lotw_rcvd',      headerName: 'LoTW rcvd', field: 'lotw_rcvd' as any,      width: 80 },
  { group: 'LoTW', label: 'LoTW sent date', colId: 'lotw_sent_date', headerName: 'LoTW S date', field: 'lotw_sent_date' as any, width: 120, valueFormatter: (p) => fmtDateOnly(p.value) },
  { group: 'LoTW', label: 'LoTW rcvd date', colId: 'lotw_rcvd_date', headerName: 'LoTW R date', field: 'lotw_rcvd_date' as any, width: 120, valueFormatter: (p) => fmtDateOnly(p.value) },

  // ── eQSL ──
  { group: 'eQSL', label: 'eQSL sent',      colId: 'eqsl_sent',      headerName: 'eQSL sent', field: 'eqsl_sent' as any,      width: 80 },
  { group: 'eQSL', label: 'eQSL rcvd',      colId: 'eqsl_rcvd',      headerName: 'eQSL rcvd', field: 'eqsl_rcvd' as any,      width: 80 },
  { group: 'eQSL', label: 'eQSL sent date', colId: 'eqsl_sent_date', headerName: 'eQSL S date', field: 'eqsl_sent_date' as any, width: 120, valueFormatter: (p) => fmtDateOnly(p.value) },
  { group: 'eQSL', label: 'eQSL rcvd date', colId: 'eqsl_rcvd_date', headerName: 'eQSL R date', field: 'eqsl_rcvd_date' as any, width: 120, valueFormatter: (p) => fmtDateOnly(p.value) },

  // ── Uploads (online logbooks) ──
  // ADIF models these as an "upload status/date" (= YOU pushed the QSO) and,
  // for QRZ only, a "download status/date" (= it came back confirmed). We
  // relabel to the same sent/rcvd wording as LoTW/eQSL. Club Log & HRDLog have
  // NO rcvd field in ADIF — they're upload-only, so only "sent" is shown.
  { group: 'Uploads', label: 'ClubLog sent',      colId: 'clublog_qso_upload_status', headerName: 'ClubLog sent',   field: 'clublog_qso_upload_status' as any, width: 100 },
  { group: 'Uploads', label: 'ClubLog sent date', colId: 'clublog_qso_upload_date',   headerName: 'ClubLog S date', field: 'clublog_qso_upload_date' as any,   width: 120, valueFormatter: (p) => fmtDateOnly(p.value) },
  { group: 'Uploads', label: 'HRDLog sent',       colId: 'hrdlog_qso_upload_status',  headerName: 'HRDLog sent',    field: 'hrdlog_qso_upload_status' as any,  width: 100 },
  { group: 'Uploads', label: 'HRDLog sent date',  colId: 'hrdlog_qso_upload_date',    headerName: 'HRDLog S date',  field: 'hrdlog_qso_upload_date' as any,    width: 120, valueFormatter: (p) => fmtDateOnly(p.value) },
  { group: 'Uploads', label: 'QRZ.com sent',      colId: 'qrzcom_qso_upload_status',   headerName: 'QRZ.com sent',   field: 'qrzcom_qso_upload_status' as any,   width: 100 },
  { group: 'Uploads', label: 'QRZ.com rcvd',      colId: 'qrzcom_qso_download_status', headerName: 'QRZ.com rcvd',   field: 'qrzcom_qso_download_status' as any, width: 100 },
  { group: 'Uploads', label: 'QRZ.com sent date', colId: 'qrzcom_qso_upload_date',     headerName: 'QRZ.com S date', field: 'qrzcom_qso_upload_date' as any,     width: 120, valueFormatter: (p) => fmtDateOnly(p.value) },
  { group: 'Uploads', label: 'QRZ.com rcvd date', colId: 'qrzcom_qso_download_date',   headerName: 'QRZ.com R date', field: 'qrzcom_qso_download_date' as any,   width: 120, valueFormatter: (p) => fmtDateOnly(p.value) },

  // ── Contest ──
  { group: 'Contest', label: 'Contest ID',  colId: 'contest_id', headerName: 'Contest',     field: 'contest_id' as any, width: 110 },
  { group: 'Contest', label: 'SRX',         colId: 'srx',        headerName: 'SRX',         field: 'srx' as any,        width: 60, type: 'rightAligned' },
  { group: 'Contest', label: 'STX',         colId: 'stx',        headerName: 'STX',         field: 'stx' as any,        width: 60, type: 'rightAligned' },
  { group: 'Contest', label: 'SRX string',  colId: 'srx_string', headerName: 'SRX str',     field: 'srx_string' as any, width: 100 },
  { group: 'Contest', label: 'STX string',  colId: 'stx_string', headerName: 'STX str',     field: 'stx_string' as any, width: 100 },
  { group: 'Contest', label: 'Check',       colId: 'check',      headerName: 'Check',       field: 'check' as any,      width: 70 },
  { group: 'Contest', label: 'Precedence',  colId: 'precedence', headerName: 'Precedence',  field: 'precedence' as any, width: 90 },
  { group: 'Contest', label: 'ARRL section',colId: 'arrl_sect',  headerName: 'ARRL sect',   field: 'arrl_sect' as any,  width: 90 },

  // ── Propagation / antenna ──
  { group: 'Propagation', label: 'Prop mode', colId: 'prop_mode', headerName: 'Prop',     field: 'prop_mode' as any, width: 80 },
  { group: 'Propagation', label: 'Sat name',  colId: 'sat_name',  headerName: 'Sat',      field: 'sat_name' as any,  width: 110 },
  { group: 'Propagation', label: 'Sat mode',  colId: 'sat_mode',  headerName: 'Sat mode', field: 'sat_mode' as any,  width: 80 },
  { group: 'Propagation', label: 'Ant az',    colId: 'ant_az',    headerName: 'Az',       field: 'ant_az' as any,    width: 70, type: 'rightAligned' },
  { group: 'Propagation', label: 'Ant el',    colId: 'ant_el',    headerName: 'El',       field: 'ant_el' as any,    width: 70, type: 'rightAligned' },
  { group: 'Propagation', label: 'Ant path',  colId: 'ant_path',  headerName: 'Path',     field: 'ant_path' as any,  width: 70 },

  // ── My station (operator side) ──
  { group: 'My station', label: 'Station call', colId: 'station_callsign', headerName: 'Station', field: 'station_callsign' as any, width: 100, cellClass: 'font-mono', defaultVisible: true },
  { group: 'My station', label: 'Operator',     colId: 'operator',         headerName: 'Operator',field: 'operator' as any,         width: 100, cellClass: 'font-mono' },
  { group: 'My station', label: 'My grid',      colId: 'my_grid',          headerName: 'My grid', field: 'my_grid' as any,          width: 85, cellClass: 'font-mono' },
  { group: 'My station', label: 'My country',   colId: 'my_country',       headerName: 'My ctry', field: 'my_country' as any,       width: 130 },
  { group: 'My station', label: 'My state',     colId: 'my_state',         headerName: 'My state',field: 'my_state' as any,         width: 80 },
  { group: 'My station', label: 'My county',    colId: 'my_cnty',          headerName: 'My cnty', field: 'my_cnty' as any,          width: 110 },
  { group: 'My station', label: 'My IOTA',      colId: 'my_iota',          headerName: 'My IOTA', field: 'my_iota' as any,          width: 80, cellClass: 'font-mono' },
  { group: 'My station', label: 'My SOTA',      colId: 'my_sota_ref',      headerName: 'My SOTA', field: 'my_sota_ref' as any,      width: 110, cellClass: 'font-mono' },
  { group: 'My station', label: 'My POTA',      colId: 'my_pota_ref',      headerName: 'My POTA', field: 'my_pota_ref' as any,      width: 110, cellClass: 'font-mono' },
  { group: 'My station', label: 'My DXCC',      colId: 'my_dxcc',          headerName: 'My DXCC#',field: 'my_dxcc' as any,          width: 80, type: 'rightAligned' },
  { group: 'My station', label: 'My CQ zone',   colId: 'my_cq_zone',       headerName: 'My CQZ',  field: 'my_cq_zone' as any,       width: 70, type: 'rightAligned' },
  { group: 'My station', label: 'My ITU zone',  colId: 'my_itu_zone',      headerName: 'My ITU',  field: 'my_itu_zone' as any,      width: 70, type: 'rightAligned' },
  { group: 'My station', label: 'My lat',       colId: 'my_lat',           headerName: 'My lat',  field: 'my_lat' as any,           width: 90, type: 'rightAligned' },
  { group: 'My station', label: 'My lon',       colId: 'my_lon',           headerName: 'My lon',  field: 'my_lon' as any,           width: 90, type: 'rightAligned' },
  { group: 'My station', label: 'My street',    colId: 'my_street',        headerName: 'Street',  field: 'my_street' as any,        width: 160 },
  { group: 'My station', label: 'My city',      colId: 'my_city',          headerName: 'City',    field: 'my_city' as any,          width: 130 },
  { group: 'My station', label: 'My ZIP',       colId: 'my_postal_code',   headerName: 'ZIP',     field: 'my_postal_code' as any,   width: 80 },
  { group: 'My station', label: 'My rig',       colId: 'my_rig',           headerName: 'My rig',  field: 'my_rig' as any,           width: 130 },
  { group: 'My station', label: 'My antenna',   colId: 'my_antenna',       headerName: 'My ant',  field: 'my_antenna' as any,       width: 130 },

  // ── Misc ──
  { group: 'Misc', label: 'Comment',  colId: 'comment',     headerName: 'Comment',    field: 'comment' as any,     flex: 1, minWidth: 160, defaultVisible: true },
  { group: 'Misc', label: 'Notes',    colId: 'notes',       headerName: 'Notes',      field: 'notes' as any,       width: 240 },
  { group: 'Misc', label: 'Created',  colId: 'created_at',  headerName: 'Created at', field: 'created_at' as any,  width: 150, valueFormatter: (p) => fmtDateUTC(p.value) },
  { group: 'Misc', label: 'Updated',  colId: 'updated_at',  headerName: 'Updated at', field: 'updated_at' as any,  width: 150, valueFormatter: (p) => fmtDateUTC(p.value) },
];

export const GROUP_ORDER = [
  'QSO', 'Contacted', 'QSL', 'LoTW', 'eQSL', 'Uploads',
  'Contest', 'Propagation', 'My station', 'Misc',
];

export function RecentQSOsGrid({ rows, onRowDoubleClicked, onRowSelected, onUpdateFromCty, onUpdateFromQRZ, onUpdateFromClublog, onSendTo, onSendRecording, onExportSelected, onExportFiltered }: Props) {
  const gridRef = useRef<any>(null);
  const [pickerOpen, setPickerOpen] = useState(false);
  const [menu, setMenu] = useState<QSOMenuState>(null);

  // Right-click: if the clicked row isn't already part of the selection,
  // select just it; then open the bulk-action menu on the whole selection.
  function onCellContextMenu(e: any) {
    const ev = e.event as MouseEvent | undefined;
    ev?.preventDefault();
    const api = gridRef.current?.api;
    if (!api) return;
    if (e.node && !e.node.isSelected()) {
      api.deselectAll();
      e.node.setSelected(true);
    }
    const ids = (api.getSelectedRows() as QSOForm[])
      .map((r) => r.id as number)
      .filter((n) => !!n);
    if (ids.length === 0) return;
    setMenu({ x: ev?.clientX ?? 0, y: ev?.clientY ?? 0, ids });
  }

  // Compute initial column defs: all columns defined, but those not marked
  // defaultVisible start hidden. The user's saved state (loaded onGridReady)
  // overrides this so a previously toggled column wins.
  const columnDefs = useMemo<ColDef<QSOForm>[]>(() => COL_CATALOG.map((c) => {
    const { group: _g, label: _l, defaultVisible, ...rest } = c;
    return { ...rest, hide: !defaultVisible };
  }), []);

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

  function onGridReady(e: GridReadyEvent) {
    const local = loadLocal(COL_STATE_KEY);
    if (local) e.api.applyColumnState({ state: local as ColumnState[], applyOrder: true });
    // Fall back to the portable DB copy when the local cache is empty
    // (fresh machine / after a reinstall), then re-seed the cache.
    loadRemote(COL_STATE_KEY).then((remote) => {
      if (remote && !local) {
        e.api.applyColumnState({ state: remote as ColumnState[], applyOrder: true });
        seedLocal(COL_STATE_KEY, remote);
      }
    });
  }
  const saveColumnState = useCallback(() => {
    const state = gridRef.current?.api?.getColumnState();
    if (state) saveState(COL_STATE_KEY, state);
  }, []);

  function handleRowDoubleClicked(e: RowDoubleClickedEvent<QSOForm>) {
    if (e.data && onRowDoubleClicked) onRowDoubleClicked(e.data);
  }
  function onSelectionChanged() {
    const sel = gridRef.current?.api?.getSelectedRows() as QSOForm[] | undefined;
    onRowSelected?.(sel && sel[0] ? (sel[0].id as number) : null);
  }

  // ── Column picker (visibility) ──
  // Drives AG Grid via setColumnsVisible(). We don't keep a parallel React
  // state for "which columns are visible" — AG Grid's column state is the
  // source of truth, and saveColumnState persists it.
  function isColVisible(colId: string): boolean {
    const col = gridRef.current?.api?.getColumn(colId);
    return col ? col.isVisible() : !!COL_CATALOG.find((c) => c.colId === colId)?.defaultVisible;
  }
  function setColVisible(colId: string, visible: boolean) {
    const api = gridRef.current?.api;
    if (!api) return;
    api.setColumnsVisible([colId], visible);
    saveColumnState();
  }
  function showAll(group?: string) {
    const api = gridRef.current?.api;
    if (!api) return;
    const ids = COL_CATALOG.filter((c) => !group || c.group === group).map((c) => c.colId!);
    api.setColumnsVisible(ids, true);
    saveColumnState();
  }
  function hideAll(group?: string) {
    const api = gridRef.current?.api;
    if (!api) return;
    const ids = COL_CATALOG.filter((c) => !group || c.group === group).map((c) => c.colId!);
    api.setColumnsVisible(ids, false);
    saveColumnState();
  }
  function resetDefaults() {
    const api = gridRef.current?.api;
    if (!api) return;
    const visible = COL_CATALOG.filter((c) => c.defaultVisible).map((c) => c.colId!);
    const hidden = COL_CATALOG.filter((c) => !c.defaultVisible).map((c) => c.colId!);
    api.setColumnsVisible(visible, true);
    api.setColumnsVisible(hidden, false);
    saveColumnState();
  }

  return (
    <>
      <div className="flex items-center justify-end gap-2 px-2.5 py-1 border-b border-border/60 bg-muted/20">
        <Button variant="ghost" size="sm" className="h-7 text-[11px]" onClick={() => gridRef.current?.api?.setFilterModel(null)}
          title="Clear all column filters">
          <FilterX className="size-3.5" /> Clear filters
        </Button>
        <Button variant="ghost" size="sm" className="h-7 text-[11px]" onClick={() => setPickerOpen(true)}>
          <Columns3 className="size-3.5" /> Columns
        </Button>
      </div>
      <div style={{ flex: 1, minHeight: 0, position: 'relative' }}>
        <div style={{ position: 'absolute', inset: 0 }}>
          <AgGridReact<QSOForm>
            ref={gridRef}
            theme={hamlogTheme}
            rowData={rows}
            columnDefs={columnDefs}
            defaultColDef={defaultColDef}
            rowSelection={{ mode: 'multiRow', checkboxes: false, headerCheckbox: false, enableClickSelection: true }}
            onGridReady={onGridReady}
            onColumnResized={saveColumnState}
            onColumnMoved={saveColumnState}
            onColumnPinned={saveColumnState}
            onColumnVisible={saveColumnState}
            onSortChanged={saveColumnState}
            onRowDoubleClicked={handleRowDoubleClicked}
            onSelectionChanged={onSelectionChanged}
            onCellContextMenu={onCellContextMenu}
            preventDefaultOnContextMenu
            animateRows={false}
            suppressCellFocus
            getRowId={(p) => String((p.data as any).id)}
          />
        </div>
      </div>

      <QSOContextMenu
        menu={menu}
        onClose={() => setMenu(null)}
        onUpdateFromCty={(ids) => onUpdateFromCty?.(ids)}
        onUpdateFromQRZ={(ids) => onUpdateFromQRZ?.(ids)}
        onUpdateFromClublog={onUpdateFromClublog}
        onSendTo={onSendTo}
        onSendRecording={onSendRecording}
        onExportSelected={onExportSelected}
        onExportFiltered={onExportFiltered}
      />

      <Dialog open={pickerOpen} onOpenChange={setPickerOpen}>
        <DialogContent className="max-w-3xl">
          <DialogHeader>
            <DialogTitle>Columns</DialogTitle>
            <DialogDescription>
              Pick the columns you want visible in the Recent QSOs table.
              Your selection is saved.
            </DialogDescription>
          </DialogHeader>
          <div className="grid grid-cols-3 gap-4 max-h-[60vh] overflow-y-auto px-5 py-3">
            {GROUP_ORDER.map((group) => {
              const cols = COL_CATALOG.filter((c) => c.group === group);
              if (cols.length === 0) return null;
              return (
                <div key={group} className="rounded-md border border-border p-2.5">
                  <div className="flex items-center justify-between mb-2 pb-1.5 border-b border-border/60">
                    <span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">{group}</span>
                    <div className="flex gap-0.5">
                      <button className="text-[10px] text-primary hover:underline px-1" onClick={() => showAll(group)}>all</button>
                      <button className="text-[10px] text-muted-foreground hover:underline px-1" onClick={() => hideAll(group)}>none</button>
                    </div>
                  </div>
                  <div className="flex flex-col gap-1">
                    {cols.map((c) => (
                      <label key={c.colId} className="flex items-center gap-2 text-xs cursor-pointer hover:bg-accent/30 rounded px-1 py-0.5">
                        <Checkbox
                          checked={isColVisible(c.colId!)}
                          onCheckedChange={(v) => setColVisible(c.colId!, !!v)}
                        />
                        {c.label}
                      </label>
                    ))}
                  </div>
                </div>
              );
            })}
          </div>
          <DialogFooter>
            <Button variant="ghost" size="sm" onClick={resetDefaults}>Reset to defaults</Button>
            <Button size="sm" onClick={() => setPickerOpen(false)}>Done</Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </>
  );
}
