/**
 * Leaflet map wrapper for the Flights Map feature.
 *
 * **This is the ONLY file in the codebase that imports `leaflet`.**
 * Per design spec, all Leaflet usage is encapsulated here.
 *
 * Wrapped in React.lazy() + <ClientOnly> by consumers for SSR safety.
 * Accepts markers, polylines, popups as props -- does not own state.
 *
 * @module
 */

import { useRef, useEffect, useCallback, type FC } from "react";
import L from "leaflet";
import "leaflet/dist/leaflet.css";
import type { IMapMarker, IMapPolyline, IMapPopup, MarkerStyle, PolylineStyle } from "../types.js";

// ---------------------------------------------------------------------------
// Marker icons
// ---------------------------------------------------------------------------

const MARKER_ICONS: Record<MarkerStyle, L.IconOptions> = {
  blue: {
    iconUrl: "/assets/img/leaflet/marker-blue.png",
    iconSize: [15, 15] as L.PointExpression,
    iconAnchor: [7, 7] as L.PointExpression,
    popupAnchor: [0, -10] as L.PointExpression,
  },
  "blue-small": {
    iconUrl: "/assets/img/leaflet/marker-blue-small.png",
    iconSize: [11, 11] as L.PointExpression,
    iconAnchor: [5, 5] as L.PointExpression,
    popupAnchor: [0, -10] as L.PointExpression,
  },
  orange: {
    iconUrl: "/assets/img/leaflet/marker-orange.png",
    iconSize: [20, 20] as L.PointExpression,
    iconAnchor: [10, 10] as L.PointExpression,
    popupAnchor: [0, -20] as L.PointExpression,
  },
};

function getIcon(style: MarkerStyle): L.Icon {
  const opts = MARKER_ICONS[style];
  return L.icon(opts);
}

// ---------------------------------------------------------------------------
// Polyline styles
// ---------------------------------------------------------------------------

const POLYLINE_STYLES: Record<PolylineStyle, L.PolylineOptions> = {
  direct: {
    color: "#2457ff",
    weight: 1,
    opacity: 1,
  },
  connecting: {
    color: "#2433ff",
    weight: 1,
    opacity: 1,
    dashArray: "4 14",
  },
};

// ---------------------------------------------------------------------------
// Great-circle arc calculation
// ---------------------------------------------------------------------------

function deg2rad(deg: number): number {
  return (deg * Math.PI) / 180;
}

function rad2deg(rad: number): number {
  return (rad * 180) / Math.PI;
}

function buildGreatCircleArc(
  from: L.LatLng,
  to: L.LatLng,
  segments = 64,
): L.LatLng[] {
  const phi1 = deg2rad(from.lat);
  const lam1 = deg2rad(from.lng);
  const phi2 = deg2rad(to.lat);
  const lam2 = deg2rad(to.lng);

  const delta =
    2 *
    Math.asin(
      Math.sqrt(
        Math.sin((phi2 - phi1) / 2) ** 2 +
          Math.cos(phi1) *
            Math.cos(phi2) *
            Math.sin((lam2 - lam1) / 2) ** 2,
      ),
    );

  if (delta === 0) return [from, to];

  const points: L.LatLng[] = [];

  for (let i = 0; i <= segments; i++) {
    const f = i / segments;
    const A = Math.sin((1 - f) * delta) / Math.sin(delta);
    const B = Math.sin(f * delta) / Math.sin(delta);

    const x =
      A * Math.cos(phi1) * Math.cos(lam1) +
      B * Math.cos(phi2) * Math.cos(lam2);
    const y =
      A * Math.cos(phi1) * Math.sin(lam1) +
      B * Math.cos(phi2) * Math.sin(lam2);
    const z = A * Math.sin(phi1) + B * Math.sin(phi2);

    const lat = rad2deg(Math.atan2(z, Math.sqrt(x * x + y * y)));
    const lng = rad2deg(Math.atan2(y, x));

    points.push(L.latLng(lat, lng));
  }

  return points;
}

// ---------------------------------------------------------------------------
// Props
// ---------------------------------------------------------------------------

export interface MapCanvasProps {
  markers: IMapMarker[];
  polylines: IMapPolyline[];
  popups?: IMapPopup[];
  tileUrl: string;
  center?: [number, number];
  zoom?: number;
  minZoom?: number;
  maxZoom?: number;
  onMarkerClick?: (markerId: string) => void;
  className?: string;
  /** When true, hide countryType="other" markers. */
  domestic?: boolean;
  /** When true, hide countryType="ru" markers. */
  international?: boolean;
  /** Marker IDs whose tooltips must be force-opened (intermediate cities on multi-hop routes). */
  intermediateIds?: string[];
}

// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------

/**
 * Leaflet map canvas. Must be loaded via React.lazy() and wrapped in
 * <ClientOnly> for SSR safety.
 */
