import { HexagonLayer } from "@deck.gl/aggregation-layers";
import { IconLayer, LineLayer, ScatterplotLayer } from "@deck.gl/layers";
import { MapboxOverlay } from "@deck.gl/mapbox";
import { type PickingInfo } from "@deck.gl/core";
import maplibregl, {
  type GeoJSONSource,
  type GeoJSONSourceSpecification,
  type LayerSpecification,
  type RasterDEMSourceSpecification,
  type StyleSpecification,
  type VectorSourceSpecification,
} from "maplibre-gl";
import { useEffect, useMemo, useRef } from "react";
import type { AisTarget } from "../../entities/aisTarget/model/types";
import type { LegacyVesselInfo } from "../../entities/legacyVessel/model/types";
import type { ZonesGeoJson } from "../../entities/zone/api/useZones";
import type { ZoneId } from "../../entities/zone/model/meta";
import { ZONE_META } from "../../entities/zone/model/meta";
import type { MapToggleState } from "../../features/mapToggles/MapToggles";
import type { FcLink, FleetCircle, PairLink } from "../../features/legacyDashboard/model/types";
import { MaplibreDeckCustomLayer } from "./MaplibreDeckCustomLayer";

export type Map3DSettings = {
  showSeamark: boolean;
  showShips: boolean;
  showDensity: boolean;
};

export type BaseMapId = "enhanced" | "legacy";
export type MapProjectionId = "mercator" | "globe";

type Props = {
  targets: AisTarget[];
  zones: ZonesGeoJson | null;
  selectedMmsi: number | null;
  settings: Map3DSettings;
  baseMap: BaseMapId;
  projection: MapProjectionId;
  overlays: MapToggleState;
  onSelectMmsi: (mmsi: number | null) => void;
  onViewBboxChange?: (bbox: [number, number, number, number]) => void;
  legacyHits?: Map<number, LegacyVesselInfo> | null;
  pairLinks?: PairLink[];
  fcLinks?: FcLink[];
  fleetCircles?: FleetCircle[];
};

const SHIP_ICON_MAPPING = {
  ship: {
    x: 0,
    y: 0,
    width: 128,
    height: 128,
    anchorX: 64,
    anchorY: 64,
    mask: true,
  },
} as const;

function isFiniteNumber(x: unknown): x is number {
  return typeof x === "number" && Number.isFinite(x);
}

const EARTH_RADIUS_M = 6371008.8; // MapLibre's `earthRadius`
const DEG2RAD = Math.PI / 180;

function lngLatToUnitSphere(lon: number, lat: number, altitudeMeters = 0): [number, number, number] {
  const lambda = lon * DEG2RAD;
  const phi = lat * DEG2RAD;
  const cosPhi = Math.cos(phi);
  const s = 1 + altitudeMeters / EARTH_RADIUS_M;
  // MapLibre globe space: x = east, y = north, z = lon=0 at equator.
  return [Math.sin(lambda) * cosPhi * s, Math.sin(phi) * s, Math.cos(lambda) * cosPhi * s];
}

const LEGACY_CODE_COLORS: Record<string, [number, number, number]> = {
  PT: [30, 64, 175], // #1e40af
  "PT-S": [234, 88, 12], // #ea580c
  GN: [16, 185, 129], // #10b981
  OT: [139, 92, 246], // #8b5cf6
  PS: [239, 68, 68], // #ef4444
  FC: [245, 158, 11], // #f59e0b
};

const DEPTH_DISABLED_PARAMS = {
  // In MapLibre+Deck (interleaved), the map often leaves a depth buffer populated.
  // For 2D overlays like zones/icons/halos we want stable painter's-order rendering
  // to avoid z-fighting flicker when layers overlap at (or near) the same z.
  depthCompare: "always",
  depthWriteEnabled: false,
} as const;

const GLOBE_OVERLAY_PARAMS = {
  // In globe mode we want depth-testing against the globe so features on the far side don't draw through.
  // Still disable depth writes so our overlays don't interfere with each other.
  depthCompare: "less-equal",
  depthWriteEnabled: false,
} as const;

function getMapTilerKey(): string | null {
  const k = import.meta.env.VITE_MAPTILER_KEY;
  if (typeof k !== "string") return null;
  const v = k.trim();
  return v ? v : null;
}

function ensureSeamarkOverlay(map: maplibregl.Map, beforeLayerId?: string) {
  const srcId = "seamark";
  const layerId = "seamark";

  if (!map.getSource(srcId)) {
    map.addSource(srcId, {
      type: "raster",
      tiles: ["https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png"],
      tileSize: 256,
      attribution: "© OpenSeaMap contributors",
    });
  }

  if (!map.getLayer(layerId)) {
    const layer: LayerSpecification = {
      id: layerId,
      type: "raster",
      source: srcId,
      paint: { "raster-opacity": 0.85 },
    } as unknown as LayerSpecification;

    // By default, MapLibre adds new layers to the top.
    // For readability we want seamarks above bathymetry fill, but below bathymetry lines/labels.
    const before = beforeLayerId && map.getLayer(beforeLayerId) ? beforeLayerId : undefined;
    map.addLayer(layer, before);
  }
}

