import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { Plus, Pencil, Trash2, Power, PlugZap, Loader2, Check, X, Compass, Square, Antenna as AntennaIcon, ArrowDownToLine, Minus, RefreshCw, GripVertical } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { cn } from '@/lib/utils';
import { useI18n } from '@/lib/i18n';
import { writeUiPref } from '@/lib/uiPref';
import { RotorCompass } from '@/components/RotorCompass';
import { AmpCard } from '@/components/AmpCard';
import { TunerCard } from '@/components/TunerCard';
import type { TGStatus } from '@/components/TunerGeniusPanel';
import {
  GetStationDevices, SaveStationDevices, GetStationStatus, StationSetRelay,
  GetRotatorHeading, RotatorGoTo, RotatorStop, SetActiveRotor,
  GetUltrabeamStatus, SetUltrabeamDirection, UltrabeamRetract, MotorSetElement, MotorReadElements,
  ListDenkoviDevices, ListSerialPorts, TestStationDevice,
  GetAmpStatuses, GetFlexState,
  GetTunerGeniusStatus, GetTunerGeniusSettings,
} from '../../wailsjs/go/main/App';

type RotatorProps = { centerLat?: number | null; centerLon?: number | null; bearing?: number | null };

type Device = { id: string; type: string; name: string; host: string; user?: string; pass?: string; channels?: number; labels: string[] };
type Relay = { number: number; label: string; on: boolean };
type DevStatus = { id: string; name: string; type: string; connected: boolean; error?: string; relays: Relay[] };

// Dashboard geometry: a grid of EQUAL columns whose cards also share a common
// HEIGHT per row — every card stretches to the tallest one beside it. Masonry
// packed tighter but left the row edges ragged, which read as untidy on a wide
// screen; aligned rows are what an operator expects from a dashboard.
const CARD_MIN = 430; // narrowest a card may get before "Auto" drops a column
const GRID_GAP = 16; // px between cards, both axes

const RELAY_COUNT: Record<string, number> = { webswitch: 5, kmtronic: 8, denkovi: 8, usbrelay: 8, dingtian: 2 };
const TYPE_LABEL: Record<string, string> = { webswitch: 'WebSwitch 1216H', kmtronic: 'KMTronic 8-relay', denkovi: 'Denkovi USB (FT245)', usbrelay: 'Denkovi USB (serial)', dingtian: 'Dingtian IOT relay' };

// Relay count for a configured device: fixed by type, except Denkovi (4/8) and
// the generic USB-serial board, whose channel count the user picks.
const chanCount = (d: Device): number =>
  (d.type === 'denkovi' || d.type === 'usbrelay') ? (d.channels && d.channels >= 1 ? d.channels : 8)
    : d.type === 'dingtian' ? (d.channels && d.channels >= 1 ? d.channels : 2)
      : (RELAY_COUNT[d.type] ?? 5);

function blankDevice(): Device {
  return { id: '', type: 'webswitch', name: '', host: '', user: '', pass: '', labels: Array(5).fill('') };
}

type Heading = { enabled: boolean; ok: boolean; azimuth: number; rotors?: string[]; active?: number };

// RotatorWidget shows the configured rotator (Settings → Rotator) right in the
// Station Control tab: a live compass you can click to turn, a heading readout, a
// GoTo box, quick N/E/S/W presets, and Stop. Heading is polled by the panel and
// passed in (so the panel can also order this widget); it drives the shared
// rotator backend.
function RotatorWidget({ hd, refetch, centerLat, centerLon, bearing, t }: RotatorProps & {
  hd: Heading; refetch: () => void; t: (k: string, v?: any) => string;
}) {
  const [goto, setGoto] = useState('');
  const [err, setErr] = useState('');

  const turn = (az: number) => {
    const a = ((Math.round(az) % 360) + 360) % 360;
    RotatorGoTo(a, -1).then(refetch).catch((e) => setErr(String(e?.message ?? e)));
  };
  const poll = refetch;

  const presets: [string, number][] = [['N', 0], ['E', 90], ['S', 180], ['W', 270]];
  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">
        <Compass className="size-4 text-primary" />
        <div className="text-sm font-semibold">{t('station.rotator')}</div>
        <span className={cn('ml-auto size-2 rounded-full', hd.ok ? 'bg-success' : 'bg-warning')}
          title={hd.ok ? t('station.online') : t('station.rotatorNoRead')} />
      </div>
      <div className="p-3 flex gap-4 items-start">
        <RotorCompass
          bearing={bearing ?? null}
          headings={hd.ok ? [hd.azimuth] : []}
          centerLat={centerLat ?? null}
          centerLon={centerLon ?? null}
          rotorEnabled={hd.ok}
          rotors={hd.rotors}
          activeRotor={hd.active}
          onSelectRotor={(i) => { SetActiveRotor(i).then(refetch).catch((e) => setErr(String(e?.message ?? e))); }}
          onGoto={(az) => turn(az)}
        />
        <div className="flex-1 min-w-0 space-y-2">
          <div className="font-mono">
            <span className="text-2xl font-bold tabular-nums">{hd.ok ? `${hd.azimuth}°` : '—'}</span>
          </div>
          <div className="flex gap-1">
            {presets.map(([lbl, az]) => (
              <button key={lbl} type="button" onClick={() => turn(az)}
                className="flex-1 rounded-md border border-border bg-muted/40 py-1 text-xs font-semibold hover:bg-muted">
                {lbl}
              </button>
            ))}
          </div>
          <div className="flex gap-1.5">
            <Input value={goto} onChange={(e) => setGoto(e.target.value.replace(/[^0-9]/g, ''))}
              onKeyDown={(e) => { if (e.key === 'Enter' && goto) turn(parseInt(goto, 10)); }}
              placeholder="0–359" className="h-8 flex-1 min-w-0 font-mono text-sm" />
            <Button size="sm" className="h-8 shrink-0" disabled={!goto} onClick={() => turn(parseInt(goto, 10))}>{t('station.go')}</Button>
          </div>
          <Button size="sm" variant="outline" className="h-8 w-full" onClick={() => RotatorStop().then(poll).catch((e) => setErr(String(e?.message ?? e)))}>
            <Square className="size-3 mr-1" /> {t('station.stop')}
          </Button>
          {err && <div className="text-[11px] text-destructive break-words">{err}</div>}
        </div>
      </div>
    </div>
  );
}

