// Live aircraft (ADS-B via OpenSky) as a BillboardCollection primitive.
// Thousands of billboards rebuilt wholesale each poll; entities would crawl.
// SPEC §6.3.
//
// Wave 2: when the clock is scrubbed off wall-clock now, instead of just
// hiding, the layer REPLAYS actual recorded traffic from history/aircraft?t=
// (served by serve.py from record.py's SQLite db). Replayed aircraft show with
// an amber status. Where no recorder ran (prod), the endpoint 404s and the
// layer degrades to the original hide-when-scrubbed behavior.

export default function create(ctx) {
  const { viewer, CONFIG, lib, ui, Cesium } = ctx;

  const bc = viewer.scene.primitives.add(new Cesium.BillboardCollection());
  const glyph = lib.aircraftGlyph(); // shared canvas, tinted per-billboard

  let enabled = true;   // HUD checkbox
  let isLive = true;    // clock at wall-clock now
  let inFlight = false; // a live fetch is currently running
  let timer = null;     // pending setTimeout handle
  let delay = CONFIG.aircraft.pollMs; // current cadence (grows on 429)

  // Replay state (only used when scrubbed off-live).
  let lastReplayT = null;    // epoch_s of the last snapshot we requested (debounce)
  let replayInFlight = false;
  let pendingReplayT = null;  // newest target requested while a fetch is in flight

  ui.addLayer('aircraft', 'Aircraft (live ADS-B)', true, (on) => {
    enabled = on;
    if (!on) { bc.show = false; return; }
    bc.show = true;
    if (isLive) kick();             // resume live polling
    else renderPlanes(lastPlanes);  // scrubbed → restore the replayed frame
  });

  // Military-callsign filter (default off): when on, only military aircraft
  // render. Military callsigns are always tinted, filter on or off.
  let militaryOnly = false;
  let lastPlanes = [];
  ui.addLayer('aircraft-mil', 'Military filter', false, (on) => {
    militaryOnly = on;
    renderPlanes(lastPlanes); // instant re-render of the current frame's planes
  });

  const MIL_PREFIXES = CONFIG.aircraft.militaryPrefixes || [];
  function isMilitary(callsign) {
    if (!callsign) return false;
    const cs = callsign.toUpperCase();
    return MIL_PREFIXES.some((p) => cs.startsWith(p));
  }

  // ---- normalized plane shape: {icao24, callsign, country, lon, lat, alt, vel, track} ----
  function stateToPlane(s) {
    return {
      icao24: s[0], callsign: (s[1] || '').trim(), country: s[2],
      lon: s[5], lat: s[6], alt: s[7], vel: s[9], track: s[10],
    };
  }
  // Recorded snapshot plane: [icao24, callsign, lon, lat, baro_alt, track, vel].
  function snapPlaneToPlane(a) {
    return {
      icao24: a[0], callsign: a[1], country: '—',
      lon: a[2], lat: a[3], alt: a[4], vel: a[6], track: a[5],
    };
  }

  // Factored build loop — rebuilds the whole collection from a plane array.
  // Shared by the live poll and by replay. Returns the feed total (the aircraft
  // row shows the total; the Military-filter row shows "N military of M"), so
  // the aircraft count stays stable whether or not the filter is engaged.
  function renderPlanes(planes) {
    lastPlanes = planes; // remembered so toggling the filter re-renders instantly
    bc.removeAll();
    let total = 0;
    let mil = 0;
    for (const p of planes) {
      if (p.lon == null || p.lat == null) continue;
      total++;
      const military = isMilitary(p.callsign);
      if (military) mil++;
      if (militaryOnly && !military) continue;
      bc.add({
        position: Cesium.Cartesian3.fromDegrees(p.lon, p.lat, p.alt || 0),
        image: glyph,
        // Military callsigns tint red regardless of altitude band; civil use the band.
        color: lib.cz(military ? CONFIG.colors.aircraftMil : lib.altitudeColor(CONFIG, p.alt)),
        rotation: lib.headingToRotation(p.track), // lib negates — do NOT negate again
        scale: military ? 0.72 : 0.55, // bump military so it stands out
        id: {
          layer: 'aircraft',
          icao24: p.icao24,
          callsign: p.callsign,
          country: p.country,
          alt: p.alt,
          vel: p.vel,
          track: p.track,
          military,
        },
      });
    }
    ui.setStatus('aircraft-mil', `${mil} military of ${total}`, militaryOnly ? 'ok' : 'off');
    return total;
  }

  // ---- live poll loop -------------------------------------------------------
  // Poll only while enabled, live, and the tab is visible.
  function canPoll() {
    return enabled && isLive && !document.hidden;
  }

  function schedule(ms) {
    if (timer) clearTimeout(timer);
    timer = setTimeout(tick, ms);
  }

  function kick() {
    if (!canPoll() || timer || inFlight) return;
    schedule(0);
  }

  async function tick() {
    timer = null;
    if (!canPoll() || inFlight) return;
    inFlight = true;
    try {
      await poll();
    } finally {
      inFlight = false;
    }
    if (canPoll()) schedule(delay);
  }

  function buildUrl() {
    let url = CONFIG.proxy.opensky; // RELATIVE — works at "/" and "/godsigh/"
    const bb = CONFIG.aircraft.bbox;
    if (bb) {
      const q = new URLSearchParams({
        lamin: bb.lamin, lomin: bb.lomin, lamax: bb.lamax, lomax: bb.lomax,
      });
      url = `${url}?${q.toString()}`;
    }
    return url;
  }

  async function poll() {
    let res;
    try {
      res = await fetch(buildUrl());
    } catch (err) {
      ui.setStatus('aircraft', 'network error — retrying', 'warn');
      return;
    }

    if (res.status === 429) {
      delay = delay * 4; // back off
      ui.setStatus('aircraft', 'rate-limited — anonymous OpenSky quota', 'warn');
      return;
    }
    if (!res.ok) {
      ui.setStatus('aircraft', `HTTP ${res.status} — retrying`, 'warn');
      return;
    }

    const remaining = res.headers.get('X-Rate-Limit-Remaining');

    let data;
    try {
      data = await res.json();
    } catch (err) {
      ui.setStatus('aircraft', 'bad response — retrying', 'warn');
      return;
    }

    // The clock may have scrubbed off-live while this fetch was in flight — if so,
    // drop it so we never paint live traffic over the replay view (mirrors the
    // isLive guards in drainReplay).
    if (!isLive) return;

    const states = Array.isArray(data && data.states) ? data.states : [];
    const planes = [];
    for (const s of states) {
      if (!s) continue;
      if (s[5] == null || s[6] == null || s[8]) continue; // no position or on-ground
      planes.push(stateToPlane(s));
    }
    const count = renderPlanes(planes);

    delay = CONFIG.aircraft.pollMs; // healthy response — reset cadence
    const stamp = data.time ? new Date(data.time * 1000).toLocaleTimeString() : '—';
    const quota = remaining != null ? ` · quota ${remaining}` : '';
    ui.setStatus('aircraft', `${count} aircraft · ${stamp}${quota}`, 'ok');
  }

  // ---- replay (scrubbed off-live) -------------------------------------------
  // Requests coalesce: while a history fetch is in flight, the newest requested
  // time is stashed and processed on completion — so a scrub that lands (even
  // paused, where onClockTick stops firing) still gets its final snapshot.
  function requestReplay(tSec) {
    pendingReplayT = tSec;
    drainReplay();
  }

  // A transient history failure (network blip / 5xx) while the clock is PAUSED
  // would otherwise wedge — onClockTick won't fire again to retry. Clear the
  // debounce and re-request once shortly. (404 is intentional graceful-degrade
  // and is NOT retried here.)
  function scheduleReplayRetry(tSec) {
    lastReplayT = null;
    setTimeout(() => { if (!isLive && !replayInFlight) requestReplay(tSec); }, 4000);
  }

  async function drainReplay() {
    if (replayInFlight || pendingReplayT === null) return;
    const tSec = pendingReplayT;
    pendingReplayT = null;
    replayInFlight = true;
    lastReplayT = tSec;
    try {
      const res = await fetch(`history/aircraft?t=${tSec}`);
      if (isLive) return; // clock snapped back to live mid-fetch — discard
      if (res.status === 404) {
        bc.removeAll();
        bc.show = false;
        ui.setStatus('aircraft', 'no history for this time', 'warn');
        return;
      }
      if (!res.ok) {
        ui.setStatus('aircraft', `history HTTP ${res.status}`, 'warn');
        scheduleReplayRetry(tSec);
        return;
      }
      let snap;
      try {
        snap = await res.json();
      } catch (err) {
        ui.setStatus('aircraft', 'bad history response', 'warn');
        scheduleReplayRetry(tSec);
        return;
      }
      if (isLive) return;
      const count = renderPlanes((snap.planes || []).map(snapPlaneToPlane));
      bc.show = enabled;
      const stamp = snap.t ? new Date(snap.t * 1000).toLocaleTimeString() : '—';
      // Amber = replayed traffic, not live (the LIVE/SCRUBBED chip already says SCRUBBED).
      ui.setStatus('aircraft', `replay · ${stamp} · ${count} aircraft`, 'warn');
    } catch (err) {
      ui.setStatus('aircraft', 'replay fetch error', 'warn');
      scheduleReplayRetry(tSec);
    } finally {
      replayInFlight = false;
      // A newer target arrived mid-fetch — process it now (covers a paused clock).
      if (pendingReplayT !== null && !isLive) drainReplay();
    }
  }

  function onClockTick(currentTime, live) {
    isLive = live;
    if (isLive) {
      lastReplayT = null;      // leaving replay — reset the debounce
      pendingReplayT = null;
      bc.show = enabled;
      if (enabled) kick();     // resume the live poll loop exactly as before
      return;
    }
    // Not live → replay recorded traffic (or gracefully hide if none recorded).
    if (!enabled) { bc.show = false; return; }
    const tSec = Math.round(Cesium.JulianDate.toDate(currentTime).getTime() / 1000);
    // Debounce on requested-time DISTANCE, not wall time: a scrub gesture fires
    // many ticks — only refetch when the target moves >60 s from the last snapshot.
    if (lastReplayT !== null && Math.abs(tSec - lastReplayT) <= 60) return;
    requestReplay(tSec);
  }

  function handlePick(picked) {
    if (picked?.id?.layer !== 'aircraft') return false;
    const id = picked.id;
    ui.showPickOverlay((id.military ? '✈⬤ ' : '✈ ') + (id.callsign || id.icao24), [
      ['Class', id.military ? 'Military (callsign)' : 'Civil'],
      ['Country', id.country || '—'],
      ['Altitude', `${Math.round(id.alt || 0)} m / ${Math.round((id.alt || 0) * 3.28084)} ft`],
      ['Speed', `${Math.round((id.vel || 0) * 1.94384)} kt`],
      ['Heading', `${Math.round(id.track || 0)}°`],
    ]);
    return true;
  }

  function clearPick() {
    ui.hidePickOverlay();
  }

  // Self-heal when the tab regains focus. A backgrounded tab kills the poll
  // loop (canPoll() is false, so tick() returns without rescheduling); if the
  // clock is also paused, onClockTick never fires again to restart it (its
  // currentTime is frozen), so the feed would stall permanently. kick() is
  // idempotent (guarded by timer/inFlight/canPoll), so this is safe.
  document.addEventListener('visibilitychange', () => { if (!document.hidden) kick(); });

  // One immediate fetch on init.
  kick();

  return { id: 'aircraft', onClockTick, handlePick, clearPick };
}
