/**
 * Renders a dashed polyline around the coverage zone boundary.
 *
 * Algorithm:
 *   1. Bucket all coverage points into grid cells (resolution-based)
 *   2. Find "edge" cells — cells that have ≥1 empty neighbour
 *   3. Compute a concave boundary by ordering edge points angularly per site
 *   4. Render as dashed Leaflet polylines
 *
 * Performance: runs once per coverage result change, O(n) where n = grid points.
 */

import { useEffect, useRef, useMemo } from 'react';
import { useMap } from 'react-leaflet';
import L from 'leaflet';
import type { CoveragePoint } from '@/types/index.ts';

interface CoverageBoundaryProps {
  points: CoveragePoint[];
  visible: boolean;
  resolution: number; // meters — used to determine grid cell size
  color?: string;
  weight?: number;
}

export default function CoverageBoundary({
  points,
  visible,
  resolution,
  color = '#7c3aed', // purple-600 — visible against both map and orange gradient
  weight = 2,
}: CoverageBoundaryProps) {
  const map = useMap();
  const layerRef = useRef<L.LayerGroup | null>(null);

  // Compute boundary paths grouped by site
  const boundaryPaths = useMemo(() => {
    if (!visible || points.length === 0) return [];

    // Group points by siteId
    const bySite = new Map<string, CoveragePoint[]>();
    for (const p of points) {
      let arr = bySite.get(p.siteId);
      if (!arr) {
        arr = [];
        bySite.set(p.siteId, arr);
      }
      arr.push(p);
    }

    const paths: L.LatLngExpression[][] = [];

    for (const sitePoints of bySite.values()) {
      const edgePath = computeEdgePath(sitePoints, resolution);
      if (edgePath.length >= 3) {
        paths.push(edgePath);
      }
    }

    return paths;
  }, [points, visible, resolution]);

  // Render / cleanup polylines
  useEffect(() => {
    // Remove old
    if (layerRef.current) {
      map.removeLayer(layerRef.current);
      layerRef.current = null;
    }

    if (!visible || boundaryPaths.length === 0) return;

    const group = L.layerGroup();

    for (const path of boundaryPaths) {
      L.polyline(path, {
        color,
        weight,
        dashArray: '8, 5',
        opacity: 0.7,
        fill: false,
        interactive: false,
      }).addTo(group);
    }

    group.addTo(map);
    layerRef.current = group;

    return () => {
      if (layerRef.current) {
        map.removeLayer(layerRef.current);
        layerRef.current = null;
      }
    };
  }, [map, visible, boundaryPaths, color, weight]);

  return null;
}

// ---------------------------------------------------------------------------
// Edge detection on the grid
// ---------------------------------------------------------------------------

/**
 * For a set of coverage points (all belonging to one site), find the
 * ordered boundary polygon.
 *
 * Steps:
 *   1. Hash every point into a grid cell
 *   2. Find edge cells (≥1 of 8 neighbours missing)
 *   3. Order edge points by angle from centroid → closed polygon
 */
function computeEdgePath(
  pts: CoveragePoint[],
  resolutionM: number
): L.LatLngExpression[] {
  if (pts.length < 3) return [];

  // Grid cell size in degrees (approximate)
  const cellLat = resolutionM / 111_000;
  const avgLat = pts.reduce((s, p) => s + p.lat, 0) / pts.length;
  const cellLon = resolutionM / (111_000 * Math.cos((avgLat * Math.PI) / 180));

  // Quantize helper
  const toKey = (lat: number, lon: number) => {
    const r = Math.round(lat / cellLat);
    const c = Math.round(lon / cellLon);
    return `${r},${c}`;
  };

  // Build occupied set
  const occupied = new Set<string>();
  // Keep one representative point per cell for coordinates
  const cellCoords = new Map<string, { lat: number; lon: number }>();
  for (const p of pts) {
    const key = toKey(p.lat, p.lon);
    occupied.add(key);
    if (!cellCoords.has(key)) {
      cellCoords.set(key, { lat: p.lat, lon: p.lon });
    }
  }

  // 8-connected neighbour offsets
  const offsets = [
    [-1, -1], [-1, 0], [-1, 1],
    [0, -1],           [0, 1],
    [1, -1],  [1, 0],  [1, 1],
  ];

  // Find edge cells: occupied cells with at least one missing neighbour
  const edgePoints: { lat: number; lon: number }[] = [];
  for (const [key, coord] of cellCoords) {
    const [rStr, cStr] = key.split(',');
    const r = Number(rStr);
    const c = Number(cStr);

    let isEdge = false;
    for (const [dr, dc] of offsets) {
      if (!occupied.has(`${r + dr},${c + dc}`)) {
        isEdge = true;
        break;
      }
    }

    if (isEdge) {
      edgePoints.push(coord);
    }
  }

  if (edgePoints.length < 3) return [];

  // Order by angle from centroid to form a closed polygon
  const cx = edgePoints.reduce((s, p) => s + p.lat, 0) / edgePoints.length;
  const cy = edgePoints.reduce((s, p) => s + p.lon, 0) / edgePoints.length;

  edgePoints.sort(
    (a, b) =>
      Math.atan2(a.lon - cy, a.lat - cx) - Math.atan2(b.lon - cy, b.lat - cx)
  );

  // Close the polygon
  const result: L.LatLngExpression[] = edgePoints.map(
    (p) => [p.lat, p.lon] as L.LatLngExpression
  );
  result.push(result[0]); // close

  return result;
}