function injectOceanBathymetryLayers(style: StyleSpecification, maptilerKey: string) {
  const oceanSourceId = "maptiler-ocean";
  const terrainSourceId = "maptiler-terrain";

  if (!style.sources) style.sources = {} as StyleSpecification["sources"];
  if (!style.layers) style.layers = [];

  if (!style.sources[oceanSourceId]) {
    style.sources[oceanSourceId] = {
      type: "vector",
      url: `https://api.maptiler.com/tiles/ocean/tiles.json?key=${encodeURIComponent(maptilerKey)}`,
    } satisfies VectorSourceSpecification as unknown as StyleSpecification["sources"][string];
  }

  if (!style.sources[terrainSourceId]) {
    style.sources[terrainSourceId] = {
      type: "raster-dem",
      url: `https://api.maptiler.com/tiles/terrain-rgb/tiles.json?key=${encodeURIComponent(maptilerKey)}`,
      tileSize: 512,
      encoding: "mapbox",
    } satisfies RasterDEMSourceSpecification as unknown as StyleSpecification["sources"][string];
  }

  const depth = ["to-number", ["get", "depth"]] as unknown as number[];
  const depthLabel = ["concat", ["to-string", ["*", depth, -1]], "m"] as unknown as string[];

  const bathyFillColor = [
    "interpolate",
    ["linear"],
    depth,
    -11000,
    "#00040b",
    -8000,
    "#010610",
    -6000,
    "#020816",
    -4000,
    "#030c1c",
    -2000,
    "#041022",
    -1000,
    "#051529",
    -500,
    "#061a30",
    -200,
    "#071f36",
    -100,
    "#08263d",
    -50,
    "#092c44",
    -20,
    "#0a334b",
    0,
    "#0b3a53",
  ] as const;

  const bathyHillshade: LayerSpecification = {
    id: "bathymetry-hillshade",
    type: "hillshade",
    source: terrainSourceId,
    paint: {
      "hillshade-illumination-anchor": "viewport",
      "hillshade-illumination-direction": 315,
      "hillshade-illumination-altitude": 45,
      "hillshade-exaggeration": ["interpolate", ["linear"], ["zoom"], 0, 0.15, 6, 0.25, 10, 0.32],
      // Dark-mode tuned shading. Alpha is baked into the colors.
      "hillshade-shadow-color": "rgba(0,0,0,0.45)",
      "hillshade-highlight-color": "rgba(255,255,255,0.18)",
      "hillshade-accent-color": "rgba(255,255,255,0.06)",
    },
  } as unknown as LayerSpecification;

  const bathyFill: LayerSpecification = {
    id: "bathymetry-fill",
    type: "fill",
    source: oceanSourceId,
    "source-layer": "contour",
    paint: {
      // Dark-mode friendly palette (shallow = slightly brighter; deep = near-black).
      "fill-color": bathyFillColor,
      "fill-opacity": ["interpolate", ["linear"], ["zoom"], 0, 0.9, 6, 0.86, 10, 0.78],
    },
  } as unknown as LayerSpecification;

  const bathyExtrusion: LayerSpecification = {
    id: "bathymetry-extrusion",
    type: "fill-extrusion",
    source: oceanSourceId,
    "source-layer": "contour",
    minzoom: 6,
    paint: {
      "fill-extrusion-color": bathyFillColor,
      // MapLibre fill-extrusion cannot go below 0m, so we exaggerate the "relative seabed height"
      // (shallow areas higher, deep areas lower) to create a stepped relief.
      "fill-extrusion-base": 0,
      // NOTE: `zoom` can only appear as the input to a top-level `step`/`interpolate`.
      "fill-extrusion-height": [
        "interpolate",
        ["linear"],
        ["zoom"],
        6,
        ["*", ["+", depth, 12000], 0.002], // depth is negative; -> range [0..12000]
        10,
        ["*", ["+", depth, 12000], 0.01],
      ],
      "fill-extrusion-opacity": ["interpolate", ["linear"], ["zoom"], 6, 0.0, 7, 0.25, 10, 0.55],
      "fill-extrusion-vertical-gradient": true,
    },
  } as unknown as LayerSpecification;

  const bathyBandBorders: LayerSpecification = {
    id: "bathymetry-borders",
    type: "line",
    source: oceanSourceId,
    "source-layer": "contour",
    minzoom: 4,
    paint: {
      "line-color": "rgba(255,255,255,0.06)",
      "line-opacity": ["interpolate", ["linear"], ["zoom"], 4, 0.12, 8, 0.18, 12, 0.22],
      "line-blur": ["interpolate", ["linear"], ["zoom"], 4, 0.8, 10, 0.2],
      "line-width": ["interpolate", ["linear"], ["zoom"], 4, 0.2, 8, 0.35, 12, 0.6],
    },
  } as unknown as LayerSpecification;

  const bathyLinesMinor: LayerSpecification = {
    id: "bathymetry-lines",
    type: "line",
    source: oceanSourceId,
    "source-layer": "contour_line",
    minzoom: 8,
    paint: {
      "line-color": [
        "interpolate",
        ["linear"],
        depth,
        -11000,
        "rgba(255,255,255,0.04)",
        -6000,
        "rgba(255,255,255,0.05)",
        -2000,
        "rgba(255,255,255,0.07)",
        0,
        "rgba(255,255,255,0.10)",
      ],
      "line-opacity": ["interpolate", ["linear"], ["zoom"], 8, 0.18, 10, 0.22, 12, 0.28],
      "line-blur": ["interpolate", ["linear"], ["zoom"], 8, 0.8, 11, 0.3],
      "line-width": ["interpolate", ["linear"], ["zoom"], 8, 0.35, 10, 0.55, 12, 0.85],
    },
  } as unknown as LayerSpecification;

  const majorDepths = [-50, -100, -200, -500, -1000, -2000, -4000, -6000, -8000, -9500];
  const bathyLinesMajor: LayerSpecification = {
    id: "bathymetry-lines-major",
    type: "line",
    source: oceanSourceId,
    "source-layer": "contour_line",
    minzoom: 8,
    // Use legacy filter syntax here (not expression "in"), so we can pass multiple values.
    filter: ["in", "depth", ...majorDepths] as unknown as unknown[],
    paint: {
      "line-color": "rgba(255,255,255,0.16)",
      "line-opacity": ["interpolate", ["linear"], ["zoom"], 8, 0.22, 10, 0.28, 12, 0.34],
      "line-blur": ["interpolate", ["linear"], ["zoom"], 8, 0.4, 11, 0.2],
      "line-width": ["interpolate", ["linear"], ["zoom"], 8, 0.6, 10, 0.95, 12, 1.3],
    },
  } as unknown as LayerSpecification;

  const bathyBandBordersMajor: LayerSpecification = {
    id: "bathymetry-borders-major",
    type: "line",
    source: oceanSourceId,
    "source-layer": "contour",
    minzoom: 4,
    // Use legacy filter syntax here (not expression "in"), so we can pass multiple values.
    filter: ["in", "depth", ...majorDepths] as unknown as unknown[],
    paint: {
      "line-color": "rgba(255,255,255,0.14)",
      "line-opacity": ["interpolate", ["linear"], ["zoom"], 4, 0.14, 8, 0.2, 12, 0.26],
      "line-blur": ["interpolate", ["linear"], ["zoom"], 4, 0.3, 10, 0.15],
      "line-width": ["interpolate", ["linear"], ["zoom"], 4, 0.35, 8, 0.55, 12, 0.85],
    },
  } as unknown as LayerSpecification;

  const bathyLabels: LayerSpecification = {
    id: "bathymetry-labels",
    type: "symbol",
    source: oceanSourceId,
    "source-layer": "contour_line",
    minzoom: 10,
    // Use legacy filter syntax here (not expression "in"), so we can pass multiple values.
    filter: ["in", "depth", ...majorDepths] as unknown as unknown[],
    layout: {
      "symbol-placement": "line",
      "text-field": depthLabel,
      "text-font": ["Noto Sans Regular", "Open Sans Regular"],
      "text-size": ["interpolate", ["linear"], ["zoom"], 10, 10, 12, 12],
      "text-allow-overlap": false,
      "text-padding": 2,
      "text-rotation-alignment": "map",
    },
    paint: {
      "text-color": "rgba(226,232,240,0.72)",
      "text-halo-color": "rgba(2,6,23,0.82)",
      "text-halo-width": 1.0,
      "text-halo-blur": 0.6,
    },
  } as unknown as LayerSpecification;

  const landformLabels: LayerSpecification = {
    id: "bathymetry-landforms",
    type: "symbol",
    source: oceanSourceId,
    "source-layer": "landform",
    minzoom: 8,
    filter: ["has", "name"] as unknown as unknown[],
    layout: {
      "text-field": ["get", "name"] as unknown as unknown[],
      "text-font": ["Noto Sans Italic", "Noto Sans Regular", "Open Sans Italic", "Open Sans Regular"],
      "text-size": ["interpolate", ["linear"], ["zoom"], 8, 11, 10, 12, 12, 13],
      "text-allow-overlap": false,
      "text-anchor": "center",
      "text-offset": [0, 0.0],
    },
    paint: {
      "text-color": "rgba(148,163,184,0.70)",
      "text-halo-color": "rgba(2,6,23,0.85)",
      "text-halo-width": 1.0,
      "text-halo-blur": 0.7,
    },
  } as unknown as LayerSpecification;

  // Insert before the first symbol layer (keep labels on top), otherwise append.
  const layers = style.layers as LayerSpecification[];
  const symbolIndex = layers.findIndex((l) => l.type === "symbol");
  const insertAt = symbolIndex >= 0 ? symbolIndex : layers.length;

  const toInsert = [
    bathyFill,
    bathyHillshade,
    bathyExtrusion,
    bathyBandBorders,
    bathyBandBordersMajor,
    bathyLinesMinor,
    bathyLinesMajor,
    bathyLabels,
    landformLabels,
  ].filter(
    (l) => !layers.some((x) => x.id === l.id),
  );
  if (toInsert.length > 0) layers.splice(insertAt, 0, ...toInsert);
}

