import { useEffect, useRef, useState } from 'react';
import { Radio, Activity, AudioLines, SlidersHorizontal, Mic } from 'lucide-react';
import {
  GetTCIPanel, GetCATState,
  SetTCIDrive, SetTCITuneDrive, SetTCIMicLevel, SetTCIVolume, SetTCIMute,
  SetTCIAGC, SetTCISquelch, SetTCISquelchLevel,
  SetTCINB, SetTCINR, SetTCIANF, SetTCIAPF, SetTCIFilter,
  SetTCIRIT, SetTCIXIT, SetTCIRITOffset, SetTCIXITOffset, SetTCILock, SetTCITune,
} 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 { WheelRange } from '@/components/WheelRange';
import { ShiftRow } from '@/components/ShiftRow';
import { LevelRow } from '@/components/LevelRow';

type TCIState = {
  connected: boolean; device?: string; protocol?: string;
  drive: number; tune_drive: number; mic_level: number; tx_enabled: boolean; tx: boolean; tuning: boolean;
  volume: number; mute: boolean; agc?: string; squelch_on: boolean; squelch: number;
  nb: boolean; nr: boolean; anf: boolean; apf: boolean;
  filter_lo: number; filter_hi: number;
  rit: boolean; rit_offset: number; xit: boolean; xit_offset: number; lock: boolean; split: boolean;
  smeter: number; modulations?: string[];
  tx_power_w: number; tx_swr: number;
};

const ZERO: TCIState = {
  connected: false, drive: 0, tune_drive: 0, mic_level: 0, tx_enabled: false, tx: false, tuning: false,
  volume: 0, mute: false, squelch_on: false, squelch: 0,
  nb: false, nr: false, anf: false, apf: false, filter_lo: 0, filter_hi: 0,
  rit: false, rit_offset: 0, xit: false, xit_offset: 0, lock: false, split: false, smeter: 0,
  tx_power_w: 0, tx_swr: 0,
};

// The widths worth a button, PER MODE — because 250 Hz is useless in SSB and
// 2.8 kHz is useless in CW, and a row offering both is a row where half the
// buttons are never pressed.
//
// CW gets the narrow end, where the difference between 250 and 500 is the
// difference between one signal and three. Voice gets the range a passband is
// actually shaped over. Digital sits between: wide enough for a whole FT8
// sub-band, narrow enough for RTTY.
const WIDTHS_CW = [100, 250, 400, 500, 700, 1000, 1800];
const WIDTHS_SSB = [1800, 2100, 2400, 2700, 2800, 3000, 3500];
const WIDTHS_DIGI = [500, 1000, 1800, 2400, 2800, 3000, 3500];

// widthsFor picks the row from the mode the radio reports.
function widthsFor(mode: string): number[] {
  if (/CW/i.test(mode)) return WIDTHS_CW;
  if (/SSB|USB|LSB|AM|FM/i.test(mode)) return WIDTHS_SSB;
  return WIDTHS_DIGI;
}

// isCW says whether the CW-only controls belong on screen at all.
function isCW(mode: string): boolean { return /CW/i.test(mode); }

function widthLabel(w: number): string {
  return w >= 1000 ? `${(w / 1000).toFixed(1)}k` : String(w);
}

// edgesFor turns a width into the pair TCI wants: 0 to the width, and nothing
// clever.
//
// It centred narrow filters on the CW note first — 250 became 575-825 — on the
// reasoning that a CW filter should contain the note. That reasoning may even be
// right for a radio, but it is not what the button says, and a button that does
// not do what it says is worse than one that does something simple. 250 means
// 0-250. The two edges are editable underneath for anything else.
function edgesFor(w: number, _mode: string): { lo: number; hi: number } {
  return { lo: 0, hi: w };
}

// dBm → S units. TCI reports a real signal level rather than a meter position,
// which is the useful way round: S9 is -73 dBm by the IARU definition and every
// S unit below it is 6 dB, so this is arithmetic rather than a calibration
// table — nothing here is provisional the way the K3's meter reading is.
function sParts(dbm: number): { s: number; over: number; label: string } {
  if (dbm === 0) return { s: 0, over: 0, label: '—' };
  if (dbm >= -73) {
    const over = Math.round((dbm + 73) / 10) * 10;
    return { s: 9, over, label: over > 0 ? `S9+${over}` : 'S9' };
  }
  const s = Math.max(0, Math.min(9, Math.round(9 + (dbm + 73) / 6)));
  return { s, over: 0, label: `S${s}` };
}

