import { useEffect, useRef, useState } from 'react';
import { Radio, AudioLines, Mic, Activity, SlidersHorizontal, Antenna, Filter, Power, Volume2, VolumeX } from 'lucide-react';
import {
  GetIcomState, IcomRefresh,
  IcomSetAFGain, IcomSetRFGain, IcomSetNB, IcomSetNBLevel, IcomSetNR, IcomSetNRLevel,
  IcomSetANF, IcomSetAPF, IcomSetAGC, IcomSetPreamp, IcomSetAtt, IcomSetFilter,
  AudioMonitorActive, AudioStartMonitor, AudioStopMonitor,
  IcomSetRFPower, IcomSetMicGain, IcomSetSplit, IcomTune, IcomSetATU, IcomConsolePTT,
  GetCATState, SetCATFrequency, SetCATMode,
  IcomSetRIT, IcomSetRITOn, IcomSetXITOn,
  IcomSetAntenna, IcomSetPBTInner, IcomSetPBTOuter, IcomSetManualNotch, IcomSetNotchPos,
  IcomSetSquelch, IcomSetComp, IcomSetCompLevel, IcomSetMonitor, IcomSetMonLevel,
  IcomSetVOX, IcomSetVOXGain, IcomSetAntiVOX, IcomSetPower, IcomRecallBand,
} from '../../wailsjs/go/main/App';
import { cn } from '@/lib/utils';
import { useI18n } from '@/lib/i18n';
import { sMeterRST } from '@/lib/rst';
import { MeterBar } from '@/components/MeterBar';
import { ShiftRow } from '@/components/ShiftRow';

type IcomState = {
  available: boolean; model?: string; mode?: string;
  transmitting: boolean; split: boolean; sub_hz?: number; atu_on?: boolean;
  s_meter: number; power_meter: number; swr_meter: number;
  rf_power: number; mic_gain: number;
  af_gain: number; rf_gain: number;
  nb: boolean; nb_level: number; nr: boolean; nr_level: number; anf: boolean; apf: boolean;
  agc?: string; preamp: number; att: number; filter: number;
  rit_hz: number; rit_on: boolean; xit_on: boolean;
  antenna: number;
  pbt_inner: number; pbt_outer: number; manual_notch: boolean; notch_pos: number;
  squelch: number; comp: boolean; comp_level: number;
  monitor: boolean; mon_level: number;
  vox: boolean; vox_gain: number; anti_vox: number;
};

const ZERO: IcomState = {
  available: false, transmitting: false, split: false,
  s_meter: 0, power_meter: 0, swr_meter: 0, rf_power: 0, mic_gain: 0,
  af_gain: 0, rf_gain: 0,
  nb: false, nb_level: 0, nr: false, nr_level: 0, anf: false, apf: false,
  preamp: 0, att: 0, filter: 1,
  rit_hz: 0, rit_on: false, xit_on: false,
  antenna: 1,
  pbt_inner: 50, pbt_outer: 50, manual_notch: false, notch_pos: 50,
  squelch: 0, comp: false, comp_level: 0,
  monitor: false, mon_level: 0,
  vox: false, vox_gain: 0, anti_vox: 0,
};

// Band buttons jump the VFO to a sensible default frequency (SSB/CW mix) using
// the plain SetFrequency command — no band-stacking codes needed. Hz values.
type Band = { l: string; hz: number };

const HF_BANDS: Band[] = [
  { l: '160', hz: 1_840_000 }, { l: '80', hz: 3_750_000 },
  // 60 m: the middle of the IARU Region 1 allocation (5351.5-5366.5 kHz), which
  // every 60 m-capable rig can display. Where the band is channelised (the US)
  // the operator moves to their channel from here — the button is a way onto
  // the band, not a claim about what may be transmitted on it.
  { l: '60', hz: 5_354_000 },
  { l: '40', hz: 7_100_000 },
  { l: '30', hz: 10_130_000 }, { l: '20', hz: 14_150_000 }, { l: '17', hz: 18_130_000 },
  { l: '15', hz: 21_250_000 }, { l: '12', hz: 24_950_000 }, { l: '10', hz: 28_400_000 },
];
const B6 = { l: '6', hz: 50_150_000 };
const B2 = { l: '2', hz: 144_300_000 };       // SSB calling
const B70 = { l: '70cm', hz: 432_200_000 };   // SSB calling
const B23 = { l: '23cm', hz: 1_296_200_000 }; // SSB calling

// These frequencies are the FALLBACK: with the band stacking registers switched
// on the radio is asked where the operator last was instead, and one of these is
// only sent for a band or a model whose register cannot be read.
//
// Which buttons to OFFER depends on the radio, exactly as the attenuator steps
// do below. An IC-9700 has no HF at all, yet the console was showing it 160
// through 6 and none of the bands it actually covers: ten dead buttons, and no
// way to change band from here on the only rig where you would want to.
function bandsFor(model?: string): Band[] {
  const m = (model ?? '').toUpperCase();
  if (m.includes('9700')) return [B2, B70, B23];          // VHF/UHF/SHF only
  if (m.includes('705')) return [...HF_BANDS, B6, B2, B70];
  if (m.includes('9100')) return [...HF_BANDS, B6, B2, B70, B23];
  // 7300 / 7610 / 7100 / unknown: HF + 6 m, the historical list.
  return [...HF_BANDS, B6];
}

// Mode buttons for the console (like RS-BA1's row).
//
// LSB and USB by NAME, not one "SSB" button that resolves by band: the band
// convention is right for a logged mode and useless when the operator means
// "put this radio in USB on 40 m", which the console could not express at all.
//
// PSK is native only on the 7610/7760/7851 class; every other rig NAKs 0x12, so
// there the button is dead furniture — see modesFor. Soundcard PSK31 rides on
// DATA, which every rig can do.
const MODES_BASE = ['LSB', 'USB', 'CW', 'RTTY', 'AM', 'FM', 'DATA'];

