/* ============================================================
   realData.ts — реальный источник данных для дашборда «Группировка».
   Заменяет встроенную симуляцию (data.js/sim.js) данными из API по контрактам
   INTEGRATION.md §5:
     • состав    — платформы НСИ (tgu-planning), станции, заявки (ROI);
     • положение — реальная трасса flight-line (сэмплер позиции по времени);
     • сброс     — зоны радиовидимости (ЗРВ) из баллистики;
     • съёмка    — пролёт подспутниковой точки над полигоном заявки;
     • тень      — астрономическая модель из sim.js.
   Возвращает { env, domain, sched } в том же виде, что buildEnv() прототипа,
   так что ConstellationDashboard рендерит реальные данные без изменений разметки.
   ============================================================ */
import { fetchPlatforms, fetchPlans } from "../../api/tguApi";
import { fetchStations, type StationDto } from "../../api/stationsApi";
import { fetchRequestsForMap } from "../../api/requestApi";
import { fetchSatelliteRva, fetchFlightLine, type FlightLinePoint } from "../../api/ballisticsApi";
import { fetchPlanModes, type PlanMode } from "../tgu-editor/editorApi";
import type { TguPlatform, TguPlan } from "../../model/tguTypes";
import type { RequestMapItem } from "../../model/requestTypes";
import { isOperationalPlatform } from "../tgu-planning/tguTimelineMapper";
import { parseWktPolygon } from "../tgu-map-2d/geometry/wktParser";
import { parseLocal } from "../../utils/localTime";
// sim.js/data.js — JS-модули прототипа (allowJs); типы any, нам достаточно.
import { buildSchedule, wrapLon, pointInPoly } from "./sim.js";
import { COL } from "./data.js";

/** Полный объём бортового накопителя, Мбайт (500 ГБ). Согласовано как модельная ёмкость. */
const ONBOARD_CAPACITY_MB = 500 * 1024;

type Payload = "optic" | "radar";

export interface ConstellationSat {
  id: string;
  name: string;
  norad: number;
  payload: Payload;
  domain: "ОПТИКА" | "ЛОКАЦИЯ";
  group: string;
  color: string;
  mem0: number;
}

export interface ConstellationStation {
  id: string;
  name: string;
  lon: number;
  lat: number;
}

export interface ConstellationRoi {
  id: string;
  name: string;
  payload: Payload;
  poly: [number, number][];
}

export interface Interval {
  start: number;
  end: number;
  label?: string;
  /** Контур съёмки (для подсветки на карте), [lon,lat][]. Только у интервалов capture. */
  poly?: [number, number][];
  /** Координаты станции приёма (для линии сброса на карте). Только у интервалов downlink. */
  lon?: number;
  lat?: number;
}

export interface SchedTracks {
  capture: Interval[];
  downlink: Interval[];
  /** Связь с КА — окна радиовидимости (ЗРВ). Раньше показывались как «сброс». */
  link: Interval[];
  prohibit: Interval[];
  shadow: Interval[];
}

export interface ConstellationEnv {
  epochMs: number;
  sats: ConstellationSat[];
  stations: ConstellationStation[];
  rois: ConstellationRoi[];
  prohibits: Record<string, Interval[]>;
  /** Подспутниковая точка по реальной трассе; null — нет данных на это время. */
  subSat: (sat: ConstellationSat, tMs: number) => { lon: number; lat: number } | null;
  /** Готовое расписание (съёмка/сброс/связь/тень) — из него statusAt берёт состояние. */
  sched?: Record<string, SchedTracks>;
  /** Заполнение бортового накопителя на момент tMs, % от ёмкости. Из объёмов маршрутов плана. */
  memAt?: (sat: ConstellationSat, tMs: number) => number;
}

export interface ConstellationConfig {
  env: ConstellationEnv;
  domain: { start: number; end: number };
  sched: Record<string, SchedTracks>;
}

// --- классификация полезной нагрузки по имени КА (из НСИ) -------------------
// Поля payload у платформы нет — определяем по имени/миссии (радиолокация vs оптика).
const RADAR_KEYS = [
  "кондор", "kondor", "обзор", "obzor", "sar", "рса", "radar", "локац",
  "аркон", "arkon", "стрела", "strela", "пион", "neitron", "нейтрон"
];

function classifyPayload(name: string, mission: string | null | undefined): Payload {
  const s = `${name} ${mission ?? ""}`.toLowerCase();
  return RADAR_KEYS.some((k) => s.includes(k)) ? "radar" : "optic";
}

