// Растровое (Monte-Carlo) покрытие заявки на клиенте — ядро «слоя поддержки решений оператора».
// Чистые функции под юнит-тесты (см. coverage.test.ts, образец slotFilters.test.ts).
//
// Идея: вместо точного векторного union контуров считаем % по детерминированной сетке сэмплов
// внутри bbox заявки. Сэмпл «закрыт», если попал в union контуров выбранных/забронированных слотов.
// % = Σ cos(lat)-вес закрытых сэмплов внутри заявки / Σ вес всех сэмплов внутри заявки. Стабильно,
// дёшево на чек/снятие (per-slot маска кэшируется), ноль зависимостей.
//
//   выбор/бронь слотов ─▶ [сетка сэмплов заявки, cos-lat вес] ─▶ маски слотов (OR) ─▶ % + слой coverage
//                          [0 валидных→null]   [битый WKT→skip]   [bbox-прунинг]      [<порога→низкая точность]

import { pointInPolygon } from "../tgu-map-2d/geometry/pointInPolygon";
import type { GeoPoint } from "../tgu-map-2d/model/mapTypes";

/** Сторона сетки сэмплов (D1): фикс 64×64 ≈ 4096 точек по bbox заявки, независимо от размера. */
export const COVERAGE_GRID_N = 64;
/** Порог «низкой точности» (codex P1): тонкий полигон даёт мало сэмплов → % абсурдный. */
export const COVERAGE_MIN_SAMPLES = 16;

/** Детерминированная сетка сэмплов по bbox заявки (кэш на заявку). */
export type CoverageGrid = {
  readonly n: number;
  /** Опорная долгота фрейма unwrap (антимеридиан): все долготы приведены к [refLon-180, refLon+180]. */
  readonly refLon: number;
  /** Долготы сэмплов в unwrap-фрейме (length n*n). */
  readonly lons: Float64Array;
  /** Широты сэмплов (length n*n). */
  readonly lats: Float64Array;
  /** Вес сэмпла = max(cos(lat), 0) — компенсация искажения площади к полюсам. */
  readonly weights: Float64Array;
  /** 1, если сэмпл внутри заявки (union колец); только такие участвуют в %. */
  readonly inRequest: Uint8Array;
  /** Σ weight по сэмплам внутри заявки (знаменатель %). */
  readonly requestWeight: number;
  /** Кол-во сэмплов внутри заявки (для порога точности). */
  readonly sampleCount: number;
};

/** Маска слота: 1, если сэмпл внутри заявки И закрыт контуром слота (length n*n). */
export type SlotMask = Uint8Array;

/** Результат расчёта покрытия набора. */
export type CoverageResult = {
  /** Доля 0..1. */
  fraction: number;
  /** Целый процент 0..100 (для панели). */
  percent: number;
  /** Кол-во сэмплов заявки (диагностика). */
  sampleCount: number;
  /** sampleCount < COVERAGE_MIN_SAMPLES — % ненадёжен, панель помечает «низкая точность». */
  lowPrecision: boolean;
};

const DEG2RAD = Math.PI / 180;

/** Приводит долготу к непрерывному фрейму вокруг ref (унос через антимеридиан). */
function unwrapLonTo(lon: number, ref: number): number {
  let x = lon;
  while (x - ref > 180) x -= 360;
  while (ref - x > 180) x += 360;
  return x;
}

/** Оставляет кольца ≥3 точек и уносит их долготы в фрейм ref (битые/мелкие — отбрасываем). */
function unwrapRings(rings: GeoPoint[][], ref: number): GeoPoint[][] {
  const out: GeoPoint[][] = [];
  for (const ring of rings) {
    if (ring.length < 3) continue;
    out.push(ring.map((p) => ({ lon: unwrapLonTo(p.lon, ref), lat: p.lat })));
  }
  return out;
}

type Bounds = { minLon: number; minLat: number; maxLon: number; maxLat: number };

