/* ============================================================
   sim.js — geometry + schedule (ESM)
   Чистая геометрия подспутниковой точки, тень, расписание событий.
   См. INTEGRATION.md — subSat()/statusAt() можно заменить на реальную эфемериду/API.
   ============================================================ */
const D2R = Math.PI / 180, R2D = 180 / Math.PI;
  const TWO_PI = Math.PI * 2;
  const SIDEREAL_MIN = 1436.07; // earth rotation period (min)

  function wrapLon(lon) {
    lon = ((lon + 180) % 360 + 360) % 360 - 180;
    return lon;
  }

  // sub-satellite point at sim time t(ms), given epoch ms.
  function subSat(sat, tMs, epochMs) {
    const m = (tMs - epochMs) / 60000; // minutes since epoch
    const o = sat.orbit;
    const M = o.phase * TWO_PI + TWO_PI * (m / o.period);
    const lat = o.latAmp * Math.sin(M);
    // one sine wave per orbit across full longitude, drifting west via earth rotation
    const lon = wrapLon(o.lon0 + 360 * (m / o.period) - 360 * (m / SIDEREAL_MIN));
    return { lon, lat };
  }

  // subsolar point (approx, declination ignored -> equatorial)
  function subSolar(tMs) {
    const secOfDay = (tMs / 1000) % 86400;
    const lon = wrapLon(180 - (secOfDay / 86400) * 360);
    // small seasonal declination wobble for life
    const dayFrac = (tMs / 86400000) % 365.25;
    const lat = 23.4 * Math.sin((dayFrac / 365.25) * TWO_PI - 1.4);
    return { lon, lat };
  }

  // angular (great-circle) distance in degrees
  function angDist(lon1, lat1, lon2, lat2) {
    const φ1 = lat1 * D2R, φ2 = lat2 * D2R;
    const dφ = (lat2 - lat1) * D2R;
    const dλ = (lon2 - lon1) * D2R;
    const a = Math.sin(dφ / 2) ** 2 + Math.cos(φ1) * Math.cos(φ2) * Math.sin(dλ / 2) ** 2;
    return 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) * R2D;
  }

  function inShadow(lon, lat, tMs) {
    const ss = subSolar(tMs);
    return angDist(lon, lat, ss.lon, ss.lat) > 90;
  }

  // point in polygon (ray casting), poly = [[lon,lat],...]
  function pointInPoly(lon, lat, poly) {
    let inside = false;
    for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
      const xi = poly[i][0], yi = poly[i][1];
      const xj = poly[j][0], yj = poly[j][1];
      const intersect = (yi > lat) !== (yj > lat) &&
        lon < ((xj - xi) * (lat - yi)) / (yj - yi) + xi;
      if (intersect) inside = !inside;
    }
    return inside;
  }

  const STATION_RANGE = 22; // deg ground range for downlink visibility

  // which ROI is the sat over (matching payload), null if none
  function overROI(sat, p, rois) {
    for (const r of rois) {
      if (r.payload !== sat.payload) continue;
      if (pointInPoly(p.lon, p.lat, r.poly)) return r;
    }
    return null;
  }
  // best (nearest in-range) station, null if none
  function nearStation(p, stations) {
    let best = null, bd = STATION_RANGE;
    for (const st of stations) {
      const d = angDist(p.lon, p.lat, st.lon, st.lat);
      if (d < bd) { bd = d; best = st; }
    }
    return best;
  }

  // --- prohibition windows (интервалы запрета) per sat ----------
  function buildProhibits(sats, epochMs) {
    const H = 3600e3;
    const out = {};
    sats.forEach((s, i) => {
      const list = [];
      // deterministic-ish spread
      const a0 = (i * 1.9) % 22 - 4;        // hours from epoch
      list.push({ start: epochMs + a0 * H, end: epochMs + (a0 + 0.7 + (i % 3) * 0.15) * H, reason: "ВНЗ" });
      if (i % 2 === 0) {
        const a1 = a0 + 8 + (i % 4);
        list.push({ start: epochMs + a1 * H, end: epochMs + (a1 + 0.9) * H, reason: "ОГРАН. ПИТАНИЕ" });
      }
      out[s.id] = list;
    });
    return out;
  }
  function inProhibit(list, tMs) {
    if (!list) return null;
    for (const w of list) if (tMs >= w.start && tMs < w.end) return w;
    return null;
  }

  // sub-satellite point: реальный сэмплер из env.subSat (эфемерида/flight-line),
  // иначе встроенная синусоидальная симуляция. См. INTEGRATION.md §5.2.
  function subSatOf(sat, tMs, env) {
    return env.subSat ? env.subSat(sat, tMs) : subSat(sat, tMs, env.epochMs);
  }

  // interval containing t in a sorted/unsorted list, else null
  function intervalAt(list, tMs) {
    if (!list) return null;
    for (const iv of list) if (tMs >= iv.start && tMs < iv.end) return iv;
    return null;
  }

  // live status of a sat at time t (priority order)
  // returns {state, roi, station, eclipse, prohibit}
  // С реальным расписанием (env.sched) состояние берём из готовых интервалов плана
  // (съёмка/сброс/запрет); иначе — встроенная геометрическая симуляция прототипа.
  function statusAt(sat, tMs, env) {
    const p = subSatOf(sat, tMs, env);
    const eclipse = inShadow(p.lon, p.lat, tMs);
    if (env.sched) {
      const tr = env.sched[sat.id] || {};
      const prohibit = intervalAt(tr.prohibit, tMs);
      if (prohibit) return { state: "prohibit", p, prohibit: { reason: prohibit.label }, eclipse };
      const cap = intervalAt(tr.capture, tMs);
      if (cap) return { state: "capture", p, roi: { name: cap.label, poly: cap.poly }, eclipse };
      const dl = intervalAt(tr.downlink, tMs);
      if (dl) return { state: "downlink", p, station: { name: dl.label, lon: dl.lon, lat: dl.lat }, eclipse };
      return { state: "operational", p, eclipse };
    }
    const prohibit = inProhibit(env.prohibits[sat.id], tMs);
    if (prohibit) return { state: "prohibit", p, prohibit, eclipse };
    const roi = overROI(sat, p, env.rois);
    if (roi) return { state: "capture", p, roi, eclipse };
    const st = nearStation(p, env.stations);
    if (st) return { state: "downlink", p, station: st, eclipse };
    return { state: "operational", p, eclipse };
  }

  // --- precompute event intervals over a domain for the timeline -
  function buildSchedule(sats, env, domainStart, domainEnd, stepMin) {
    const step = stepMin * 60000;
    const sched = {};
    for (const s of sats) {
      const tracks = { capture: [], downlink: [], prohibit: [], shadow: [] };
      // prohibits are explicit
      (env.prohibits[s.id] || []).forEach((w) =>
        tracks.prohibit.push({ start: w.start, end: w.end, label: w.reason }));
      let cur = { capture: null, downlink: null, shadow: null };
      for (let t = domainStart; t <= domainEnd; t += step) {
        const p = subSatOf(s, t, env);
        const proh = inProhibit(env.prohibits[s.id], t);
        const shadow = inShadow(p.lon, p.lat, t);
        const roi = !proh ? overROI(s, p, env.rois) : null;
        const st = !proh && !roi ? nearStation(p, env.stations) : null;
        // shadow band
        if (shadow) { if (!cur.shadow) cur.shadow = { start: t, end: t }; else cur.shadow.end = t; }
        else if (cur.shadow) { tracks.shadow.push(cur.shadow); cur.shadow = null; }
        // capture band
        if (roi) { if (!cur.capture) cur.capture = { start: t, end: t, label: roi.name }; else cur.capture.end = t; }
        else if (cur.capture) { tracks.capture.push(cur.capture); cur.capture = null; }
        // downlink band
        if (st) { if (!cur.downlink) cur.downlink = { start: t, end: t, label: st.name }; else cur.downlink.end = t; }
        else if (cur.downlink) { tracks.downlink.push(cur.downlink); cur.downlink = null; }
      }
      if (cur.shadow) tracks.shadow.push(cur.shadow);
      if (cur.capture) tracks.capture.push(cur.capture);
      if (cur.downlink) tracks.downlink.push(cur.downlink);
      sched[s.id] = tracks;
    }
    return sched;
  }

export {
  D2R, R2D, wrapLon, subSat, subSolar, angDist, inShadow,
  pointInPoly, overROI, nearStation, buildProhibits, inProhibit,
  statusAt, intervalAt, buildSchedule, STATION_RANGE,
};
