import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import { RefreshCw, Star, Satellite as SatIcon, ClipboardPaste, Radio } from 'lucide-react';
import {
  GetSatelliteBirds, GetSatellitePositions, GetSatellitePasses, GetSatelliteTuning,
  GetSatelliteGroundTrack, GetSatelliteTLEInfo, RefreshSatelliteTLE, AddSatelliteElements,
  GetSatSettings, SaveSatSettings,
  StartSatelliteTracking, StopSatelliteTracking, GetSatelliteTracking,
} from '../../wailsjs/go/main/App';
import { EventsOn } from '../../wailsjs/runtime/runtime';
import { Button } from '@/components/ui/button';
import { gridToLatLon, splitAtAntimeridian } from '@/lib/maidenhead';
import { BASEMAPS, type BasemapKey } from '@/components/MainMap';
import { loadMapView, saveMapView } from '@/lib/mapView';
import { cn } from '@/lib/utils';
import { useI18n } from '@/lib/i18n';

// Satellites — where the birds are, and what to do with the radio.
//
// The tab answers three questions at once because on a pass there is no time to
// go looking for any of them: where the satellite is (the map), when the next
// one comes (the table), and what to tune (the readout). Everything is computed
// in Go from the same element set, so the dial and the map can never tell
// different stories.

type Bird = {
  name: string; norad: number; geostationary: boolean; favorite: boolean;
  has_elements: boolean; element_name: string; epoch_age_h: number;
  transponders?: {
    label: string; mode: string; down_lo: number; down_hi: number;
    up_lo: number; up_hi: number; inverting: boolean; ctcss: number; linear: boolean;
  }[];
};
type Position = {
  name: string; lat: number; lon: number; alt_km: number; footprint_km: number;
  az: number; el: number; range_km: number; range_rate: number;
};
type Pass = {
  name: string; aos: string; los: string; aos_az: number; los_az: number;
  max_el: number; max_el_az: number; max_el_at: string; duration_s: number;
};
type Tuning = {
  name: string; transponder: string; mode: string;
  nominal_down: number; nominal_up: number; down_hz: number; up_hz: number;
  ctcss: number; inverting: boolean;
  az: number; el: number; range_km: number; range_rate: number; visible: boolean;
};
type Track = {
  on: boolean; name: string; transponder: string; mode: string;
  nominal_down: number; nominal_up: number; down_hz: number; up_hz: number;
  az: number; el: number; visible: boolean;
  radio: string; // "sat" | "downlink-only" | ""
  error: string;
};

const MAP_VIEW_SAT = 'opslog.satMapView';

// Above the horizon is green, below is grey. Nothing subtler: on a pass the one
// thing an operator needs to read from across the room is whether the bird is
// up.
const upColour = (up: boolean) => (up ? 'var(--success)' : 'var(--muted-foreground)');

const fmtHz = (hz: number) => {
  if (!hz) return '—';
  const mhz = hz / 1e6;
  // Six decimals: a linear transponder is tuned to the hundred hertz, and the
  // Doppler correction moves the last three digits every second.
  return mhz.toFixed(6).replace(/(\d)(?=(\d{3})+\.)/g, '$1 ');
};
const fmtDeg = (d: number) => `${d.toFixed(1)}°`;
const hhmm = (iso: string) => {
  const d = new Date(iso);
  return Number.isNaN(d.getTime()) ? '—' : d.toISOString().slice(11, 16);
};
const inMin = (iso: string) => Math.round((Date.parse(iso) - Date.now()) / 60000);