function hasNativePSK(model?: string): boolean {
  const m = (model ?? '').toUpperCase();
  return m.includes('7610') || m.includes('7760') || m.includes('7851') ||
    m.includes('7800') || m.includes('7700');
}

function modesFor(model?: string): string[] {
  if (!hasNativePSK(model)) return MODES_BASE;
  return [...MODES_BASE.slice(0, 4), 'PSK', ...MODES_BASE.slice(4)];
}

// Which radios actually have an antenna selector on the CI-V command (0x12).
// An IC-7300 has ONE socket: offering it ANT1/ANT2 was two buttons that could
// only ever disagree with the front panel.
function hasAntennaSelector(model?: string): boolean {
  const m = (model ?? '').toUpperCase();
  return m.includes('7610') || m.includes('7760') || m.includes('7851') ||
    m.includes('7800') || m.includes('7700') || m.includes('9700');
}

// Attenuator steps are MODEL-dependent even though the CI-V command (0x11) is the
// same: the value byte is the dB. The IC-7610 (and 7700/7800/7851) have a 6/12/18
// dB stepped attenuator, and the IC-7760 the same (confirmed on a real one); the
// IC-7300/705/7100 have a single 20 dB attenuator; the
// IC-9700 a single 10 dB. Offering the wrong steps = a dead button (the rig NAKs
// e.g. 6 dB on a 7300). Default to the common single 20 dB for unknown models.
function attOptions(model?: string): { v: string; l: string }[] {
  const m = (model ?? '').toUpperCase();
  const OFF = { v: '0', l: 'OFF' };
  if (/(7610|7700|7760|7800|7850|7851)/.test(m)) {
    return [OFF, { v: '6', l: '6dB' }, { v: '12', l: '12dB' }, { v: '18', l: '18dB' }];
  }
  if (m.includes('9700')) return [OFF, { v: '10', l: '10dB' }];
  return [OFF, { v: '20', l: '20dB' }]; // IC-7300 / IC-705 / IC-7100 / default
}

// bandOfHz names the amateur band a frequency falls in, so the band row can show
// where the rig actually is. The buttons only ever SENT a frequency and carried
// no active state at all, so nothing was highlighted whatever the rig reported.
// Edges are the ITU/IARU band limits, wide enough to cover regional differences —
// out-of-band (transverter IF, general coverage RX) matches nothing, as it should.
function bandOfHz(hz?: number): string {
  if (!hz || hz <= 0) return '';
  const mhz = hz / 1_000_000;
  const bands: [string, number, number][] = [
    ['160', 1.8, 2.0], ['80', 3.5, 4.0], ['60', 5.25, 5.45], ['40', 7.0, 7.3],
    ['30', 10.1, 10.15], ['20', 14.0, 14.35], ['17', 18.068, 18.168],
    ['15', 21.0, 21.45], ['12', 24.89, 24.99], ['10', 28.0, 29.7],
    // Labels must match the band buttons' exactly — this is only used to light
    // the button for the band the rig is on, and '70' never matched '70cm'.
    ['6', 50.0, 54.0], ['4', 70.0, 70.5], ['2', 144.0, 148.0],
    ['70cm', 430.0, 450.0], ['23cm', 1240.0, 1300.0],
  ];
  for (const [name, lo, hi] of bands) if (mhz >= lo && mhz <= hi) return name;
  return '';
}

// fmtVFO renders a Hz frequency the way an Icom front panel does:
// MHz "." 3-digit-kHz "." 2-digit-(10 Hz). 21032000 → "21.032.00".
function fmtVFO(hz?: number): string {
  if (!hz || hz <= 0) return '––.–––.––';
  const mhz = Math.floor(hz / 1_000_000);
  const khz = Math.floor((hz % 1_000_000) / 1000);
  const h2 = Math.floor((hz % 1000) / 10);
  return `${mhz}.${String(khz).padStart(3, '0')}.${String(h2).padStart(2, '0')}`;
}

// modeMatches marks a mode button active, folding the rig's USB/LSB into SSB.
// icomWatts turns the backend's 0-100 meter percentage back into watts on the
// IC-7760's own meter face. The backend value is linear in the RAW meter byte
// (0-255 → 0-100), but Icom's calibration is not: raw 143 is half deflection
// and raw 213 is full scale. On a real 7760 a measured 100 W sits at half
// deflection of the 250 W face — the linear ×2.5 first tried showed 140 W for
// it. Below half scale watts run 0→100, above it 100→250.
// The anchors are MEASURED on the real radio, not derived: a known 50 W read
// raw ≈89 and a known 100 W read raw 143 (Icom's documented half-deflection),
// with raw 213 = full scale = 250 W. The face is not linear in watts at the
// bottom — a two-segment guess showed 50 W as 62 — so watts interpolate
// between the measured anchors, and a new measurement just adds a row.
// Third measured anchor (2026-08-30): a real 200 W read full deflection —
// raw 213 is 200 W on this rig, not the 250 the printed face suggests.
const ICOM_7760_PO: [number, number][] = [[0, 0], [89, 50], [143, 100], [213, 200]];
function icomWatts(pct: number): { w: number; defl: number } {
  const raw = Math.max(0, pct * 2.55);
  const defl = raw <= 143 ? (raw / 143) * 50 : Math.min(100, 50 + ((raw - 143) / 70) * 50);
  let w = 200;
  for (let i = 1; i < ICOM_7760_PO.length; i++) {
    const [r0, w0] = ICOM_7760_PO[i - 1], [r1, w1] = ICOM_7760_PO[i];
    if (raw <= r1) { w = w0 + ((raw - r0) / (r1 - r0)) * (w1 - w0); break; }
  }
  return { w: Math.round(w), defl };
}