function boundsOfRings(rings: GeoPoint[][]): Bounds {
  let minLon = Number.POSITIVE_INFINITY;
  let minLat = Number.POSITIVE_INFINITY;
  let maxLon = Number.NEGATIVE_INFINITY;
  let maxLat = Number.NEGATIVE_INFINITY;
  for (const ring of rings) {
    for (const p of ring) {
      if (p.lon < minLon) minLon = p.lon;
      if (p.lon > maxLon) maxLon = p.lon;
      if (p.lat < minLat) minLat = p.lat;
      if (p.lat > maxLat) maxLat = p.lat;
    }
  }
  return { minLon, minLat, maxLon, maxLat };
}

/**
 * Строит сетку сэмплов по bbox заявки. Дегенерат (нет валидных колец или 0 сэмплов внутри) → null
 * («покрытие недоступно»). Антимеридиан — собственный unwrap-фрейм (примитив spatialIndex приватный).
 */
export function buildCoverageGrid(requestRings: GeoPoint[][]): CoverageGrid | null {
  const valid0 = requestRings.filter((r) => r.length >= 3);
  if (valid0.length === 0) return null;

  const refLon = valid0[0][0]!.lon;
  const rings = unwrapRings(valid0, refLon);
  if (rings.length === 0) return null;

  const b = boundsOfRings(rings);
  if (!Number.isFinite(b.minLon) || !Number.isFinite(b.minLat)) return null;

  const n = COVERAGE_GRID_N;
  const total = n * n;
  const lons = new Float64Array(total);
  const lats = new Float64Array(total);
  const weights = new Float64Array(total);
  const inRequest = new Uint8Array(total);

  const spanLon = b.maxLon - b.minLon;
  const spanLat = b.maxLat - b.minLat;
  let requestWeight = 0;
  let sampleCount = 0;

  for (let gy = 0; gy < n; gy++) {
    // Сэмплируем по центрам ячеек ((gy+0.5)/n), чтобы не попадать ровно на границу bbox/контура.
    const lat = b.minLat + ((gy + 0.5) / n) * spanLat;
    const w = Math.max(Math.cos(lat * DEG2RAD), 0);
    for (let gx = 0; gx < n; gx++) {
      const idx = gy * n + gx;
      const lon = b.minLon + ((gx + 0.5) / n) * spanLon;
      lons[idx] = lon;
      lats[idx] = lat;
      weights[idx] = w;
      const pt = { lon, lat };
      let inside = false;
      // union колец: нечётное число попаданий не требуется — это отдельные внешние контуры (MULTIPOLYGON),
      // достаточно попасть хотя бы в одно. Дыры (внутренние кольца WKT) игнорируются (лимит MVP-парсера).
      for (const ring of rings) {
        if (pointInPolygon(pt, ring)) {
          inside = true;
          break;
        }
      }
      if (inside) {
        inRequest[idx] = 1;
        requestWeight += w;
        sampleCount += 1;
      }
    }
  }

  if (sampleCount === 0) return null;

  return { n, refLon, lons, lats, weights, inRequest, requestWeight, sampleCount };
}

/**
 * Маска слота на сетке заявки: 1 там, где сэмпл внутри заявки И закрыт контуром слота.
 * bbox-прунинг (D2): сначала отбрасываем сэмплы вне bbox слота, потом точный pointInPolygon.
 * Битый WKT / кольца <3 → пустая маска (слот не участвует в покрытии, без падения).
 */
export function sampleSlotMask(grid: CoverageGrid, slotRings: GeoPoint[][]): SlotMask {
  const mask = new Uint8Array(grid.lons.length);
  const rings = unwrapRings(slotRings, grid.refLon);
  if (rings.length === 0) return mask;

  const b = boundsOfRings(rings);
  if (!Number.isFinite(b.minLon)) return mask;

  for (let idx = 0; idx < mask.length; idx++) {
    if (grid.inRequest[idx] === 0) continue;
    const lon = grid.lons[idx]!;
    const lat = grid.lats[idx]!;
    if (lon < b.minLon || lon > b.maxLon || lat < b.minLat || lat > b.maxLat) continue;
    const pt = { lon, lat };
    for (const ring of rings) {
      if (pointInPolygon(pt, ring)) {
        mask[idx] = 1;
        break;
      }
    }
  }
  return mask;
}

