import type { Feature, FeatureCollection, MultiLineString, Polygon, MultiPolygon, Position } from 'geojson';

/**
 * Loads the Palestine/Israel outer boundary GeoJSON from /public.
 *
 * This is intentionally fetched at runtime (not bundled) so the boundary file
 * can be swapped without rebuilding the app. Replace
 * `/pi-boundary.json` with whatever authoritative source the project
 * settles on later.
 */
export async function loadPIBoundary(): Promise<FeatureCollection<Polygon | MultiPolygon>> {
  const res = await fetch('/pi-boundary.json');
  if (!res.ok) {
    throw new Error(`Failed to load PI boundary: ${res.status} ${res.statusText}`);
  }
  const fc = (await res.json()) as FeatureCollection<Polygon | MultiPolygon>;
  return dropSliverHoles(fc);
}

/**
 * Minimum interior-ring area, in square kilometres, for a hole to be treated as
 * a real enclave rather than an artefact.
 */
const MIN_HOLE_KM2 = 1;

/**
 * Remove sliver holes from the boundary polygons.
 *
 * The shipped boundary is a union of two independently-drawn sources (OSM for
 * Israel, with a Golan cut, plus UN OCHA for Palestine). Wherever those
 * outlines disagreed by a few metres the union left a gap, and the file carries
 * 238 interior rings totalling under 2 km² against a 27,000 km² region -- 201
 * of them smaller than a single hectare.
 *
 * They are not enclaves, but every inside-PI test honours them, so a vertex
 * placed on one is silently refused with no visible reason. Dropping anything
 * below `MIN_HOLE_KM2` also makes each point-in-polygon test markedly cheaper,
 * since those tests run on every mousemove while drawing.
 *
 * Real enclaves, if the boundary ever gains any, are far larger than the
 * threshold and pass through untouched.
 */
function dropSliverHoles(
  fc: FeatureCollection<Polygon | MultiPolygon>,
): FeatureCollection<Polygon | MultiPolygon> {
  const keepRings = (rings: Position[][]): Position[][] =>
    rings.filter((ring, i) => i === 0 || ringAreaKm2(ring) >= MIN_HOLE_KM2);

  return {
    ...fc,
    features: fc.features.map((feat) => {
      const geom = feat.geometry;
      if (geom.type === 'Polygon') {
        return {
          ...feat,
          geometry: { ...geom, coordinates: keepRings(geom.coordinates) },
        };
      }
      return {
        ...feat,
        geometry: { ...geom, coordinates: geom.coordinates.map(keepRings) },
      };
    }),
  };
}

/** Approximate ring area in km², via the shoelace formula in degrees. */
function ringAreaKm2(ring: Position[]): number {
  let area = 0;
  for (let i = 0, n = ring.length - 1; i < n; i++) {
    area += ring[i][0] * ring[i + 1][1] - ring[i + 1][0] * ring[i][1];
  }
  const sqDeg = Math.abs(area) / 2;
  // At PI latitudes one degree of longitude is 111.32 km * cos(lat).
  return sqDeg * 111.32 * 111.32 * Math.cos((31.5 * Math.PI) / 180);
}

/**
 * Convert the PI boundary polygons into a single LineString/MultiLineString
 * representing just the outer rings. Used for proximity tests and for the
 * boundary trace that auto-complete walks when closing a shape.
 *
 * Rings are returned at their SOURCE resolution. An earlier version densified
 * every ring to 50 m spacing as a workaround: turf's `lineSlice` returns only
 * the two snap points on sparsely-vertexed stretches, so the traced closing
 * edge came out looking like a plain straight close. That workaround inflated
 * the boundary from 14k to 33k vertices and made every proximity query
 * proportionally slower, for no gain in fidelity -- interpolated points along
 * an already-straight segment carry no extra information about where the
 * border runs.
 *
 * `lib/boundaryIndex.ts` now walks the ring by vertex index instead, so the
 * trace reproduces the source geometry exactly and densification is unnecessary.
 */
export function piBoundaryAsLines(
  fc: FeatureCollection<Polygon | MultiPolygon>,
): Feature<MultiLineString> {
  const lines: Position[][] = [];

  for (const feat of fc.features) {
    const geom = feat.geometry;
    if (geom.type === 'Polygon') {
      if (geom.coordinates[0]) lines.push(geom.coordinates[0]);
    } else if (geom.type === 'MultiPolygon') {
      for (const poly of geom.coordinates) {
        if (poly[0]) lines.push(poly[0]);
      }
    }
  }

  return {
    type: 'Feature',
    properties: {},
    geometry: { type: 'MultiLineString', coordinates: lines },
  };
}