// Which sideband a bare "SSB" means at this frequency — the same convention the
// backend applies when it resolves the mode for the radio.
function sideForHz(hz?: number): string | null {
  if (!hz || hz <= 0) return null;
  return hz < 10_000_000 ? 'LSB' : 'USB';
}

function modeMatches(btn: string, cur?: string, hz?: number): boolean {
  if (!cur) return false;
  // A rig that reports the folded ADIF "SSB" still lights the side its
  // frequency implies, so the row is never blank on a phone contact.
  if (btn === 'USB' || btn === 'LSB') {
    if (cur === btn) return true;
    return cur === 'SSB' && btn === (sideForHz(hz) ?? '');
  }
  // The backend surfaces USB-D as the operator's digital default (FT8…), or as
  // plain DATA — either way it is the DATA button that should light.
  if (btn === 'DATA') return ['DATA', 'FT8', 'FT4', 'JS8', 'JT65', 'JT9', 'MFSK', 'OLIVIA'].includes(cur);
  if (btn === 'PSK') return cur === 'PSK' || cur === 'PSK31';
  return btn === cur;
}

function Slider({ value, onChange, disabled, accent = '#2563eb', step = 1 }: {
  value: number; onChange: (v: number) => void; disabled?: boolean; accent?: string; step?: number;
}) {
  const v = Math.max(0, Math.min(100, value));
  const ref = useRef<HTMLInputElement>(null);
  // Mouse-wheel adjusts the slider. React's onWheel is passive (preventDefault
  // is ignored), so attach a non-passive native listener; read live values via
  // refs to avoid stale closures.
  const valRef = useRef(value); valRef.current = value;
  const cbRef = useRef(onChange); cbRef.current = onChange;
  const disRef = useRef(disabled); disRef.current = disabled;
  const stepRef = useRef(step); stepRef.current = step;
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const onWheel = (e: WheelEvent) => {
      if (disRef.current) return;
      e.preventDefault();
      const d = e.deltaY < 0 ? stepRef.current : -stepRef.current;
      const nv = Math.max(0, Math.min(100, valRef.current + d));
      if (nv !== valRef.current) cbRef.current(nv);
    };
    el.addEventListener('wheel', onWheel, { passive: false });
    return () => el.removeEventListener('wheel', onWheel);
  }, []);
  return (
    <input
      ref={ref}
      type="range" min={0} max={100} value={v} disabled={disabled}
      onChange={(e) => onChange(parseInt(e.target.value, 10))}
      className={cn('flex-1 h-1.5 rounded-full appearance-none cursor-pointer disabled:opacity-30 disabled:cursor-default',
        '[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:size-3.5 [&::-webkit-slider-thumb]:rounded-full',
        '[&::-webkit-slider-thumb]:bg-card [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:shadow-sm')}
      style={{ background: `linear-gradient(to right, ${accent} ${v}%, #d8cfb8 ${v}%)`, borderColor: accent }}
    />
  );
}

function Segmented({ value, options, onChange }: {
  value: string; options: { v: string; l: string }[]; onChange: (v: string) => void;
}) {
  return (
    <div className="inline-flex rounded-md border border-border overflow-hidden shrink-0">
      {options.map((o) => (
        <button key={o.v} type="button" onClick={() => onChange(o.v)}
          className={cn('px-2 py-1 text-[11px] font-bold tracking-wide transition-colors border-l border-border first:border-l-0',
            value === o.v ? 'bg-primary text-primary-foreground' : 'bg-card text-muted-foreground hover:bg-muted')}>
          {o.l}
        </button>
      ))}
    </div>
  );
}

function Chip({ on, onClick, label }: { on: boolean; onClick: () => void; label: string }) {
  return (
    <button type="button" onClick={onClick}
      className={cn('w-14 shrink-0 px-2 py-1 rounded-md text-[11px] font-bold border transition-colors',
        on ? 'bg-success border-success text-success-foreground' : 'bg-card text-muted-foreground border-border hover:bg-muted')}>
      {label}
    </button>
  );
}

function LevelRow({ label, on, onToggle, value, onLevel }: {
  label: string; on: boolean; onToggle: () => void; value: number; onLevel: (v: number) => void;
}) {
  return (
    <div className="flex items-center gap-2">
      <Chip on={on} onClick={onToggle} label={label} />
      <Slider value={value} disabled={!on} onChange={onLevel} />
      <span className="w-8 text-right text-xs font-mono tabular-nums text-muted-foreground">{value}</span>
    </div>
  );
}

function Card({ icon: Icon, title, accent, children }: { icon: any; title: string; accent?: string; children: React.ReactNode }) {
  return (
    <div className="rounded-xl border border-border bg-card shadow-sm overflow-hidden">
      <div className="flex items-center gap-2 px-3 py-2 border-b border-border/60 bg-muted/30">
        <Icon className="size-4" style={{ color: accent ?? 'var(--primary)' }} />
        <span className="text-xs font-bold uppercase tracking-wider text-foreground/80">{title}</span>
      </div>
      <div className="p-3 space-y-3">{children}</div>
    </div>
  );
}

function Row({ label, children }: { label: string; children: React.ReactNode }) {
  return (
    <div className="flex items-center gap-2">
      <span className="w-16 shrink-0 text-[11px] font-bold uppercase tracking-wider text-muted-foreground">{label}</span>
      {children}
    </div>
  );
}