type AntStatus = { enabled: boolean; type: string; connected: boolean; direction: number; frequency: number; moving: boolean; elements: number[] };

// MotorAntennaWidget controls a motorized antenna (Ultrabeam / SteppIR) from the
// Station Control tab: pattern (Normal / 180° / Bi), Retract, and — Ultrabeam
// only — per-element length adjustment. Heading/state is polled by the panel.
const ELEMENT_STEP = 2; // the physical console adjusts 2 mm per press

// elementName maps an element index to a ham-radio name: 0 = reflector,
// 1 = driven element, then Director 1, 2, 3…
function elementName(i: number, t: (k: string, v?: any) => string): string {
  if (i === 0) return t('station.reflector');
  if (i === 1) return t('station.driven');
  return `${t('station.director')} ${i - 1}`;
}

function MotorAntennaWidget({ ant, refetch, t }: { ant: AntStatus; refetch: () => void; t: (k: string, v?: any) => string }) {
  const [err, setErr] = useState('');
  const [lengths, setLengths] = useState<number[]>([]); // current element lengths (mm), from ReadElements
  const [reading, setReading] = useState(false);
  const [busyEl, setBusyEl] = useState<number | null>(null);
  const [editingEl, setEditingEl] = useState<number | null>(null); // element whose mm is being typed
  const [editVal, setEditVal] = useState('');
  const run = (p: Promise<any>) => p.then(refetch).catch((e) => setErr(String(e?.message ?? e)));
  const isUB = ant.type !== 'steppir';

  const readLengths = useCallback(async () => {
    setReading(true); setErr('');
    try { setLengths(((await MotorReadElements()) ?? []) as number[]); }
    catch (e: any) { setErr(String(e?.message ?? e)); }
    finally { setReading(false); }
  }, []);
  // Read the current lengths once when the Ultrabeam widget mounts/connects, so
  // +/- starts from the real values rather than blind.
  useEffect(() => { if (isUB && ant.connected) readLengths(); }, [isUB, ant.connected, readLengths]);

  // Send an absolute length to one element and remember it as the new baseline.
  const setLen = async (i: number, mm: number) => {
    const next = Math.max(0, Math.round(mm));
    const prev = lengths[i] ?? 0;
    setBusyEl(i); setErr('');
    setLengths((ls) => { const c = [...ls]; c[i] = next; return c; }); // optimistic
    try { await MotorSetElement(i, next); refetch(); }
    catch (e: any) {
      // Rejected by the controller — most often the element is at its travel
      // limit for this band (extending on a low band). Revert the optimistic
      // value so the display stays truthful, and explain the likely cause when
      // the failed move was an extension.
      setLengths((ls) => { const c = [...ls]; c[i] = prev; return c; });
      setErr(next > prev ? t('station.atMax') : String(e?.message ?? e));
    }
    finally { setBusyEl(null); }
  };
  // Nudge one element by ±2 mm from its current known length.
  const nudge = (i: number, delta: number) => setLen(i, (lengths[i] ?? 0) + delta);
  // Commit a typed exact length (click on the mm value). Lets the operator fix
  // the baseline when the auto-read is off, so +/- then work reliably.
  const commitEdit = (i: number) => {
    const v = parseInt(editVal, 10);
    setEditingEl(null);
    if (!isNaN(v) && v >= 0 && v !== lengths[i]) setLen(i, v);
  };
  const dirs: [number, string][] = [[0, 'N'], [1, '180°'], [2, t('station.bi')]];
  return (
    <div className="rounded-xl border border-border bg-card shadow-sm overflow-hidden h-full">
      <div className="flex items-center gap-2 px-3 py-2 border-b border-border/60 bg-muted/30">
        <AntennaIcon className="size-4 text-primary" />
        <div className="min-w-0">
          <div className="text-sm font-semibold truncate">{isUB ? 'Ultrabeam' : 'SteppIR'}</div>
          {ant.frequency > 0 && <div className="text-[10px] text-muted-foreground font-mono">{(ant.frequency / 1000).toFixed(3)} MHz</div>}
        </div>
        {ant.moving && <span className="ml-auto text-[10px] font-semibold text-warning animate-pulse">{t('station.moving')}</span>}
        <span className={cn('size-2 rounded-full shrink-0', ant.moving ? '' : 'ml-auto', ant.connected ? 'bg-success' : 'bg-muted-foreground/40')}
          title={ant.connected ? t('station.online') : t('station.offline')} />
      </div>
      <div className="p-3 space-y-3">
        <div>
          <div className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground mb-1">{t('station.pattern')}</div>
          <div className="flex gap-1">
            {dirs.map(([d, lbl]) => (
              <button key={d} type="button" disabled={!ant.connected}
                onClick={() => run(SetUltrabeamDirection(d))}
                className={cn('flex-1 rounded-md border py-1.5 text-xs font-semibold transition-colors disabled:opacity-40',
                  ant.direction === d ? 'bg-primary text-primary-foreground border-primary' : 'border-border hover:bg-muted')}>
                {lbl}
              </button>
            ))}
          </div>
        </div>
        <button type="button" disabled={!ant.connected}
          onClick={() => run(UltrabeamRetract())}
          className="w-full flex items-center justify-center gap-1.5 rounded-md border border-warning-border bg-warning-muted text-warning-muted-foreground py-1.5 text-xs font-semibold hover:brightness-95 disabled:opacity-40">
          <ArrowDownToLine className="size-3.5" /> {t('station.retract')}
        </button>

        {isUB && (
          <div>
            <div className="flex items-center gap-2 mb-1.5">
              <span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">{t('station.elements')}</span>
              <button type="button" onClick={readLengths} disabled={!ant.connected || reading}
                className="text-[10px] text-primary hover:underline inline-flex items-center gap-1 disabled:opacity-40" title={t('station.readLengths')}>
                <RefreshCw className={cn('size-3', reading && 'animate-spin')} /> {t('station.read')}
              </button>
            </div>
            {lengths.length === 0 ? (
              <p className="text-[11px] text-muted-foreground">{t('station.noLengths')}</p>
            ) : (
              <div className="space-y-1.5">
                {/* The READ_BANDS reply is undocumented and its 16-bit parse picks
                    up structural bytes past the real data (varying by band), which
                    made 6+ bogus "elements" appear. Ultrabeam beams are 3-element,
                    and ModifyElement addresses elements 0..2 — so show just those. */}
                {lengths.map((mm, i) => ({ mm, i })).slice(0, 3).map(({ mm, i }) => (
                  <div key={i} className="flex items-center gap-2">
                    <span className="text-xs w-20 shrink-0 text-muted-foreground truncate">{elementName(i, t)}</span>
                    <button type="button" disabled={!ant.connected || busyEl !== null}
                      onClick={() => nudge(i, -ELEMENT_STEP)}
                      className="flex items-center justify-center size-7 rounded-md border border-border hover:bg-muted disabled:opacity-40 shrink-0">
                      <Minus className="size-3.5" />
                    </button>
                    {editingEl === i ? (
                      <input autoFocus type="number" value={editVal}
                        onChange={(e) => setEditVal(e.target.value)}
                        onBlur={() => commitEdit(i)}
                        onKeyDown={(e) => { if (e.key === 'Enter') commitEdit(i); if (e.key === 'Escape') setEditingEl(null); }}
                        className="text-sm font-mono font-bold flex-1 min-w-0 text-center tabular-nums bg-transparent border border-primary rounded px-1" />
                    ) : (
                      <span className="text-sm font-mono font-bold flex-1 min-w-0 text-center tabular-nums cursor-pointer hover:underline"
                        title={t('station.setExactLen')}
                        onClick={() => { setEditingEl(i); setEditVal(String(mm)); }}>
                        {busyEl === i ? <Loader2 className="size-3.5 animate-spin inline" /> : `${mm} mm`}
                      </span>
                    )}
                    <button type="button" disabled={!ant.connected || busyEl !== null}
                      onClick={() => nudge(i, ELEMENT_STEP)}
                      className="flex items-center justify-center size-7 rounded-md border border-border hover:bg-muted disabled:opacity-40 shrink-0">
                      <Plus className="size-3.5" />
                    </button>
                  </div>
                ))}
              </div>
            )}
            <p className="text-[10px] text-muted-foreground mt-1">{t('station.elementsHint')}</p>
          </div>
        )}
        {err && <div className="text-[11px] text-destructive break-words">{err}</div>}
      </div>
    </div>
  );
}

