import { useEffect, useMemo, useState } from 'react';
import { Star, Radio, Sunrise, Sunset, X, Loader2 } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { cn } from '@/lib/utils';
import { sunTimes } from '@/lib/sun';
import { isQSLConfirmed } from '@/lib/qsl';
import { useI18n } from '@/lib/i18n';
import { BandSlotQSOs } from '../../wailsjs/go/main/App';
import type { WorkedBeforeView } from '@/types';

type WorkedBefore = WorkedBeforeView;

interface Props {
  wb: WorkedBefore | null;
  busy: boolean;
  currentBand: string;
  currentMode: string;
  bands?: string[];
  // The operator's configured mode list, in THEIR order: the digital row
  // rotates through it.
  modes?: string[]; // operator's configured bands; falls back to DEFAULT_BANDS
  hasCall?: boolean; // a callsign is being entered — only then highlight the "current entry" cell
  // DX station coordinates, for its sunrise/sunset. Optional: many spots resolve
  // to an entity with no position at all, and the block simply does not appear.
  lat?: number;
  lon?: number;
  // Set when the matrix is showing a QSO picked in the log grid rather than the
  // entry form — labelled so the two can never be confused.
  forCall?: string;
  // Open the QSO editor for a contact — makes callsigns in the cell drill-down
  // clickable. Threaded down from App.openEdit.
  onEditQso?: (id: number) => void;
}

// Compact column label for a band tag: keep the classic V/U for 2m/70cm,
// strip the trailing "m" for meter bands (160m→160), and shorten cm bands
// (13cm→13c) so the column stays narrow.
function bandColLabel(tag: string): string {
  if (tag === '2m') return 'V';
  if (tag === '70cm') return 'U';
  if (tag.endsWith('cm')) return tag.replace('cm', 'c');
  return tag.replace(/m$/, '');
}

// Default 13-column band layout, used when the operator hasn't configured bands.
const DEFAULT_BANDS: { tag: string; label: string }[] = [
  { tag: '160m', label: '160' },
  { tag: '80m',  label: '80'  },
  { tag: '60m',  label: '60'  },
  { tag: '40m',  label: '40'  },
  { tag: '30m',  label: '30'  },
  { tag: '20m',  label: '20'  },
  { tag: '17m',  label: '17'  },
  { tag: '15m',  label: '15'  },
  { tag: '12m',  label: '12'  },
  { tag: '10m',  label: '10'  },
  { tag: '6m',   label: '6'   },
  { tag: '2m',   label: 'V'   },
  { tag: '70cm', label: 'U'   },
];
const CLASSES = ['PH', 'CW', 'DIG'] as const;

const PHONE_MODES = new Set(['SSB','USB','LSB','AM','FM','DIGITALVOICE','PHONE']);
function classMatchesMode(cls: string, mode: string): boolean {
  const u = (mode || '').toUpperCase();
  if (cls === 'PH') return PHONE_MODES.has(u);
  if (cls === 'CW') return u === 'CW';
  return u !== '' && u !== 'CW' && !PHONE_MODES.has(u);
}

// Dedicated matrix palette (--mx-* tokens, per theme in style.css): a 3-level
// ramp per hue so confirmed / worked / not-worked stay distinguishable on both
// light and dark surfaces (the generic status -muted fills were too dark on the
// dark themes to tell "worked" from "not worked" apart). Light-warm reproduces
// the original emerald/indigo/stone colours exactly.
const STATUS_CLASSES: Record<string, string> = {
  call_c: 'bg-mx-call-conf',
  call_w: 'bg-mx-call-work',
  dxcc_c: 'bg-mx-dx-conf',
  dxcc_w: 'bg-mx-dx-work',
};

// Legend entries, in the same colour order as the cells — and the same order and
// i18n keys the Appearance panel's colour pickers use, so the two can never
// disagree about which green is which. swatch = the background class (or a
// special ring marker for the current-entry cell).
const LEGEND: { swatch: string; ring?: boolean; mark?: string; label: string }[] = [
  { swatch: 'bg-mx-call-conf', label: 'mx.callConf' },
  { swatch: 'bg-mx-call-work', label: 'mx.callWork' },
  { swatch: 'bg-mx-dx-conf', label: 'mx.dxConf' },
  { swatch: 'bg-mx-dx-work', label: 'mx.dxWork' },
  { swatch: 'bg-mx-none', label: 'mx.none' },
  { swatch: 'bg-mx-none', ring: true, label: 'mx.current' },
  { swatch: 'bg-mx-none', mark: 'w', label: 'mx.markWork' },
  { swatch: 'bg-mx-none', mark: 'c', label: 'mx.markConf' },
];