async function resolveInitialMapStyle(signal: AbortSignal): Promise<string | StyleSpecification> {
  const key = getMapTilerKey();
  if (!key) return "/map/styles/osm-seamark.json";

  const baseMapId = (import.meta.env.VITE_MAPTILER_BASE_MAP_ID || "dataviz-dark").trim();
  const styleUrl = `https://api.maptiler.com/maps/${encodeURIComponent(baseMapId)}/style.json?key=${encodeURIComponent(key)}`;

  const res = await fetch(styleUrl, { signal, headers: { accept: "application/json" } });
  if (!res.ok) throw new Error(`MapTiler style fetch failed: ${res.status} ${res.statusText}`);
  const json = (await res.json()) as StyleSpecification;
  injectOceanBathymetryLayers(json, key);
  return json;
}

async function resolveMapStyle(baseMap: BaseMapId, signal: AbortSignal): Promise<string | StyleSpecification> {
  if (baseMap === "legacy") return "/map/styles/carto-dark.json";
  return resolveInitialMapStyle(signal);
}

function getShipColor(
  t: AisTarget,
  selectedMmsi: number | null,
  legacyShipCode: string | null,
): [number, number, number, number] {
  if (selectedMmsi && t.mmsi === selectedMmsi) return [255, 255, 255, 255];
  if (legacyShipCode) {
    const rgb = LEGACY_CODE_COLORS[legacyShipCode];
    if (rgb) return [rgb[0], rgb[1], rgb[2], 235];
    return [245, 158, 11, 235];
  }
  if (!isFiniteNumber(t.sog)) return [100, 116, 139, 160];
  if (t.sog >= 10) return [59, 130, 246, 220];
  if (t.sog >= 1) return [34, 197, 94, 210];
  return [100, 116, 139, 160];
}

