import type { GeoJSONPolygon } from "@/types/provider";

/** Leaflet's tuple order — latitude first, the reverse of GeoJSON */
export type LatLngTuple = [number, number];

const EARTH_RADIUS_KM = 6371;

/**
 * GeoJSON rings (`[lng, lat]`) → Leaflet rings (`[lat, lng]`).
 *
 * The first ring is the outer boundary; any further rings are holes, which
 * Leaflet's `Polygon` accepts in exactly this nested shape.
 */
export function toLeafletRings(polygon: GeoJSONPolygon): LatLngTuple[][] {
  return polygon.coordinates.map((ring) =>
    ring
      .filter((point) => point.length >= 2)
      .map(([lng, lat]) => [lat, lng] as LatLngTuple),
  );
}

/** Outer ring only — holes don't affect the bounds or the vertex count */
function outerRing(polygon: GeoJSONPolygon): LatLngTuple[] {
  return toLeafletRings(polygon)[0] ?? [];
}

/**
 * Number of distinct boundary vertices on the outer ring. GeoJSON repeats the
 * first point as the last one to close the ring, so that duplicate is dropped.
 */
export function countBoundaryPoints(polygon: GeoJSONPolygon): number {
  const ring = outerRing(polygon);
  if (ring.length < 2) return ring.length;

  const [firstLat, firstLng] = ring[0];
  const [lastLat, lastLng] = ring[ring.length - 1];
  const isClosed = firstLat === lastLat && firstLng === lastLng;

  return isClosed ? ring.length - 1 : ring.length;
}

/** Bounding box as Leaflet expects it: [[southWest], [northEast]] */
export function getBounds(
  polygon: GeoJSONPolygon,
): [LatLngTuple, LatLngTuple] | null {
  const ring = outerRing(polygon);
  if (ring.length === 0) return null;

  let minLat = Infinity;
  let maxLat = -Infinity;
  let minLng = Infinity;
  let maxLng = -Infinity;

  for (const [lat, lng] of ring) {
    if (lat < minLat) minLat = lat;
    if (lat > maxLat) maxLat = lat;
    if (lng < minLng) minLng = lng;
    if (lng > maxLng) maxLng = lng;
  }

  return [
    [minLat, minLng],
    [maxLat, maxLng],
  ];
}

/** Centre of the bounding box — good enough to label "where" the area is */
export function getCenter(polygon: GeoJSONPolygon): LatLngTuple | null {
  const bounds = getBounds(polygon);
  if (!bounds) return null;

  const [[minLat, minLng], [maxLat, maxLng]] = bounds;
  return [(minLat + maxLat) / 2, (minLng + maxLng) / 2];
}

/**
 * Approximate area in km².
 *
 * Uses the shoelace formula on an equirectangular projection centred on the
 * polygon's mean latitude — accurate to well under a percent at city scale
 * (the Jeddah pilot), and it avoids pulling in a geodesy library for a figure
 * that is only ever displayed as a rough "approximate area".
 *
 * Holes are subtracted; returns 0 for a degenerate ring.
 */
export function getAreaKm2(polygon: GeoJSONPolygon): number {
  const rings = toLeafletRings(polygon);
  if (rings.length === 0) return 0;

  const outer = rings[0];
  if (outer.length < 3) return 0;

  const meanLat = outer.reduce((sum, [lat]) => sum + lat, 0) / outer.length;
  const latScale = (Math.PI / 180) * EARTH_RADIUS_KM;
  const lngScale = latScale * Math.cos((meanLat * Math.PI) / 180);

  const ringArea = (ring: LatLngTuple[]): number => {
    if (ring.length < 3) return 0;
    let sum = 0;
    for (let i = 0; i < ring.length; i += 1) {
      const [lat1, lng1] = ring[i];
      const [lat2, lng2] = ring[(i + 1) % ring.length];
      sum +=
        lng1 * lngScale * (lat2 * latScale) -
        lng2 * lngScale * (lat1 * latScale);
    }
    return Math.abs(sum) / 2;
  };

  // Ring 0 is the boundary; every later ring is a hole cut out of it.
  return rings
    .slice(1)
    .reduce((area, hole) => area - ringArea(hole), ringArea(outer));
}

/** True when there is at least one usable ring to draw */
export function hasDrawableArea(polygon: GeoJSONPolygon | null): boolean {
  if (!polygon) return false;
  return outerRing(polygon).length >= 3;
}