// Человеческие названия известных миссий НСИ → подгруппа в списке/таймлайне.
const MISSION_RU: Record<string, string> = {
  RESURS_P: "Ресурс-П",
  KANOPUS_V: "Канопус-В",
  KONDOR_FKA: "Кондор-ФКА",
  OBZOR_R: "Обзор-Р",
  TERRA_AQUA: "Terra/Aqua"
};

function prettifyMission(mission: string): string {
  return mission
    .toLowerCase()
    .split("_")
    .map((w) => (w ? w[0].toUpperCase() + w.slice(1) : w))
    .join(" ");
}

function deriveGroup(name: string, mission: string | null | undefined): string {
  if (mission) return MISSION_RU[mission] ?? prettifyMission(mission);
  // нет миссии — отрезаем хвост «No.3» / «№3» / завершающий номер
  const base = name.replace(/\s*(No\.?|№)\s*\d+.*$/i, "").trim();
  return base || name;
}

// --- сэмплер позиции по трассе flight-line ---------------------------------
type Sample = { t: number; lon: number; lat: number };

function buildSampler(points: FlightLinePoint[]): (t: number) => { lon: number; lat: number } | null {
  const arr: Sample[] = points
    .map((p) => ({ t: parseLocal(p.time), lon: wrapLon(p.long), lat: p.lat }))
    .filter((s) => Number.isFinite(s.t) && Number.isFinite(s.lon) && Number.isFinite(s.lat))
    .sort((a, b) => a.t - b.t);

  if (arr.length === 0) return () => null;
  const first = arr[0];
  const last = arr[arr.length - 1];

  return (t: number) => {
    if (t <= first.t) return { lon: first.lon, lat: first.lat };
    if (t >= last.t) return { lon: last.lon, lat: last.lat };
    let lo = 0;
    let hi = arr.length - 1;
    while (hi - lo > 1) {
      const mid = (lo + hi) >> 1;
      if (arr[mid].t <= t) lo = mid;
      else hi = mid;
    }
    const a = arr[lo];
    const b = arr[hi];
    const f = (t - a.t) / (b.t - a.t || 1);
    const lat = a.lat + (b.lat - a.lat) * f;
    // долготу интерполируем по кратчайшей дельте, чтобы не прыгать через антимеридиан
    let dlon = b.lon - a.lon;
    if (dlon > 180) dlon -= 360;
    else if (dlon < -180) dlon += 360;
    return { lon: wrapLon(a.lon + dlon * f), lat };
  };
}

// --- ROI из заявок ----------------------------------------------------------
const DEAD_REQUEST = new Set(["DELETED", "EXPIRED"]);

function roiFromRequest(item: RequestMapItem): ConstellationRoi | null {
  if (DEAD_REQUEST.has(item.status)) return null;
  const rings = parseWktPolygon(item.geometry);
  const ring = rings[0];
  if (!ring || ring.length < 3) return null;
  const poly: [number, number][] = ring.map((pt) => [wrapLon(pt.lon), pt.lat]);
  const payload: Payload = item.surveyType === "RSA" ? "radar" : "optic";
  return { id: item.id, name: item.name || item.id, payload, poly };
}

// --- порядок КА: домен (ОПТИКА → ЛОКАЦИЯ) → группа → имя --------------------
// Список и таймлайн группируют по соседству, поэтому порядок важен.
function compareSats(a: ConstellationSat, b: ConstellationSat): number {
  if (a.domain !== b.domain) return a.domain === "ОПТИКА" ? -1 : 1;
  if (a.group !== b.group) return a.group.localeCompare(b.group, "ru");
  return a.name.localeCompare(b.name, "ru");
}

// --- съёмка/сброс из реального плана -----------------------------------------
type StationGeo = { name: string; lon: number; lat: number };

interface PlanTracks {
  capture: Interval[];
  downlink: Interval[];
  /** Точки изменения заполнения борта: +volumeMb на конце съёмки, −volumeMb на конце сброса. */
  memEvents: { t: number; delta: number }[];
}

// Историю тянем так, чтобы покрыть начало горизонта (now-6ч): дни назад + запас.
function planHistoryDays(domainStart: number): number {
  const back = Math.ceil(Math.max(0, Date.now() - domainStart) / 86400e3);
  return Math.max(1, back + 1);
}