/** OR нескольких масок в одну (union покрытия набора). */
export function unionMasks(grid: CoverageGrid, masks: readonly SlotMask[]): SlotMask {
  const out = new Uint8Array(grid.lons.length);
  for (const mask of masks) {
    for (let i = 0; i < out.length; i++) if (mask[i]) out[i] = 1;
  }
  return out;
}

/** Покрытие по готовому union-маске. requestWeight<=0 (заявка у полюса) → null. */
export function coverageOf(grid: CoverageGrid, union: SlotMask): CoverageResult | null {
  if (grid.requestWeight <= 0) return null;
  let covered = 0;
  for (let idx = 0; idx < union.length; idx++) {
    if (union[idx]) covered += grid.weights[idx]!;
  }
  const fraction = covered / grid.requestWeight;
  return {
    fraction,
    percent: Math.round(fraction * 100),
    sampleCount: grid.sampleCount,
    lowPrecision: grid.sampleCount < COVERAGE_MIN_SAMPLES,
  };
}

/** Покрытие набора масок (booked∪selected): union → coverageOf. Пустой набор → 0%. */
export function coverageOfMasks(grid: CoverageGrid, masks: readonly SlotMask[]): CoverageResult | null {
  return coverageOf(grid, unionMasks(grid, masks));
}

/** Маргинальный вклад слота поверх текущего union: Σ cos-lat весов НОВЫХ закрытых сэмплов (не popcount). */
export function marginalGainWeight(grid: CoverageGrid, union: SlotMask, mask: SlotMask): number {
  let gain = 0;
  for (let idx = 0; idx < mask.length; idx++) {
    if (mask[idx] && !union[idx]) gain += grid.weights[idx]!;
  }
  return gain;
}

/** Кандидат для ранжирования по вкладу. */
export type RankCandidate = { key: string; mask: SlotMask };

/** Ранжированный кандидат: вклад на момент ранжирования (жадный порядок), доля и целый %. */
export type RankedCandidate = { key: string; gainFraction: number; gainPercent: number };

/**
 * Жадное ранжирование по маргинальному вкладу (взвешенному, codex P1): на каждом шаге берём
 * кандидата с максимальным приростом покрытия поверх уже отобранного union (старт — baseUnion,
 * напр. забронированное). Тай-брейк — исходный порядок (stable). Вклад фиксируется на момент
 * ранжирования (для колонки «+N%», подпись «вклад на момент ранжирования», не live).
 */
export function rankByMarginalGain(
  grid: CoverageGrid,
  candidates: readonly RankCandidate[],
  baseUnion?: SlotMask,
): RankedCandidate[] {
  const denom = grid.requestWeight > 0 ? grid.requestWeight : 1;
  const union = baseUnion ? Uint8Array.from(baseUnion) : new Uint8Array(grid.lons.length);
  const remaining = candidates.map((c, order) => ({ ...c, order }));
  const ranked: RankedCandidate[] = [];

  while (remaining.length > 0) {
    let bestIdx = 0;
    let bestGain = -1;
    for (let i = 0; i < remaining.length; i++) {
      const gain = marginalGainWeight(grid, union, remaining[i]!.mask);
      // Строгое > сохраняет исходный порядок при равенстве (стабильный тай-брейк).
      if (gain > bestGain) {
        bestGain = gain;
        bestIdx = i;
      }
    }
    const [picked] = remaining.splice(bestIdx, 1);
    const gainFraction = bestGain / denom;
    ranked.push({
      key: picked!.key,
      gainFraction,
      gainPercent: Math.round(gainFraction * 100),
    });
    for (let idx = 0; idx < union.length; idx++) if (picked!.mask[idx]) union[idx] = 1;
  }

  return ranked;
}