// CallMark — "this callsign has already been worked on this slot".
//
// Drawn the same way on every cell, whatever colour the entity status gave it:
// the operator learns one shape and reads it without first working out what the
// background means. Only the fill changes, and only with the callsign's own
// state (worked / confirmed) — never with the entity's. The ring is the theme
// background, which is what keeps the dot legible over all five cell colours.
function CallMark({ state = 'w' }: { state?: string }) {
  return (
    <span
      className={cn(
        'pointer-events-none absolute top-[4px] right-[4px] size-[5px] rounded-full ring-1 ring-background',
        state === 'c' ? 'bg-mx-mark-conf' : 'bg-mx-mark-work',
      )}
    />
  );
}

function cellTitle(t: (k: string) => string, band: string, cls: string, status: string, current: boolean, call = ''): string {
  const desc =
    status === 'call_c' ? t('mx.tipCallConf') :
    status === 'call_w' ? t('mx.tipCallWork') :
    status === 'dxcc_c' ? t('mx.tipDxConf') :
    status === 'dxcc_w' ? t('mx.tipDxWork') :
                          t('mx.tipNone');
  const mine = call === 'c' ? t('mx.tipThisCallConf') : call === 'w' ? t('mx.tipThisCall') : '';
  return `${band} ${cls}: ${desc}${mine ? ' — ' + mine : ''}${current ? ' — ' + t('mx.current') : ''}`;
}