// The meter bar wants 0-100; -127 dBm is the bottom of the scale and -13 dBm
// (S9+60) the top.
function sBar(dbm: number): number {
  if (dbm === 0) return 0;
  return Math.max(0, Math.min(100, ((dbm + 127) / 114) * 100));
}

function sSegColor(frac: number): string {
  return frac > 0.75 ? '#dc2626' : frac > 0.55 ? '#f59e0b' : '#16a34a';
}

function Card({ icon: Icon, title, children }: { icon: any; title: 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 text-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>
  );
}

// One button shape for every on/off control, as on the other consoles.
function Toggle({ label, on, off, onClick, title }: {
  label: string; on: boolean; off: boolean; onClick: () => void; title?: string;
}) {
  return (
    <button type="button" disabled={off} onClick={onClick} title={title}
      className={cn('rounded-lg border-2 px-2 py-1.5 text-xs font-bold transition-all disabled:opacity-30',
        on ? 'bg-primary text-primary-foreground border-primary shadow-[0_0_10px] shadow-primary/40'
          : 'bg-card text-muted-foreground border-border hover:bg-muted')}>
      {label}
    </button>
  );
}

function Row({ label, value, children }: { label: string; value: string; children: React.ReactNode }) {
  return (
    <div className="space-y-1">
      <div className="flex items-baseline justify-between">
        <span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">{label}</span>
        <span className="text-xs font-mono tabular-nums">{value}</span>
      </div>
      {children}
    </div>
  );
}