// Имя заявки, над которой идёт съёмка (по центру контура) — для подписи на таймлайне/карте.
function captureLabel(lon: number, lat: number, rois: ConstellationRoi[]): string {
  for (const r of rois) if (pointInPoly(lon, lat, r.poly)) return r.name;
  return "Съёмка";
}

// Режимы плана (SURVEY/DROP) → интервалы съёмки/сброса + дельты заполнения борта.
function buildPlanTracks(
  modes: PlanMode[],
  rois: ConstellationRoi[],
  stationByNumber: Map<number, StationGeo>
): PlanTracks {
  const capture: Interval[] = [];
  const downlink: Interval[] = [];
  const memEvents: { t: number; delta: number }[] = [];

  for (const m of modes) {
    const start = m.timeStart ? parseLocal(m.timeStart) : NaN;
    if (!Number.isFinite(start)) continue;
    const end = start + (m.duration ?? 0) * 1000;
    const vol = m.volumeMb ?? 0;
    if (m.type === "SURVEY") {
      const rings = m.contourWkt ? parseWktPolygon(m.contourWkt) : [];
      const poly = rings[0]?.map((pt) => [wrapLon(pt.lon), pt.lat] as [number, number]);
      capture.push({ start, end, label: captureLabel(m.longitude, m.lat, rois), poly });
      memEvents.push({ t: end, delta: vol });
    } else if (m.type === "DROP") {
      const st = m.station != null ? stationByNumber.get(Number(m.station)) : undefined;
      downlink.push({
        start,
        end,
        label: st?.name ?? (m.station != null ? `ст. ${m.station}` : "Сброс"),
        lon: st?.lon,
        lat: st?.lat
      });
      memEvents.push({ t: end, delta: -vol });
    }
  }

  capture.sort((a, b) => a.start - b.start);
  downlink.sort((a, b) => a.start - b.start);
  memEvents.sort((a, b) => a.t - b.t);
  return { capture, downlink, memEvents };
}

// Планы из pcp-tgu, пересекающие горизонт, → их режимы по КА (spacecraftId == NORAD).
async function loadPlanModesBySat(
  satIds: Set<string>,
  domainStart: number,
  domainEnd: number
): Promise<Map<string, PlanMode[]>> {
  const byId = new Map<string, PlanMode[]>();
  let plans: TguPlan[];
  try {
    plans = await fetchPlans(planHistoryDays(domainStart));
  } catch {
    return byId; // нет планов — съёмка/сброс просто не покажутся
  }
  const relevant = plans.filter((p) => {
    if (!satIds.has(p.spacecraftId)) return false;
    const s = parseLocal(p.startTime);
    const e = parseLocal(p.endTime);
    return Number.isFinite(s) && Number.isFinite(e) && e >= domainStart && s <= domainEnd;
  });
  const loaded = await Promise.all(
    relevant.map((p) =>
      fetchPlanModes(p.planId)
        .then((m) => ({ sid: p.spacecraftId, m }))
        .catch(() => ({ sid: p.spacecraftId, m: [] as PlanMode[] }))
    )
  );
  for (const { sid, m } of loaded) {
    const arr = byId.get(sid) ?? [];
    arr.push(...m);
    byId.set(sid, arr);
  }
  return byId;
}

// Заполнение борта на момент t: накопленный объём маршрутов (старт 0), % от ёмкости.
function memorySampler(memEvents: { t: number; delta: number }[]): (t: number) => number {
  if (memEvents.length === 0) return () => 0;
  return (t: number) => {
    let mb = 0;
    for (const e of memEvents) {
      if (e.t > t) break;
      mb += e.delta;
    }
    const pct = (Math.max(0, Math.min(ONBOARD_CAPACITY_MB, mb)) / ONBOARD_CAPACITY_MB) * 100;
    return pct;
  };
}

/**
 * Грузит реальный состав и геометрию группировки и собирает конфиг дашборда.
 * Горизонт по умолчанию now-6ч … now+18ч (как в прототипе). КА без трассы
 * (например, не эксплуатируемые) на карту не попадают.
 */