export function BandSlotGrid({ wb, busy, currentBand, currentMode, bands, modes, hasCall = true, lat, lon, forCall, onEditQso }: Props) {
  const { t } = useI18n();
  // Cell drill-down: which band+class the operator clicked, or null.
  const [slot, setSlot] = useState<{ band: string; cls: string } | null>(null);

  // The DIGITAL row is a rotation, not a fixed row.
  //
  // One row for every digital mode would be the honest layout and there is no
  // height for it — the matrix sits in a fixed panel beside a dozen widgets.
  // So the row keeps its place and changes what it answers: DIG (all of them),
  // then each digital mode the operator actually uses, in the order their mode
  // list gives, then back to DIG. The backend publishes the same cells under
  // both the class name and the raw mode, so a rotation costs no round trip.
  const digModes = useMemo(
    () => (modes ?? [])
      .map((m) => (m || '').toUpperCase().trim())
      .filter((m) => m !== '' && m !== 'CW' && !PHONE_MODES.has(m)),
    [modes],
  );
  const [digIdx, setDigIdx] = useState(0); // 0 = the DIG group itself
  // A shorter mode list (the operator edited it) must not strand the rotation
  // on a row that no longer exists.
  const digPos = digModes.length ? digIdx % (digModes.length + 1) : 0;
  const digRow = digPos === 0 ? 'DIG' : digModes[digPos - 1];
  const cycleDig = () => setDigIdx((i) => (digModes.length ? (i + 1) % (digModes.length + 1) : 0));
  // Columns from the operator's configured bands (so the matrix shows only the
  // bands they actually use), falling back to the built-in default set.
  const cols = useMemo(
    () => (bands && bands.length ? bands.map((tag) => ({ tag, label: bandColLabel(tag) })) : DEFAULT_BANDS),
    [bands],
  );
  const dxcc = wb?.dxcc ?? 0;
  const dxccName = wb?.dxcc_name ?? '';
  const dxccCount = wb?.dxcc_count ?? 0;
  const mwRank = wb?.mw_rank ?? 0; // ClubLog Most Wanted rank (1 = most wanted; 0 = off/unknown)
  const callCount = wb?.count ?? 0; // QSOs with this exact callsign
  const hasDxcc = dxcc > 0;
  const newOne = hasDxcc && dxccCount === 0;

  const statusMap = useMemo(() => {
    const m = new Map<string, string>();
    for (const s of wb?.band_status ?? []) {
      m.set(`${s.band}|${s.class}`, s.status);
    }
    return m;
  }, [wb]);

  // Worked-with-this-callsign, per cell — carried separately by the backend
  // because collapsing it into the status is what hid it.
  const callMap = useMemo(() => {
    const m = new Map<string, string>();
    for (const s of wb?.band_status ?? []) {
      if ((s as any).call) m.set(`${s.band}|${s.class}`, (s as any).call);
    }
    return m;
  }, [wb]);

  // "Newness" of the current band+mode entry, for the award/DX-chase badges.
  // Derived straight from the entity's real band_status (all bands it was
  // worked on — not just the operator's configured column list).
  // By default newness uses the ACTUAL mode (FT8 / FT4 / RTTY…): DIG is a
  // group, so FT4 after FT8 is genuinely a new mode. The operator can opt into
  // DXCC-style grouping instead (Settings → General), where all digital modes
  // count as ONE — then FT4 after FT8 is just "worked".
  const groupDigital = localStorage.getItem('opslog.groupDigitalSlots') === '1';
  const normMode = (m: string): string => {
    const u = (m || '').toUpperCase().trim();
    if (!groupDigital) return u;
    return u === '' || u === 'CW' || PHONE_MODES.has(u) ? u : 'DIG';
  };
  const bandModes = (wb?.dxcc_band_modes ?? []) as { band: string; mode: string }[];
  const curMode = normMode(currentMode);
  const bandWorked = bandModes.some((bm) => bm.band === currentBand);          // entity worked on this band (any mode)
  const modeWorked = !!curMode && bandModes.some((bm) => normMode(bm.mode) === curMode); // …in this (normalised) mode (any band)
  const slotWorked = !!curMode && bandModes.some((bm) => bm.band === currentBand && normMode(bm.mode) === curMode);
  // Mutually-exclusive badges, shown only when the entity is worked but this
  // exact band+mode is NOT yet:
  //   New Band & Mode = both the band AND the mode are new for this entity.
  //   New Band        = the band is new (the mode was worked on another band).
  //   New Mode        = the mode is new (the band was worked in another mode).
  //   New Slot        = both band and mode already worked — just not together.
  const slotNew = hasDxcc && !newOne && !!curMode && !slotWorked;
  const newBandMode = slotNew && !bandWorked && !modeWorked;
  const newBand = slotNew && !bandWorked && modeWorked;
  const newMode = slotNew && bandWorked && !modeWorked;
  const newSlot = slotNew && bandWorked && modeWorked;

  // ClubLog Most Wanted rank pill (shown next to the entity name when the feature
  // is on). Hotter colour the more wanted the entity is.
  const mwBadge = mwRank > 0 ? (
    <Badge
      title={`ClubLog Most Wanted #${mwRank}`}
      className={cn('px-1.5 py-0 text-[10px] font-bold shrink-0',
        mwRank <= 100 ? 'bg-danger text-danger-foreground'
          : mwRank <= 250 ? 'bg-warning text-warning-foreground'
            : 'bg-muted text-muted-foreground')}
    >
      MW #{mwRank}
    </Badge>
  ) : null;

  // Sunrise / sunset AT THE DX, in UTC. A glance tells you whether the path is
  // about to open or close on their side, which is the reason to look at a DX
  // station's grey line at all. Recomputed only when the position changes.
  const sun = useMemo(
    () => (lat == null || lon == null ? null : sunTimes(new Date(), lat, lon)),
    [lat, lon],
  );
  // Stacked, not side by side. Laid out in a row this cost about 150 px of a
  // header that has to hold the callsign, the badges and the band grid, and it
  // was what pushed the whole row onto a second line. Two short times one above
  // the other take a fraction of that and no extra height: the row is already
  // taller than one line of text.
  //
  // "UTC" moves into the tooltip with them — the times are monospaced and always
  // UTC everywhere in OpsLog, so the label was spending width to repeat a
  // convention the operator already lives by.
  const sunBlock = sun ? (
    <div className="ml-auto flex flex-col items-end leading-tight text-xs shrink-0"
      title="Sunrise / sunset at the DX station (UTC)">
      {sun.polarDay ? (
        <span className="font-semibold text-warning">midnight sun</span>
      ) : sun.polarNight ? (
        <span className="font-semibold text-info">polar night</span>
      ) : (
        <>
          <span className="flex items-center gap-1">
            <Sunrise className="size-3 text-warning" />
            <span className="font-mono tabular-nums">{sun.rise || '—'}</span>
          </span>
          <span className="flex items-center gap-1">
            <Sunset className="size-3 text-info" />
            <span className="font-mono tabular-nums">{sun.set || '—'}</span>
          </span>
        </>
      )}
    </div>
  ) : null;

  return (
    <section
      className={cn(
        'flex items-center gap-2 px-2 py-2 flex-wrap shrink-0',
        newOne && 'bg-gradient-to-br from-warning-muted to-warning-muted rounded-lg',
      )}
    >
      <div className="flex items-center gap-2 min-w-0 flex-1">
        {newOne ? (
          <>
            <Badge className="bg-warning text-warning-foreground gap-1 px-3 py-1 text-[11px]">
              <Star className="size-3 fill-current" />
              NEW ONE
            </Badge>
            <span className="text-xs text-warning-muted-foreground">
              <strong className="text-warning-muted-foreground font-semibold">{dxccName || `DXCC #${dxcc}`}</strong>
              {' '}· never worked this entity
            </span>
            {mwBadge}
          </>
        ) : hasDxcc ? (
          <>
            {/* Says WHOSE stats these are when they come from a row picked in
                the log rather than from what's being typed. */}
            {forCall && (
              <Badge variant="outline" className="px-2 py-0.5 text-[10px] font-mono shrink-0"
                title="Stats for the QSO selected in the log">
                {forCall}
              </Badge>
            )}
            <Badge className="bg-primary text-primary-foreground px-2 py-0.5 text-xs normal-case font-semibold tracking-normal leading-tight">
              {dxccName || `DXCC #${dxcc}`}
            </Badge>
            {mwBadge}
            <span className="text-xs text-muted-foreground">
              <strong className="text-foreground font-semibold">{dxccCount}</strong>{' '}
              QSO{dxccCount > 1 ? 's' : ''} with this entity
              {callCount > 0 && (
                <>
                  {' · '}
                  <strong className="text-foreground font-semibold">{callCount}</strong>{' '}
                  with this call
                </>
              )}
            </span>
            {(newBand || newMode || newBandMode || newSlot) && (
              <div className="flex flex-wrap items-center gap-1">
                {newBandMode && <Badge className="bg-warning text-warning-foreground px-1.5 py-0 text-[10px]">New Band &amp; Mode</Badge>}
                {newBand && <Badge className="bg-warning text-warning-foreground px-1.5 py-0 text-[10px]">New Band</Badge>}
                {newMode && <Badge className="bg-warning text-warning-foreground px-1.5 py-0 text-[10px]">New Mode</Badge>}
                {newSlot && <Badge className="bg-info text-info-foreground px-1.5 py-0 text-[10px]">New Slot</Badge>}
              </div>
            )}
          </>
        ) : busy ? (
          <span className="flex items-center gap-2 text-xs text-muted-foreground italic">
            <Radio className="size-3.5 animate-pulse" />
            looking up…
          </span>
        ) : (
          <span className="text-xs text-muted-foreground italic">
            Type a callsign to see entity stats
          </span>
        )}
        {sunBlock}
      </div>

      <div className="flex flex-col gap-2">
      <table className="border-separate" style={{ borderSpacing: 3 }}>
        <thead>
          <tr>
            <th className="w-[38px] min-w-[38px] max-w-[38px]" />
            {cols.map((b) => (
              <th
                key={b.tag}
                className={cn(
                  'font-mono text-[11px] font-semibold px-1 text-center',
                  b.tag === currentBand ? 'text-primary font-extrabold' : 'text-muted-foreground',
                )}
              >
                {b.label}
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {CLASSES.map((clsBase) => {
            const cls = clsBase === 'DIG' ? digRow : clsBase;
            // On a specific digital mode the "you are here" mark has to be that
            // mode, not any digital one — otherwise every FT4 entry lights the
            // FT8 row it happens to be cycled to.
            const classCurrent = cls === clsBase
              ? classMatchesMode(cls, currentMode)
              : (currentMode || '').toUpperCase() === cls;
            return (
              <tr key={cls}>
                <th
                  onClick={clsBase === 'DIG' && digModes.length ? cycleDig : undefined}
                  title={clsBase === 'DIG' && digModes.length ? t('bsg.digCycle') : undefined}
                  className={cn(
                    // Sized once for the LONGEST label the rotation can show,
                    // and pinned there: a column that grows when RTTY comes
                    // round shifts every band beneath it, and the eye reads
                    // that as the matrix moving rather than the row changing.
                    'font-mono font-semibold pr-1.5 text-right w-[38px] min-w-[38px] max-w-[38px] overflow-hidden',
                    // Beyond four characters (PSK31, MSK144) the type gives way
                    // instead of the column.
                    cls.length > 4 ? 'text-[9px]' : 'text-[11px]',
                    clsBase === 'DIG' && digModes.length ? 'cursor-pointer hover:text-foreground' : '',
                    classCurrent ? 'text-primary font-extrabold' : 'text-muted-foreground',
                  )}
                >
                  {cls}
                </th>
                {cols.map((b) => {
                  const st = statusMap.get(`${b.tag}|${cls}`) ?? '';
                  // The same cell's other answer: worked with THIS callsign
                  // here. The status above is the entity's, and a confirmed
                  // entity outranks a worked call — so chasing a DXpedition,
                  // the cell could say "confirmed" about a contact made years
                  // ago and nothing about the one made this morning.
                  const mine = callMap.get(`${b.tag}|${cls}`) ?? '';
                  const isCurrent = hasCall && b.tag === currentBand && classCurrent;
                  return (
                    <td
                      key={b.tag}
                      title={cellTitle(t, b.tag, cls, st, isCurrent, mine) + (st ? ' — ' + t('mx.tipClick') : '')}
                      onClick={st ? () => setSlot({ band: b.tag, cls }) : undefined}
                      className={cn(
                        'relative w-[28px] h-[24px] rounded transition-colors p-0',
                        st ? STATUS_CLASSES[st] : 'bg-mx-none',
                        // Only a filled cell has anything to show — an empty one
                        // stays inert rather than opening a "no QSOs" dialog.
                        st && 'cursor-pointer hover:brightness-110',
                        isCurrent && 'ring-2 ring-mx-cur ring-inset',
                      )}
                    >
                      {mine ? <CallMark state={mine} /> : null}
                    </td>
                  );
                })}
              </tr>
            );
          })}
        </tbody>
      </table>

      {/* Colour legend — sits in the spare room under the matrix. */}
      <div className="flex items-center gap-x-3 gap-y-1 flex-wrap pl-[26px]">
        {LEGEND.map((l) => (
          <span key={l.label} className="flex items-center gap-1.5 text-[10px] text-muted-foreground">
            <span
              className={cn(
                'relative inline-block size-3 rounded shrink-0',
                l.swatch,
                l.ring && 'ring-2 ring-mx-cur ring-inset',
              )}
            >
              {l.mark ? <CallMark state={l.mark} /> : null}
            </span>
            {t(l.label)}
          </span>
        ))}
      </div>
      </div>

      {slot && (
        <SlotQSOModal
          call={wb?.callsign ?? ''}
          dxcc={dxcc}
          entity={dxccName}
          band={slot.band}
          cls={slot.cls}
          onClose={() => setSlot(null)}
          onEdit={onEditQso}
        />
      )}
    </section>
  );
}

// ── Cell drill-down ──────────────────────────────────────────────────────────
// The contacts behind one band+class cell: this exact callsign AND anyone else
// in the entity, because that is the pair of facts the cell's colour encodes.
// The call's own QSOs are marked so the two never blur together.

function SlotQSOModal({ call, dxcc, entity, band, cls, onClose, onEdit }: {
  call: string; dxcc: number; entity: string; band: string; cls: string; onClose: () => void; onEdit?: (id: number) => void;
}) {
  const [rows, setRows] = useState<any[] | null>(null);
  const [err, setErr] = useState('');

  useEffect(() => {
    let dead = false;
    BandSlotQSOs(call, dxcc, band, cls)
      .then((r: any) => { if (!dead) setRows((r ?? []) as any[]); })
      .catch((e: any) => { if (!dead) { setErr(String(e?.message ?? e)); setRows([]); } });
    return () => { dead = true; };
  }, [call, dxcc, band, cls]);

  useEffect(() => {
    // Capture phase: the app's global ESC handler resets the entry form, and
    // closing a dialog must not also wipe what the operator was typing.
    function onKey(e: KeyboardEvent) {
      if (e.key === 'Escape') { e.stopImmediatePropagation(); e.preventDefault(); onClose(); }
    }
    window.addEventListener('keydown', onKey, true);
    return () => window.removeEventListener('keydown', onKey, true);
  }, [onClose]);

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-[2px] p-4" onClick={onClose}>
      <div className="bg-card border border-border rounded-xl shadow-2xl w-[760px] max-w-full max-h-[72vh] flex flex-col overflow-hidden ring-1 ring-black/5"
        onClick={(e) => e.stopPropagation()}>
        <div className="flex items-center gap-2.5 px-4 py-2.5 border-b border-border bg-muted/30">
          <span className="inline-flex items-center justify-center h-6 min-w-6 px-1.5 rounded-md bg-primary/15 text-primary text-[11px] font-bold shrink-0 tabular-nums">{band}</span>
          <div className="min-w-0">
            <div className="font-semibold text-sm leading-tight truncate">
              {cls}{entity && <span className="text-muted-foreground font-normal"> · {entity}</span>}
            </div>
            {onEdit && <div className="text-[10px] text-muted-foreground leading-tight">Click a callsign to edit the QSO</div>}
          </div>
          <span className="ml-auto text-xs text-muted-foreground tabular-nums shrink-0">{rows ? `${rows.length} QSO${rows.length > 1 ? 's' : ''}` : ''}</span>
          <button type="button" onClick={onClose} className="text-muted-foreground hover:text-foreground shrink-0 rounded p-0.5 hover:bg-muted transition-colors">
            <X className="size-4" />
          </button>
        </div>
        <div className="flex-1 overflow-auto">
          {err && <p className="p-3 text-xs text-destructive">{err}</p>}
          {!rows ? (
            <p className="p-4 text-xs text-muted-foreground flex items-center gap-2">
              <Loader2 className="size-3.5 animate-spin" /> Loading…
            </p>
          ) : rows.length === 0 ? (
            <p className="p-4 text-xs text-muted-foreground italic">No QSOs.</p>
          ) : (
            <table className="w-full text-xs border-collapse">
              <thead className="sticky top-0 z-10 bg-muted/80 backdrop-blur text-[10px] uppercase tracking-wider text-muted-foreground">
                <tr>
                  <th className="text-left font-medium px-3 py-1.5">Date UTC</th>
                  <th className="text-left font-medium px-3 py-1.5">Callsign</th>
                  <th className="text-left font-medium px-3 py-1.5">Mode</th>
                  <th className="text-left font-medium px-3 py-1.5">Freq</th>
                  <th className="text-left font-medium px-3 py-1.5">Name</th>
                  <th className="text-center font-medium px-3 py-1.5">Cfm</th>
                </tr>
              </thead>
              <tbody>
                {rows.map((q, i) => {
                  const cfm = isQSLConfirmed(q.lotw_rcvd) || isQSLConfirmed(q.eqsl_rcvd) || isQSLConfirmed(q.qsl_rcvd);
                  const mine = q.callsign === call;
                  return (
                    <tr key={q.id ?? i} className="border-t border-border/40 even:bg-muted/[0.06] hover:bg-primary/[0.06] transition-colors">
                      <td className="px-3 py-1.5 tabular-nums text-muted-foreground whitespace-nowrap">
                        {String(q.qso_date ?? '').slice(0, 16).replace('T', ' ')}
                      </td>
                      <td className="px-3 py-1.5">
                        {onEdit ? (
                          <button type="button" onClick={() => { onEdit(q.id as number); onClose(); }} title="Edit this QSO"
                            className={cn('font-mono cursor-pointer text-left hover:underline underline-offset-2', mine ? 'font-bold text-primary' : 'text-foreground hover:text-primary')}>
                            {q.callsign}
                          </button>
                        ) : (
                          <span className={cn('font-mono', mine && 'font-bold text-primary')}>{q.callsign}</span>
                        )}
                      </td>
                      <td className="px-3 py-1.5">{q.mode}</td>
                      <td className="px-3 py-1.5 tabular-nums text-muted-foreground">{q.freq_hz ? (q.freq_hz / 1e6).toFixed(3) : ''}</td>
                      <td className="px-3 py-1.5 text-muted-foreground truncate max-w-[180px]">{q.name}</td>
                      <td className="px-3 py-1.5 text-center">{cfm ? <span className="text-success">✓</span> : <span className="text-muted-foreground/30">·</span>}</td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          )}
        </div>
      </div>
    </div>
  );
}