export function TCIPanel({ onReportRST }: { onReportRST?: (rst: string) => void } = {}) {
  const { t } = useI18n();
  const [st, setSt] = useState<TCIState>(ZERO);
  const [freqHz, setFreqHz] = useState(0);
  const [mode, setMode] = useState('');
  const [err, setErr] = useState('');

  // OPTIMISTIC, like the Icom console — and for a reason found on a real radio.
  //
  // This panel used to show only what the radio reported back, on the principle
  // that the radio is the truth. But ExpertSDR3 does not echo every setting it
  // is given: press MED and the radio changes, says nothing, and the button
  // stays lit on SLOW. Waiting for an answer that never comes reads as a dead
  // control.
  //
  // So a change is shown at once and held for a moment. Whatever the radio
  // announces afterwards — the new value, or a refusal that leaves the old one
  // — wins once the hold expires, which keeps a clamped or rejected setting
  // honest without making every working one look broken.
  // Held UNTIL THE RADIO SPEAKS, not for a fixed moment.
  //
  // A timeout was wrong in both directions. Too short and a setting the radio
  // never echoes — AGC is one — snapped back to its old value a second after
  // the click. Too long and a setting the radio REFUSES looked accepted: a real
  // log shows this one answering 'sql_enable:0,false' and then, eighty
  // milliseconds later, 'sql_enable:0,true' — it puts the squelch straight back
  // on. Holding through that would have shown the operator a lie.
  //
  // So the requested value stands while the radio says nothing about it, and
  // the instant it reports ANY change for that setting, its word replaces ours.
  const holdRef = useRef<Record<string, { v: any; reported: any }>>({});
  const [, forceRender] = useState(0);
  const hold = <T,>(key: string, reported: T): T => {
    const h = holdRef.current[key];
    if (!h) return reported;
    // The radio has said something different from what it was saying when the
    // click happened — whether that is our value or a refusal, it is now the
    // truth and the hold is over.
    if (reported !== h.reported) {
      delete holdRef.current[key];
      return reported;
    }
    return h.v as T;
  };
  const setHold = (key: string, v: any, reported: any) => {
    holdRef.current[key] = { v, reported };
    forceRender((n) => n + 1);
  };

  useEffect(() => {
    let alive = true;
    const tick = async () => {
      try {
        const p: any = await GetTCIPanel();
        if (!alive) return;
        setSt(p as TCIState);
        const cs: any = await GetCATState();
        if (!alive) return;
        setFreqHz(Number(cs?.rx_freq_hz) || Number(cs?.freq_hz) || 0);
        setMode(String(cs?.mode || ''));
      } catch (e: any) {
        if (alive) setErr(String(e?.message ?? e));
      }
    };
    tick();
    const id = window.setInterval(tick, 400);
    return () => { alive = false; window.clearInterval(id); };
  }, []);

  const off = !st.connected;

  const call = (fn: () => Promise<any>) => { fn().catch((e: any) => setErr(String(e?.message ?? e))); };

  const drive = hold('drive', st.drive);
  const tuneDrive = hold('tune_drive', st.tune_drive);
  const mic = hold('mic', st.mic_level);
  const vol = hold('vol', st.volume);
  const sql = hold('sql', st.squelch);
  const agc = hold('agc', st.agc || '');
  const nb = hold('nb', st.nb), nr = hold('nr', st.nr);
  const anf = hold('anf', st.anf), apf = hold('apf', st.apf);
  const sqlOn = hold('sql_on', st.squelch_on), muted = hold('mute', st.mute);
  const rit = hold('rit', st.rit), xit = hold('xit', st.xit);
  const ritHz = hold('rit_hz', st.rit_offset), xitHz = hold('xit_hz', st.xit_offset);
  const s = sParts(st.smeter);

  // Ctrl+Left/Right shifts the RIT by ±10 Hz, the same keys the Icom console
  // uses. Two consoles for two radios should not need two habits.
  const ritRef = useRef({ on: false, hz: 0, off: true });
  ritRef.current = { on: rit, hz: ritHz, off };
  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      if (!e.ctrlKey || (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight')) return;
      const r = ritRef.current;
      if (r.off || !r.on) return;
      e.preventDefault();
      const v = r.hz + (e.key === 'ArrowRight' ? 10 : -10);
      setHold('rit_hz', v, ritRef.current.hz);
      SetTCIRITOffset(v).catch(() => {});
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, []);

  return (
    <div className="h-full min-h-0 overflow-auto bg-background">
      {/* Capped and centred, like the Elecraft, Yaesu, Icom and Flex consoles.
          Stretched across a wide window a console puts each slider a hand's
          width from its own label and stops reading as one instrument. */}
      <div className="max-w-5xl mx-auto p-3 space-y-3">
        {/* VFO + identity */}
        <div className="rounded-xl border border-border bg-card shadow-sm px-4 py-3 flex items-center justify-between gap-3 flex-wrap">
          <div>
            <div className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground flex items-center gap-1.5">
              <Radio className="size-3.5" />
              {st.device || 'SunSDR'} {st.protocol ? `· ${st.protocol}` : ''}
              <span className={cn('size-2 rounded-full', off ? 'bg-muted-foreground/40' : 'bg-success')} />
            </div>
            <div className="text-2xl font-mono tabular-nums font-bold">
              {freqHz > 0 ? (freqHz / 1e6).toFixed(6) : '—'}
            </div>
          </div>
          <div className="flex items-center gap-2">
            {st.tx && <span className="rounded-md bg-danger px-2 py-1 text-[11px] font-bold text-danger-foreground">TX</span>}
            {st.split && <span className="rounded-md border border-border px-2 py-1 text-[11px] font-bold">SPLIT</span>}
            <Toggle label={t('tcip.lock')} on={st.lock} off={off} onClick={() => call(() => SetTCILock(!st.lock))} />
          </div>
        </div>

        {off && <div className="text-xs text-muted-foreground px-1">{t('tcip.waiting')}</div>}
        {!!err && <div className="text-[11px] text-danger px-1">{err}</div>}

        {/* Meters. The S-meter while receiving, power and SWR while
            transmitting — the radio answers TX_POWER and TX_SWR only when it is
            keyed, so showing them the rest of the time would be showing the
            last thing that happened as if it were now.
            There is no temperature: the protocol has no such command, and a
            made-up figure on a transmitter is the kind somebody trusts. */}
        <Card icon={Activity} title={t('tcip.meters')}>
          <MeterBar label="S-METER" value={st.tx || st.tuning ? 0 : sBar(st.smeter)} lo={0} hi={100}
            accent="#16a34a" segColor={sSegColor}
            display={st.tx || st.tuning ? '—' : `${s.label}  ${st.smeter} dBm`}
            onClick={() => {
              if (st.tx || !onReportRST) return;
              onReportRST(sMeterRST(s.s, s.over, mode));
            }}
            title={t('tcip.sMeterHint')} />
          {(st.tx || st.tuning) && (
            <div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
              <MeterBar label="PWR" value={st.tx_power_w} lo={0} hi={Math.max(10, Math.ceil(st.tx_power_w / 10) * 10)}
                accent="#0ea5e9" display={`${st.tx_power_w.toFixed(1)} W`} />
              {/* 0 is "not measured yet", and it must not draw as a perfect
                  match: an SWR of 1.0 on an antenna nobody has measured is the
                  one reading an operator should not be handed. */}
              <MeterBar label="SWR" value={st.tx_swr > 0 ? Math.min(100, (st.tx_swr - 1) * 50) : 0} lo={0} hi={100}
                accent="#f59e0b" display={st.tx_swr > 0 ? st.tx_swr.toFixed(1) : '—'}
                segColor={(f) => (f > 0.5 ? '#dc2626' : f > 0.25 ? '#f59e0b' : '#16a34a')} />
            </div>
          )}
        </Card>

        {/* Transmit */}
        <Card icon={SlidersHorizontal} title={t('tcip.transmit')}>
          {/* One level per ROW, full width. Two half-width sliders side by side
              left each of them a couple of centimetres long — small enough that
              setting 15% took aim. */}
          <LevelRow label={t('tcip.drive')} unit="%" value={drive} disabled={off}
            onSet={(v) => { setHold('drive', v, st.drive); call(() => SetTCIDrive(v)); }} />
          <LevelRow label={t('tcip.tuneDrive')} unit="%" value={tuneDrive} disabled={off} accent="#f59e0b"
            onSet={(v) => { setHold('tune_drive', v, st.tune_drive); call(() => SetTCITuneDrive(v)); }} />
          <div className="flex items-center gap-2 flex-wrap">
            {/* TUNE transmits, and at the tune drive rather than the main one —
                which is why both numbers are above the button rather than one of
                them being in a menu somewhere. */}
            <button type="button" disabled={off || !st.tx_enabled}
              onClick={() => call(() => SetTCITune(!st.tuning))}
              className={cn('rounded-lg border-2 px-4 py-2 text-sm font-extrabold tracking-wide transition-all disabled:opacity-30',
                st.tuning ? 'bg-warning text-warning-foreground border-warning shadow-[0_0_14px] shadow-warning/50'
                  : 'bg-card text-warning border-warning hover:bg-warning-muted')}>
              {st.tuning ? t('tcip.tuning') : t('tcip.tune')}
            </button>
            {!st.tx_enabled && !off && (
              <span className="text-[11px] text-muted-foreground">{t('tcip.txDisabled')}</span>
            )}
          </div>
          <LevelRow label={t('tcip.mic')} unit="%" value={mic} disabled={off} accent="#a855f7"
            onSet={(v) => { setHold('mic', v, st.mic_level); call(() => SetTCIMicLevel(v)); }} />
        </Card>

        {/* Receive */}
        <Card icon={AudioLines} title={t('tcip.receive')}>
          {/* Both in the radio's OWN units — volume in dB, negative, and the
              squelch as a dBm threshold — so the numbers match the ones in
              ExpertSDR3's window rather than being percentages of something. */}
          <LevelRow label={t('tcip.volume')} unit=" dB" min={-60} max={0} value={vol} disabled={off}
            onSet={(v) => { setHold('vol', v, st.volume); call(() => SetTCIVolume(v)); }} />
          <LevelRow label={t('tcip.squelch')} unit=" dBm" min={-140} max={0} value={sql}
            disabled={off || !sqlOn} accent="#38bdf8"
            onSet={(v) => { setHold('sql', v, st.squelch); call(() => SetTCISquelchLevel(v)); }} />
          <div className={cn('grid gap-2', isCW(mode) ? 'grid-cols-3 sm:grid-cols-6' : 'grid-cols-3 sm:grid-cols-5')}>
            <Toggle label="NB" on={nb} off={off} onClick={() => { setHold('nb', !nb, st.nb); call(() => SetTCINB(!nb)); }} />
            <Toggle label="NR" on={nr} off={off} onClick={() => { setHold('nr', !nr, st.nr); call(() => SetTCINR(!nr)); }} />
            <Toggle label="ANF" on={anf} off={off} onClick={() => { setHold('anf', !anf, st.anf); call(() => SetTCIANF(!anf)); }} />
            {/* APF is an audio PEAK filter — it rings a single tone out of the
                noise, which is a CW tool and nothing else. Shown only there:
                off CW it is not a control, it is a puzzle. */}
            {isCW(mode) && (
              <Toggle label="APF" on={apf} off={off} onClick={() => { setHold('apf', !apf, st.apf); call(() => SetTCIAPF(!apf)); }} />
            )}
            <Toggle label="SQL" on={sqlOn} off={off} onClick={() => { setHold('sql_on', !sqlOn, st.squelch_on); call(() => SetTCISquelch(!sqlOn)); }} />
            <Toggle label={t('tcip.mute')} on={muted} off={off} onClick={() => { setHold('mute', !muted, st.mute); call(() => SetTCIMute(!muted)); }} />
          </div>
          <div className="space-y-1">
            <span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">{t('tcip.agc')}</span>
            {/* LONG is gone. The protocol accepts it, but it is a hang time
                nobody reaches for between overs, and a fifth button that has to
                be explained is worse than four that do not. */}
            <div className="grid grid-cols-4 gap-2">
              {['off', 'slow', 'med', 'fast'].map((m) => (
                <Toggle key={m} label={m.toUpperCase()} on={agc === m} off={off}
                  onClick={() => { setHold('agc', m, st.agc || ''); call(() => SetTCIAGC(m)); }} />
              ))}
            </div>
          </div>
          <div className="space-y-1">
            <div className="flex items-baseline justify-between">
              <span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">{t('tcip.filter')}</span>
              <span className="text-xs font-mono tabular-nums">{st.filter_lo}–{st.filter_hi} Hz</span>
            </div>
            <div className="flex items-center gap-2 pb-1">
              <LevelRow label="LO" unit=" Hz" min={-5000} max={5000} step={10}
                value={st.filter_lo} disabled={off}
                onSet={(v) => call(() => SetTCIFilter(v, st.filter_hi))} />
            </div>
            <div className="flex items-center gap-2 pb-1">
              <LevelRow label="HI" unit=" Hz" min={-5000} max={5000} step={10}
                value={st.filter_hi} disabled={off}
                onSet={(v) => call(() => SetTCIFilter(st.filter_lo, v))} />
            </div>
            <div className="grid grid-cols-4 sm:grid-cols-7 gap-2">
              {widthsFor(mode).map((w) => {
                const e = edgesFor(w, mode);
                // Lit by the WIDTH the radio is actually using, not by an exact
                // pair of edges: the operator may have moved one edge on the
                // radio, and a button that only lights on our own numbers would
                // go dark for a filter that is plainly 500 Hz wide.
                const on = Math.abs((st.filter_hi - st.filter_lo) - w) <= 50;
                return (
                  <Toggle key={w} label={widthLabel(w)} on={on} off={off}
                    title={`${e.lo}–${e.hi} Hz`}
                    onClick={() => call(() => SetTCIFilter(e.lo, e.hi))} />
                );
              })}
            </div>
          </div>
        </Card>

        {/* RIT / XIT — the SAME control the Icom console uses, now shared
            rather than reinvented: a chip, a signed offset you can type into,
            scroll on, or step with ±, and a zero. Ctrl+←/→ shifts the RIT. */}
        <Card icon={Mic} title="RIT / XIT">
          <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
            <ShiftRow label="RIT" on={rit} hz={ritHz} accent="#38bdf8" disabled={off}
              onToggle={() => { setHold('rit', !rit, st.rit); call(() => SetTCIRIT(!rit)); }}
              onSet={(v) => { setHold('rit_hz', v, st.rit_offset); call(() => SetTCIRITOffset(v)); }} />
            <ShiftRow label="XIT" on={xit} hz={xitHz} accent="#f59e0b" disabled={off}
              onToggle={() => { setHold('xit', !xit, st.xit); call(() => SetTCIXIT(!xit)); }}
              onSet={(v) => { setHold('xit_hz', v, st.xit_offset); call(() => SetTCIXITOffset(v)); }} />
          </div>
        </Card>
      </div>
    </div>
  );
}