export function StationControlPanel({ centerLat, centerLon, bearing }: RotatorProps) {
  const { t } = useI18n();
  const [devices, setDevices] = useState<Device[]>([]);
  const [status, setStatus] = useState<Record<string, DevStatus>>({});
  const [editing, setEditing] = useState<Device | null>(null); // device being added/edited
  const [busy, setBusy] = useState<Record<string, boolean>>({}); // per-relay in-flight
  const [rot, setRot] = useState<Heading>({ enabled: false, ok: false, azimuth: 0 });
  const [ant, setAnt] = useState<AntStatus>({ enabled: false, type: '', connected: false, direction: 0, frequency: 0, moving: false, elements: [] });
  // Widget order (rotator + device ids), drag-and-drop reorderable, persisted.
  const [order, setOrder] = useState<string[]>(() => {
    try { const v = JSON.parse(localStorage.getItem('opslog.stationOrder') || '[]'); return Array.isArray(v) ? v : []; } catch { return []; }
  });
  const dragId = useRef<string | null>(null);
  // Max columns per row (persisted). "Auto" fills the window; a number caps the
  // row so cards wrap onto further lines even when there's horizontal room.
  const [cols, setCols] = useState<string>(() => localStorage.getItem('opslog.stationCols') || 'auto');

  // How many columns the dashboard shows. "Auto" fits as many CARD_MIN-wide
  // cards as the window allows; an explicit count is honoured as asked, even
  // past that width — the operator can see his own screen.
  const gridRef = useRef<HTMLDivElement | null>(null);
  const [colCount, setColCount] = useState(1);

  useLayoutEffect(() => {
    if (cols !== 'auto') { setColCount(Number(cols) || 1); return; }
    const el = gridRef.current;
    if (!el) return;
    const measure = () => setColCount(Math.max(1, Math.floor((el.clientWidth + GRID_GAP) / (CARD_MIN + GRID_GAP))));
    measure();
    const ro = new ResizeObserver(measure);
    ro.observe(el);
    return () => ro.disconnect();
  }, [cols]);
  // Amplifiers (Settings → Amplifier): shown here so operators without a
  // FlexRadio panel still get the controls. EVERY configured amp gets its own
  // card (identical to the Flex panel's) — no dropdown. Polled fast (1.5s) so the
  // FlexRadio meters read live; the Flex state rides along because a PGXL's
  // OPERATE/meters come from the radio, not the direct GSCP link.
  const [amps, setAmps] = useState<any[]>([]);
  const [flexState, setFlexState] = useState<any>(null);
  useEffect(() => {
    let alive = true;
    const load = () => Promise.all([
      GetAmpStatuses().catch(() => []),
      GetFlexState().catch(() => null),
    ]).then(([l, fx]: any[]) => { if (alive) { setAmps((l ?? []) as any[]); setFlexState(fx); } });
    load();
    const id = window.setInterval(load, 1500);
    return () => { alive = false; window.clearInterval(id); };
  }, []);
  // Tuner Genius XL (4O3A): a card here too, so operators without a FlexRadio
  // panel still get the controls. Re-read the enabled flag so it appears/hides
  // without a restart.
  const [tg, setTg] = useState<TGStatus>({ connected: false });
  const [tgEnabled, setTgEnabled] = useState(false);
  useEffect(() => {
    let alive = true;
    const load = async () => {
      try { const en: any = await GetTunerGeniusSettings(); if (alive) setTgEnabled(!!en?.enabled); } catch {}
      try { const s: any = await GetTunerGeniusStatus(); if (alive && s) setTg(s as TGStatus); } catch {}
    };
    load();
    const id = window.setInterval(load, 500); // fast so meters track TX (see App.tsx)
    return () => { alive = false; window.clearInterval(id); };
  }, []);

  const loadDevices = useCallback(async () => {
    try { setDevices(((await GetStationDevices()) ?? []) as Device[]); } catch { /* db not ready */ }
  }, []);

  const poll = useCallback(async () => {
    try {
      const s = ((await GetStationStatus()) ?? []) as DevStatus[];
      setStatus(Object.fromEntries(s.map((d) => [d.id, d])));
    } catch { /* ignore transient */ }
  }, []);
  const pollRot = useCallback(async () => {
    try { setRot((await GetRotatorHeading()) as any); } catch { /* ignore */ }
  }, []);
  const pollAnt = useCallback(async () => {
    try { setAnt((await GetUltrabeamStatus()) as any); } catch { /* ignore */ }
  }, []);

  useEffect(() => { loadDevices(); }, [loadDevices]);
  useEffect(() => {
    poll(); pollRot(); pollAnt();
    const id = window.setInterval(() => { poll(); pollRot(); pollAnt(); }, 3000);
    return () => window.clearInterval(id);
  }, [poll, pollRot, pollAnt, devices.length]);

  const persistOrder = (next: string[]) => { setOrder(next); writeUiPref('opslog.stationOrder', JSON.stringify(next)); };
  // Reorder so `dragged` lands just before `target`.
  const onDrop = (targetId: string) => {
    const src = dragId.current; dragId.current = null;
    if (!src || src === targetId) return;
    const ids = widgetIds.filter((id) => id !== src);
    const at = ids.indexOf(targetId);
    ids.splice(at < 0 ? ids.length : at, 0, src);
    persistOrder(ids);
  };

  const persist = async (next: Device[]) => {
    setDevices(next);
    try { await SaveStationDevices(next as any); } catch { /* surfaced by status */ }
    await loadDevices();
    poll();
  };

  const toggle = async (dev: Device, relay: number, on: boolean) => {
    const key = `${dev.id}:${relay}`;
    setBusy((b) => ({ ...b, [key]: true }));
    // Optimistic flip so the switch feels instant; the poll reconciles.
    setStatus((st) => {
      const d = st[dev.id]; if (!d) return st;
      return { ...st, [dev.id]: { ...d, relays: d.relays.map((r) => (r.number === relay ? { ...r, on } : r)) } };
    });
    try { await StationSetRelay(dev.id, relay, on); } catch { /* poll will correct */ }
    await poll();
    setBusy((b) => ({ ...b, [key]: false }));
  };

  const saveEdit = async () => {
    if (!editing) return;
    const d = { ...editing, name: editing.name.trim() || TYPE_LABEL[editing.type], host: editing.host.trim() };
    const exists = devices.some((x) => x.id && x.id === d.id);
    await persist(exists ? devices.map((x) => (x.id === d.id ? d : x)) : [...devices, d]);
    setEditing(null);
  };

  const removeDevice = async (id: string) => { await persist(devices.filter((x) => x.id !== id)); };

  // Build the widget list (rotator first by default, then devices), then order it
  // by the saved drag order — unknown ids fall to the end in their natural order.
  const deviceCard = (dev: Device) => {
    const st = status[dev.id];
    const relays = st?.relays ?? dev.labels.map((label, i) => ({ number: i + 1, label, on: false }));
    return (
      <div className="rounded-xl border border-border bg-card shadow-sm overflow-hidden h-full">
        <div className="flex items-center gap-2 px-3 py-2 border-b border-border/60 bg-muted/30">
          <PlugZap className="size-4 text-primary" />
          <div className="min-w-0">
            <div className="text-sm font-semibold truncate">{dev.name || TYPE_LABEL[dev.type]}</div>
            <div className="text-[10px] text-muted-foreground font-mono truncate">{TYPE_LABEL[dev.type]} · {dev.host}</div>
          </div>
          <span className={cn('ml-auto size-2 rounded-full shrink-0', st?.connected ? 'bg-success' : 'bg-muted-foreground/40')}
            title={st?.connected ? t('station.online') : (st?.error || t('station.offline'))} />
          <button className="text-muted-foreground hover:text-foreground" title={t('station.edit')}
            onClick={() => setEditing({ ...dev, labels: [...dev.labels] })}><Pencil className="size-3.5" /></button>
          <button className="text-muted-foreground hover:text-destructive" title={t('station.delete')}
            onClick={() => removeDevice(dev.id)}><Trash2 className="size-3.5" /></button>
        </div>
        {/* Compact one-line relay buttons at a FIXED width so they don't stretch
            across the whole card — they wrap to fill the available width instead. */}
        <div className="p-2 flex flex-wrap gap-1.5">
          {relays.map((r) => {
            const key = `${dev.id}:${r.number}`;
            const label = r.label || `${t('station.relay')} ${r.number}`;
            return (
              <button key={r.number} type="button" disabled={!st?.connected}
                title={label}
                onClick={() => toggle(dev, r.number, !r.on)}
                className={cn('w-[150px] flex items-center gap-1.5 rounded-md border px-2 py-1 text-left transition-colors disabled:opacity-40',
                  r.on ? 'bg-success/15 border-success/50' : 'bg-muted/30 border-border hover:bg-muted')}>
                <span className={cn('flex items-center justify-center size-5 rounded shrink-0',
                  r.on ? 'bg-success text-success-foreground' : 'bg-muted-foreground/15 text-muted-foreground')}>
                  {busy[key] ? <Loader2 className="size-3 animate-spin" /> : <Power className="size-3" />}
                </span>
                <span className="flex-1 min-w-0 text-xs font-medium truncate">{label}</span>
                <span className={cn('text-[9px] font-bold shrink-0', r.on ? 'text-success' : 'text-muted-foreground/50')}>
                  {r.on ? t('station.on') : t('station.off')}
                </span>
              </button>
            );
          })}
        </div>
      </div>
    );
  };

  // `wide` cards take two grid columns instead of one. The amplifier and tuner
  // carry meter rows and a channel selector that are unreadable squeezed into a
  // single ~430px column — they are the same cards the FlexRadio panel shows
  // full-width, and they need that room here too.
  const widgets: { id: string; node: React.ReactNode; wide?: boolean }[] = [];
  if (rot.enabled) {
    widgets.push({ id: 'rotator', node: <RotatorWidget hd={rot} refetch={pollRot} centerLat={centerLat} centerLon={centerLon} bearing={bearing} t={t} /> });
  }
  if (ant.enabled) {
    widgets.push({ id: 'antenna', node: <MotorAntennaWidget ant={ant} refetch={pollAnt} t={t} /> });
  }
  // One card per configured amplifier (identical to the Flex panel's card).
  for (const amp of amps) widgets.push({ id: `amp:${amp.id}`, node: <AmpCard amp={amp} flex={flexState} t={t} />, wide: true });
  // Tuner Genius XL card (identical to the Flex panel's).
  if (tgEnabled) widgets.push({ id: 'tuner', node: <TunerCard status={tg} t={t} />, wide: true });
  for (const dev of devices) widgets.push({ id: dev.id, node: deviceCard(dev) });

  const rank = (id: string) => { const i = order.indexOf(id); return i < 0 ? 1e6 : i; };
  const ordered = widgets.map((w, i) => ({ ...w, i })).sort((a, b) => (rank(a.id) - rank(b.id)) || (a.i - b.i));
  const widgetIds = ordered.map((w) => w.id);

  const noDevices = devices.length === 0 && !rot.enabled && !ant.enabled && amps.length === 0 && !tgEnabled;

  return (
    <div className="flex-1 min-h-0 overflow-auto p-4">
      <div className="flex items-center justify-between mb-3">
        <h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground">{t('station.title')}</h2>
        <div className="flex items-center gap-2">
          {/* Max columns per row: Auto fills the window; 1/2/3/4 cap the row so a
              card can sit on a further line even with horizontal room to spare. */}
          <div className="flex items-center rounded-md border border-border overflow-hidden text-[11px]">
            {(['auto', '1', '2', '3', '4', '5', '6'] as const).map((c) => (
              <button key={c} type="button"
                onClick={() => { setCols(c); writeUiPref('opslog.stationCols', c); }}
                className={cn('px-2 py-1 font-medium', cols === c ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-muted')}>
                {c === 'auto' ? t('station.colsAuto') : c}
              </button>
            ))}
          </div>
          <Button size="sm" variant="outline" onClick={() => setEditing(blankDevice())}>
            <Plus className="size-3.5 mr-1" /> {t('station.addDevice')}
          </Button>
        </div>
      </div>

      {noDevices && !editing && (
        <div className="max-w-4xl rounded-lg border border-dashed border-border p-8 text-center text-sm text-muted-foreground">
          {t('station.empty')}
        </div>
      )}

      {/* Dashboard grid: equal columns, and every card stretches to the height of
          the tallest one on its row, so the rows line up instead of ending
          ragged. One column each, two for the amplifier/tuner whose meter rows
          need the room. Each card has a grip handle (left rail) as the drag
          initiator, since the body is full of buttons. */}
      <div ref={gridRef} style={{
        display: 'grid',
        gridTemplateColumns: `repeat(${colCount}, minmax(0, 1fr))`,
        gap: `${GRID_GAP}px`,
      }}>
        {ordered.map((w) => (
          <div key={w.id}
            // A wide card takes two columns — but never more than there are, or
            // the grid would grow a phantom column on a narrow window.
            style={{ gridColumn: w.wide ? `span ${Math.min(2, colCount)}` : undefined }}
            onDragOver={(e) => { if (dragId.current) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; } }}
            onDrop={(e) => { if (dragId.current) { e.preventDefault(); onDrop(w.id); } }}>
            {/* h-full down the chain is what makes the card fill the row height
                rather than sit at its natural size in a taller cell. */}
            <div className="flex items-stretch h-full">
              <div draggable
                onDragStart={(e) => { dragId.current = w.id; e.dataTransfer.effectAllowed = 'move'; }}
                onDragEnd={() => { dragId.current = null; }}
                title={t('station.dragMove')}
                className={cn('flex items-center shrink-0 px-0.5 rounded-l-xl cursor-grab active:cursor-grabbing text-muted-foreground/30 hover:text-muted-foreground hover:bg-muted/40 transition-colors',
                  dragId.current === w.id && 'opacity-60')}>
                <GripVertical className="size-4" />
              </div>
              <div className="flex-1 min-w-0 [&>*]:h-full">{w.node}</div>
            </div>
          </div>
        ))}
      </div>

      {!noDevices && (
        <p className="text-[11px] text-muted-foreground mt-2">{t('station.dragHint')}</p>
      )}

      {editing && (
        <DeviceEditor device={editing} onChange={setEditing} onSave={saveEdit} onCancel={() => setEditing(null)} t={t} />
      )}
    </div>
  );
}

function DeviceEditor({ device, onChange, onSave, onCancel, t }: {
  device: Device; onChange: (d: Device) => void; onSave: () => void; onCancel: () => void; t: (k: string, v?: any) => string;
}) {
  const ref = useRef<HTMLDivElement>(null);
  useEffect(() => { ref.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }, []);
  const setType = (type: string) => {
    const channels = (type === 'denkovi' || type === 'usbrelay') ? (device.channels || 8) : undefined;
    const n = chanCount({ ...device, type, channels });
    const labels = Array.from({ length: n }, (_, i) => device.labels[i] ?? '');
    onChange({ ...device, type, channels, labels });
  };
  const setChannels = (channels: number) => {
    const n = chanCount({ ...device, channels });
    const labels = Array.from({ length: n }, (_, i) => device.labels[i] ?? '');
    onChange({ ...device, channels, labels });
  };
  const isKM = device.type === 'kmtronic';
  const isDingtian = device.type === 'dingtian';
  const isDenkovi = device.type === 'denkovi';
  const isUsbRelay = device.type === 'usbrelay';
  // COM ports for the generic USB-serial relay picker.
  const [serialPorts, setSerialPorts] = useState<string[]>([]);
  useEffect(() => {
    if (!isUsbRelay) return;
    ListSerialPorts().then((p) => setSerialPorts((p ?? []) as string[])).catch(() => setSerialPorts([]));
  }, [isUsbRelay]);
  // Detected FTDI serials for the Denkovi picker.
  const [denkoviSerials, setDenkoviSerials] = useState<string[]>([]);
  const [detecting, setDetecting] = useState(false);
  const [detectMsg, setDetectMsg] = useState('');
  const detectDenkovi = async () => {
    setDetecting(true);
    setDetectMsg('');
    try {
      const list = ((await ListDenkoviDevices()) ?? []) as string[];
      setDenkoviSerials(list);
      // Auto-fill when there's exactly one and nothing chosen yet.
      if (list.length >= 1 && !device.host.trim()) onChange({ ...device, host: list[0] });
      setDetectMsg(list.length === 0 ? t('station.detectNone') : t('station.detectFound', { n: list.length }));
    } catch (e: any) { setDenkoviSerials([]); setDetectMsg(String(e?.message || e || t('station.detectNone'))); }
    finally { setDetecting(false); }
  };
  // Connection test result for the current device config.
  const [testing, setTesting] = useState(false);
  const [testMsg, setTestMsg] = useState<{ ok: boolean; text: string } | null>(null);
  const testDevice = async () => {
    setTesting(true);
    setTestMsg(null);
    try {
      const r: any = await TestStationDevice(device as any);
      setTestMsg(r?.ok
        ? { ok: true, text: t('station.testOk', { n: r.relays }) }
        : { ok: false, text: r?.error || t('station.testFail') });
    } catch (e: any) { setTestMsg({ ok: false, text: String(e?.message || e || t('station.testFail')) }); }
    finally { setTesting(false); }
  };
  useEffect(() => { if (isDenkovi) void detectDenkovi(); /* eslint-disable-next-line */ }, [isDenkovi]);
  return (
    <div ref={ref} className="mt-4 max-w-4xl rounded-xl border border-primary/40 bg-card p-4 space-y-3">
      <div className="text-sm font-semibold">{device.id ? t('station.editDevice') : t('station.addDevice')}</div>
      <div className="grid grid-cols-2 gap-3">
        <div className="space-y-1">
          <Label>{t('station.type')}</Label>
          <Select value={device.type} onValueChange={setType}>
            <SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
            <SelectContent>
              <SelectItem value="webswitch">WebSwitch 1216H (5 relays)</SelectItem>
              <SelectItem value="kmtronic">KMTronic 8-relay (LAN)</SelectItem>
              <SelectItem value="dingtian">Dingtian IOT relay (LAN / WiFi)</SelectItem>
              <SelectItem value="denkovi">Denkovi USB (FT245 / D2XX)</SelectItem>
              <SelectItem value="usbrelay">Denkovi USB (serial / COM)</SelectItem>
            </SelectContent>
          </Select>
        </div>
        <div className="space-y-1">
          <Label>{t('station.name')}</Label>
          <Input value={device.name} placeholder={TYPE_LABEL[device.type]} onChange={(e) => onChange({ ...device, name: e.target.value })} />
        </div>
      </div>
      {(isDenkovi || isUsbRelay || isDingtian) && (
        <div className="space-y-1 max-w-[10rem]">
          <Label>{t('station.channels')}</Label>
          <Select value={String(chanCount(device))} onValueChange={(v) => setChannels(parseInt(v, 10))}>
            <SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
            <SelectContent>
              {(isDenkovi ? [4, 8] : isDingtian ? [2, 4, 8, 16, 24, 32] : [1, 2, 4, 8, 16]).map((n) => (
                <SelectItem key={n} value={String(n)}>{n}</SelectItem>
              ))}
            </SelectContent>
          </Select>
        </div>
      )}
      {isDenkovi ? (
        <div className="space-y-1 max-w-md">
          <Label>{t('station.ftdiSerial')}</Label>
          <div className="flex items-center gap-2">
            <Input className="font-mono flex-1" value={device.host} placeholder="DAE0006K"
              list="denkovi-serials"
              onChange={(e) => onChange({ ...device, host: e.target.value })} />
            <datalist id="denkovi-serials">
              {denkoviSerials.map((s) => <option key={s} value={s} />)}
            </datalist>
            <Button size="sm" variant="outline" onClick={detectDenkovi} disabled={detecting}>
              {detecting ? <Loader2 className="size-3.5 mr-1 animate-spin" /> : <RefreshCw className="size-3.5 mr-1" />}
              {t('station.detect')}
            </Button>
          </div>
          <p className="text-[10px] text-muted-foreground">{t('station.ftdiHint')}</p>
          {detectMsg && <p className="text-[11px] font-medium text-muted-foreground">{detectMsg}</p>}
        </div>
      ) : isUsbRelay ? (
        <div className="space-y-1 max-w-md">
          <Label>{t('station.comPort')}</Label>
          <div className="flex items-center gap-2">
            <Select value={device.host || '_'} onValueChange={(v) => onChange({ ...device, host: v === '_' ? '' : v })}>
              <SelectTrigger className="h-9 flex-1"><SelectValue placeholder="— COM —" /></SelectTrigger>
              <SelectContent>
                {serialPorts.length === 0 && <SelectItem value="_" disabled>{t('station.noPorts')}</SelectItem>}
                {serialPorts.map((p) => <SelectItem key={p} value={p}>{p}</SelectItem>)}
              </SelectContent>
            </Select>
            <Button size="sm" variant="outline" onClick={() => ListSerialPorts().then((p) => setSerialPorts((p ?? []) as string[])).catch(() => {})}>
              <RefreshCw className="size-3.5" />
            </Button>
          </div>
          <p className="text-[10px] text-muted-foreground">{t('station.usbRelayHint')}</p>
        </div>
      ) : (
      <div className={cn('grid gap-3', (isKM || isDingtian) ? 'grid-cols-3' : 'grid-cols-1')}>
        <div className={cn('space-y-1', (isKM || isDingtian) ? '' : 'max-w-xs')}>
          <Label>{t('station.host')}</Label>
          <Input className="font-mono" value={device.host} placeholder="192.168.1.100" onChange={(e) => onChange({ ...device, host: e.target.value })} />
        </div>
        {/* Dingtian: both are OFF on a factory board — the session ID only when
            "HTTP Session" is enabled in its web page, the password only when a
            relay password is set. */}
        {isDingtian && (
          <>
            <div className="space-y-1">
              <Label>{t('station.dtSession')}</Label>
              <Input className="font-mono" value={device.user ?? ''} placeholder={t('station.optional')}
                onChange={(e) => onChange({ ...device, user: e.target.value })} />
            </div>
            <div className="space-y-1">
              <Label>{t('station.dtPwd')}</Label>
              <Input className="font-mono" value={device.pass ?? ''} placeholder="0"
                onChange={(e) => onChange({ ...device, pass: e.target.value.replace(/[^0-9]/g, '') })} />
            </div>
          </>
        )}
        {isKM && (
          <>
            <div className="space-y-1">
              <Label>{t('station.user')}</Label>
              <Input value={device.user ?? ''} placeholder={t('station.optional')} onChange={(e) => onChange({ ...device, user: e.target.value })} />
            </div>
            <div className="space-y-1">
              <Label>{t('station.pass')}</Label>
              <Input type="password" value={device.pass ?? ''} placeholder={t('station.optional')} onChange={(e) => onChange({ ...device, pass: e.target.value })} />
            </div>
          </>
        )}
      </div>
      )}
      <div className="space-y-1">
        <Label>{t('station.labels')}</Label>
        <div className="grid grid-cols-4 gap-2">
          {device.labels.map((lab, i) => (
            <Input key={i} value={lab} placeholder={`${t('station.relay')} ${i + 1}`} className="h-8 text-xs"
              onChange={(e) => { const labels = [...device.labels]; labels[i] = e.target.value; onChange({ ...device, labels }); }} />
          ))}
        </div>
      </div>
      <div className="flex items-center gap-2">
        {testMsg && (
          <span className={cn('inline-flex items-center gap-1.5 text-xs font-medium', testMsg.ok ? 'text-success' : 'text-danger')}>
            <span className={cn('size-2 rounded-full', testMsg.ok ? 'bg-success' : 'bg-danger')} />
            {testMsg.text}
          </span>
        )}
        <div className="ml-auto flex gap-2">
          <Button size="sm" variant="outline" onClick={testDevice} disabled={testing || !device.host.trim()}>
            {testing ? <Loader2 className="size-3.5 mr-1 animate-spin" /> : <PlugZap className="size-3.5 mr-1" />}
            {t('station.test')}
          </Button>
          <Button size="sm" variant="ghost" onClick={onCancel}><X className="size-3.5 mr-1" />{t('station.cancel')}</Button>
          <Button size="sm" onClick={onSave} disabled={!device.host.trim()}><Check className="size-3.5 mr-1" />{t('station.save')}</Button>
        </div>
      </div>
    </div>
  );
}