export async function loadConstellationEnv(): Promise<ConstellationConfig> {
  const now = Date.now();
  const domain = { start: now - 6 * 3600e3, end: now + 18 * 3600e3 };

  const [platforms, stationDtos, requestItems] = await Promise.all([
    fetchPlatforms(),
    fetchStations(),
    fetchRequestsForMap({})
      .then((r) => r.items)
      .catch(() => [] as RequestMapItem[])
  ]);

  const stations: ConstellationStation[] = (stationDtos as StationDto[]).map((s) => ({
    id: s.id ?? String(s.number),
    name: s.name,
    lon: s.position.long,
    lat: s.position.lat
  }));
  const stationByNumber = new Map<number, StationGeo>(
    (stationDtos as StationDto[]).map((s) => [
      s.number,
      { name: s.name, lon: s.position.long, lat: s.position.lat }
    ])
  );

  const rois: ConstellationRoi[] = requestItems
    .map(roiFromRequest)
    .filter((r): r is ConstellationRoi => r !== null);

  // Эксплуатируемые платформы с NORAD — кандидаты в группировку.
  const candidates = (platforms as TguPlatform[]).filter(
    (p) => isOperationalPlatform(p) && p.noradId != null
  );

  // Трассу и ЗРВ тянем по каждому КА параллельно; сбой по одному непрозрачен.
  const perSat = await Promise.all(
    candidates.map(async (p) => {
      const norad = Number(p.noradId);
      const [line, rva] = await Promise.all([
        fetchFlightLine(String(norad), domain.start, domain.end).catch(() => [] as FlightLinePoint[]),
        fetchSatelliteRva(norad, domain.start, domain.end).catch(() => [])
      ]);
      return { p, norad, line, rva };
    })
  );

  const sats: ConstellationSat[] = [];
  const samplers: Record<string, (t: number) => { lon: number; lat: number } | null> = {};
  const rvaBySat: Record<string, typeof perSat[number]["rva"]> = {};

  for (const { p, norad, line, rva } of perSat) {
    if (line.length === 0) continue; // нет трассы — на карте не разместить
    const id = String(norad);
    const name = p.name ?? id;
    const payload = classifyPayload(name, p.mission);
    sats.push({
      id,
      name,
      norad,
      payload,
      domain: payload === "radar" ? "ЛОКАЦИЯ" : "ОПТИКА",
      group: deriveGroup(name, p.mission),
      color: COL[payload],
      mem0: (norad % 60) + 15 // памяти борта нет в телеметрии — модельная стартовая
    });
    samplers[id] = buildSampler(line);
    rvaBySat[id] = rva;
  }

  sats.sort(compareSats);

  // Запреты работы (ВНЗ/питание) — источника в проекте нет: оставляем пустыми (заготовка).
  const prohibits: Record<string, Interval[]> = Object.fromEntries(sats.map((s) => [s.id, []]));

  const env: ConstellationEnv = {
    epochMs: now,
    sats,
    stations,
    rois,
    prohibits,
    subSat: (sat, t) => (samplers[sat.id] ? samplers[sat.id](t) : null)
  };

  // Съёмку и сброс берём из реального плана; тень считаем геометрически по трассе.
  const satIds = new Set(sats.map((s) => s.id));
  const planModesBySat = await loadPlanModesBySat(satIds, domain.start, domain.end);

  // buildSchedule даёт нам тень (shadow) по реальным позициям; съёмку/сброс из него игнорируем.
  const sched = buildSchedule(sats, env, domain.start, domain.end, 0.5) as ConstellationConfig["sched"];

  const memSamplers: Record<string, (t: number) => number> = {};
  for (const s of sats) {
    const tracks = sched[s.id];
    // Съёмка и сброс — из режимов плана (SURVEY/DROP).
    const plan = buildPlanTracks(planModesBySat.get(s.id) ?? [], rois, stationByNumber);
    tracks.capture = plan.capture;
    tracks.downlink = plan.downlink;
    memSamplers[s.id] = memorySampler(plan.memEvents);

    // «Связь с КА» — окна радиовидимости (ЗРВ). Раньше показывались как «сброс».
    const rva = rvaBySat[s.id];
    tracks.link = (rva ?? [])
      .map((z) => {
        const st = stationByNumber.get(z.stationId);
        return {
          start: parseLocal(z.onStart.time),
          end: parseLocal(z.onStop.time),
          label: st?.name ?? `ст. ${z.stationId}`,
          lon: st?.lon,
          lat: st?.lat
        };
      })
      .filter((iv) => Number.isFinite(iv.start) && Number.isFinite(iv.end) && iv.end > iv.start)
      .sort((a, b) => a.start - b.start);
  }

  // Расписание и заполнение борта — на env, чтобы statusAt/панель читали авторитетные данные.
  env.sched = sched;
  env.memAt = (sat, t) => (memSamplers[sat.id] ? memSamplers[sat.id](t) : 0);

  return { env, domain, sched };
}