// Meter — a thin horizontal bar for a live 0-100 reading (S / Po / SWR).
// Optional onClick makes the row a button (used to send the S reading to RST tx).
function Meter({ label, value, accent, scale, onClick, title }: { label: string; value: number; accent: string; scale?: string; onClick?: () => void; title?: string }) {
  const v = Math.max(0, Math.min(100, value));
  const body = (
    <>
      <span className="w-9 shrink-0 text-[11px] font-bold uppercase tracking-wider text-muted-foreground">{label}</span>
      <div className="flex-1 h-2.5 rounded-full bg-muted/60 overflow-hidden">
        <div className="h-full rounded-full transition-[width] duration-150" style={{ width: `${v}%`, background: accent }} />
      </div>
      <span className="w-14 shrink-0 whitespace-nowrap text-right text-[11px] font-mono tabular-nums text-muted-foreground">{scale ?? v}</span>
    </>
  );
  if (onClick) {
    return <button type="button" onClick={onClick} title={title} className="flex items-center gap-2 w-full rounded cursor-pointer hover:bg-muted/50 -mx-1 px-1 py-0.5">{body}</button>;
  }
  return <div className="flex items-center gap-2">{body}</div>;
}

// sParts turns the raw 0-100 S-meter into S-unit + dB-over-S9 (S9 ≈ 47% on the
// CI-V 0-255 scale, +60 dB near full scale). Used for both the display label and
// the RST-tx value on click.
// Green to S9, amber through +20, red above — the Elecraft console's scale.
function sSegColor(frac: number) {
  if (frac > 0.78) return '#dc2626';
  if (frac > 0.55) return '#f59e0b';
  return '#16a34a';
}

function sParts(v: number): { s: number; over: number; label: string } {
  if (v >= 47) {
    const over = Math.max(0, Math.round((v - 47) * 60 / 47));
    return { s: 9, over, label: over > 0 ? `S9+${over}` : 'S9' };
  }
  const s = Math.max(0, Math.min(9, Math.round(v / 5.2)));
  return { s, over: 0, label: `S${s}` };
}

// wfColor maps a 0-1 amplitude to a classic waterfall colour ramp
// (near-black → blue → cyan → green → amber → red).
const WF_STOPS: [number, [number, number, number]][] = [
  [0.0, [8, 12, 28]], [0.22, [26, 58, 138]], [0.42, [0, 150, 190]],
  [0.62, [46, 200, 120]], [0.80, [240, 210, 70]], [1.0, [244, 63, 60]],
];
function wfColor(v: number): [number, number, number] {
  v = Math.max(0, Math.min(1, Math.pow(v, 0.7))); // gamma-lift so the noise floor still has hue
  for (let i = 1; i < WF_STOPS.length; i++) {
    if (v <= WF_STOPS[i][0]) {
      const [a, ca] = WF_STOPS[i - 1], [b, cb] = WF_STOPS[i];
      const f = (v - a) / (b - a || 1);
      return [Math.round(ca[0] + (cb[0] - ca[0]) * f), Math.round(ca[1] + (cb[1] - ca[1]) * f), Math.round(ca[2] + (cb[2] - ca[2]) * f)];
    }
  }
  return WF_STOPS[WF_STOPS.length - 1][1];
}

// The spectrum scope is GONE, deliberately. Every Icom streams its waveform
// differently — the IC-7851 controls a scope it never streams, and a real
// IC-7760 stops answering CI-V altogether a few frames in, taking CAT and
// audio down with it — and chasing a per-model frame layout for a decoration
// is not worth a console that drops the link. The radio has a better scope
// on its own front panel.