export function SatellitePanel({ myGrid }: { myGrid: string }) {
  const { t } = useI18n();
  const [birds, setBirds] = useState<Bird[]>([]);
  const [sel, setSel] = useState<string>(() => localStorage.getItem('opslog.satSelected') || '');
  const [tpIdx, setTpIdx] = useState(0);
  const [positions, setPositions] = useState<Position[]>([]);
  const [passes, setPasses] = useState<Pass[]>([]);
  const [tuning, setTuning] = useState<Tuning | null>(null);
  const [tle, setTle] = useState<{ count: number; age_h: number; stale: boolean; custom: number } | null>(null);
  const [tracking, setTracking] = useState<Track | null>(null);
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState('');
  const [favs, setFavs] = useState<string[]>([]);

  const bird = useMemo(() => birds.find((b) => b.name === sel) ?? null, [birds, sel]);
  const tp = bird?.transponders?.[tpIdx] ?? null;

  // ── Data ─────────────────────────────────────────────────────────────────

  const loadBirds = useCallback(async () => {
    try {
      const list: Bird[] = (await GetSatelliteBirds()) as any;
      setBirds(list ?? []);
      setSel((cur) => {
        if (cur && (list ?? []).some((b) => b.name === cur)) return cur;
        // Nothing chosen yet: the first bird we can both find and tune.
        const first = (list ?? []).find((b) => b.has_elements && (b.transponders?.length ?? 0) > 0);
        return first?.name ?? cur;
      });
    } catch (e: any) { setErr(String(e?.message ?? e)); }
  }, []);

  const loadSettings = useCallback(async () => {
    try {
      const s: any = await GetSatSettings();
      setFavs(s?.favorites ?? []);
    } catch { /* favourites are a convenience; the list works without them */ }
  }, []);

  const loadTle = useCallback(async () => {
    try { setTle((await GetSatelliteTLEInfo()) as any); } catch { /* shown as unknown */ }
  }, []);

  const loadPasses = useCallback(async () => {
    try {
      setPasses(((await GetSatellitePasses([], 0)) as any) ?? []);
      setErr('');
    } catch (e: any) { setErr(String(e?.message ?? e)); }
  }, []);

  useEffect(() => { loadBirds(); loadSettings(); loadTle(); loadPasses(); }, [loadBirds, loadSettings, loadTle, loadPasses]);
  useEffect(() => EventsOn('sat:tle', () => { loadTle(); loadBirds(); loadPasses(); }), [loadTle, loadBirds, loadPasses]);
  useEffect(() => { if (sel) localStorage.setItem('opslog.satSelected', sel); }, [sel]);
  useEffect(() => { setTpIdx(0); }, [sel]);

  // The map's satellites, every five seconds: a low orbit moves about a third of
  // a degree of longitude in that time, which is a pixel or two at this zoom.
  useEffect(() => {
    let live = true;
    const tick = async () => {
      try {
        const p: Position[] = ((await GetSatellitePositions([])) as any) ?? [];
        if (live) setPositions(p);
      } catch { /* a missing locator is already reported by the passes call */ }
    };
    tick();
    const id = window.setInterval(tick, 5000);
    return () => { live = false; window.clearInterval(id); };
  }, []);

  // The readout, every second: this is the number an operator types into a
  // radio, and a Doppler correction on 70 cm moves by a few tens of hertz a
  // second at the middle of a pass.
  useEffect(() => {
    if (!sel) { setTuning(null); return; }
    let live = true;
    const tick = async () => {
      try {
        const tn: Tuning = (await GetSatelliteTuning(sel, tpIdx, 0)) as any;
        if (live) setTuning(tn);
      } catch { if (live) setTuning(null); }
    };
    tick();
    const id = window.setInterval(tick, 1000);
    return () => { live = false; window.clearInterval(id); };
  }, [sel, tpIdx]);

  // Passes are cheap but not free, and they change slowly.
  useEffect(() => {
    const id = window.setInterval(loadPasses, 5 * 60_000);
    return () => window.clearInterval(id);
  }, [loadPasses]);

  // The tracker's own state, pushed as it moves. Polled as well, at a lazy
  // rate, so a panel opened while tracking is already running is not blank
  // until the next tick.
  useEffect(() => {
    const read = async () => {
      try { setTracking((await GetSatelliteTracking()) as any); } catch { /* not tracking */ }
    };
    read();
    const off = EventsOn('sat:track', (s: any) => setTracking(s ?? null));
    const id = window.setInterval(read, 10_000);
    return () => { off(); window.clearInterval(id); };
  }, []);

  const toggleTracking = async () => {
    setErr('');
    try {
      if (tracking?.on) {
        await StopSatelliteTracking();
        setTracking(null);
      } else {
        await StartSatelliteTracking(sel, tpIdx);
        setTracking((await GetSatelliteTracking()) as any);
      }
    } catch (e: any) { setErr(String(e?.message ?? e)); }
  };

  const refreshTle = async () => {
    setBusy(true); setErr('');
    try {
      setTle((await RefreshSatelliteTLE()) as any);
      await loadBirds(); await loadPasses();
    } catch (e: any) { setErr(String(e?.message ?? e)); }
    setBusy(false);
  };

  const pasteElements = async () => {
    const text = window.prompt(t('sat.pastePrompt'));
    if (!text) return;
    try {
      const n: number = (await AddSatelliteElements(text)) as any;
      await loadBirds(); await loadPasses();
      setErr(n > 0 ? '' : t('sat.pasteNone'));
    } catch (e: any) { setErr(String(e?.message ?? e)); }
  };

  const toggleFav = async (name: string) => {
    const next = favs.includes(name) ? favs.filter((f) => f !== name) : [...favs, name];
    setFavs(next);
    try {
      const s: any = await GetSatSettings();
      await SaveSatSettings({ ...s, favorites: next });
      await loadBirds(); await loadPasses();
    } catch (e: any) { setErr(String(e?.message ?? e)); }
  };

  // ── Map ──────────────────────────────────────────────────────────────────

  const divRef = useRef<HTMLDivElement>(null);
  const mapRef = useRef<L.Map | null>(null);
  const layerRef = useRef<L.LayerGroup | null>(null);
  const baseRef = useRef<L.TileLayer | null>(null);
  const labelsRef = useRef<L.TileLayer | null>(null);
  const [basemap, setBasemap] = useState<BasemapKey>(() =>
    (localStorage.getItem('opslog.satMapBase') as BasemapKey) || 'light');
  const saved = useRef(loadMapView(MAP_VIEW_SAT));
  const [track, setTrack] = useState<Position[]>([]);

  const home = useMemo(() => gridToLatLon(myGrid), [myGrid]);

  useEffect(() => {
    if (!divRef.current || mapRef.current) return;
    const m = L.map(divRef.current, {
      zoomControl: true, attributionControl: true,
      worldCopyJump: false, preferCanvas: true,
      // Opened on the station, not on the Atlantic: the passes that matter are
      // the ones over the operator's own head.
      center: saved.current ? [saved.current.lat, saved.current.lon] : [home?.lat ?? 25, home?.lon ?? 0],
      zoom: saved.current ? saved.current.zoom : 3,
      minZoom: 2,
    });
    m.on('moveend', () => {
      const c = m.getCenter();
      saveMapView(MAP_VIEW_SAT, c.lat, c.lng, m.getZoom());
    });
    mapRef.current = m;
    layerRef.current = L.layerGroup().addTo(m);
    const ro = new ResizeObserver(() => m.invalidateSize({ animate: false }));
    ro.observe(divRef.current);
    const settle = window.setTimeout(() => m.invalidateSize({ animate: false }), 100);
    return () => {
      window.clearTimeout(settle);
      ro.disconnect();
      m.remove();
      mapRef.current = null;
      layerRef.current = null;
    };
  }, [home?.lat, home?.lon]);

  useEffect(() => {
    const m = mapRef.current;
    if (!m) return;
    baseRef.current?.remove(); labelsRef.current?.remove();
    const bm = BASEMAPS[basemap];
    const opts: L.TileLayerOptions = {
      maxNativeZoom: bm.maxNativeZoom, noWrap: true,
      bounds: L.latLngBounds(L.latLng(-85.0511, -180), L.latLng(85.0511, 180)),
    };
    baseRef.current = L.tileLayer(bm.url, { ...opts, attribution: bm.attr, subdomains: bm.subdomains ?? 'abc' }).addTo(m);
    if (bm.labelsUrl) labelsRef.current = L.tileLayer(bm.labelsUrl, opts).addTo(m);
    localStorage.setItem('opslog.satMapBase', basemap);
  }, [basemap]);

  // The selected bird's path over the ground, redrawn when the selection
  // changes and every couple of minutes as it walks off the front of it.
  useEffect(() => {
    if (!sel) { setTrack([]); return; }
    let live = true;
    const load = async () => {
      try {
        const pts: Position[] = ((await GetSatelliteGroundTrack(sel, 100)) as any) ?? [];
        if (live) setTrack(pts);
      } catch { if (live) setTrack([]); }
    };
    load();
    const id = window.setInterval(load, 120_000);
    return () => { live = false; window.clearInterval(id); };
  }, [sel]);

  useEffect(() => {
    const layer = layerRef.current;
    if (!layer) return;
    layer.clearLayers();
    if (home) {
      L.circleMarker([home.lat, home.lon], {
        radius: 5, color: '#fff', weight: 2, fillColor: '#e11d48', fillOpacity: 1,
      }).bindTooltip(myGrid, { direction: 'top' }).addTo(layer);
    }
    if (track.length > 1) {
      const pts = splitAtAntimeridian(track.map((p) => [p.lat, p.lon] as [number, number]));
      L.polyline(pts as L.LatLngExpression[][], {
        color: 'var(--info)', weight: 1.2, opacity: 0.7, dashArray: '4 4', smoothFactor: 0,
      }).addTo(layer);
    }
    for (const p of positions) {
      const chosen = p.name === sel;
      const up = p.el > 0;
      const colour = chosen ? '#22c55e' : up ? '#f59e0b' : '#9ca3af';
      // The footprint is the honest answer to "can I hear it": everything inside
      // the circle has the satellite above its horizon.
      L.circle([p.lat, p.lon], {
        radius: p.footprint_km * 1000,
        color: colour, weight: chosen ? 1.2 : 0.8, opacity: chosen ? 0.7 : 0.35,
        fillColor: colour, fillOpacity: chosen ? 0.1 : 0.05,
      }).addTo(layer);
      L.circleMarker([p.lat, p.lon], {
        radius: chosen ? 6 : 4, color: '#fff', weight: 1,
        fillColor: colour, fillOpacity: 1,
      })
        .bindTooltip(`${p.name} · ${fmtDeg(p.el)} · ${Math.round(p.alt_km)} km`, { direction: 'top' })
        .on('click', () => setSel(p.name))
        .addTo(layer);
    }
  }, [positions, track, home?.lat, home?.lon, myGrid, sel]);

  // ── Render ───────────────────────────────────────────────────────────────

  const tleLabel = !tle ? '—'
    : tle.count === 0 ? t('sat.tleNone')
    : t('sat.tleAge', { n: tle.count, h: Math.round(tle.age_h) });

  return (
    <div className="flex flex-col h-full min-h-0 gap-1 p-1">
      {/* Header: what to track, and what we know about the elements. */}
      <div className="flex items-center gap-2 flex-wrap px-1 shrink-0">
        <SatIcon className="size-4 text-muted-foreground" />
        <select
          className="h-7 rounded-md border border-border bg-background px-2 text-xs min-w-[12rem]"
          value={sel}
          onChange={(e) => setSel(e.target.value)}
        >
          {birds.map((b) => (
            <option key={b.name} value={b.name} disabled={!b.has_elements && !b.geostationary}>
              {b.favorite ? '★ ' : ''}{b.name}{b.has_elements ? '' : ` — ${t('sat.noElements')}`}
            </option>
          ))}
        </select>
        {(bird?.transponders?.length ?? 0) > 1 && (
          <select
            className="h-7 rounded-md border border-border bg-background px-2 text-xs"
            value={tpIdx}
            onChange={(e) => setTpIdx(Number(e.target.value))}
          >
            {bird!.transponders!.map((x, i) => (
              <option key={i} value={i}>{x.label}</option>
            ))}
          </select>
        )}
        {bird && (
          <Button variant="ghost" size="sm" className="h-7 px-2" onClick={() => toggleFav(bird.name)}
            title={t('sat.favTip')}>
            <Star className={cn('size-3.5', favs.includes(bird.name) && 'fill-warning text-warning')} />
          </Button>
        )}
        {/* Tracking is the one button on this panel that touches the radio, so
            it says which of the two things it is doing: holding both ends of
            the pass, or only the receiver on a rig with one. */}
        <Button
          size="sm"
          variant={tracking?.on ? 'default' : 'outline'}
          className={cn('h-7 px-2 gap-1.5', tracking?.on && 'bg-success text-background hover:bg-success/90')}
          onClick={toggleTracking}
          disabled={!bird?.transponders?.length}
          title={tracking?.on
            ? (tracking.radio === 'sat' ? t('sat.trackingFull') : t('sat.trackingDown'))
            : t('sat.trackTip')}
        >
          <Radio className="size-3.5" />
          {tracking?.on ? t('sat.tracking') : t('sat.track')}
        </Button>
        {tracking?.on && tracking.radio === 'downlink-only' && (
          <span className="text-[11px] text-warning">{t('sat.downlinkOnly')}</span>
        )}
        <div className="flex-1" />
        <span className={cn('text-[11px] tabular-nums', tle?.stale ? 'text-warning' : 'text-muted-foreground')}>
          {tleLabel}{tle?.custom ? ` · ${t('sat.tleCustom', { n: tle.custom })}` : ''}
        </span>
        <Button variant="outline" size="sm" className="h-7 px-2 gap-1.5" onClick={refreshTle} disabled={busy}>
          <RefreshCw className={cn('size-3.5', busy && 'animate-spin')} />
          {t('sat.refreshTle')}
        </Button>
        <Button variant="ghost" size="sm" className="h-7 px-2 gap-1.5" onClick={pasteElements} title={t('sat.pasteTip')}>
          <ClipboardPaste className="size-3.5" />
          {t('sat.paste')}
        </Button>
        <select
          className="h-7 rounded-md border border-border bg-background px-2 text-xs"
          value={basemap}
          onChange={(e) => setBasemap(e.target.value as BasemapKey)}
        >
          {Object.entries(BASEMAPS).map(([k, v]) => <option key={k} value={k}>{v.label}</option>)}
        </select>
      </div>

      {err && <div className="px-2 text-[11px] text-danger shrink-0">{err}</div>}

      <div className="flex gap-1 flex-1 min-h-0">
        {/* The map. */}
        <div className="flex-1 min-w-0 rounded-lg overflow-hidden border border-border">
          <div ref={divRef} className="h-full w-full" />
        </div>

        {/* The readout and the pass list. */}
        <div className="w-[19rem] shrink-0 flex flex-col gap-1 min-h-0">
          <div className="rounded-lg border border-border bg-card p-2">
            <div className="flex items-baseline justify-between">
              <span className="font-medium text-sm">{bird?.name ?? '—'}</span>
              <span className="text-[11px] text-muted-foreground">{tp?.label ?? ''}</span>
            </div>

            <div className="mt-2 grid grid-cols-2 gap-2">
              <Readout label={t('sat.az')} value={tuning ? fmtDeg(tuning.az) : '—'} />
              <Readout label={t('sat.el')} value={tuning ? fmtDeg(tuning.el) : '—'}
                colour={tuning ? upColour(tuning.visible) : undefined} />
            </div>

            <div className="mt-2 space-y-1">
              <FreqRow label={t('sat.down')} hz={tuning?.down_hz ?? 0} nominal={tuning?.nominal_down ?? 0} />
              <FreqRow label={t('sat.up')} hz={tuning?.up_hz ?? 0} nominal={tuning?.nominal_up ?? 0} />
            </div>

            <div className="mt-2 flex flex-wrap gap-x-3 gap-y-0.5 text-[11px] text-muted-foreground tabular-nums">
              {tuning && tuning.range_km > 0 && <span>{Math.round(tuning.range_km)} km</span>}
              {tuning && <span>{tuning.range_rate > 0 ? '+' : ''}{tuning.range_rate.toFixed(2)} km/s</span>}
              {!!tp?.ctcss && <span>CTCSS {tp.ctcss.toFixed(1)}</span>}
              {tp?.inverting && <span>{t('sat.inverting')}</span>}
              {bird?.geostationary && <span>{t('sat.geo')}</span>}
            </div>
          </div>

          <div className="rounded-lg border border-border bg-card flex-1 min-h-0 flex flex-col">
            <div className="px-2 py-1 text-[11px] font-medium text-muted-foreground border-b border-border shrink-0">
              {t('sat.nextPasses')}
            </div>
            <div className="flex-1 min-h-0 overflow-auto">
              {passes.length === 0 && (
                <div className="p-2 text-[11px] text-muted-foreground">{t('sat.noPasses')}</div>
              )}
              {passes.map((p, i) => {
                const mins = inMin(p.aos);
                const now = mins <= 0 && Date.parse(p.los) > Date.now();
                return (
                  <button
                    key={`${p.name}-${p.aos}-${i}`}
                    className={cn(
                      'w-full text-left px-2 py-1 flex items-center gap-2 text-[11px] tabular-nums hover:bg-accent/50',
                      p.name === sel && 'bg-accent/30',
                      now && 'text-success font-medium',
                    )}
                    onClick={() => setSel(p.name)}
                  >
                    <span className="w-20 truncate font-medium">{p.name}</span>
                    <span>{hhmm(p.aos)}</span>
                    <span className="text-muted-foreground">→ {hhmm(p.los)}</span>
                    <span className="ml-auto">{Math.round(p.max_el)}°</span>
                    <span className="w-14 text-right text-muted-foreground">
                      {now ? t('sat.now') : mins < 60 ? `${mins}m` : `${Math.round(mins / 60)}h`}
                    </span>
                  </button>
                );
              })}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

function Readout({ label, value, colour }: { label: string; value: string; colour?: string }) {
  return (
    <div className="rounded-md bg-muted/40 px-2 py-1">
      <div className="text-[10px] uppercase tracking-wide text-muted-foreground">{label}</div>
      <div className="text-lg font-semibold tabular-nums leading-tight" style={colour ? { color: colour } : undefined}>
        {value}
      </div>
    </div>
  );
}

// The corrected frequency large, the nominal one small underneath. Showing only
// one of them leaves an operator unable to tell a Doppler correction from a
// mistuned transponder.
function FreqRow({ label, hz, nominal }: { label: string; hz: number; nominal: number }) {
  const shift = hz && nominal ? hz - nominal : 0;
  return (
    <div className="flex items-baseline gap-2">
      <span className="w-10 text-[10px] uppercase tracking-wide text-muted-foreground">{label}</span>
      <span className="text-base font-semibold tabular-nums">{fmtHz(hz)}</span>
      {!!shift && (
        <span className="text-[10px] tabular-nums text-muted-foreground">
          {shift > 0 ? '+' : '−'}{Math.abs(Math.round(shift))} Hz
        </span>
      )}
    </div>
  );
}
