// Satellites layer (SPEC §6.2): SGP4-propagated EO/recon satellites as
// time-dynamic Cesium entities with orbit paths. Fetches TLEs from Celestrak
// via the same-origin proxy; one failed source never sinks the layer.

export default function create(ctx) {
  const { viewer, CONFIG, lib, ui, Cesium, start, stop } = ctx;
  const satellite = window.satellite;

  const ds = new Cesium.CustomDataSource('satellites');
  viewer.dataSources.add(ds);

  // Ordered token → color lookup (first match wins).
  const COLOR_TOKENS = ['GAOFEN', 'COSMOS', 'WORLDVIEW', 'LEGION', 'CAPELLA',
                        'PLEIADES', 'SENTINEL', 'ISS', 'CSS'];

  // Live toggle state — also applied to entities created later during the
  // chunked propagation loop, so toggling off mid-load stays honored.
  let showSats = true;
  let showPaths = true;

  ui.addLayer('satellites', 'Satellites', true, (on) => {
    showSats = on;
    for (const e of ds.entities.values) {
      if (e.point) e.point.show = on;
      if (e.label) e.label.show = on;
    }
    if (!on) groundTrack.show = false; // don't leave a stray track when sats are hidden
  });
  ui.addLayer('sat-paths', 'Orbit paths', true, (on) => {
    showPaths = on;
    for (const e of ds.entities.values) {
      if (e.path) e.path.show = on;
    }
  });

  ui.setStatus('satellites', 'loading TLEs…', 'warn');
  ui.setStatus('sat-paths', '', 'ok');

  // ---- ground track on selection (Wave 2 polish) ----
  // One reusable dashed polyline showing the selected sat's sub-satellite path
  // (height 0) over ±half an orbit around now. Recomputed on demand from the
  // satrec so we don't precompute 97 tracks up front.
  const satMeta = new Map(); // entity.id → { satrec, hex, periodSec }
  const groundTrack = ds.entities.add({
    id: 'sat-ground-track',
    show: false,
    polyline: {
      positions: [],
      width: 1.5,
      material: new Cesium.PolylineDashMaterialProperty({ color: Cesium.Color.WHITE }),
    },
  });

  function groundTrackPositions(satrec, periodSec) {
    const positions = [];
    const half = periodSec / 2;
    const step = Math.max(20, periodSec / 180); // ~one orbit in ~180 samples
    // Center on the VIEWER clock, not wall-clock now, so a scrubbed timeline's
    // track still passes under the (time-dynamic) satellite dot.
    const nowJd = viewer.clock.currentTime.clone();
    for (let dt = -half; dt <= half; dt += step) {
      const date = Cesium.JulianDate.toDate(
        Cesium.JulianDate.addSeconds(nowJd, dt, new Cesium.JulianDate()));
      let pv;
      try { pv = satellite.propagate(satrec, date); } catch { pv = null; }
      if (!pv || !pv.position) continue;
      const geo = satellite.eciToGeodetic(pv.position, satellite.gstime(date));
      if (!isFinite(geo.longitude) || !isFinite(geo.latitude)) continue;
      // Cartesian3 positions → the 3D polyline hugs the globe, no antimeridian streak.
      positions.push(Cesium.Cartesian3.fromRadians(geo.longitude, geo.latitude, 0));
    }
    return positions;
  }

  // selectedEntityChanged fires with undefined on deselect — handle it.
  viewer.selectedEntityChanged.addEventListener((sel) => {
    const meta = sel && satMeta.get(sel.id);
    if (!meta) { groundTrack.show = false; return; }
    groundTrack.polyline.positions = groundTrackPositions(meta.satrec, meta.periodSec);
    groundTrack.polyline.material =
      new Cesium.PolylineDashMaterialProperty({ color: lib.cz(meta.hex) });
    groundTrack.show = true;
  });

  // --- Parse raw TLE text into {name, l1, l2} triplets. ---
  function parseTLE(text) {
    const lines = text.split(/\r?\n/).map((s) => s.replace(/\s+$/, '')).filter((s) => s.length);
    // Celestrak "NAME=" queries can return "No GP data found" as plain text:
    // a valid TLE stream's first data line (line1) starts with '1'.
    const out = [];
    for (let i = 0; i + 2 < lines.length; i += 3) {
      const name = lines[i];
      const l1 = lines[i + 1];
      const l2 = lines[i + 2];
      if (!l1 || !l2 || l1[0] !== '1' || l2[0] !== '2') return out.length ? out : [];
      out.push({ name: name.trim(), l1, l2 });
    }
    return out;
  }

  function passesSource(name, source) {
    if (source.keepOnly) return source.keepOnly.includes(name);
    if (source.filter) {
      const up = name.toUpperCase();
      return source.filter.some((tok) => up.includes(tok.toUpperCase()));
    }
    return true;
  }

  function colorForName(name) {
    const up = name.toUpperCase();
    for (const tok of COLOR_TOKENS) {
      if (up.includes(tok)) return CONFIG.colors[tok] || CONFIG.colors.satDefault;
    }
    return CONFIG.colors.satDefault;
  }

  const yieldTick = () => new Promise((r) => setTimeout(r, 0));

  async function run() {
    const sources = CONFIG.satellites.sources;
    const settled = await Promise.allSettled(
      sources.map((s) => fetch(`${CONFIG.proxy.celestrak}?${s.q}`).then((res) => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.text();
      }))
    );

    // Collect candidate sats, deduped by NORAD id, tagged with their source.
    const seen = new Set();
    const candidates = [];
    let failedSources = 0;
    settled.forEach((r, i) => {
      const source = sources[i];
      if (r.status === 'rejected') { failedSources++; return; }
      const triplets = parseTLE(r.value);
      let srcCount = 0;
      for (const t of triplets) {
        if (candidates.length >= CONFIG.satellites.cap) break;
        if (source.max && srcCount >= source.max) break;
        if (!passesSource(t.name, source)) continue;
        let satrec;
        try { satrec = satellite.twoline2satrec(t.l1, t.l2); } catch { continue; }
        if (!satrec || satrec.error) continue;
        const id = satrec.satnum;
        if (seen.has(id)) continue;
        seen.add(id);
        candidates.push({ name: t.name, satrec, q: source.q });
        srcCount++;
      }
    });

    if (!candidates.length) {
      const msg = failedSources ? `no data (${failedSources}/${sources.length} sources failed)` : 'no satellites';
      ui.setStatus('satellites', msg, 'err');
      return;
    }

    // --- Chunked SGP4 propagation so first paint isn't blocked. ---
    const stepSec = CONFIG.satellites.sampleStepSec;
    let built = 0;
    const perSource = new Map(); // q → count

    for (let i = 0; i < candidates.length; i += 10) {
      const chunk = candidates.slice(i, i + 10);
      for (const c of chunk) {
        const entity = buildEntity(c, stepSec);
        if (entity) {
          ds.entities.add(entity);
          built++;
          perSource.set(c.q, (perSource.get(c.q) || 0) + 1);
        }
      }
      ui.setStatus('satellites', `propagating ${Math.min(i + 10, candidates.length)}/${candidates.length}…`, 'warn');
      await yieldTick();
    }

    if (!built) {
      ui.setStatus('satellites', 'all TLEs failed propagation', 'err');
      return;
    }

    const parts = [...perSource.entries()].map(([q, n]) => `${n}×${q.split('&')[0]}`);
    const summary = `${built} sats` + (failedSources ? ` (${failedSources} src failed)` : '');
    ui.setStatus('satellites', summary, failedSources ? 'warn' : 'ok');
    ui.setStatus('sat-paths', parts.join(', '), 'ok');
  }

  function buildEntity(c, stepSec) {
    const { name, satrec, q } = c;
    const posProp = new Cesium.SampledPositionProperty();
    let valid = 0;

    let t = start.clone();
    while (Cesium.JulianDate.lessThanOrEquals(t, stop)) {
      const date = Cesium.JulianDate.toDate(t);
      let pv;
      try { pv = satellite.propagate(satrec, date); } catch { pv = null; }
      if (pv && pv.position) {
        const p = pv.position;
        if (isFinite(p.x) && isFinite(p.y) && isFinite(p.z)) {
          const gmst = satellite.gstime(date);
          const geo = satellite.eciToGeodetic(p, gmst);
          if (isFinite(geo.longitude) && isFinite(geo.latitude) && isFinite(geo.height)) {
            posProp.addSample(t.clone(), Cesium.Cartesian3.fromRadians(
              geo.longitude, geo.latitude, geo.height * 1000));
            valid++;
          }
        }
      }
      t = Cesium.JulianDate.addSeconds(t, stepSec, new Cesium.JulianDate());
    }

    if (valid < 50) return null;
    posProp.setInterpolationOptions({
      interpolationDegree: 5,
      interpolationAlgorithm: Cesium.LagrangePolynomialApproximation,
    });

    const hex = colorForName(name);
    const color = lib.cz(hex);
    const periodSec = (2 * Math.PI / satrec.no) * 60; // satrec.no is rad/min

    // Orbital element derivations for the InfoBox.
    const incDeg = Cesium.Math.toDegrees(satrec.inclo);
    const nRadS = satrec.no / 60; // rad/s
    const a = Math.pow(398600.4418 / (nRadS * nRadS), 1 / 3); // km
    const e = satrec.ecco;
    const apogee = a * (1 + e) - 6378.137;
    const perigee = a * (1 - e) - 6378.137;
    const description =
      `<table class="cesium-infoBox-defaultTable"><tbody>` +
      `<tr><th>NORAD</th><td>${satrec.satnum}</td></tr>` +
      `<tr><th>Source</th><td>${q}</td></tr>` +
      `<tr><th>Period</th><td>${(periodSec / 60).toFixed(1)} min</td></tr>` +
      `<tr><th>Inclination</th><td>${incDeg.toFixed(2)}°</td></tr>` +
      `<tr><th>Apogee</th><td>${apogee.toFixed(0)} km</td></tr>` +
      `<tr><th>Perigee</th><td>${perigee.toFixed(0)} km</td></tr>` +
      `</tbody></table>`;

    const entity = new Cesium.Entity({
      name,
      position: posProp,
      point: {
        pixelSize: 6,
        color,
        show: showSats,
      },
      label: {
        text: name,
        show: showSats,
        font: '10px ui-monospace',
        pixelOffset: new Cesium.Cartesian2(0, -12),
        translucencyByDistance: new Cesium.NearFarScalar(1.5e6, 1.0, 1.5e7, 0.0),
        disableDepthTestDistance: Number.POSITIVE_INFINITY,
      },
      path: {
        width: 1,
        show: showPaths,
        resolution: 120,
        leadTime: periodSec / 2,
        trailTime: periodSec / 2,
        material: new Cesium.PolylineGlowMaterialProperty({
          glowPower: 0.2,
          color: color.withAlpha(0.35),
        }),
      },
      description,
    });
    // Stash what the ground-track overlay needs to recompute on selection.
    satMeta.set(entity.id, { satrec, hex, periodSec });
    return entity;
  }

  run().catch((err) => {
    console.error('[godsigh] satellites layer failed:', err);
    ui.setStatus('satellites', `error: ${err.message || err}`, 'err');
  });

  return { id: 'satellites' };
}