export const MapCanvas: FC<MapCanvasProps> = ({
  markers,
  polylines,
  popups,
  tileUrl,
  center = [53, 45],
  zoom = 5,
  minZoom = 3,
  maxZoom = 6,
  onMarkerClick,
  className,
  domestic = false,
  international = false,
  intermediateIds,
}) => {
  const containerRef = useRef<HTMLDivElement>(null);
  const mapRef = useRef<L.Map | null>(null);
  const markersLayerRef = useRef<L.LayerGroup | null>(null);
  const polylinesLayerRef = useRef<L.LayerGroup | null>(null);
  const popupsLayerRef = useRef<L.LayerGroup | null>(null);

  // Categorized rendering: 10 zoom layers + 1 highlight layer + an id→marker index.
  // Indexed as zoomLayersRef.current[countryIdx][zoomTier] where:
  //   countryIdx: 0 = "ru", 1 = "other"
  //   zoomTier: 2..6 (tiers outside this range clamp to 6)
  const zoomLayersRef = useRef<L.LayerGroup[][] | null>(null);
  const highlightLayerRef = useRef<L.LayerGroup | null>(null);
  const markerIndexRef = useRef<Map<string, L.Marker>>(new Map());
  const highlightedIdsRef = useRef<Set<string>>(new Set());
  const intermediateIdsRef = useRef<Set<string>>(new Set());

  // Track latest toggles so the zoomend handler sees current values
  const domesticRef = useRef(domestic);
  domesticRef.current = domestic;
  const internationalRef = useRef(international);
  internationalRef.current = international;

  const onMarkerClickRef = useRef(onMarkerClick);
  onMarkerClickRef.current = onMarkerClick;

  // Capture initial values in refs so the init effect has no deps
  const centerRef = useRef(center);
  const zoomRef = useRef(zoom);
  const tileUrlRef = useRef(tileUrl);
  const minZoomRef = useRef(minZoom);
  const maxZoomRef = useRef(maxZoom);

  function syncVisibility(): void {
    const map = mapRef.current;
    const zoomLayers = zoomLayersRef.current;
    if (!map || !zoomLayers) return;

    const currentZoom = map.getZoom();
    const dom = domesticRef.current;
    const intl = internationalRef.current;

    for (let c = 0; c < 2; c++) {
      const countryType = c === 0 ? "ru" : "other";
      for (let t = 0; t < 5; t++) {
        const tier = t + 2;
        const layer = zoomLayers[c]![t]!;
        const inRange = tier <= currentZoom;
        const hiddenByIntl = intl && countryType === "ru";
        const hiddenByDom = dom && countryType === "other";
        const shouldShow = inRange && !hiddenByIntl && !hiddenByDom;
        if (shouldShow) {
          map.addLayer(layer);
        } else {
          map.removeLayer(layer);
        }
      }
    }
  }

  function syncTooltips(): void {
    const map = mapRef.current;
    if (!map) return;

    const currentZoom = map.getZoom();
    const highlighted = highlightedIdsRef.current;
    const closeNonHighlighted = currentZoom <= 3 || highlighted.size >= 2;

    // Pass 1: zoom/highlight rules.
    markerIndexRef.current.forEach((marker, id) => {
      if (closeNonHighlighted && !highlighted.has(id)) {
        marker.closeTooltip();
      } else if (!closeNonHighlighted) {
        marker.openTooltip();
      }
    });

    // Pass 2: force-open intermediate route tooltips (Angular parity).
    for (const id of intermediateIdsRef.current) {
      const m = markerIndexRef.current.get(id);
      if (m) m.openTooltip();
    }
  }

  function syncPolylines(): void {
    const map = mapRef.current;
    const layer = polylinesLayerRef.current;
    if (!map || !layer) return;

    layer.clearLayers();

    for (const pl of polylines) {
      const styleOpts = POLYLINE_STYLES[pl.style];

      const visible: L.Marker[] = [];
      for (const cityId of pl.cityIds) {
        const m = markerIndexRef.current.get(cityId);
        if (m && map.hasLayer(m)) visible.push(m);
      }
      if (visible.length < 2) continue;

      const segments: L.LatLng[] = [];
      for (let i = 0; i < visible.length - 1; i++) {
        const from = visible[i]!.getLatLng();
        const to = visible[i + 1]!.getLatLng();
        const arc = buildGreatCircleArc(from, to);
        segments.push(...(i === 0 ? arc : arc.slice(1)));
      }

      L.polyline(segments, styleOpts).addTo(layer);
    }
  }

  // --- Initialize map ---
  useEffect(() => {
    if (!containerRef.current || mapRef.current) return;

    const southWest: L.LatLngExpression = [-70, -185];
    const northEast: L.LatLngExpression = [80, 200];

    const map = L.map(containerRef.current, {
      center: centerRef.current,
      zoom: zoomRef.current,
      attributionControl: false,
      maxBounds: [southWest, northEast],
      maxBoundsViscosity: 1,
    });

    L.tileLayer(tileUrlRef.current, {
      maxZoom: maxZoomRef.current,
      minZoom: minZoomRef.current,
    }).addTo(map);

    markersLayerRef.current = L.layerGroup().addTo(map);
    polylinesLayerRef.current = L.layerGroup().addTo(map);
    popupsLayerRef.current = L.layerGroup().addTo(map);

    // Highlight layer — always on.
    highlightLayerRef.current = L.layerGroup().addTo(map);

    // Zoom layers — 2 country types × 5 tiers (2..6). NOT added to the map
    // yet; the visibility sync effect decides which to add based on current
    // zoom + toggles.
    const zoomLayers: L.LayerGroup[][] = [[], []];
    for (let c = 0; c < 2; c++) {
      for (let t = 0; t < 5; t++) {
        zoomLayers[c]![t] = L.layerGroup();
      }
    }
    zoomLayersRef.current = zoomLayers;

    // Rerun visibility + tooltip rules on every zoom change.
    map.on("zoomend", () => {
      syncVisibility();
      syncPolylines();
      syncTooltips();
    });

    mapRef.current = map;

    return () => {
      map.remove();
      mapRef.current = null;
      markersLayerRef.current = null;
      polylinesLayerRef.current = null;
      popupsLayerRef.current = null;
      highlightLayerRef.current = null;
      zoomLayersRef.current = null;
      markerIndexRef.current = new Map();
      highlightedIdsRef.current = new Set();
      intermediateIdsRef.current = new Set();
    };
  }, []);

  // --- Sync markers ---
  const syncMarkers = useCallback(() => {
    const flatLayer = markersLayerRef.current;
    const zoomLayers = zoomLayersRef.current;
    const highlightLayer = highlightLayerRef.current;
    if (!flatLayer || !zoomLayers || !highlightLayer) return;

    // Clear everything before re-adding.
    flatLayer.clearLayers();
    highlightLayer.clearLayers();
    for (let c = 0; c < 2; c++) {
      for (let t = 0; t < 5; t++) {
        zoomLayers[c]![t]!.clearLayers();
      }
    }
    markerIndexRef.current = new Map();
    highlightedIdsRef.current = new Set();

    for (const m of markers) {
      const isCategorized =
        m.zoomLevel !== undefined && m.countryType !== undefined;

      const iconStyle: MarkerStyle = m.highlighted ? "orange" : m.style;
      const marker = L.marker([m.lat, m.lng], {
        icon: getIcon(iconStyle),
        title: m.id,
      });

      if (m.label) {
        marker.bindTooltip(m.label, {
          permanent: m.tooltipPermanent ?? false,
          direction: "top",
          className: "city-label",
        });
      }

      marker.on("click", () => {
        onMarkerClickRef.current?.(m.id);
      });

      markerIndexRef.current.set(m.id, marker);

      if (m.highlighted) {
        highlightedIdsRef.current.add(m.id);
        marker.addTo(highlightLayer);
      } else if (isCategorized) {
        const countryIdx = m.countryType === "ru" ? 0 : 1;
        const rawTier = m.zoomLevel!;
        const tier = rawTier >= 2 && rawTier <= 6 ? rawTier : 6;
        const tierIdx = tier - 2;
        marker.addTo(zoomLayers[countryIdx]![tierIdx]!);
      } else {
        marker.addTo(flatLayer);
      }
    }

    syncVisibility();
    syncTooltips();
  }, [markers]);

  useEffect(() => {
    syncMarkers();
  }, [syncMarkers]);

  // --- Sync polylines ---
  useEffect(() => {
    syncPolylines();
  }, [polylines]);

  // --- Sync popups ---
  useEffect(() => {
    const layer = popupsLayerRef.current;
    const map = mapRef.current;
    if (!layer || !map) return;

    layer.clearLayers();

    if (!popups) return;

    for (const p of popups) {
      L.popup({
        closeButton: true,
        autoClose: false,
        closeOnClick: false,
      })
        .setLatLng([p.lat, p.lng])
        .setContent(p.content)
        .addTo(layer);
    }
  }, [popups]);

  // --- Re-sync visibility + tooltips when toggles change ---
  useEffect(() => {
    syncVisibility();
    syncPolylines();
    syncTooltips();
  }, [domestic, international]);

  // --- Re-sync tooltips when intermediateIds change ---
  useEffect(() => {
    intermediateIdsRef.current = new Set(intermediateIds ?? []);
    syncTooltips();
  }, [intermediateIds]);

  return (
    <div
      ref={containerRef}
      className={className}
      style={{ width: "100%", height: "100%" }}
      data-testid="map-canvas"
    />
  );
};