export function IcomPanel({ onReportRST, isNetwork = false }: { onReportRST?: (rst: string) => void; isNetwork?: boolean } = {}) {
  // The speaker toggle lives HERE, next to ON/OFF, because that is where the
  // operator is looking — burying "stop listening" behind Settings → Audio
  // meant a trip through two panels to mute a radio sitting in the same room.
  const [listening, setListening] = useState(false);
  useEffect(() => {
    if (!isNetwork) return;
    let alive = true;
    const ask = () => AudioMonitorActive().then((v) => { if (alive) setListening(!!v); }).catch(() => {});
    ask();
    const id = window.setInterval(ask, 2000);
    return () => { alive = false; window.clearInterval(id); };
  }, [isNetwork]);
  const toggleListening = () => {
    const next = !listening;
    setListening(next);
    (next ? AudioStartMonitor() : Promise.resolve(AudioStopMonitor())).catch(() => setListening(!next));
  };
  const { t } = useI18n();
  const [st, setSt] = useState<IcomState>(ZERO);
  const [cat, setCat] = useState<any>(null); // RigState (freq/mode/split) for the VFO display
  const [tuning, setTuning] = useState(false);
  // Band buttons: recall the radio's own band stacking register instead of
  // sending a frequency picked here. Remembered per operator, not per session —
  // it is a preference about how a button behaves, and having to set it again
  // at every launch would make it not worth having.
  const [bandStack, setBandStack] = useState(() => localStorage.getItem('opslog.icomBandStack') === '1');
  const toggleBandStack = () => setBandStack((v) => {
    const n = !v;
    try { localStorage.setItem('opslog.icomBandStack', n ? '1' : '0'); } catch { /* private mode */ }
    return n;
  });
  // Which register each band was last recalled from, so pressing the same band
  // again walks 1 → 2 → 3 → 1, exactly as the radio's own band key does.
  const bandRegRef = useRef<Record<string, number>>({});
  const txRef = useRef(false);
  const stRef = useRef<IcomState>(ZERO); stRef.current = st;

  const load = () => {
    GetIcomState().then((s) => setSt((s ?? ZERO) as IcomState)).catch(() => {});
    GetCATState().then((c) => setCat(c ?? null)).catch(() => {});
  };
  const setMode = (m: string) => { setCat((c: any) => (c ? { ...c, mode: m } : c)); SetCATMode(m).catch(() => {}); };

  // A band button. With the stacking registers on, ask the radio where the
  // operator last was on that band; pressing the band it is already on steps to
  // the next register, and the fixed frequency below is the fallback for a band
  // or a model whose register the backend will not read — never a dead button.
  const bandClick = (b: Band, here: boolean) => {
    if (!bandStack) { SetCATFrequency(b.hz).catch(() => {}); return; }
    const reg = here ? (bandRegRef.current[b.l] ?? 1) % 3 + 1 : 1;
    IcomRecallBand(b.l, reg)
      .then(() => { bandRegRef.current[b.l] = reg; load(); })
      .catch(() => SetCATFrequency(b.hz).catch(() => {}));
  };
  // Initial one-shot read of the rig's DSP snapshot on mount (the 500ms poll only
  // re-reads the cache; the backend also loads DSP on the first responsive read).
  const refresh = async () => {
    try { await IcomRefresh(); } catch {}
    await load();
  };

  useEffect(() => {
    refresh();
    const id = window.setInterval(load, 500); // fast poll so meters/TX feel live
    return () => window.clearInterval(id);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // Optimistic local update + fire the command; the cache poll reconciles.
  const set = (patch: Partial<IcomState>, fn: () => Promise<void>) => {
    setSt((s) => ({ ...s, ...patch }));
    fn().catch(() => {});
  };

  const toggleMox = () => {
    const next = !txRef.current;
    txRef.current = next;
    // Through the console binding: on a network station the PC microphone
    // rides with the PTT (silence otherwise); on USB it keys and nothing more.
    set({ transmitting: next }, () => IcomConsolePTT(next));
  };

  const tune = async () => {
    setTuning(true);
    try { await IcomTune(); } catch {}
    window.setTimeout(() => setTuning(false), 4000);
  };

  // RIT/ΔTX offset (signed Hz, clamped ±9999). Optimistic like the DSP controls.
  const setRit = (hz: number) => {
    const v = Math.max(-9999, Math.min(9999, hz));
    set({ rit_hz: v }, () => IcomSetRIT(v));
  };

  // Ctrl+Left/Right shifts the RIT by ±10 Hz while RIT is active — a keyboard
  // clarifier for zero-beating a caller without touching the mouse.
  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      if (!e.ctrlKey || (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight')) return;
      const s = stRef.current;
      if (!s.available || !s.rit_on) return;
      e.preventDefault();
      setRit(s.rit_hz + (e.key === 'ArrowRight' ? 10 : -10));
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  if (!st.available) {
    return (
      <div className="h-full flex items-center justify-center text-sm text-muted-foreground p-6 text-center">
        {t('icmp.notConnected')}
      </div>
    );
  }

  const tx = st.transmitting;
  // VFO readout. In split the active/listening VFO is RX (freq_rx_hz) and the
  // other is TX (freq_hz); otherwise there's a single VFO (freq_hz).
  const split = !!cat?.split;
  const mainHz: number = split ? (cat?.freq_rx_hz || 0) : (cat?.freq_hz || 0);
  // The sub receiver's dial is worth seeing whether or not split is on —
  // in split the CAT state's TX freq is the authority, otherwise the panel's
  // own sub_hz read.
  const subHz: number = split ? (cat?.freq_hz || 0) : (st.sub_hz || 0);
  // The panel's own mode first: it carries the sideband (USB/LSB) where the
  // CAT state folds both into ADIF's SSB.
  const curMode: string = st.mode || cat?.mode || '';
  // Mode-dependent controls: VOX / speech-comp / mic are voice-only (hidden on
  // CW and data); APF (audio peak filter) is CW-only. Fold USB/LSB into phone.
  const um = curMode.toUpperCase();
  const isCW = um === 'CW' || um === 'CWR';
  const isPhone = um === 'SSB' || um === 'USB' || um === 'LSB' || um === 'AM' || um === 'FM';

  return (
    <div className="h-full min-h-0 overflow-auto bg-background">
      <div className="max-w-5xl mx-auto p-3 space-y-3">
      {/* Header strip: model + mode + live RX/TX indicator + split badge. */}
      <div className="flex items-center justify-between rounded-xl border border-border bg-card px-3 py-2 shadow-sm">
        <div className="flex items-center gap-2">
          <span className={cn('inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-[11px] font-bold uppercase tracking-wider',
            tx ? 'bg-destructive text-destructive-foreground' : 'bg-success text-success-foreground')}>
            <span className={cn('size-2 rounded-full', tx ? 'bg-card animate-pulse' : 'bg-card/90')} />
            {tx ? 'TX' : 'RX'}
          </span>
          <span className="text-sm font-bold">{st.model || 'Icom'}</span>
          {st.mode ? <span className="text-xs font-mono text-muted-foreground">{st.mode}</span> : null}
          {st.split ? <span className="rounded-md bg-warning/20 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider text-warning">Split</span> : null}
        </div>
        <div className="flex items-center gap-1.5">
          {/* Radio power ON / OFF — NETWORK only. Over USB the CI-V interface is
              unpowered while the rig is off, so power-ON can't reach it (OFF works
              but ON doesn't); hiding both avoids a dead button. On the network the
              rig's LAN server stays alive in standby, so both work. */}
          {isNetwork && (
            <>
              <button type="button" onClick={toggleListening}
                title={listening ? t('icmp.speakerOffHint') : t('icmp.speakerOnHint')}
                className={cn('inline-flex items-center gap-1 rounded-md border px-2 py-1 text-xs font-bold',
                  listening
                    ? 'border-primary/60 bg-primary/10 text-primary hover:bg-primary/20'
                    : 'border-border bg-card text-muted-foreground hover:bg-muted')}>
                {listening ? <Volume2 className="size-3.5" /> : <VolumeX className="size-3.5" />}
              </button>
              <button type="button" onClick={() => IcomSetPower(true).catch(() => {})} title={t('icmp.powerOnHint')}
                className="inline-flex items-center gap-1 rounded-md border border-success/60 bg-success/10 px-2 py-1 text-xs font-bold text-success hover:bg-success/20">
                <Power className="size-3.5" /> ON
              </button>
              <button type="button" onClick={() => IcomSetPower(false).catch(() => {})} title={t('icmp.powerOffHint')}
                className="inline-flex items-center gap-1 rounded-md border border-destructive/60 bg-destructive/10 px-2 py-1 text-xs font-bold text-destructive hover:bg-destructive/20">
                <Power className="size-3.5" /> OFF
              </button>
            </>
          )}
        </div>
      </div>

      {/* VFO readout — the RS-BA1-style twin display: MAIN (active) + SUB, the big
          tabular frequency, mode badge, band, and the RIT/ΔTX offset. */}
      <div className="rounded-xl border border-border bg-muted/25 shadow-inner overflow-hidden">
        <div className="grid grid-cols-2 divide-x divide-border/60">
          {/* MAIN VFO */}
          <div className="px-4 py-3">
            <div className="flex items-center justify-between mb-1.5">
              <span className={cn('text-[10px] font-bold uppercase tracking-widest', tx ? 'text-destructive' : 'text-success')}>{tx ? 'Main · TX' : 'Main'}</span>
              {curMode ? <span className="rounded px-1.5 py-0.5 text-[10px] font-bold bg-primary/15 text-primary">{curMode}</span> : null}
            </div>
            <div className="font-mono font-bold tabular-nums leading-none text-foreground" style={{ fontSize: 'clamp(1.5rem, 4.5vw, 2.25rem)' }}>{fmtVFO(mainHz)}</div>
            <div className="mt-1.5 flex items-center gap-2 text-[11px] font-mono text-muted-foreground">
              <span>{cat?.band || (mainHz ? '' : '—')}</span>
              {st.rit_on ? <span className="text-primary">RIT {st.rit_hz > 0 ? '+' : st.rit_hz < 0 ? '−' : ''}{Math.abs(st.rit_hz)}</span> : null}
              {st.xit_on ? <span className="text-warning">ΔTX</span> : null}
            </div>
          </div>
          {/* SUB VFO (populated in split; dimmed otherwise) */}
          <div className={cn('px-4 py-3', !split && 'opacity-40')}>
            <div className="flex items-center justify-between mb-1.5">
              <span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">Sub</span>
              {split ? <span className="rounded px-1.5 py-0.5 text-[10px] font-bold bg-warning/15 text-warning">SPLIT</span> : null}
            </div>
            <div className="font-mono font-bold tabular-nums leading-none text-muted-foreground" style={{ fontSize: 'clamp(1.5rem, 4.5vw, 2.25rem)' }}>{fmtVFO(subHz)}</div>
            <div className="mt-1.5 text-[11px] font-mono text-muted-foreground">{split ? 'TX' : ''}</div>
          </div>
        </div>
        {/* Mode selector row (RS-BA1's SSB/CW/RTTY/PSK/AM/FM). */}
        <div className="grid border-t border-border/60 divide-x divide-border/60"
          style={{ gridTemplateColumns: `repeat(${modesFor(st.model).length}, minmax(0, 1fr))` }}>
          {modesFor(st.model).map((m) => {
            const on = modeMatches(m, curMode, mainHz);
            return (
              <button key={m} type="button" onClick={() => setMode(m)}
                className={cn('py-1.5 text-[11px] font-bold tracking-wide transition-colors',
                  on ? 'bg-primary text-primary-foreground' : 'bg-card/40 text-muted-foreground hover:bg-muted')}>
                {m}
              </button>
            );
          })}
        </div>
      </div>

      {/* Live meters — the SAME LED MeterBar every other console uses (Flex,
          Elecraft, the amp cards): one instrument look across the app, per the
          operator's "les consoles doivent se ressembler". S is clickable → RST. */}
      <div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
        {(() => { const sp = sParts(st.s_meter); return (
          <MeterBar label="S-METER" value={st.transmitting ? 0 : st.s_meter} lo={0} hi={100}
            accent="#16a34a" segColor={sSegColor}
            display={st.transmitting ? '—' : sp.label}
            title={onReportRST ? t('rst.clickToFill') : undefined}
            onClick={onReportRST ? () => { if (!st.transmitting) onReportRST(sMeterRST(sp.s, sp.over, st.mode)); } : undefined} />
        ); })()}
        {(() => {
          if ((st.model ?? '').includes('7760')) {
            const { w, defl } = icomWatts(st.power_meter);
            return <MeterBar label="PWR" value={defl} lo={0} hi={100} accent="#0ea5e9" display={`${w} W`} />;
          }
          return <MeterBar label="PWR" value={st.power_meter} lo={0} hi={100} accent="#0ea5e9" display={`${st.power_meter} W`} />;
        })()}
        <MeterBar label="SWR" value={st.swr_meter > 0 ? 1 + st.swr_meter / 33.3 : 0} lo={1} hi={4} accent="#f59e0b"
          display={st.swr_meter > 0 ? (1 + st.swr_meter / 33.3).toFixed(1) : '—'} />
      </div>

      <div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
      {/* Band buttons + antenna selection. */}
      <Card icon={Antenna} title={t('icmp.bandsAntenna')} accent="#0891b2">
        <label className="flex items-center gap-1.5 mb-1.5 text-[11px] text-muted-foreground cursor-pointer select-none"
          title={t('icmp.bandStackHint')}>
          <input type="checkbox" checked={bandStack} onChange={toggleBandStack} className="accent-primary" />
          {t('icmp.bandStack')}
        </label>
        <div className="grid grid-cols-5 gap-1.5">
          {bandsFor(st.model).map((b) => {
            const here = bandOfHz(mainHz) === b.l;
            return (
              <button key={b.l} type="button" onClick={() => bandClick(b, here)}
                title={here ? t('icmp.bandCurrent', { b: b.l }) : undefined}
                className={cn('px-1 py-1.5 rounded-md text-[11px] font-bold border transition-colors',
                  here
                    ? 'border-primary bg-primary text-primary-foreground shadow-[0_0_8px] shadow-primary/40'
                    : 'border-border bg-card text-foreground hover:bg-muted')}>
                {b.l}
              </button>
            );
          })}
        </div>
        {hasAntennaSelector(st.model) && (
          <Row label={t('icmp.antenna')}>
            <Segmented value={String(st.antenna)} options={[{ v: '1', l: 'ANT1' }, { v: '2', l: 'ANT2' }]}
              onChange={(v) => set({ antenna: parseInt(v) }, () => IcomSetAntenna(parseInt(v)))} />
          </Row>
        )}
      </Card>

      {/* Clarifiers: RIT & ΔTX (XIT) — wheel or ± to shift, Ctrl+←/→ shifts RIT. */}
      <Card icon={SlidersHorizontal} title={t('icmp.clarifiers')} accent="#8b5cf6">
        <ShiftRow label="RIT" accent="#8b5cf6" on={st.rit_on} hz={st.rit_hz}
          onToggle={() => set({ rit_on: !st.rit_on }, () => IcomSetRITOn(!st.rit_on))}
          onSet={setRit} />
        <ShiftRow label="ΔTX" accent="#f59e0b" on={st.xit_on} hz={st.rit_hz}
          onToggle={() => set({ xit_on: !st.xit_on }, () => IcomSetXITOn(!st.xit_on))}
          onSet={setRit} />
        <p className="text-[11px] text-muted-foreground">{t('icmp.ritHint')}</p>
      </Card>

      {/* Transmit controls. */}
      <Card icon={Mic} title={t('icmp.transmit')} accent="#ef4444">
        <Row label={t('icmp.power')}>
          <Slider value={st.rf_power} accent="#ef4444" onChange={(v) => set({ rf_power: v }, () => IcomSetRFPower(v))} />
          {/* PC is a percentage of the rig's rated power; on a 200 W rig the
              operator thinks in watts, so say it in watts there. */}
          <span className="w-12 text-right text-xs font-mono tabular-nums text-muted-foreground">
            {(st.model ?? '').includes('7760') ? `${st.rf_power * 2} W` : st.rf_power}
          </span>
        </Row>
        {/* Not phone-only: on USB-D the same control still sets what the radio
            transmits at, and hiding it left an operator who lives in FT8 with
            no mic gain at all. */}
        {(
          <Row label={t('icmp.mic')}>
            <Slider value={st.mic_gain} accent="#ef4444" onChange={(v) => set({ mic_gain: v }, () => IcomSetMicGain(v))} />
            <span className="w-8 text-right text-xs font-mono tabular-nums text-muted-foreground">{st.mic_gain}</span>
          </Row>
        )}
        <div className="flex items-center gap-2 pt-1">
          <button type="button" onClick={toggleMox}
            className={cn('flex-1 px-3 py-1.5 rounded-md text-xs font-bold border transition-colors',
              tx ? 'bg-destructive border-destructive text-destructive-foreground' : 'bg-card text-foreground border-border hover:bg-muted')}>
            {tx ? 'TX ON' : 'MOX'}
          </button>
          <Chip label="SPLIT" on={st.split} onClick={() => set({ split: !st.split }, () => IcomSetSplit(!st.split))} />
          {/* Tuner IN/OUT — TUNE below starts a cycle but could never take the
              tuner back out of line. */}
          <Chip label="ATU" on={!!st.atu_on} onClick={() => set({ atu_on: !st.atu_on } as any, () => IcomSetATU(!st.atu_on))} />
          <button type="button" onClick={tune} disabled={tuning}
            className={cn('w-14 shrink-0 px-2 py-1.5 rounded-md text-[11px] font-bold border transition-colors',
              tuning ? 'bg-warning border-warning text-warning-foreground animate-pulse' : 'bg-card text-foreground border-border hover:bg-muted')}>
            TUNE
          </button>
        </div>
        {/* Monitor (all modes) + speech processor / VOX (voice modes only —
            they don't exist on CW or data). */}
        <div className="pt-2 mt-1 border-t border-border/60 space-y-3">
          <LevelRow label="MON" on={st.monitor} value={st.mon_level}
            onToggle={() => set({ monitor: !st.monitor }, () => IcomSetMonitor(!st.monitor))}
            onLevel={(v) => set({ mon_level: v }, () => IcomSetMonLevel(v))} />
          {isPhone && (
            <>
              <LevelRow label="COMP" on={st.comp} value={st.comp_level}
                onToggle={() => set({ comp: !st.comp }, () => IcomSetComp(!st.comp))}
                onLevel={(v) => set({ comp_level: v }, () => IcomSetCompLevel(v))} />
              <LevelRow label="VOX" on={st.vox} value={st.vox_gain}
                onToggle={() => set({ vox: !st.vox }, () => IcomSetVOX(!st.vox))}
                onLevel={(v) => set({ vox_gain: v }, () => IcomSetVOXGain(v))} />
              <Row label="Anti-VOX">
                <Slider value={st.anti_vox} disabled={!st.vox} onChange={(v) => set({ anti_vox: v }, () => IcomSetAntiVOX(v))} />
                <span className="w-8 text-right text-xs font-mono tabular-nums text-muted-foreground">{st.anti_vox}</span>
              </Row>
            </>
          )}
        </div>
      </Card>

      <Card icon={Radio} title={t('icmp.receive')} accent="#2563eb">
        <Row label="AF">
          <Slider value={st.af_gain} onChange={(v) => set({ af_gain: v }, () => IcomSetAFGain(v))} />
          <span className="w-8 text-right text-xs font-mono tabular-nums text-muted-foreground">{st.af_gain}</span>
        </Row>
        <Row label="RF">
          <Slider value={st.rf_gain} onChange={(v) => set({ rf_gain: v }, () => IcomSetRFGain(v))} />
          <span className="w-8 text-right text-xs font-mono tabular-nums text-muted-foreground">{st.rf_gain}</span>
        </Row>
        <Row label={t('icmp.squelch')}>
          <Slider value={st.squelch} onChange={(v) => set({ squelch: v }, () => IcomSetSquelch(v))} />
          <span className="w-8 text-right text-xs font-mono tabular-nums text-muted-foreground">{st.squelch}</span>
        </Row>
        <Row label="AGC">
          <Segmented value={st.agc || ''} options={[{ v: 'FAST', l: 'FAST' }, { v: 'MID', l: 'MID' }, { v: 'SLOW', l: 'SLOW' }]}
            onChange={(v) => set({ agc: v }, () => IcomSetAGC(v))} />
        </Row>
        <Row label={t('icmp.preamp')}>
          <Segmented value={String(st.preamp)} options={[{ v: '0', l: 'OFF' }, { v: '1', l: 'P1' }, { v: '2', l: 'P2' }]}
            onChange={(v) => set({ preamp: parseInt(v) }, () => IcomSetPreamp(parseInt(v)))} />
        </Row>
        <Row label="Att">
          <Segmented value={String(st.att)} options={attOptions(cat?.rig)}
            onChange={(v) => set({ att: parseInt(v) }, () => IcomSetAtt(parseInt(v)))} />
        </Row>
        <Row label={t('icmp.filter')}>
          <Segmented value={String(st.filter)} options={[{ v: '1', l: 'FIL1' }, { v: '2', l: 'FIL2' }, { v: '3', l: 'FIL3' }]}
            onChange={(v) => set({ filter: parseInt(v) }, () => IcomSetFilter(parseInt(v)))} />
        </Row>
      </Card>

      {/* Twin PBT + manual notch. Sliders are 0-100 with 50 = centre. */}
      <Card icon={Filter} title={t('icmp.passband')} accent="#7c3aed">
        <Row label="PBT-IN">
          <Slider value={st.pbt_inner} accent="#7c3aed" onChange={(v) => set({ pbt_inner: v }, () => IcomSetPBTInner(v))} />
          <span className="w-8 text-right text-xs font-mono tabular-nums text-muted-foreground">{st.pbt_inner - 50 > 0 ? '+' : ''}{st.pbt_inner - 50}</span>
        </Row>
        <Row label="PBT-OUT">
          <Slider value={st.pbt_outer} accent="#7c3aed" onChange={(v) => set({ pbt_outer: v }, () => IcomSetPBTOuter(v))} />
          <span className="w-8 text-right text-xs font-mono tabular-nums text-muted-foreground">{st.pbt_outer - 50 > 0 ? '+' : ''}{st.pbt_outer - 50}</span>
        </Row>
        <button type="button"
          onClick={() => { set({ pbt_inner: 50 }, () => IcomSetPBTInner(50)); set({ pbt_outer: 50 }, () => IcomSetPBTOuter(50)); }}
          className="w-full py-1 rounded-md text-[11px] font-bold border border-border bg-card text-muted-foreground hover:bg-muted">
          {t('icmp.pbtCenter')}
        </button>
        <div className="pt-1 border-t border-border/60 space-y-3">
          <div className="flex items-center gap-2">
            <Chip label="MN" on={st.manual_notch} onClick={() => set({ manual_notch: !st.manual_notch }, () => IcomSetManualNotch(!st.manual_notch))} />
            <Slider value={st.notch_pos} disabled={!st.manual_notch} accent="#7c3aed" onChange={(v) => set({ notch_pos: v }, () => IcomSetNotchPos(v))} />
            <span className="w-8 text-right text-xs font-mono tabular-nums text-muted-foreground">{st.notch_pos}</span>
          </div>
          <p className="text-[11px] text-muted-foreground">{t('icmp.manualNotch')}</p>
        </div>
      </Card>

      <Card icon={AudioLines} title={t('icmp.noiseNotch')} accent="#16a34a">
        <LevelRow label="NB" on={st.nb} value={st.nb_level}
          onToggle={() => set({ nb: !st.nb }, () => IcomSetNB(!st.nb))}
          onLevel={(v) => set({ nb_level: v }, () => IcomSetNBLevel(v))} />
        <LevelRow label="NR" on={st.nr} value={st.nr_level}
          onToggle={() => set({ nr: !st.nr }, () => IcomSetNR(!st.nr))}
          onLevel={(v) => set({ nr_level: v }, () => IcomSetNRLevel(v))} />
        <div className="flex items-center gap-2">
          <Chip label="ANF" on={st.anf} onClick={() => set({ anf: !st.anf }, () => IcomSetANF(!st.anf))} />
          <span className="text-xs text-muted-foreground">{t('icmp.autoNotch')}</span>
        </div>
        {/* APF (audio peak filter) — CW only: peaks the CW tone. */}
        {isCW && (
          <div className="flex items-center gap-2">
            <Chip label="APF" on={st.apf} onClick={() => set({ apf: !st.apf }, () => IcomSetAPF(!st.apf))} />
            <span className="text-xs text-muted-foreground">{t('icmp.apf')}</span>
          </div>
        )}
      </Card>
      </div>
      </div>
    </div>
  );
}