type DashSeg = { from: [number, number]; to: [number, number]; suspicious: boolean };

function dashifyLine(from: [number, number], to: [number, number], suspicious: boolean): DashSeg[] {
  // Simple dashed effect: split into segments and render every other one.
  const segs: DashSeg[] = [];
  const steps = 14;
  for (let i = 0; i < steps; i++) {
    if (i % 2 === 1) continue;
    const a0 = i / steps;
    const a1 = (i + 1) / steps;
    const lon0 = from[0] + (to[0] - from[0]) * a0;
    const lat0 = from[1] + (to[1] - from[1]) * a0;
    const lon1 = from[0] + (to[0] - from[0]) * a1;
    const lat1 = from[1] + (to[1] - from[1]) * a1;
    segs.push({ from: [lon0, lat0], to: [lon1, lat1], suspicious });
  }
  return segs;
}

type PairRangeCircle = {
  center: [number, number]; // [lon, lat]
  radiusNm: number;
  warn: boolean;
};

const DECK_VIEW_ID = "mapbox";

export function Map3D({
  targets,
  zones,
  selectedMmsi,
  settings,
  baseMap,
  projection,
  overlays,
  onSelectMmsi,
  onViewBboxChange,
  legacyHits,
  pairLinks,
  fcLinks,
  fleetCircles,
}: Props) {
  const containerRef = useRef<HTMLDivElement | null>(null);
  const mapRef = useRef<maplibregl.Map | null>(null);
  const overlayRef = useRef<MapboxOverlay | null>(null);
  const globeDeckLayerRef = useRef<MaplibreDeckCustomLayer | null>(null);
  const showSeamarkRef = useRef(settings.showSeamark);
  const baseMapRef = useRef<BaseMapId>(baseMap);
  const projectionRef = useRef<MapProjectionId>(projection);

  useEffect(() => {
    showSeamarkRef.current = settings.showSeamark;
  }, [settings.showSeamark]);

  useEffect(() => {
    baseMapRef.current = baseMap;
  }, [baseMap]);

  useEffect(() => {
    projectionRef.current = projection;
  }, [projection]);

  // Init MapLibre + Deck.gl (single WebGL context via MapboxOverlay)
  useEffect(() => {
    if (!containerRef.current || mapRef.current) return;

    let map: maplibregl.Map | null = null;
    let overlay: MapboxOverlay | null = null;
    let cancelled = false;
    const controller = new AbortController();

    (async () => {
      let style: string | StyleSpecification = "/map/styles/osm-seamark.json";
      try {
        style = await resolveMapStyle(baseMapRef.current, controller.signal);
      } catch (e) {
        // Don't block the app if MapTiler isn't configured yet.
        // This is expected in early dev environments without `VITE_MAPTILER_KEY`.
        console.warn("Map style init failed, falling back to local raster style:", e);
        style = "/map/styles/osm-seamark.json";
      }
      if (cancelled || !containerRef.current) return;

      map = new maplibregl.Map({
        container: containerRef.current,
        style,
        center: [126.5, 34.2],
        zoom: 7,
        pitch: 45,
        bearing: 0,
        maxPitch: 85,
        dragRotate: true,
        pitchWithRotate: true,
        touchPitch: true,
        scrollZoom: { around: "center" },
      });

      map.addControl(new maplibregl.NavigationControl({ showZoom: true, showCompass: true }), "top-left");
      map.addControl(new maplibregl.ScaleControl({ maxWidth: 120, unit: "metric" }), "bottom-left");

      mapRef.current = map;

      // Initial Deck integration:
      // - mercator: MapboxOverlay interleaved (fast, feature-rich)
      // - globe: MapLibre custom layer that feeds Deck the globe MVP matrix (keeps basemap+layers aligned)
      if (projectionRef.current === "mercator") {
        overlay = new MapboxOverlay({ interleaved: true, layers: [] } as unknown as never);
        map.addControl(overlay);
        overlayRef.current = overlay;
      } else {
        globeDeckLayerRef.current = new MaplibreDeckCustomLayer({
          id: "deck-globe",
          viewId: DECK_VIEW_ID,
          deckProps: { layers: [] },
        });
      }

      function applyProjection() {
        if (!map) return;
        const next = projectionRef.current;
        if (next === "mercator") return;
        try {
          map.setProjection({ type: next });
          // Globe mode renders a single world; copies can look odd and aren't needed for KR region.
          map.setRenderWorldCopies(next !== "globe");
        } catch (e) {
          console.warn("Projection apply failed:", e);
        }
      }

      // Ensure the seamark raster overlay exists even when using MapTiler vector styles.
      map.on("style.load", () => {
        applyProjection();
        // Globe deck layer lives inside the style and must be re-added after any style swap.
        if (projectionRef.current === "globe" && globeDeckLayerRef.current && !map!.getLayer(globeDeckLayerRef.current.id)) {
          try {
            map!.addLayer(globeDeckLayerRef.current);
          } catch {
            // ignore
          }
        }
        if (!showSeamarkRef.current) return;
        try {
          ensureSeamarkOverlay(map!, "bathymetry-lines");
        } catch {
          // ignore (style not ready / already has it)
        }
      });

      // Send initial bbox and update on move end (useful for lists / debug)
      const emitBbox = () => {
        const cb = onViewBboxChange;
        if (!cb || !map) return;
        const b = map.getBounds();
        cb([b.getWest(), b.getSouth(), b.getEast(), b.getNorth()]);
      };
      map.on("load", emitBbox);
      map.on("moveend", emitBbox);

      function applySeamarkOpacity() {
        if (!map) return;
        const opacity = settings.showSeamark ? 0.85 : 0;
        try {
          map.setPaintProperty("seamark", "raster-opacity", opacity);
        } catch {
          // style not ready yet
        }
      }

      map.once("load", () => {
        if (showSeamarkRef.current) {
          try {
            ensureSeamarkOverlay(map!, "bathymetry-lines");
          } catch {
            // ignore
          }
          applySeamarkOpacity();
        }
      });
    })();

	    return () => {
	      cancelled = true;
	      controller.abort();

	      // If we are unmounting, ensure the globe Deck instance is finalized (style reload would keep it alive).
	      try {
	        globeDeckLayerRef.current?.requestFinalize();
	      } catch {
	        // ignore
	      }

	      if (map) {
	        map.remove();
	        map = null;
	      }
      if (overlay) {
        overlay.finalize();
        overlay = null;
      }

      overlayRef.current = null;
      globeDeckLayerRef.current = null;
      mapRef.current = null;
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // Projection toggle (mercator <-> globe)
  useEffect(() => {
    const map = mapRef.current;
    if (!map) return;
    let cancelled = false;

    const syncProjectionAndDeck = () => {
      if (cancelled) return;
      try {
        map.setProjection({ type: projection });
        map.setRenderWorldCopies(projection !== "globe");
      } catch (e) {
        console.warn("Projection switch failed:", e);
      }

      if (projection === "globe") {
        // Tear down MapboxOverlay (mercator) and use a MapLibre custom layer that renders Deck
        // with MapLibre's globe MVP matrix. This avoids the Deck <-> MapLibre globe mismatch.
        const old = overlayRef.current;
        if (old) {
          try {
            old.finalize();
          } catch {
            // ignore
          }
          overlayRef.current = null;
        }

        if (!globeDeckLayerRef.current) {
          globeDeckLayerRef.current = new MaplibreDeckCustomLayer({
            id: "deck-globe",
            viewId: DECK_VIEW_ID,
            deckProps: { layers: [] },
          });
        }

        const layer = globeDeckLayerRef.current;
        if (layer && map.isStyleLoaded() && !map.getLayer(layer.id)) {
          try {
            map.addLayer(layer);
          } catch {
            // ignore
          }
        }
      } else {
        // Tear down globe custom layer (if present), restore MapboxOverlay interleaved.
        const globeLayer = globeDeckLayerRef.current;
        if (globeLayer && map.getLayer(globeLayer.id)) {
          try {
            globeLayer.requestFinalize();
            map.removeLayer(globeLayer.id);
          } catch {
            // ignore
          }
        }

        if (!overlayRef.current) {
          try {
            const next = new MapboxOverlay({ interleaved: true, layers: [] } as unknown as never);
            map.addControl(next);
            overlayRef.current = next;
          } catch (e) {
            console.warn("Deck overlay create failed:", e);
          }
        }
      }
    };

    if (map.isStyleLoaded()) syncProjectionAndDeck();
    else map.once("style.load", syncProjectionAndDeck);

    return () => {
      cancelled = true;
      try {
        map.off("style.load", syncProjectionAndDeck);
      } catch {
        // ignore
      }
    };
  }, [projection]);

  // Base map toggle
  useEffect(() => {
    const map = mapRef.current;
    if (!map) return;

    let cancelled = false;
    const controller = new AbortController();

    (async () => {
      try {
        const style = await resolveMapStyle(baseMap, controller.signal);
        if (cancelled) return;
        // Disable style diff to avoid warnings with custom layers (Deck MapboxOverlay) and
        // to ensure a clean rebuild when switching between very different styles.
        map.setStyle(style, { diff: false });
      } catch (e) {
        if (cancelled) return;
        console.warn("Base map switch failed:", e);
      }
    })();

    return () => {
      cancelled = true;
      controller.abort();
    };
  }, [baseMap]);

  // Globe rendering + bathymetry tuning.
  // Some terrain/hillshade/extrusion effects look unstable under globe and can occlude Deck overlays.
  useEffect(() => {
    const map = mapRef.current;
    if (!map) return;

    const apply = () => {
      if (!map.isStyleLoaded()) return;
      const disableBathy3D = projection === "globe" && baseMap === "enhanced";
      const vis = disableBathy3D ? "none" : "visible";
      for (const id of ["bathymetry-extrusion", "bathymetry-hillshade"]) {
        try {
          if (map.getLayer(id)) map.setLayoutProperty(id, "visibility", vis);
        } catch {
          // ignore
        }
      }
    };

    if (map.isStyleLoaded()) apply();
    map.on("style.load", apply);
    return () => {
      try {
        map.off("style.load", apply);
      } catch {
        // ignore
      }
    };
  }, [projection, baseMap]);

  // seamark toggle
  useEffect(() => {
    const map = mapRef.current;
    if (!map) return;
    if (settings.showSeamark) {
      try {
        ensureSeamarkOverlay(map, "bathymetry-lines");
        map.setPaintProperty("seamark", "raster-opacity", 0.85);
      } catch {
        // ignore until style is ready
      }
      return;
    }

    // If seamark is off, remove the layer+source to avoid unnecessary network tile requests.
    try {
      if (map.getLayer("seamark")) map.removeLayer("seamark");
    } catch {
      // ignore
    }
    try {
      if (map.getSource("seamark")) map.removeSource("seamark");
    } catch {
      // ignore
    }
  }, [settings.showSeamark]);

  // Zones (MapLibre-native GeoJSON layers; works in both mercator + globe)
  useEffect(() => {
    const map = mapRef.current;
    if (!map) return;

    const srcId = "zones-src";
    const fillId = "zones-fill";
    const lineId = "zones-line";

    const zoneColorExpr: unknown[] = ["match", ["get", "zoneId"]];
    for (const k of Object.keys(ZONE_META) as ZoneId[]) {
      zoneColorExpr.push(k, ZONE_META[k].color);
    }
    zoneColorExpr.push("#3B82F6");

    const ensure = () => {
      // Always update visibility if the layers exist.
      const visibility = overlays.zones ? "visible" : "none";
      try {
        if (map.getLayer(fillId)) map.setLayoutProperty(fillId, "visibility", visibility);
      } catch {
        // ignore
      }
      try {
        if (map.getLayer(lineId)) map.setLayoutProperty(lineId, "visibility", visibility);
      } catch {
        // ignore
      }

      if (!zones) return;
      if (!map.isStyleLoaded()) return;

      try {
        const existing = map.getSource(srcId) as GeoJSONSource | undefined;
        if (existing) {
          existing.setData(zones);
        } else {
          map.addSource(srcId, { type: "geojson", data: zones } as GeoJSONSourceSpecification);
        }

        // Keep zones below Deck layers (ships / deck-globe), and below seamarks if enabled.
        const style = map.getStyle();
        const firstSymbol = (style.layers || []).find((l) => (l as { type?: string } | undefined)?.type === "symbol") as
          | { id?: string }
          | undefined;
        const before = map.getLayer("deck-globe")
          ? "deck-globe"
          : map.getLayer("ships")
            ? "ships"
            : map.getLayer("seamark")
              ? "seamark"
              : firstSymbol?.id;

        if (!map.getLayer(fillId)) {
          map.addLayer(
            {
              id: fillId,
              type: "fill",
              source: srcId,
              paint: {
                "fill-color": zoneColorExpr as never,
                "fill-opacity": 0.12,
              },
              layout: { visibility },
            } as unknown as LayerSpecification,
            before,
          );
        }

        if (!map.getLayer(lineId)) {
          map.addLayer(
            {
              id: lineId,
              type: "line",
              source: srcId,
              paint: {
                "line-color": zoneColorExpr as never,
                "line-opacity": 0.85,
                "line-width": ["interpolate", ["linear"], ["zoom"], 4, 0.8, 10, 1.4, 14, 2.1],
              },
              layout: { visibility },
            } as unknown as LayerSpecification,
            before,
          );
        }
      } catch (e) {
        console.warn("Zones layer setup failed:", e);
      }
    };

    if (map.isStyleLoaded()) ensure();
    map.on("style.load", ensure);
    return () => {
      try {
        map.off("style.load", ensure);
      } catch {
        // ignore
      }
    };
  }, [zones, overlays.zones]);

  const shipData = useMemo(() => {
    return targets.filter((t) => isFiniteNumber(t.lat) && isFiniteNumber(t.lon));
  }, [targets]);

  const globePosByMmsi = useMemo(() => {
    if (projection !== "globe") return null;
    const m = new Map<number, [number, number, number]>();
    for (const t of shipData) {
      // Slightly above the sea surface to keep the icon readable and avoid depth-fighting.
      m.set(t.mmsi, lngLatToUnitSphere(t.lon, t.lat, 12));
    }
    return m;
  }, [projection, shipData]);

  const legacyTargets = useMemo(() => {
    if (!legacyHits) return [];
    return shipData.filter((t) => legacyHits.has(t.mmsi));
  }, [shipData, legacyHits]);

  const fcDashed = useMemo(() => {
    const segs: DashSeg[] = [];
    for (const l of fcLinks || []) segs.push(...dashifyLine(l.from, l.to, l.suspicious));
    return segs;
  }, [fcLinks]);

  const pairRanges = useMemo(() => {
    const out: PairRangeCircle[] = [];
    for (const p of pairLinks || []) {
      const center: [number, number] = [(p.from[0] + p.to[0]) / 2, (p.from[1] + p.to[1]) / 2];
      out.push({ center, radiusNm: Math.max(0.05, p.distanceNm / 2), warn: p.warn });
    }
    return out;
  }, [pairLinks]);

  // When the selected MMSI changes due to external UI (e.g., list click), fly to it.
  useEffect(() => {
    const map = mapRef.current;
    if (!map) return;
    if (!selectedMmsi) return;
    const t = shipData.find((x) => x.mmsi === selectedMmsi);
    if (!t) return;
    map.easeTo({ center: [t.lon, t.lat], zoom: Math.max(map.getZoom(), 10), duration: 600 });
  }, [selectedMmsi, shipData]);

  // Update Deck.gl layers
  useEffect(() => {
    const map = mapRef.current;
    if (!map) return;
    const deckTarget = projection === "globe" ? globeDeckLayerRef.current : overlayRef.current;
    if (!deckTarget) return;

    const overlayParams = projection === "globe" ? GLOBE_OVERLAY_PARAMS : DEPTH_DISABLED_PARAMS;
    const layers = [];

    if (settings.showDensity && projection !== "globe") {
      layers.push(
        new HexagonLayer<AisTarget>({
          id: "density",
          data: shipData,
          pickable: true,
          extruded: true,
          radius: 2500,
          elevationScale: 35,
          coverage: 0.92,
          opacity: 0.35,
          getPosition: (d) => [d.lon, d.lat],
        }),
      );
    }

    if (overlays.fleetCircles && projection !== "globe" && (fleetCircles?.length ?? 0) > 0) {
      layers.push(
        new ScatterplotLayer<FleetCircle>({
          id: "fleet-circles",
          data: fleetCircles,
          pickable: false,
          billboard: false,
          parameters: overlayParams,
          filled: false,
          stroked: true,
          radiusUnits: "meters",
          getRadius: (d) => d.radiusNm * 1852,
          lineWidthUnits: "pixels",
          getLineWidth: 1,
          getLineColor: () => [245, 158, 11, 140],
          getPosition: (d) => d.center,
        }),
      );
    }

    if (overlays.pairRange && projection !== "globe" && pairRanges.length > 0) {
      layers.push(
        new ScatterplotLayer<PairRangeCircle>({
          id: "pair-range",
          data: pairRanges,
          pickable: false,
          billboard: false,
          parameters: overlayParams,
          filled: false,
          stroked: true,
          radiusUnits: "meters",
          getRadius: (d) => d.radiusNm * 1852,
          radiusMinPixels: 10,
          lineWidthUnits: "pixels",
          getLineWidth: () => 1,
          getLineColor: (d) => (d.warn ? [245, 158, 11, 170] : [59, 130, 246, 110]),
          getPosition: (d) => d.center,
        }),
      );
    }

    if (overlays.pairLines && (pairLinks?.length ?? 0) > 0) {
      layers.push(
        new LineLayer<PairLink>({
          id: "pair-lines",
          data: pairLinks,
          pickable: false,
          parameters: overlayParams,
          getSourcePosition: (d) => (projection === "globe" ? lngLatToUnitSphere(d.from[0], d.from[1]) : d.from),
          getTargetPosition: (d) => (projection === "globe" ? lngLatToUnitSphere(d.to[0], d.to[1]) : d.to),
          getColor: (d) => (d.warn ? [245, 158, 11, 220] : [59, 130, 246, 85]),
          getWidth: (d) => (d.warn ? 2.2 : 1.4),
          widthUnits: "pixels",
        }),
      );
    }

    if (overlays.fcLines && fcDashed.length > 0) {
      layers.push(
        new LineLayer<DashSeg>({
          id: "fc-lines",
          data: fcDashed,
          pickable: false,
          parameters: overlayParams,
          getSourcePosition: (d) => (projection === "globe" ? lngLatToUnitSphere(d.from[0], d.from[1]) : d.from),
          getTargetPosition: (d) => (projection === "globe" ? lngLatToUnitSphere(d.to[0], d.to[1]) : d.to),
          getColor: (d) => (d.suspicious ? [239, 68, 68, 220] : [217, 119, 6, 200]),
          getWidth: () => 1.3,
          widthUnits: "pixels",
        }),
      );
    }

    if (settings.showShips && legacyTargets.length > 0) {
      layers.push(
        new ScatterplotLayer<AisTarget>({
          id: "legacy-halo",
          data: legacyTargets,
          pickable: false,
          billboard: projection === "globe",
          // This ring is most prone to z-fighting, so force it into pure painter's-order rendering.
          parameters: overlayParams,
          filled: false,
          stroked: true,
          radiusUnits: "pixels",
          getRadius: (d) => (selectedMmsi && d.mmsi === selectedMmsi ? 22 : 16),
          lineWidthUnits: "pixels",
          getLineWidth: 2,
          getLineColor: (d) => {
            const l = legacyHits?.get(d.mmsi);
            const rgb = l ? LEGACY_CODE_COLORS[l.shipCode] : null;
            if (!rgb) return [245, 158, 11, 200];
            return [rgb[0], rgb[1], rgb[2], 200];
          },
          getPosition: (d) =>
            projection === "globe"
              ? (globePosByMmsi?.get(d.mmsi) ?? lngLatToUnitSphere(d.lon, d.lat, 12))
              : ([d.lon, d.lat] as [number, number]),
          updateTriggers: {
            getRadius: [selectedMmsi],
            getLineColor: [legacyHits],
          },
        }),
      );
    }

    if (settings.showShips) {
      layers.push(
        new IconLayer<AisTarget>({
          id: "ships",
          data: shipData,
          pickable: true,
          // Mercator: keep icons horizontal on the sea surface when view is pitched/rotated.
          // Globe: billboard to keep the icon visible and glued to the globe.
          billboard: projection === "globe",
          parameters: projection === "globe" ? ({ ...overlayParams, cullMode: "none" } as const) : overlayParams,
          iconAtlas: "/assets/ship.svg",
          iconMapping: SHIP_ICON_MAPPING,
          getIcon: () => "ship",
          getPosition: (d) =>
            projection === "globe"
              ? (globePosByMmsi?.get(d.mmsi) ?? lngLatToUnitSphere(d.lon, d.lat, 12))
              : ([d.lon, d.lat] as [number, number]),
          getAngle: (d) => (isFiniteNumber(d.cog) ? d.cog : 0),
          sizeUnits: "pixels",
          getSize: (d) => (selectedMmsi && d.mmsi === selectedMmsi ? 34 : 22),
          getColor: (d) => getShipColor(d, selectedMmsi, legacyHits?.get(d.mmsi)?.shipCode ?? null),
          alphaCutoff: 0.05,
          updateTriggers: {
            getSize: [selectedMmsi],
            getColor: [selectedMmsi, legacyHits],
          },
        }),
      );
    }

    const deckProps = {
      layers,
      getTooltip: (info: PickingInfo) => {
        if (!info.object) return null;
        if (info.layer && info.layer.id === "density") {
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          const o: any = info.object;
          const n = Array.isArray(o?.points) ? o.points.length : 0;
          return { text: `AIS density: ${n}` };
        }
        // zones
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const obj: any = info.object;
        if (typeof obj.mmsi === "number") {
          const t = obj as AisTarget;
          const name = (t.name || "").trim() || "(no name)";
          const legacy = legacyHits?.get(t.mmsi);
          const legacyHtml = legacy
            ? `<div style="margin-top: 6px; padding-top: 6px; border-top: 1px solid rgba(255,255,255,.08)">
                <div><b>CN Permit</b> · <b>${legacy.shipCode}</b> · ${legacy.permitNo}</div>
              </div>`
            : "";
          return {
            html: `<div style="font-family: system-ui; font-size: 12px;">
              <div style="font-weight: 700; margin-bottom: 4px;">${name}</div>
              <div>MMSI: <b>${t.mmsi}</b> · ${t.vesselType || "Unknown"}</div>
              <div>SOG: <b>${t.sog ?? "?"}</b> kt · COG: <b>${t.cog ?? "?"}</b>°</div>
              <div style="opacity:.8">${t.status || ""}</div>
              <div style="opacity:.7; font-size: 11px; margin-top: 4px;">${t.messageTimestamp || ""}</div>
              ${legacyHtml}
            </div>`,
          };
        }

        const p = obj.properties as { zoneName?: string; zoneLabel?: string } | undefined;
        const label = p?.zoneName ?? p?.zoneLabel;
        if (label) return { text: label };
        return null;
      },
      onClick: (info: PickingInfo) => {
        if (!info.object) {
          onSelectMmsi(null);
          return;
        }
        if (info.layer && info.layer.id === "density") return;
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const obj: any = info.object;
        if (typeof obj.mmsi === "number") {
          const t = obj as AisTarget;
          onSelectMmsi(t.mmsi);
          map.easeTo({ center: [t.lon, t.lat], zoom: Math.max(map.getZoom(), 10), duration: 600 });
        }
      },
    } as const;

    if (projection === "globe") globeDeckLayerRef.current?.setProps(deckProps);
    else overlayRef.current?.setProps(deckProps as unknown as never);
  }, [
    projection,
    shipData,
    zones,
    selectedMmsi,
    overlays.zones,
    settings.showShips,
    settings.showDensity,
    onSelectMmsi,
    legacyHits,
    legacyTargets,
    overlays.pairLines,
    overlays.pairRange,
    overlays.fcLines,
    overlays.fleetCircles,
    pairLinks,
    pairRanges,
    fcDashed,
    fleetCircles,
    globePosByMmsi,
  ]);

  return <div ref={containerRef} style={{ width: "100%", height: "100%" }} />;
}
