import type { Feature, FeatureCollection, MultiPolygon, Polygon } from 'geojson';
import union from '@turf/union';
import intersect from '@turf/intersect';
import unkinkPolygon from '@turf/unkink-polygon';
import booleanPointInPolygon from '@turf/boolean-point-in-polygon';
import { featureCollection, point as turfPoint } from '@turf/helpers';

/**
 * Drawing-constraint boundary.
 *
 * The "active drawing boundary" defines where the user is allowed to draw.
 * Anything outside is clipped away on finish. Phase 3 will swap this for the
 * ~50km buffer boundary at runtime; the only public surface needed for that
 * is `setDrawingBoundary` -- nothing else has to change.
 */

let activeBoundary: Feature<Polygon | MultiPolygon> | null = null;

/** Get the currently active drawing-constraint boundary (or null when off). */
export function getDrawingBoundary(): Feature<Polygon | MultiPolygon> | null {
  return activeBoundary;
}

/**
 * Set (or clear) the active drawing-constraint boundary. Pass `null` to
 * disable clipping entirely. Phase 3 will call this with the ~50km buffer.
 */
export function setDrawingBoundary(boundary: Feature<Polygon | MultiPolygon> | null): void {
  activeBoundary = boundary;
}

/**
 * Convenience: derive a single boundary feature by unioning all polygons in
 * a FeatureCollection, then set it as the active boundary.
 *
 * The PI boundary GeoJSON ships as a multi-feature collection; we need a
 * single feature for `turf.intersect` to clip against.
 */
export function setDrawingBoundaryFromCollection(
  fc: FeatureCollection<Polygon | MultiPolygon>,
): void {
  if (fc.features.length === 0) {
    activeBoundary = null;
    return;
  }
  if (fc.features.length === 1) {
    activeBoundary = fc.features[0];
    return;
  }
  const merged = union(fc);
  activeBoundary = merged ?? fc.features[0];
}

/**
 * Clip the user-drawn polygon to the active drawing boundary.
 *
 * Returns:
 *   - the original polygon if no boundary is configured (clipping disabled)
 *   - a clipped Polygon when the drawing partially overlaps the boundary
 *   - `null` if the drawing lies entirely outside the boundary
 *
 * When the intersection produces a MultiPolygon (a stroke that crossed the
 * boundary in multiple places), the largest piece by area is returned.
 */
/**
 * Pure clip: takes the boundary as an argument rather than reading
 * module-level state. Useful for callers (e.g. the React drawing hook) that
 * want to keep the boundary in component state so it survives module hot
 * reloads.
 */
export function clipPolygonToBoundary(
  polygon: Polygon,
  boundary: Feature<Polygon | MultiPolygon> | null,
): Polygon | null {
  if (!boundary) return polygon;

  const drawn: Feature<Polygon> = { type: 'Feature', properties: {}, geometry: polygon };

  let result: Feature<Polygon | MultiPolygon> | null = null;
  try {
    result = intersect(featureCollection([boundary, drawn]));
  } catch {
    // Turf's `intersect` throws on self-intersecting input, which auto-completed
    // boundary-hugging polygons can produce.
    //
    // This used to return the polygon UNCLIPPED, on the reasoning that keeping
    // the shape beat making it vanish. But unclipped means unconstrained: the
    // orange area in Bill's 18 Aug screenshot spills across the border and out
    // over the Gulf of Aqaba, which is exactly this path firing. Allowing
    // geometry outside PI defeats the point of the constraint.
    //
    // Repair and retry instead -- splitting the self-intersection into simple
    // rings usually lets the clip succeed on each piece.
    return clipViaRepair(polygon, boundary);
  }
  if (!result) return null;

  if (result.geometry.type === 'Polygon') return result.geometry;

  // Pick the largest piece by absolute shoelace area.
  let best: Polygon | null = null;
  let bestArea = -Infinity;
  for (const coords of result.geometry.coordinates) {
    const a = absoluteRingArea(coords[0]);
    if (a > bestArea) {
      bestArea = a;
      best = { type: 'Polygon', coordinates: coords };
    }
  }
  return best;
}

/**
 * Fallback clip for polygons `turf.intersect` refuses outright.
 *
 * `unkinkPolygon` splits a self-intersecting ring into simple, non-overlapping
 * ones; each of those clips cleanly, and the largest resulting piece stands in
 * for the whole. If even that fails the shape is only kept when every vertex is
 * genuinely inside the boundary -- anything else is dropped rather than drawn
 * outside PI.
 */
function clipViaRepair(
  polygon: Polygon,
  boundary: Feature<Polygon | MultiPolygon>,
): Polygon | null {
  try {
    const pieces = unkinkPolygon({
      type: 'Feature',
      properties: {},
      geometry: polygon,
    });
    let best: Polygon | null = null;
    let bestArea = -Infinity;
    for (const piece of pieces.features) {
      let clipped: Feature<Polygon | MultiPolygon> | null = null;
      try {
        clipped = intersect(featureCollection([boundary, piece]));
      } catch {
        continue;
      }
      if (!clipped) continue;
      const rings =
        clipped.geometry.type === 'Polygon'
          ? [clipped.geometry.coordinates]
          : clipped.geometry.coordinates;
      for (const coords of rings) {
        const a = absoluteRingArea(coords[0]);
        if (a > bestArea) {
          bestArea = a;
          best = { type: 'Polygon', coordinates: coords };
        }
      }
    }
    if (best) return best;
  } catch {
    // fall through to the containment test
  }

  const ring = polygon.coordinates[0];
  if (!ring) return null;
  for (const c of ring) {
    if (!booleanPointInPolygon(turfPoint(c), boundary)) return null;
  }
  return polygon;
}

/** Backwards-compatible wrapper using module-level boundary state. */
export function clipToActiveBoundary(polygon: Polygon): Polygon | null {
  return clipPolygonToBoundary(polygon, activeBoundary);
}

function absoluteRingArea(ring: number[][]): 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];
  }
  return Math.abs(area) / 2;
}
