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 StyleSpecification,
  type VectorSourceSpecification,
} from "maplibre-gl";
import { useCallback, useEffect, useMemo, useRef, useState } 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;
  hoveredMmsiSet?: number[];
  hoveredFleetMmsiSet?: number[];
  hoveredPairMmsiSet?: number[];
  hoveredFleetOwnerKey?: string | null;
  highlightedMmsiSet?: number[];
  settings: Map3DSettings;
  baseMap: BaseMapId;
  projection: MapProjectionId;
  overlays: MapToggleState;
  onSelectMmsi: (mmsi: number | null) => void;
  onToggleHighlightMmsi?: (mmsi: number) => void;
  onViewBboxChange?: (bbox: [number, number, number, number]) => void;
  legacyHits?: Map<number, LegacyVesselInfo> | null;
  pairLinks?: PairLink[];
  fcLinks?: FcLink[];
  fleetCircles?: FleetCircle[];
  onProjectionLoadingChange?: (loading: boolean) => void;
  fleetFocus?: {
    id: string | number;
    center: [number, number];
    zoom?: number;
  };
  onHoverFleet?: (ownerKey: string | null, fleetMmsiSet: number[]) => void;
  onClearFleetHover?: () => void;
  onHoverMmsi?: (mmsiList: number[]) => void;
  onClearMmsiHover?: () => void;
  onHoverPair?: (mmsiList: number[]) => void;
  onClearPairHover?: () => void;
};

function toNumberSet(values: number[] | undefined | null) {
  const out = new Set<number>();
  if (!values) return out;
  for (const value of values) {
    if (Number.isFinite(value)) {
      out.add(value);
    }
  }
  return out;
}

function mergeNumberSets(...sets: Set<number>[]) {
  const out = new Set<number>();
  for (const s of sets) {
    for (const v of s) {
      out.add(v);
    }
  }
  return out;
}

function makeSetSignature(values: Set<number>) {
  return Array.from(values).sort((a, b) => a - b).join(",");
}

function toTextValue(value: unknown): string {
  if (value == null) return "";
  return String(value).trim();
}

function getZoneIdFromProps(props: Record<string, unknown> | null | undefined): string {
  const safeProps = props || {};
  const candidates = [
    "zoneId",
    "zone_id",
    "zoneIdNo",
    "zoneKey",
    "zoneCode",
    "ZONE_ID",
    "ZONECODE",
    "id",
  ];

  for (const key of candidates) {
    const value = toTextValue(safeProps[key]);
    if (value) return value;
  }

  return "";
}

function getZoneDisplayNameFromProps(props: Record<string, unknown> | null | undefined): string {
  const safeProps = props || {};
  const nameCandidates = ["zoneName", "zoneLabel", "NAME", "name", "ZONE_NM", "label"];
  for (const key of nameCandidates) {
    const name = toTextValue(safeProps[key]);
    if (name) return name;
  }
  const zoneId = getZoneIdFromProps(safeProps);
  if (!zoneId) return "수역";
  return ZONE_META[(zoneId as ZoneId)]?.name || `수역 ${zoneId}`;
}

function makeOrderedPairKey(a: number, b: number) {
  const left = Math.trunc(Math.min(a, b));
  const right = Math.trunc(Math.max(a, b));
  return `${left}-${right}`;
}

function makePairLinkFeatureId(a: number, b: number, suffix?: string) {
  const pair = makeOrderedPairKey(a, b);
  return suffix ? `pair-${pair}-${suffix}` : `pair-${pair}`;
}

function makeFcSegmentFeatureId(a: number, b: number, segmentIndex: number) {
  const pair = makeOrderedPairKey(a, b);
  return `fc-${pair}-${segmentIndex}`;
}

function makeFleetCircleFeatureId(ownerKey: string) {
  return `fleet-${ownerKey}`;
}

function makeMmsiPairHighlightExpr(aField: string, bField: string, hoveredMmsiList: number[]) {
  if (!Array.isArray(hoveredMmsiList) || hoveredMmsiList.length < 2) {
    return false;
  }
  const inA = ["in", ["to-number", ["get", aField]], ["literal", hoveredMmsiList]] as unknown[];
  const inB = ["in", ["to-number", ["get", bField]], ["literal", hoveredMmsiList]] as unknown[];
  return ["all", inA, inB] as unknown[];
}

function makeMmsiAnyEndpointExpr(aField: string, bField: string, hoveredMmsiList: number[]) {
  if (!Array.isArray(hoveredMmsiList) || hoveredMmsiList.length === 0) {
    return false;
  }
  const literal = ["literal", hoveredMmsiList] as unknown[];
  return [
    "any",
    ["in", ["to-number", ["get", aField]], literal],
    ["in", ["to-number", ["get", bField]], literal],
  ] as unknown[];
}

function makeFleetOwnerMatchExpr(hoveredOwnerKeys: string[]) {
  if (!Array.isArray(hoveredOwnerKeys) || hoveredOwnerKeys.length === 0) {
    return false;
  }
  const expr = ["match", ["to-string", ["coalesce", ["get", "ownerKey"], ""]]] as unknown[];
  for (const ownerKey of hoveredOwnerKeys) {
    expr.push(String(ownerKey), true);
  }
  expr.push(false);
  return expr;
}

function makeFleetMemberMatchExpr(hoveredFleetMmsiList: number[]) {
  if (!Array.isArray(hoveredFleetMmsiList) || hoveredFleetMmsiList.length === 0) {
    return false;
  }
  const clauses = hoveredFleetMmsiList.map((mmsi) =>
    ["in", mmsi, ["coalesce", ["get", "vesselMmsis"], []]] as unknown[],
  );
  return ["any", ...clauses] as unknown[];
}

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);
}

function kickRepaint(map: maplibregl.Map | null) {
  if (!map) return;
  try {
    map.triggerRepaint();
  } catch {
    // ignore
  }
  try {
    requestAnimationFrame(() => {
      try {
        map.triggerRepaint();
      } catch {
        // ignore
      }
    });
    requestAnimationFrame(() => {
      try {
        map.triggerRepaint();
      } catch {
        // ignore
      }
    });
  } catch {
    // ignore (e.g., non-browser env)
  }
}

function onMapStyleReady(map: maplibregl.Map | null, callback: () => void) {
  if (!map) {
    return () => {
      // noop
    };
  }
  if (map.isStyleLoaded()) {
    callback();
    return () => {
      // noop
    };
  }

  let fired = false;
  const runOnce = () => {
    if (!map || fired || !map.isStyleLoaded()) return;
    fired = true;
    callback();
    try {
      map.off("style.load", runOnce);
      map.off("styledata", runOnce);
      map.off("idle", runOnce);
    } catch {
      // ignore
    }
  };

  map.on("style.load", runOnce);
  map.on("styledata", runOnce);
  map.on("idle", runOnce);

  return () => {
    if (fired) return;
    fired = true;
    try {
      if (!map) return;
      map.off("style.load", runOnce);
      map.off("styledata", runOnce);
      map.off("idle", runOnce);
    } catch {
      // ignore
    }
  };
}

function extractProjectionType(map: maplibregl.Map): MapProjectionId | undefined {
  const projection = map.getProjection?.();
  if (!projection || typeof projection !== "object") return undefined;

  const rawType = (projection as { type?: unknown; name?: unknown }).type ?? (projection as { type?: unknown; name?: unknown }).name;
  if (rawType === "globe") return "globe";
  if (rawType === "mercator") return "mercator";
  return undefined;
}

const DEG2RAD = Math.PI / 180;
const GLOBE_ICON_HEADING_OFFSET_DEG = -90;
const MAP_SELECTED_SHIP_RGB: [number, number, number] = [34, 211, 238];
const MAP_HIGHLIGHT_SHIP_RGB: [number, number, number] = [245, 158, 11];
const MAP_DEFAULT_SHIP_RGB: [number, number, number] = [100, 116, 139];

const clampNumber = (value: number, minValue: number, maxValue: number) => Math.max(minValue, Math.min(maxValue, value));

function getLayerId(value: unknown): string | null {
  if (!value || typeof value !== "object") return null;
  const candidate = (value as { id?: unknown }).id;
  return typeof candidate === "string" ? candidate : null;
}

function sanitizeDeckLayerList(value: unknown): unknown[] {
  if (!Array.isArray(value)) return [];
  const seen = new Set<string>();
  const out: unknown[] = [];
  let dropped = 0;

  for (const layer of value) {
    const layerId = getLayerId(layer);
    if (!layerId) {
      dropped += 1;
      continue;
    }
    if (seen.has(layerId)) {
      dropped += 1;
      continue;
    }
    seen.add(layerId);
    out.push(layer);
  }

  if (dropped > 0 && import.meta.env.DEV) {
    console.warn(`Sanitized deck layer list: dropped ${dropped} invalid/duplicate entries`);
  }

  return out;
}

function normalizeAngleDeg(value: number, offset = 0): number {
  const v = value + offset;
  return ((v % 360) + 360) % 360;
}

function getDisplayHeading({
  cog,
  heading,
  offset = 0,
}: {
  cog: number | null | undefined;
  heading: number | null | undefined;
  offset?: number;
}) {
  const raw =
    isFiniteNumber(heading) && heading >= 0 && heading <= 360 && heading !== 511 ? heading : isFiniteNumber(cog) ? cog : 0;
  return normalizeAngleDeg(raw, offset);
}

function toSafeNumber(value: unknown): number | null {
  if (typeof value === "number" && Number.isFinite(value)) return value;
  return null;
}

function toIntMmsi(value: unknown): number | null {
  const n = toSafeNumber(value);
  if (n == null) return null;
  return Math.trunc(n);
}

function formatNm(value: number | null | undefined) {
  if (!isFiniteNumber(value)) return "-";
  return `${value.toFixed(2)} NM`;
}

function getLegacyTag(legacyHits: Map<number, LegacyVesselInfo> | null | undefined, mmsi: number) {
  const legacy = legacyHits?.get(mmsi);
  if (!legacy) return null;
  return `${legacy.permitNo} (${legacy.shipCode})`;
}

function getTargetName(mmsi: number, targetByMmsi: Map<number, AisTarget>, legacyHits: Map<number, LegacyVesselInfo> | null | undefined) {
  const legacy = legacyHits?.get(mmsi);
  const target = targetByMmsi.get(mmsi);
  return (
    (target?.name || "").trim() || legacy?.shipNameCn || legacy?.shipNameRoman || `MMSI ${mmsi}`
  );
}

function getShipTooltipHtml({
  mmsi,
  targetByMmsi,
  legacyHits,
}: {
  mmsi: number;
  targetByMmsi: Map<number, AisTarget>;
  legacyHits: Map<number, LegacyVesselInfo> | null | undefined;
}) {
  const legacy = legacyHits?.get(mmsi);
  const t = targetByMmsi.get(mmsi);
  const name = getTargetName(mmsi, targetByMmsi, legacyHits);
  const sog = isFiniteNumber(t?.sog) ? t.sog : null;
  const cog = isFiniteNumber(t?.cog) ? t.cog : null;
  const msg = t?.messageTimestamp ?? null;
  const vesselType = t?.vesselType || "";

  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>유효범위: ${legacy.workSeaArea || "-"}</div>
      </div>`
    : "";

  return {
    html: `<div style="font-family: system-ui; font-size: 12px; white-space: nowrap;">
      <div style="font-weight: 700; margin-bottom: 4px;">${name}</div>
      <div>MMSI: <b>${mmsi}</b>${vesselType ? ` · ${vesselType}` : ""}</div>
      <div>SOG: <b>${sog ?? "?"}</b> kt · COG: <b>${cog ?? "?"}</b>°</div>
      ${msg ? `<div style="opacity:.7; font-size: 11px; margin-top: 4px;">${msg}</div>` : ""}
      ${legacyHtml}
    </div>`,
  };
}

function getPairLinkTooltipHtml({
  warn,
  distanceNm,
  aMmsi,
  bMmsi,
  legacyHits,
  targetByMmsi,
}: {
  warn: boolean;
  distanceNm: number | null | undefined;
  aMmsi: number;
  bMmsi: number;
  legacyHits: Map<number, LegacyVesselInfo> | null | undefined;
  targetByMmsi: Map<number, AisTarget>;
}) {
  const d = formatNm(distanceNm);
  const a = getTargetName(aMmsi, targetByMmsi, legacyHits);
  const b = getTargetName(bMmsi, targetByMmsi, legacyHits);
  const aTag = getLegacyTag(legacyHits, aMmsi);
  const bTag = getLegacyTag(legacyHits, bMmsi);
  return {
    html: `<div style="font-family: system-ui; font-size: 12px;">
      <div style="font-weight: 700; margin-bottom: 4px;">쌍 연결</div>
      <div>${aTag ?? `MMSI ${aMmsi}`}</div>
      <div style="opacity:.85;">↔ ${bTag ?? `MMSI ${bMmsi}`}</div>
      <div style="margin-top: 4px;">거리: <b>${d}</b> · 상태: <b>${warn ? "주의" : "정상"}</b></div>
      <div style="opacity:.6; margin-top: 3px;">${a} / ${b}</div>
    </div>`,
  };
}

function getFcLinkTooltipHtml({
  suspicious,
  distanceNm,
  fcMmsi,
  otherMmsi,
  legacyHits,
  targetByMmsi,
}: {
  suspicious: boolean;
  distanceNm: number | null | undefined;
  fcMmsi: number;
  otherMmsi: number;
  legacyHits: Map<number, LegacyVesselInfo> | null | undefined;
  targetByMmsi: Map<number, AisTarget>;
}) {
  const d = formatNm(distanceNm);
  const a = getTargetName(fcMmsi, targetByMmsi, legacyHits);
  const b = getTargetName(otherMmsi, targetByMmsi, legacyHits);
  const aTag = getLegacyTag(legacyHits, fcMmsi);
  const bTag = getLegacyTag(legacyHits, otherMmsi);
  return {
    html: `<div style="font-family: system-ui; font-size: 12px;">
      <div style="font-weight: 700; margin-bottom: 4px;">환적 연결</div>
      <div>${aTag ?? `MMSI ${fcMmsi}`}</div>
      <div style="opacity:.85;">→ ${bTag ?? `MMSI ${otherMmsi}`}</div>
      <div style="margin-top: 4px;">거리: <b>${d}</b> · 상태: <b>${suspicious ? "의심" : "일반"}</b></div>
      <div style="opacity:.6; margin-top: 3px;">${a} / ${b}</div>
    </div>`,
  };
}

function getRangeTooltipHtml({
  warn,
  distanceNm,
  aMmsi,
  bMmsi,
  legacyHits,
}: {
  warn: boolean;
  distanceNm: number | null | undefined;
  aMmsi: number;
  bMmsi: number;
  legacyHits: Map<number, LegacyVesselInfo> | null | undefined;
}) {
  const d = formatNm(distanceNm);
  const aTag = getLegacyTag(legacyHits, aMmsi);
  const bTag = getLegacyTag(legacyHits, bMmsi);
  const radiusNm = toSafeNumber(distanceNm);
  return {
    html: `<div style="font-family: system-ui; font-size: 12px;">
      <div style="font-weight: 700; margin-bottom: 4px;">쌍 연결범위</div>
      <div>${aTag ?? `MMSI ${aMmsi}`}</div>
      <div style="opacity:.85;">↔ ${bTag ?? `MMSI ${bMmsi}`}</div>
      <div style="margin-top: 4px;">범위: <b>${d}</b> · 반경: <b>${formatNm(radiusNm == null ? null : radiusNm / 2)}</b> · 상태: <b>${warn ? "주의" : "정상"}</b></div>
    </div>`,
  };
}

function getFleetCircleTooltipHtml({
  ownerKey,
  ownerLabel,
  count,
}: {
  ownerKey: string;
  ownerLabel?: string;
  count: number;
}) {
  const displayOwner = ownerLabel && ownerLabel.trim() ? ownerLabel : ownerKey;
  return {
    html: `<div style="font-family: system-ui; font-size: 12px;">
      <div style="font-weight: 700; margin-bottom: 4px;">선단 범위</div>
      <div>소유주: ${displayOwner || "-"}</div>
      <div>선박 수: <b>${count}</b></div>
    </div>`,
  };
}

function rgbToHex(rgb: [number, number, number]) {
  const toHex = (v: number) => {
    const clamped = Math.max(0, Math.min(255, Math.round(v)));
    return clamped.toString(16).padStart(2, "0");
  };

  return `#${toHex(rgb[0])}${toHex(rgb[1])}${toHex(rgb[2])}`;
}

function lightenColor(rgb: [number, number, number], ratio = 0.32) {
  const out = rgb.map((v) => Math.round(v + (255 - v) * ratio) as number) as [number, number, number];
  return out;
}

function getGlobeBaseShipColor({
  legacy,
  sog,
}: {
  legacy: string | null;
  sog: number | null;
}) {
  if (legacy) {
    const rgb = LEGACY_CODE_COLORS[legacy];
    if (rgb) return rgbToHex(lightenColor(rgb, 0.38));
  }

  if (!isFiniteNumber(sog)) return "rgba(100,116,139,0.75)";
  if (sog >= 10) return "#3b82f6";
  if (sog >= 1) return "#22c55e";
  return "rgba(100,116,139,0.75)";
}

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
  C21: [236, 72, 153], // #ec4899
};

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 FLAT_SHIP_ICON_SIZE = 19;
const FLAT_SHIP_ICON_SIZE_SELECTED = 28;
const FLAT_SHIP_ICON_SIZE_HIGHLIGHTED = 25;
const FLAT_LEGACY_HALO_RADIUS = 14;
const FLAT_LEGACY_HALO_RADIUS_SELECTED = 18;
const FLAT_LEGACY_HALO_RADIUS_HIGHLIGHTED = 16;

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 makeGlobeCircleRadiusExpr() {
  const base3 = 4;
  const base7 = 6;
  const base10 = 8;
  const base14 = 11;

  return [
    "interpolate",
    ["linear"],
    ["zoom"],
    3,
    ["case", ["==", ["get", "selected"], 1], 4.6, ["==", ["get", "highlighted"], 1], 4.2, base3],
    7,
    ["case", ["==", ["get", "selected"], 1], 6.8, ["==", ["get", "highlighted"], 1], 6.2, base7],
    10,
    ["case", ["==", ["get", "selected"], 1], 9.0, ["==", ["get", "highlighted"], 1], 8.2, base10],
    14,
    ["case", ["==", ["get", "selected"], 1], 11.8, ["==", ["get", "highlighted"], 1], 10.8, base14],
  ];
}

const GLOBE_SHIP_CIRCLE_RADIUS_EXPR = makeGlobeCircleRadiusExpr() as never;

function buildFallbackGlobeShipIcon() {
  const size = 96;
  const canvas = document.createElement("canvas");
  canvas.width = size;
  canvas.height = size;
  const ctx = canvas.getContext("2d");
  if (!ctx) return null;

  ctx.clearRect(0, 0, size, size);
  ctx.fillStyle = "rgba(255,255,255,1)";
  ctx.beginPath();
  ctx.moveTo(size / 2, 6);
  ctx.lineTo(size / 2 - 14, 24);
  ctx.lineTo(size / 2 - 18, 58);
  ctx.lineTo(size / 2 - 10, 88);
  ctx.lineTo(size / 2 + 10, 88);
  ctx.lineTo(size / 2 + 18, 58);
  ctx.lineTo(size / 2 + 14, 24);
  ctx.closePath();
  ctx.fill();

  ctx.fillRect(size / 2 - 8, 34, 16, 18);

  return ctx.getImageData(0, 0, size, size);
}

function ensureFallbackShipImage(map: maplibregl.Map, imageId: string) {
  if (!map || map.hasImage(imageId)) return;
  const image = buildFallbackGlobeShipIcon();
  if (!image) return;

  try {
    map.addImage(imageId, image, { pixelRatio: 2, sdf: true });
  } catch {
    // ignore
  }
}

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) {
  // NOTE: Vector-only bathymetry injection.
  // Raster/DEM hillshade was intentionally removed for now because it caused ocean flicker
  // and extra PNG tile traffic under globe projection in our setup.
  const oceanSourceId = "maptiler-ocean";

  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];
  }

  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 bathyFill: LayerSpecification = {
    id: "bathymetry-fill",
    type: "fill",
    source: oceanSourceId,
    "source-layer": "contour",
    // Very low zoom tiles can contain extremely complex polygons (coastline/detail),
    // which may exceed MapLibre's per-segment 16-bit vertex limit and render incorrectly.
    // We keep the fill starting at a more reasonable zoom.
    minzoom: 6,
    maxzoom: 12,
    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 bathyBandBorders: LayerSpecification = {
    id: "bathymetry-borders",
    type: "line",
    source: oceanSourceId,
    "source-layer": "contour",
    minzoom: 6,
    maxzoom: 14,
    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 bathyMajorDepthFilter: unknown[] = [
    "in",
    ["to-number", ["get", "depth"]],
    ["literal", majorDepths],
  ] as unknown[];

  const bathyLinesMajor: LayerSpecification = {
    id: "bathymetry-lines-major",
    type: "line",
    source: oceanSourceId,
    "source-layer": "contour_line",
    minzoom: 8,
    maxzoom: 14,
    filter: bathyMajorDepthFilter 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,
    maxzoom: 14,
    filter: bathyMajorDepthFilter 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,
    filter: bathyMajorDepthFilter 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 rawLayers = Array.isArray(style.layers) ? style.layers : [];
  const layers = rawLayers.filter((layer): layer is LayerSpecification => {
    if (!layer || typeof layer !== "object") return false;
    return typeof (layer as { id?: unknown }).id === "string";
  });
  const symbolIndex = layers.findIndex((l) => l.type === "symbol");
  const insertAt = symbolIndex >= 0 ? symbolIndex : layers.length;

  const toInsert = [
    bathyFill,
    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,
  highlightedMmsis: Set<number>,
): [number, number, number, number] {
  if (selectedMmsi && t.mmsi === selectedMmsi) {
    return [MAP_SELECTED_SHIP_RGB[0], MAP_SELECTED_SHIP_RGB[1], MAP_SELECTED_SHIP_RGB[2], 255];
  }

  if (highlightedMmsis.has(t.mmsi)) {
    return [MAP_HIGHLIGHT_SHIP_RGB[0], MAP_HIGHLIGHT_SHIP_RGB[1], MAP_HIGHLIGHT_SHIP_RGB[2], 235];
  }
  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 [MAP_DEFAULT_SHIP_RGB[0], MAP_DEFAULT_SHIP_RGB[1], MAP_DEFAULT_SHIP_RGB[2], 160];
  if (t.sog >= 10) return [59, 130, 246, 220];
  if (t.sog >= 1) return [34, 197, 94, 210];
  return [MAP_DEFAULT_SHIP_RGB[0], MAP_DEFAULT_SHIP_RGB[1], MAP_DEFAULT_SHIP_RGB[2], 160];
}

type DashSeg = {
  from: [number, number];
  to: [number, number];
  suspicious: boolean;
  distanceNm?: number;
  fromMmsi?: number;
  toMmsi?: number;
};

function dashifyLine(
  from: [number, number],
  to: [number, number],
  suspicious: boolean,
  distanceNm?: number,
  fromMmsi?: number,
  toMmsi?: number,
): 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,
      distanceNm,
      fromMmsi,
      toMmsi,
    });
  }
  return segs;
}

function circleRingLngLat(center: [number, number], radiusMeters: number, steps = 72): [number, number][] {
  const [lon0, lat0] = center;
  const latRad = lat0 * DEG2RAD;
  const cosLat = Math.max(1e-6, Math.cos(latRad));
  const r = Math.max(0, radiusMeters);

  const ring: [number, number][] = [];
  for (let i = 0; i <= steps; i++) {
    const a = (i / steps) * Math.PI * 2;
    const dy = r * Math.sin(a);
    const dx = r * Math.cos(a);
    const dLat = (dy / EARTH_RADIUS_M) / DEG2RAD;
    const dLon = (dx / (EARTH_RADIUS_M * cosLat)) / DEG2RAD;
    ring.push([lon0 + dLon, lat0 + dLat]);
  }
  return ring;
}

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

const toNumberArray = (values: unknown): number[] => {
  if (values == null) return [];
  if (Array.isArray(values)) {
    return values as unknown as number[];
  }
  if (typeof values === "number" && Number.isFinite(values)) {
    return [values];
  }
  if (typeof values === "string") {
    const value = toSafeNumber(Number(values));
    return value == null ? [] : [value];
  }
  if (typeof values === "object") {
    if (typeof (values as { [Symbol.iterator]?: unknown })?.[Symbol.iterator] === "function") {
      try {
        return Array.from(values as Iterable<unknown>) as number[];
      } catch {
        return [];
      }
    }
  }
  return [];
};

const makeUniqueSorted = (values: unknown) => {
  const maybeArray = toNumberArray(values);
  const normalized = Array.isArray(maybeArray) ? maybeArray : [];
  const unique = Array.from(new Set(normalized.filter((value) => Number.isFinite(value))));
  unique.sort((a, b) => a - b);
  return unique;
};

const equalNumberArrays = (a: number[], b: number[]) => {
  if (a.length !== b.length) return false;
  for (let i = 0; i < a.length; i += 1) {
    if (a[i] !== b[i]) return false;
  }
  return true;
};

const EARTH_RADIUS_M = 6371008.8; // MapLibre's `earthRadius`
const DECK_VIEW_ID = "mapbox";

export function Map3D({
  targets,
  zones,
  selectedMmsi,
  hoveredMmsiSet = [],
  hoveredFleetMmsiSet = [],
  hoveredPairMmsiSet = [],
  hoveredFleetOwnerKey = null,
  highlightedMmsiSet = [],
  settings,
  baseMap,
  projection,
  overlays,
  onSelectMmsi,
  onToggleHighlightMmsi,
  onViewBboxChange,
  legacyHits,
  pairLinks,
  fcLinks,
  fleetCircles,
  onProjectionLoadingChange,
  fleetFocus,
  onHoverFleet,
  onClearFleetHover,
  onHoverMmsi,
  onClearMmsiHover,
  onHoverPair,
  onClearPairHover,
}: 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 globeShipsEpochRef = useRef(-1);
  const showSeamarkRef = useRef(settings.showSeamark);
  const baseMapRef = useRef<BaseMapId>(baseMap);
  const projectionRef = useRef<MapProjectionId>(projection);
  const globeShipIconLoadingRef = useRef(false);
  const projectionBusyRef = useRef(false);
  const projectionBusyTokenRef = useRef(0);
  const projectionBusyTimerRef = useRef<ReturnType<typeof window.setTimeout> | null>(null);
  const projectionPrevRef = useRef<MapProjectionId>(projection);
  const mapTooltipRef = useRef<maplibregl.Popup | null>(null);
  const deckHoverRafRef = useRef<number | null>(null);
  const deckHoverHasHitRef = useRef(false);
  const [hoveredDeckMmsiSet, setHoveredDeckMmsiSet] = useState<number[]>([]);
  const [hoveredDeckPairMmsiSet, setHoveredDeckPairMmsiSet] = useState<number[]>([]);
  const [hoveredDeckFleetOwnerKey, setHoveredDeckFleetOwnerKey] = useState<string | null>(null);
  const [hoveredDeckFleetMmsiSet, setHoveredDeckFleetMmsiSet] = useState<number[]>([]);
  const [hoveredZoneId, setHoveredZoneId] = useState<string | null>(null);
  const hoveredMmsiSetRef = useMemo(() => toNumberSet(hoveredMmsiSet), [hoveredMmsiSet]);
  const hoveredFleetMmsiSetRef = useMemo(() => toNumberSet(hoveredFleetMmsiSet), [hoveredFleetMmsiSet]);
  const hoveredPairMmsiSetRef = useMemo(() => toNumberSet(hoveredPairMmsiSet), [hoveredPairMmsiSet]);
  const externalHighlightedSetRef = useMemo(() => toNumberSet(highlightedMmsiSet), [highlightedMmsiSet]);
  const hoveredDeckMmsiSetRef = useMemo(() => toNumberSet(hoveredDeckMmsiSet), [hoveredDeckMmsiSet]);
  const hoveredDeckPairMmsiSetRef = useMemo(() => toNumberSet(hoveredDeckPairMmsiSet), [hoveredDeckPairMmsiSet]);
  const hoveredDeckFleetMmsiSetRef = useMemo(() => toNumberSet(hoveredDeckFleetMmsiSet), [hoveredDeckFleetMmsiSet]);
  const hoveredFleetOwnerKeys = useMemo(() => {
    const keys = new Set<string>();
    if (hoveredFleetOwnerKey) keys.add(hoveredFleetOwnerKey);
    if (hoveredDeckFleetOwnerKey) keys.add(hoveredDeckFleetOwnerKey);
    return keys;
  }, [hoveredFleetOwnerKey, hoveredDeckFleetOwnerKey]);
  const fleetFocusId = fleetFocus?.id;
  const fleetFocusLon = fleetFocus?.center?.[0];
  const fleetFocusLat = fleetFocus?.center?.[1];
  const fleetFocusZoom = fleetFocus?.zoom;

  const reorderGlobeFeatureLayers = useCallback(() => {
    const map = mapRef.current;
    if (!map || projectionRef.current !== "globe") return;
    if (projectionBusyRef.current) return;

    const ordering = [
      "zones-fill",
      "zones-line",
      "zones-label",
      "ships-globe-halo",
      "ships-globe-outline",
      "ships-globe",
      "pair-lines-ml",
      "fc-lines-ml",
      "pair-range-ml",
      "fleet-circles-ml-fill",
      "fleet-circles-ml",
    ];

    for (const layerId of ordering) {
      try {
        if (map.getLayer(layerId)) map.moveLayer(layerId);
      } catch {
        // ignore
      }
    }

    kickRepaint(map);
  }, []);

  const effectiveHoveredPairMmsiSet = useMemo(
    () => mergeNumberSets(hoveredPairMmsiSetRef, hoveredDeckPairMmsiSetRef),
    [hoveredPairMmsiSetRef, hoveredDeckPairMmsiSetRef],
  );
  const effectiveHoveredFleetMmsiSet = useMemo(
    () => mergeNumberSets(hoveredFleetMmsiSetRef, hoveredDeckFleetMmsiSetRef),
    [hoveredFleetMmsiSetRef, hoveredDeckFleetMmsiSetRef],
  );
  const [mapSyncEpoch, setMapSyncEpoch] = useState(0);
  const highlightedMmsiSetCombined = useMemo(
    () =>
      mergeNumberSets(
        hoveredMmsiSetRef,
        hoveredDeckMmsiSetRef,
        externalHighlightedSetRef,
        effectiveHoveredFleetMmsiSet,
        effectiveHoveredPairMmsiSet,
      ),
    [
      hoveredMmsiSetRef,
      hoveredDeckMmsiSetRef,
      externalHighlightedSetRef,
      effectiveHoveredFleetMmsiSet,
      effectiveHoveredPairMmsiSet,
    ],
  );
  const hoveredShipSignature = useMemo(
    () =>
      `${makeSetSignature(hoveredMmsiSetRef)}|${makeSetSignature(externalHighlightedSetRef)}|${makeSetSignature(
        hoveredDeckMmsiSetRef,
      )}|${makeSetSignature(effectiveHoveredFleetMmsiSet)}|${makeSetSignature(effectiveHoveredPairMmsiSet)}`,
    [
      hoveredMmsiSetRef,
      externalHighlightedSetRef,
      hoveredDeckMmsiSetRef,
      effectiveHoveredFleetMmsiSet,
      effectiveHoveredPairMmsiSet,
    ],
  );
  const hoveredFleetSignature = useMemo(
    () => `${makeSetSignature(effectiveHoveredFleetMmsiSet)}|${[...hoveredFleetOwnerKeys].sort().join(",")}`,
    [effectiveHoveredFleetMmsiSet, hoveredFleetOwnerKeys],
  );
  const hoveredPairSignature = useMemo(() => makeSetSignature(effectiveHoveredPairMmsiSet), [effectiveHoveredPairMmsiSet]);
  const hoveredPairMmsiList = useMemo(() => makeUniqueSorted(Array.from(effectiveHoveredPairMmsiSet)), [effectiveHoveredPairMmsiSet]);
  const hoveredFleetOwnerKeyList = useMemo(() => Array.from(hoveredFleetOwnerKeys).sort(), [hoveredFleetOwnerKeys]);
  const hoveredFleetMmsiList = useMemo(() => makeUniqueSorted(Array.from(effectiveHoveredFleetMmsiSet)), [effectiveHoveredFleetMmsiSet]);

  const isHighlightedMmsi = useCallback(
    (mmsi: number) => highlightedMmsiSetCombined.has(mmsi),
    [highlightedMmsiSetCombined],
  );

  const isHighlightedPair = useCallback(
    (aMmsi: number, bMmsi: number) =>
      effectiveHoveredPairMmsiSet.size === 2 &&
      effectiveHoveredPairMmsiSet.has(aMmsi) &&
      effectiveHoveredPairMmsiSet.has(bMmsi),
    [effectiveHoveredPairMmsiSet],
  );

  const isHighlightedFleet = useCallback(
    (ownerKey: string, vesselMmsis: number[]) => {
      if (hoveredFleetOwnerKeys.has(ownerKey)) return true;
      return vesselMmsis.some((x) => isHighlightedMmsi(x));
    },
    [hoveredFleetOwnerKeys, isHighlightedMmsi],
  );

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

  const shipByMmsi = useMemo(() => {
    const byMmsi = new Map<number, AisTarget>();
    for (const t of shipData) byMmsi.set(t.mmsi, t);
    return byMmsi;
  }, [shipData]);

  const hasAuxiliarySelectModifier = (ev?: {
    shiftKey?: boolean;
    ctrlKey?: boolean;
    metaKey?: boolean;
  } | null): boolean => {
    if (!ev) return false;
    return !!(ev.shiftKey || ev.ctrlKey || ev.metaKey);
  };

  const setHoveredDeckFleetMmsis = useCallback((next: number[]) => {
    const normalized = makeUniqueSorted(next);
    setHoveredDeckFleetMmsiSet((prev) => (equalNumberArrays(prev, normalized) ? prev : normalized));
  }, []);

  const setHoveredDeckFleetOwner = useCallback((ownerKey: string | null) => {
    setHoveredDeckFleetOwnerKey((prev) => (prev === ownerKey ? prev : ownerKey));
  }, []);

  const onHoverFleetRef = useRef(onHoverFleet);
  const onClearFleetHoverRef = useRef(onClearFleetHover);
  const onHoverMmsiRef = useRef(onHoverMmsi);
  const onClearMmsiHoverRef = useRef(onClearMmsiHover);
  const onHoverPairRef = useRef(onHoverPair);
  const onClearPairHoverRef = useRef(onClearPairHover);
  const mapDeckMmsiHoverRef = useRef<number[]>([]);
  const mapDeckPairHoverRef = useRef<number[]>([]);
  const mapFleetHoverStateRef = useRef<{
    ownerKey: string | null;
    vesselMmsis: number[];
  }>({ ownerKey: null, vesselMmsis: [] });

  useEffect(() => {
    onHoverFleetRef.current = onHoverFleet;
    onClearFleetHoverRef.current = onClearFleetHover;
    onHoverMmsiRef.current = onHoverMmsi;
    onClearMmsiHoverRef.current = onClearMmsiHover;
    onHoverPairRef.current = onHoverPair;
    onClearPairHoverRef.current = onClearPairHover;
  }, [onHoverFleet, onClearFleetHover, onHoverMmsi, onClearMmsiHover, onHoverPair, onClearPairHover]);

  const clearMapFleetHoverState = useCallback(() => {
    const nextOwner = null;
    const prev = mapFleetHoverStateRef.current;
    const shouldNotify = prev.ownerKey !== null || prev.vesselMmsis.length !== 0;
    mapFleetHoverStateRef.current = { ownerKey: nextOwner, vesselMmsis: [] };
    setHoveredDeckFleetOwner(null);
    setHoveredDeckFleetMmsis([]);
    if (shouldNotify) {
      onClearFleetHoverRef.current?.();
    }
  }, [setHoveredDeckFleetOwner, setHoveredDeckFleetMmsis]);

  const clearDeckHoverPairs = useCallback(() => {
    const prev = mapDeckPairHoverRef.current;
    mapDeckPairHoverRef.current = [];
    setHoveredDeckPairMmsiSet((prevState) => (prevState.length === 0 ? prevState : []));
    if (prev.length > 0) {
      onClearPairHoverRef.current?.();
    }
  }, [setHoveredDeckPairMmsiSet]);

  const clearDeckHoverMmsi = useCallback(() => {
    const prev = mapDeckMmsiHoverRef.current;
    mapDeckMmsiHoverRef.current = [];
    setHoveredDeckMmsiSet((prevState) => (prevState.length === 0 ? prevState : []));
    if (prev.length > 0) {
      onClearMmsiHoverRef.current?.();
    }
  }, [setHoveredDeckMmsiSet]);

  const scheduleDeckHoverResolve = useCallback(() => {
    if (deckHoverRafRef.current != null) return;
    deckHoverRafRef.current = window.requestAnimationFrame(() => {
      deckHoverRafRef.current = null;
      if (!deckHoverHasHitRef.current) {
        clearDeckHoverMmsi();
        clearDeckHoverPairs();
        clearMapFleetHoverState();
      }
      deckHoverHasHitRef.current = false;
    });
  }, [clearDeckHoverMmsi, clearDeckHoverPairs, clearMapFleetHoverState]);

  const touchDeckHoverState = useCallback(
    (isHover: boolean) => {
      if (isHover) deckHoverHasHitRef.current = true;
      scheduleDeckHoverResolve();
    },
    [scheduleDeckHoverResolve],
  );

  const setDeckHoverMmsi = useCallback(
    (next: number[]) => {
      const normalized = makeUniqueSorted(next);
      touchDeckHoverState(normalized.length > 0);
      setHoveredDeckMmsiSet((prev) => (equalNumberArrays(prev, normalized) ? prev : normalized));
      if (!equalNumberArrays(mapDeckMmsiHoverRef.current, normalized)) {
        mapDeckMmsiHoverRef.current = normalized;
        onHoverMmsiRef.current?.(normalized);
      }
    },
    [setHoveredDeckMmsiSet, touchDeckHoverState],
  );

  const setDeckHoverPairs = useCallback(
    (next: number[]) => {
      const normalized = makeUniqueSorted(next);
      touchDeckHoverState(normalized.length > 0);
      setHoveredDeckPairMmsiSet((prev) => (equalNumberArrays(prev, normalized) ? prev : normalized));
      if (!equalNumberArrays(mapDeckPairHoverRef.current, normalized)) {
        mapDeckPairHoverRef.current = normalized;
        onHoverPairRef.current?.(normalized);
      }
    },
    [setHoveredDeckPairMmsiSet, touchDeckHoverState],
  );

  const setMapFleetHoverState = useCallback(
    (ownerKey: string | null, vesselMmsis: number[]) => {
      const normalized = makeUniqueSorted(vesselMmsis);
      const prev = mapFleetHoverStateRef.current;
      if (prev.ownerKey === ownerKey && equalNumberArrays(prev.vesselMmsis, normalized)) {
        return;
      }
      touchDeckHoverState(!!ownerKey || normalized.length > 0);
      setHoveredDeckFleetOwner(ownerKey);
      setHoveredDeckFleetMmsis(normalized);
      mapFleetHoverStateRef.current = { ownerKey, vesselMmsis: normalized };
      onHoverFleetRef.current?.(ownerKey, normalized);
    },
    [setHoveredDeckFleetOwner, setHoveredDeckFleetMmsis, touchDeckHoverState],
  );

  useEffect(() => {
    return () => {
      if (deckHoverRafRef.current != null) {
        window.cancelAnimationFrame(deckHoverRafRef.current);
        deckHoverRafRef.current = null;
      }
      deckHoverHasHitRef.current = false;
    };
  }, []);

  useEffect(() => {
    mapFleetHoverStateRef.current = {
      ownerKey: hoveredFleetOwnerKey,
      vesselMmsis: hoveredFleetMmsiSet,
    };
  }, [hoveredFleetOwnerKey, hoveredFleetMmsiSet]);

  const clearProjectionBusyTimer = useCallback(() => {
    if (projectionBusyTimerRef.current == null) return;
    clearTimeout(projectionBusyTimerRef.current);
    projectionBusyTimerRef.current = null;
  }, []);

  const endProjectionLoading = useCallback(() => {
    if (!projectionBusyRef.current) return;
    projectionBusyRef.current = false;
    clearProjectionBusyTimer();
    if (onProjectionLoadingChange) {
      onProjectionLoadingChange(false);
    }
  }, [clearProjectionBusyTimer, onProjectionLoadingChange]);

  const setProjectionLoading = useCallback(
    (loading: boolean) => {
      if (projectionBusyRef.current === loading) return;
      if (!loading) {
        endProjectionLoading();
        return;
      }

      clearProjectionBusyTimer();
      projectionBusyRef.current = true;
      const token = ++projectionBusyTokenRef.current;
      if (onProjectionLoadingChange) {
        onProjectionLoadingChange(true);
      }

      projectionBusyTimerRef.current = setTimeout(() => {
        if (!projectionBusyRef.current || projectionBusyTokenRef.current !== token) return;
        console.debug("Projection loading fallback timeout reached.");
        endProjectionLoading();
      }, 4000);
    },
    [clearProjectionBusyTimer, endProjectionLoading, onProjectionLoadingChange],
  );

  const pulseMapSync = () => {
    setMapSyncEpoch((prev) => prev + 1);
    requestAnimationFrame(() => {
      kickRepaint(mapRef.current);
      setMapSyncEpoch((prev) => prev + 1);
    });
  };

  useEffect(() => {
    return () => {
      clearProjectionBusyTimer();
      endProjectionLoading();
    };
  }, [clearProjectionBusyTimer, endProjectionLoading]);

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

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

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

  const removeLayerIfExists = useCallback((map: maplibregl.Map, layerId: string | null | undefined) => {
    if (!layerId) return;
    try {
      if (map.getLayer(layerId)) {
        map.removeLayer(layerId);
      }
    } catch {
      // ignore
    }
  }, []);

  const removeSourceIfExists = useCallback((map: maplibregl.Map, sourceId: string) => {
    try {
      if (map.getSource(sourceId)) {
        map.removeSource(sourceId);
      }
    } catch {
      // ignore
    }
  }, []);

  const ensureMercatorOverlay = useCallback(() => {
    const map = mapRef.current;
    if (!map) return null;
    if (overlayRef.current) return overlayRef.current;

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

  const clearGlobeNativeLayers = useCallback(() => {
    const map = mapRef.current;
    if (!map) return;

    const layerIds = [
      "ships-globe-halo",
      "ships-globe-outline",
      "ships-globe",
      "pair-lines-ml",
      "fc-lines-ml",
      "fleet-circles-ml-fill",
      "fleet-circles-ml",
      "pair-range-ml",
      "deck-globe",
    ];

    for (const id of layerIds) {
      removeLayerIfExists(map, id);
    }

    const sourceIds = [
      "ships-globe-src",
      "pair-lines-ml-src",
      "fc-lines-ml-src",
      "fleet-circles-ml-src",
      "fleet-circles-ml-fill-src",
      "pair-range-ml-src",
    ];
    for (const id of sourceIds) {
      removeSourceIfExists(map, id);
    }
  }, [removeLayerIfExists, removeSourceIfExists]);

  // 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.
      onMapStyleReady(map, () => {
        applyProjection();
        // Globe deck layer lives inside the style and must be re-added after any style swap.
        const deckLayer = globeDeckLayerRef.current;
        if (projectionRef.current === "globe" && deckLayer && !map!.getLayer(deckLayer.id)) {
          try {
            map!.addLayer(deckLayer);
          } 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;
    let retries = 0;
    const maxRetries = 18;
    const isTransition = projectionPrevRef.current !== projection;
    projectionPrevRef.current = projection;
    let settleScheduled = false;
    let settleCleanup: (() => void) | null = null;

    const startProjectionSettle = () => {
      if (!isTransition || settleScheduled) return;
      settleScheduled = true;

      const finalize = () => {
        if (!cancelled && isTransition) setProjectionLoading(false);
      };

      const finalizeSoon = () => {
        if (cancelled || !isTransition || projectionBusyRef.current === false) return;
        if (!map.isStyleLoaded()) {
          requestAnimationFrame(finalizeSoon);
          return;
        }
        requestAnimationFrame(() => requestAnimationFrame(finalize));
      };

      const onIdle = () => finalizeSoon();
      try {
        map.on("idle", onIdle);
        const styleReadyCleanup = onMapStyleReady(map, finalizeSoon);
        settleCleanup = () => {
          map.off("idle", onIdle);
          styleReadyCleanup();
        };
      } catch {
        requestAnimationFrame(finalize);
        settleCleanup = null;
      }

      finalizeSoon();
    };

    if (isTransition) setProjectionLoading(true);

    const disposeMercatorOverlay = () => {
      const current = overlayRef.current;
      if (!current) return;
      try {
        current.setProps({ layers: [] } as never);
      } catch {
        // ignore
      }
      try {
        map.removeControl(current as never);
      } catch {
        // ignore
      }
      try {
        current.finalize();
      } catch {
        // ignore
      }
      overlayRef.current = null;
    };

    const disposeGlobeDeckLayer = () => {
      const current = globeDeckLayerRef.current;
      if (!current) return;
      removeLayerIfExists(map, current.id);
      try {
        current.requestFinalize();
      } catch {
        // ignore
      }
      globeDeckLayerRef.current = null;
    };

    const syncProjectionAndDeck = () => {
      if (cancelled) return;
      if (!isTransition) {
        return;
      }

      if (!map.isStyleLoaded()) {
        if (!cancelled && retries < maxRetries) {
          retries += 1;
          window.requestAnimationFrame(() => syncProjectionAndDeck());
        }
        return;
      }

      const next = projection;
      const currentProjection = extractProjectionType(map);
      const shouldSwitchProjection = currentProjection !== next;

      if (projection === "globe") {
        disposeMercatorOverlay();
        clearGlobeNativeLayers();
      } else {
        disposeGlobeDeckLayer();
        clearGlobeNativeLayers();
      }

      try {
        if (shouldSwitchProjection) {
          map.setProjection({ type: next });
        }
        map.setRenderWorldCopies(next !== "globe");
        if (shouldSwitchProjection && currentProjection !== next && !cancelled && retries < maxRetries) {
          retries += 1;
          window.requestAnimationFrame(() => syncProjectionAndDeck());
          return;
        }
      } catch (e) {
        if (!cancelled && retries < maxRetries) {
          retries += 1;
          window.requestAnimationFrame(() => syncProjectionAndDeck());
          return;
        }
        if (isTransition) setProjectionLoading(false);
        console.warn("Projection switch failed:", e);
      }

      if (projection === "globe") {
        // Start with a clean globe Deck layer state to avoid partially torn-down renders.
        disposeGlobeDeckLayer();

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

        const layer = globeDeckLayerRef.current;
        const layerId = layer?.id;
        if (layer && layerId && map.isStyleLoaded() && !map.getLayer(layerId)) {
          try {
            map.addLayer(layer);
          } catch {
            // ignore
          }
          if (!map.getLayer(layerId) && !cancelled && retries < maxRetries) {
            retries += 1;
            window.requestAnimationFrame(() => syncProjectionAndDeck());
            return;
          }
        }
      } else {
        // Tear down globe custom layer (if present), restore MapboxOverlay interleaved.
        disposeGlobeDeckLayer();

        ensureMercatorOverlay();
      }

      // MapLibre may not schedule a frame immediately after projection swaps if the map is idle.
      // Kick a few repaints so overlay sources (ships/zones) appear instantly.
      reorderGlobeFeatureLayers();
      kickRepaint(map);
      try {
        map.resize();
      } catch {
        // ignore
      }
      if (isTransition) {
        startProjectionSettle();
      }
      pulseMapSync();
    };

    if (!isTransition) return;

    if (map.isStyleLoaded()) syncProjectionAndDeck();
    else {
      const stop = onMapStyleReady(map, syncProjectionAndDeck);
      return () => {
        cancelled = true;
        if (settleCleanup) settleCleanup();
        stop();
        if (isTransition) setProjectionLoading(false);
      };
    }

    return () => {
      cancelled = true;
      if (settleCleanup) settleCleanup();
      if (isTransition) setProjectionLoading(false);
    };
  }, [projection, clearGlobeNativeLayers, ensureMercatorOverlay, removeLayerIfExists, reorderGlobeFeatureLayers, setProjectionLoading]);

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

    let cancelled = false;
    const controller = new AbortController();
    let stop: (() => void) | null = null;

    (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 });
        stop = onMapStyleReady(map, () => {
          kickRepaint(map);
          requestAnimationFrame(() => kickRepaint(map));
          pulseMapSync();
        });
      } catch (e) {
        if (cancelled) return;
        console.warn("Base map switch failed:", e);
      }
    })();

    return () => {
      cancelled = true;
      controller.abort();
      stop?.();
    };
  }, [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 disableBathyHeavy = projection === "globe" && baseMap === "enhanced";
      const visHeavy = disableBathyHeavy ? "none" : "visible";
      const seaVisibility = "visible" as const;
      const seaRegex = /(water|sea|ocean|river|lake|coast|bay)/i;

      // Globe + our injected bathymetry fill polygons can exceed MapLibre's per-segment vertex limit
      // (65535), causing broken ocean rendering. Keep globe mode stable by disabling the heavy fill.
      const heavyIds = [
        "bathymetry-fill",
        "bathymetry-borders",
        "bathymetry-borders-major",
        "bathymetry-extrusion",
        "bathymetry-hillshade",
      ];
      for (const id of heavyIds) {
        try {
          if (map.getLayer(id)) map.setLayoutProperty(id, "visibility", visHeavy);
        } catch {
          // ignore
        }
      }

      // Vector basemap water layers can be tuned per-style. Keep visible by default,
      // only toggling layers that match an explicit water/sea signature.
      try {
        const style = map.getStyle();
        const styleLayers = style && Array.isArray(style.layers) ? style.layers : [];
        for (const layer of styleLayers) {
          const id = getLayerId(layer);
          if (!id) continue;
          const sourceLayer = String((layer as Record<string, unknown>)["source-layer"] ?? "").toLowerCase();
          const source = String((layer as { source?: unknown }).source ?? "").toLowerCase();
          const type = String((layer as { type?: unknown }).type ?? "").toLowerCase();
          const isSea = seaRegex.test(id) || seaRegex.test(sourceLayer) || seaRegex.test(source);
          const isRaster = type === "raster";
          if (!isSea) continue;
          if (!map.getLayer(id)) continue;
          if (isRaster && id === "seamark") continue;
          try {
            map.setLayoutProperty(id, "visibility", seaVisibility);
          } catch {
            // ignore
          }
        }
      } catch {
        // ignore
      }
    };

    const stop = onMapStyleReady(map, apply);
    return () => {
      stop();
    };
  }, [projection, baseMap, mapSyncEpoch]);

  // 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 labelId = "zones-label";

    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 zoneLabelExpr: unknown[] = ["match", ["to-string", ["coalesce", ["get", "zoneId"], ""]]];
    for (const k of Object.keys(ZONE_META) as ZoneId[]) {
      zoneLabelExpr.push(k, ZONE_META[k].name);
    }
    zoneLabelExpr.push(["coalesce", ["get", "zoneName"], ["get", "zoneLabel"], ["get", "NAME"], "수역"]);

    const ensure = () => {
      if (projectionBusyRef.current) return;
      // 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
      }
      try {
        if (map.getLayer(labelId)) map.setLayoutProperty(labelId, "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 styleLayers = style && Array.isArray(style.layers) ? style.layers : [];
        const firstSymbol = styleLayers.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;

        const zoneMatchExpr =
          hoveredZoneId !== null
            ? (["==", ["to-string", ["coalesce", ["get", "zoneId"], ""]], hoveredZoneId] as unknown[])
            : false;
        const zoneLineWidthExpr = hoveredZoneId
          ? ([
              "interpolate",
              ["linear"],
              ["zoom"],
              4,
              ["case", zoneMatchExpr, 1.6, 0.8],
              10,
              ["case", zoneMatchExpr, 2.0, 1.4],
              14,
              ["case", zoneMatchExpr, 2.8, 2.1],
            ] as unknown as never)
          : (["interpolate", ["linear"], ["zoom"], 4, 0.8, 10, 1.4, 14, 2.1] as never);

        if (map.getLayer(fillId)) {
          try {
            map.setPaintProperty(
              fillId,
              "fill-opacity",
              hoveredZoneId ? (["case", zoneMatchExpr, 0.24, 0.1] as unknown as number) : 0.12,
            );
          } catch {
            // ignore
          }
        }

        if (map.getLayer(lineId)) {
          try {
            map.setPaintProperty(
              lineId,
              "line-color",
              hoveredZoneId
                ? (["case", zoneMatchExpr, "rgba(125,211,252,0.98)", zoneColorExpr as never] as never)
                : (zoneColorExpr as never),
            );
          } catch {
            // ignore
          }
          try {
            map.setPaintProperty(lineId, "line-opacity", hoveredZoneId ? (["case", zoneMatchExpr, 1, 0.85] as never) : 0.85);
          } catch {
            // ignore
          }
          try {
            map.setPaintProperty(lineId, "line-width", zoneLineWidthExpr);
          } catch {
            // ignore
          }
        }

        if (!map.getLayer(fillId)) {
          map.addLayer(
            {
              id: fillId,
              type: "fill",
              source: srcId,
              paint: {
                "fill-color": zoneColorExpr as never,
                "fill-opacity": hoveredZoneId
                  ? ([
                      "case",
                      zoneMatchExpr,
                      0.24,
                      0.1,
                    ] as unknown as number)
                  : 0.12,
              },
              layout: { visibility },
            } as unknown as LayerSpecification,
            before,
          );
        }

        if (!map.getLayer(lineId)) {
          map.addLayer(
            {
              id: lineId,
              type: "line",
              source: srcId,
              paint: {
                "line-color": hoveredZoneId
                  ? (["case", zoneMatchExpr, "rgba(125,211,252,0.98)", zoneColorExpr as never] as never)
                  : (zoneColorExpr as never),
                "line-opacity": hoveredZoneId
                  ? (["case", zoneMatchExpr, 1, 0.85] as never)
                  : 0.85,
                "line-width": zoneLineWidthExpr,
              },
              layout: { visibility },
            } as unknown as LayerSpecification,
            before,
          );
        }

        if (!map.getLayer(labelId)) {
          map.addLayer(
              {
                id: labelId,
                type: "symbol",
                source: srcId,
                layout: {
                  visibility,
                  "symbol-placement": "point",
                  "text-field": zoneLabelExpr as never,
                  "text-size": 11,
                  "text-font": ["Noto Sans Regular", "Open Sans Regular"],
                "text-anchor": "top",
                "text-offset": [0, 0.35],
                "text-allow-overlap": false,
                "text-ignore-placement": false,
              },
              paint: {
                "text-color": "#dbeafe",
                "text-halo-color": "rgba(2,6,23,0.85)",
                "text-halo-width": 1.2,
                "text-halo-blur": 0.8,
              },
            } as unknown as LayerSpecification,
            undefined,
          );
        }
      } catch (e) {
        console.warn("Zones layer setup failed:", e);
      } finally {
        reorderGlobeFeatureLayers();
        kickRepaint(map);
      }
    };

    const stop = onMapStyleReady(map, ensure);
    return () => {
      stop();
    };
  }, [zones, overlays.zones, projection, baseMap, hoveredZoneId, mapSyncEpoch, reorderGlobeFeatureLayers]);

  // Ships in globe mode: render with MapLibre symbol layers so icons can be aligned to the globe surface.
  // Deck IconLayer billboards or uses a fixed plane, which looks like ships are pointing into the sky/ground on globe.
  useEffect(() => {
    const map = mapRef.current;
    if (!map) return;

    const imgId = "ship-globe-icon";
    const srcId = "ships-globe-src";
    const haloId = "ships-globe-halo";
    const outlineId = "ships-globe-outline";
    const symbolId = "ships-globe";

    const remove = () => {
      for (const id of [symbolId, outlineId, haloId]) {
        try {
          if (map.getLayer(id)) map.removeLayer(id);
        } catch {
          // ignore
        }
      }
      try {
        if (map.getSource(srcId)) map.removeSource(srcId);
      } catch {
        // ignore
      }
      reorderGlobeFeatureLayers();
      kickRepaint(map);
    };

    const ensureImage = () => {
      ensureFallbackShipImage(map, imgId);
      if (globeShipIconLoadingRef.current) return;
      if (map.hasImage(imgId)) return;

      const addFallbackImage = () => {
        ensureFallbackShipImage(map, imgId);
        kickRepaint(map);
      };

      let fallbackTimer: ReturnType<typeof window.setTimeout> | null = null;
      try {
        globeShipIconLoadingRef.current = true;
        fallbackTimer = window.setTimeout(() => {
          addFallbackImage();
        }, 80);
        void map
          .loadImage("/assets/ship.svg")
          .then((response) => {
            globeShipIconLoadingRef.current = false;
            if (fallbackTimer != null) {
              clearTimeout(fallbackTimer);
              fallbackTimer = null;
            }

            const loadedImage = (response as { data?: HTMLImageElement | ImageBitmap } | undefined)?.data;
            if (!loadedImage) {
              addFallbackImage();
              return;
            }

            try {
              if (map.hasImage(imgId)) {
                try {
                  map.removeImage(imgId);
                } catch {
                  // ignore
                }
              }
              map.addImage(imgId, loadedImage, { pixelRatio: 2, sdf: true });
              kickRepaint(map);
            } catch (e) {
              console.warn("Ship icon image add failed:", e);
            }
          })
          .catch(() => {
            globeShipIconLoadingRef.current = false;
            if (fallbackTimer != null) {
              clearTimeout(fallbackTimer);
              fallbackTimer = null;
            }
            addFallbackImage();
          });
      } catch (e) {
        globeShipIconLoadingRef.current = false;
        if (fallbackTimer != null) {
          clearTimeout(fallbackTimer);
          fallbackTimer = null;
        }
        try {
          addFallbackImage();
        } catch (fallbackError) {
          console.warn("Ship icon image setup failed:", e, fallbackError);
        }
      }
    };

    const ensure = () => {
      if (projectionBusyRef.current) return;
      if (!map.isStyleLoaded()) return;

      if (projection !== "globe" || !settings.showShips) {
        remove();
        return;
      }

      if (globeShipsEpochRef.current !== mapSyncEpoch) {
        remove();
        globeShipsEpochRef.current = mapSyncEpoch;
      }

      try {
        ensureImage();
      } catch (e) {
        console.warn("Ship icon image setup failed:", e);
      }

      const globeShipData = shipData;
      const geojson: GeoJSON.FeatureCollection<GeoJSON.Point> = {
        type: "FeatureCollection",
        features: globeShipData.map((t) => {
          const legacy = legacyHits?.get(t.mmsi) ?? null;
          const heading = getDisplayHeading({
            cog: t.cog,
            heading: t.heading,
            offset: GLOBE_ICON_HEADING_OFFSET_DEG,
          });
          const hull = clampNumber((isFiniteNumber(t.length) ? t.length : 0) + (isFiniteNumber(t.width) ? t.width : 0) * 3, 50, 420);
          const sizeScale = clampNumber(0.85 + (hull - 50) / 420, 0.82, 1.85);
          const selected = t.mmsi === selectedMmsi;
          const highlighted = isHighlightedMmsi(t.mmsi);
          const selectedScale = selected ? 1.08 : 1;
          const highlightScale = highlighted ? 1.06 : 1;
          const iconScale = selected ? selectedScale : highlightScale;
          const iconSize3 = clampNumber(0.35 * sizeScale * selectedScale, 0.25, 1.3);
          const iconSize7 = clampNumber(0.45 * sizeScale * selectedScale, 0.3, 1.45);
          const iconSize10 = clampNumber(0.56 * sizeScale * selectedScale, 0.35, 1.7);
          const iconSize14 = clampNumber(0.72 * sizeScale * selectedScale, 0.45, 2.1);
          return {
            type: "Feature",
            ...(isFiniteNumber(t.mmsi) ? { id: Math.trunc(t.mmsi) } : {}),
            geometry: { type: "Point", coordinates: [t.lon, t.lat] },
            properties: {
              mmsi: t.mmsi,
              name: t.name || "",
              cog: heading,
              heading,
              sog: isFiniteNumber(t.sog) ? t.sog : 0,
              shipColor: getGlobeBaseShipColor({
                legacy: legacy?.shipCode || null,
                sog: isFiniteNumber(t.sog) ? t.sog : null,
              }),
              iconSize3: iconSize3 * iconScale,
              iconSize7: iconSize7 * iconScale,
              iconSize10: iconSize10 * iconScale,
              iconSize14: iconSize14 * iconScale,
              sizeScale,
              selected: selected ? 1 : 0,
              highlighted: highlighted ? 1 : 0,
              permitted: !!legacy,
              code: legacy?.shipCode || "",
            },
          };
        }),
      };

      try {
        const existing = map.getSource(srcId) as GeoJSONSource | undefined;
        if (existing) existing.setData(geojson);
        else map.addSource(srcId, { type: "geojson", data: geojson } as GeoJSONSourceSpecification);
      } catch (e) {
        console.warn("Ship source setup failed:", e);
        return;
      }

      const visibility = settings.showShips ? "visible" : "none";

      const before = undefined;

      if (!map.getLayer(haloId)) {
        try {
          map.addLayer(
            {
              id: haloId,
              type: "circle",
              source: srcId,
              layout: {
                visibility,
                "circle-sort-key": [
                  "case",
                  ["==", ["get", "selected"], 1],
                  90,
                  ["==", ["get", "highlighted"], 1],
                  80,
                  ["==", ["get", "permitted"], 1],
                  60,
                  20,
                ] as never,
              },
              paint: {
                "circle-radius": GLOBE_SHIP_CIRCLE_RADIUS_EXPR,
                "circle-color": ["coalesce", ["get", "shipColor"], "#64748b"] as never,
                "circle-opacity": [
                  "case",
                  ["==", ["get", "selected"], 1],
                  0.38,
                  ["==", ["get", "highlighted"], 1],
                  0.34,
                  0.16,
                ] as never,
              },
            } as unknown as LayerSpecification,
            before,
          );
        } catch (e) {
          console.warn("Ship halo layer add failed:", e);
        }
      } else {
        try {
          map.setLayoutProperty(haloId, "visibility", visibility);
          map.setPaintProperty(
            haloId,
            "circle-color",
            [
              "case",
              ["==", ["get", "selected"], 1],
              "rgba(14,234,255,1)",
              ["==", ["get", "highlighted"], 1],
              "rgba(245,158,11,1)",
              ["==", ["get", "permitted"], 1],
              "rgba(125,211,252,0.95)",
              "rgba(59,130,246,1)",
            ] as never,
          );
          map.setPaintProperty(haloId, "circle-opacity", [
            "case",
            ["==", ["get", "selected"], 1],
            0.38,
            ["==", ["get", "highlighted"], 1],
            0.34,
            0.16,
          ] as never);
          map.setPaintProperty(haloId, "circle-radius", GLOBE_SHIP_CIRCLE_RADIUS_EXPR);
        } catch {
          // ignore
        }
      }

      if (!map.getLayer(outlineId)) {
        try {
          map.addLayer(
            {
              id: outlineId,
              type: "circle",
              source: srcId,
              paint: {
                "circle-radius": GLOBE_SHIP_CIRCLE_RADIUS_EXPR,
                "circle-color": "rgba(0,0,0,0)",
                "circle-stroke-color": [
                  "case",
                  ["==", ["get", "selected"], 1],
                  "rgba(14,234,255,0.95)",
                  ["==", ["get", "highlighted"], 1],
                  "rgba(245,158,11,0.95)",
                  ["==", ["get", "permitted"], 1],
                  "rgba(125,211,252,0.95)",
                  "rgba(59,130,246,0.75)",
                ] as never,
                "circle-stroke-width": [
                  "case",
                  ["==", ["get", "selected"], 1],
                  3.4,
                  ["==", ["get", "highlighted"], 1],
                  2.7,
                  ["==", ["get", "permitted"], 1],
                  1.8,
                  0.0,
                ] as never,
                "circle-stroke-opacity": 0.85,
              },
              layout: {
                visibility,
                "circle-sort-key": [
                  "case",
                  ["==", ["get", "selected"], 1],
                  100,
                  ["==", ["get", "highlighted"], 1],
                  90,
                  ["==", ["get", "permitted"], 1],
                  70,
                  30,
                ] as never,
              },
            } as unknown as LayerSpecification,
            before,
          );
        } catch (e) {
          console.warn("Ship outline layer add failed:", e);
        }
      } else {
        try {
          map.setLayoutProperty(outlineId, "visibility", visibility);
          map.setPaintProperty(
            outlineId,
            "circle-stroke-color",
            [
              "case",
              ["==", ["get", "selected"], 1],
              "rgba(14,234,255,0.95)",
              ["==", ["get", "highlighted"], 1],
              "rgba(245,158,11,0.95)",
              ["==", ["get", "permitted"], 1],
              "rgba(125,211,252,0.95)",
              "rgba(59,130,246,0.75)",
            ] as never,
          );
          map.setPaintProperty(
            outlineId,
            "circle-stroke-width",
            [
              "case",
              ["==", ["get", "selected"], 1],
              3.4,
              ["==", ["get", "highlighted"], 1],
              2.7,
              ["==", ["get", "permitted"], 1],
              1.8,
              0.0,
            ] as never,
          );
        } catch {
          // ignore
        }
      }

      if (!map.getLayer(symbolId)) {
        try {
          map.addLayer(
            {
              id: symbolId,
              type: "symbol",
              source: srcId,
              layout: {
                visibility,
                "symbol-sort-key": [
                  "case",
                  ["==", ["get", "selected"], 1],
                  95,
                  ["==", ["get", "highlighted"], 1],
                  85,
                  ["==", ["get", "permitted"], 1],
                  65,
                  45,
                ] as never,
                "icon-image": imgId,
                "icon-size": [
                  "interpolate",
                  ["linear"],
                  ["zoom"],
                  3,
                  ["to-number", ["get", "iconSize3"], 0.35],
                  7,
                  ["to-number", ["get", "iconSize7"], 0.45],
                  10,
                  ["to-number", ["get", "iconSize10"], 0.56],
                  14,
                  ["to-number", ["get", "iconSize14"], 0.72],
                ] as unknown as number[],
                "icon-allow-overlap": true,
                "icon-ignore-placement": true,
                "icon-anchor": "center",
                "icon-rotate": ["to-number", ["get", "heading"], 0],
                // Keep the icon on the sea surface.
                "icon-rotation-alignment": "map",
                "icon-pitch-alignment": "map",
              },
              paint: {
                "icon-color": "#ffffff",
                "icon-opacity": 1,
                "icon-halo-color": "rgba(15,23,42,0.22)",
                "icon-halo-width": 0,
              },
            } as unknown as LayerSpecification,
            before,
          );
        } catch (e) {
          console.warn("Ship symbol layer add failed:", e);
        }
      } else {
        try {
          map.setLayoutProperty(symbolId, "visibility", visibility);
        } catch {
          // ignore
        }
      }

      // Selection and highlight are now source-data driven.
      reorderGlobeFeatureLayers();
      kickRepaint(map);
    };

    const stop = onMapStyleReady(map, ensure);
    return () => {
      stop();
    };
  }, [
    projection,
    settings.showShips,
    shipData,
    legacyHits,
    selectedMmsi,
    hoveredMmsiSetRef,
    hoveredFleetMmsiSetRef,
    hoveredPairMmsiSetRef,
    isHighlightedMmsi,
    mapSyncEpoch,
    reorderGlobeFeatureLayers,
  ]);

  // Globe ship click selection (MapLibre-native ships layer)
  useEffect(() => {
    const map = mapRef.current;
    if (!map) return;
    if (projection !== "globe" || !settings.showShips) return;

    const symbolId = "ships-globe";
    const haloId = "ships-globe-halo";
    const outlineId = "ships-globe-outline";
    const clickedRadiusDeg2 = Math.pow(0.08, 2);

    const onClick = (e: maplibregl.MapMouseEvent) => {
      try {
        const layerIds = [symbolId, haloId, outlineId].filter((id) => map.getLayer(id));
        let feats: unknown[] = [];
        if (layerIds.length > 0) {
          try {
            feats = map.queryRenderedFeatures(e.point, { layers: layerIds }) as unknown[];
          } catch {
            feats = [];
          }
        }
        const f = feats?.[0];
        const props = ((f as { properties?: Record<string, unknown> } | undefined)?.properties || {}) as Record<
          string,
          unknown
        >;
        const mmsi = Number(props.mmsi);
        if (Number.isFinite(mmsi)) {
          if (hasAuxiliarySelectModifier(e as unknown as { shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean })) {
            onToggleHighlightMmsi?.(mmsi);
            return;
          }
          onSelectMmsi(mmsi);
          return;
        }

        const clicked = { lat: e.lngLat.lat, lon: e.lngLat.lng };
        const cosLat = Math.cos(clicked.lat * DEG2RAD);
        let bestMmsi: number | null = null;
        let bestD2 = Number.POSITIVE_INFINITY;
        for (const t of targets) {
          if (!isFiniteNumber(t.lat) || !isFiniteNumber(t.lon)) continue;
          const dLon = (clicked.lon - t.lon) * cosLat;
          const dLat = clicked.lat - t.lat;
          const d2 = dLon * dLon + dLat * dLat;
          if (d2 <= clickedRadiusDeg2 && d2 < bestD2) {
            bestD2 = d2;
            bestMmsi = t.mmsi;
          }
        }
        if (bestMmsi != null) {
          if (hasAuxiliarySelectModifier(e as unknown as { shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean })) {
            onToggleHighlightMmsi?.(bestMmsi);
            return;
          }
          onSelectMmsi(bestMmsi);
          return;
        }
      } catch {
        // ignore
      }
      onSelectMmsi(null);
    };

    map.on("click", onClick);
    return () => {
      try {
        map.off("click", onClick);
      } catch {
        // ignore
      }
    };
  }, [projection, settings.showShips, onSelectMmsi, onToggleHighlightMmsi, mapSyncEpoch, targets]);


  // Globe overlays (pair links / FC links / ranges) rendered as MapLibre GeoJSON layers.
  // Deck custom layers are more fragile under globe projection; MapLibre-native rendering stays aligned like zones.
  useEffect(() => {
    const map = mapRef.current;
    if (!map) return;

    const srcId = "pair-lines-ml-src";
    const layerId = "pair-lines-ml";

    const remove = () => {
      try {
        if (map.getLayer(layerId)) map.setLayoutProperty(layerId, "visibility", "none");
      } catch {
        // ignore
      }
    };

    const ensure = () => {
      if (projectionBusyRef.current) return;
      if (!map.isStyleLoaded()) return;
      if (projection !== "globe" || !overlays.pairLines || (pairLinks?.length ?? 0) === 0) {
        remove();
        return;
      }

      const fc: GeoJSON.FeatureCollection<GeoJSON.LineString> = {
        type: "FeatureCollection",
        features: (pairLinks || []).map((p) => ({
          type: "Feature",
          id: makePairLinkFeatureId(p.aMmsi, p.bMmsi),
          geometry: { type: "LineString", coordinates: [p.from, p.to] },
            properties: {
              type: "pair",
              aMmsi: p.aMmsi,
              bMmsi: p.bMmsi,
              distanceNm: p.distanceNm,
              warn: p.warn,
            },
        })),
      };

      try {
        const existing = map.getSource(srcId) as GeoJSONSource | undefined;
        if (existing) existing.setData(fc);
        else map.addSource(srcId, { type: "geojson", data: fc } as GeoJSONSourceSpecification);
      } catch (e) {
        console.warn("Pair lines source setup failed:", e);
        return;
      }

      const before = undefined;

      if (!map.getLayer(layerId)) {
        try {
          map.addLayer(
            {
              id: layerId,
              type: "line",
              source: srcId,
              layout: { "line-cap": "round", "line-join": "round", visibility: "visible" },
              paint: {
                "line-color": [
                  "case",
                  ["==", ["get", "highlighted"], 1],
                  "rgba(245,158,11,0.98)",
                  ["boolean", ["get", "warn"], false],
                  "rgba(245,158,11,0.95)",
                  "rgba(59,130,246,0.55)",
                ] as never,
                "line-width": [
                  "case",
                  ["==", ["get", "highlighted"], 1],
                  2.8,
                  ["boolean", ["get", "warn"], false],
                  2.2,
                  1.4,
                ] as never,
                "line-opacity": 0.9,
              },
        } as unknown as LayerSpecification,
            before,
          );
        } catch (e) {
          console.warn("Pair lines layer add failed:", e);
        }
      } else {
        try {
          map.setLayoutProperty(layerId, "visibility", "visible");
        } catch {
          // ignore
        }
      }

      reorderGlobeFeatureLayers();
      kickRepaint(map);
    };

    const stop = onMapStyleReady(map, ensure);
    ensure();
    return () => {
      stop();
    };
  }, [
    projection,
    overlays.pairLines,
    pairLinks,
    mapSyncEpoch,
    reorderGlobeFeatureLayers,
  ]);

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

    const srcId = "fc-lines-ml-src";
    const layerId = "fc-lines-ml";

    const remove = () => {
      try {
        if (map.getLayer(layerId)) map.setLayoutProperty(layerId, "visibility", "none");
      } catch {
        // ignore
      }
    };

    const ensure = () => {
      if (projectionBusyRef.current) return;
      if (!map.isStyleLoaded()) return;
      if (projection !== "globe" || !overlays.fcLines) {
        remove();
        return;
      }

      const segs: DashSeg[] = [];
      for (const l of fcLinks || []) {
        segs.push(...dashifyLine(l.from, l.to, l.suspicious, l.distanceNm, l.fcMmsi, l.otherMmsi));
      }
      if (segs.length === 0) {
        remove();
        return;
      }

      const fc: GeoJSON.FeatureCollection<GeoJSON.LineString> = {
        type: "FeatureCollection",
        features: segs.map((s, idx) => ({
          type: "Feature",
          id: makeFcSegmentFeatureId(s.fromMmsi ?? -1, s.toMmsi ?? -1, idx),
          geometry: { type: "LineString", coordinates: [s.from, s.to] },
            properties: {
              type: "fc",
              suspicious: s.suspicious,
              distanceNm: s.distanceNm,
              fcMmsi: s.fromMmsi ?? -1,
              otherMmsi: s.toMmsi ?? -1,
            },
        })),
      };

      try {
        const existing = map.getSource(srcId) as GeoJSONSource | undefined;
        if (existing) existing.setData(fc);
        else map.addSource(srcId, { type: "geojson", data: fc } as GeoJSONSourceSpecification);
      } catch (e) {
        console.warn("FC lines source setup failed:", e);
        return;
      }

      const before = undefined;

      if (!map.getLayer(layerId)) {
        try {
          map.addLayer(
            {
              id: layerId,
              type: "line",
              source: srcId,
              layout: { "line-cap": "round", "line-join": "round", visibility: "visible" },
              paint: {
                "line-color": [
                  "case",
                  ["==", ["get", "highlighted"], 1],
                  "rgba(245,158,11,0.98)",
                  ["boolean", ["get", "suspicious"], false],
                  "rgba(239,68,68,0.95)",
                  "rgba(217,119,6,0.92)",
                ] as never,
                "line-width": ["case", ["==", ["get", "highlighted"], 1], 2.0, 1.3] as never,
                "line-opacity": 0.9,
              },
        } as unknown as LayerSpecification,
            before,
          );
        } catch (e) {
          console.warn("FC lines layer add failed:", e);
        }
      } else {
        try {
          map.setLayoutProperty(layerId, "visibility", "visible");
        } catch {
          // ignore
        }
      }

      reorderGlobeFeatureLayers();
      kickRepaint(map);
    };

    const stop = onMapStyleReady(map, ensure);
    ensure();
    return () => {
      stop();
    };
  }, [
    projection,
    overlays.fcLines,
    fcLinks,
    mapSyncEpoch,
    reorderGlobeFeatureLayers,
  ]);

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

    const srcId = "fleet-circles-ml-src";
    const fillSrcId = "fleet-circles-ml-fill-src";
    const layerId = "fleet-circles-ml";
    const fillLayerId = "fleet-circles-ml-fill";

    const remove = () => {
      try {
        if (map.getLayer(fillLayerId)) map.setLayoutProperty(fillLayerId, "visibility", "none");
      } catch {
        // ignore
      }
      try {
        if (map.getLayer(layerId)) map.setLayoutProperty(layerId, "visibility", "none");
      } catch {
        // ignore
      }
    };

    const ensure = () => {
      if (projectionBusyRef.current) return;
      if (!map.isStyleLoaded()) return;
      if (projection !== "globe" || !overlays.fleetCircles || (fleetCircles?.length ?? 0) === 0) {
        remove();
        return;
      }

      const fcLine: GeoJSON.FeatureCollection<GeoJSON.LineString> = {
        type: "FeatureCollection",
        features: (fleetCircles || []).map((c) => {
          const ring = circleRingLngLat(c.center, c.radiusNm * 1852);
          return {
            type: "Feature",
            id: makeFleetCircleFeatureId(c.ownerKey),
            geometry: { type: "LineString", coordinates: ring },
            properties: {
              type: "fleet",
              ownerKey: c.ownerKey,
              ownerLabel: c.ownerLabel,
              count: c.count,
              vesselMmsis: c.vesselMmsis,
              // Kept for backward compatibility with existing paint expressions.
              // Actual hover-state highlighting is now handled in
              // updateGlobeOverlayPaintStates.
              highlighted: 0,
            },
          };
        }),
      };

      const fcFill: GeoJSON.FeatureCollection<GeoJSON.Polygon> = {
        type: "FeatureCollection",
        features: (fleetCircles || []).map((c) => {
          const ring = circleRingLngLat(c.center, c.radiusNm * 1852);
          return {
            type: "Feature",
            id: `${makeFleetCircleFeatureId(c.ownerKey)}-fill`,
            geometry: { type: "Polygon", coordinates: [ring] },
            properties: {
              type: "fleet-fill",
              ownerKey: c.ownerKey,
              ownerLabel: c.ownerLabel,
              count: c.count,
              vesselMmsis: c.vesselMmsis,
              // Kept for backward compatibility with existing paint expressions.
              // Actual hover-state highlighting is now handled in
              // updateGlobeOverlayPaintStates.
              highlighted: 0,
            },
          };
        }),
      };

      try {
        const existing = map.getSource(srcId) as GeoJSONSource | undefined;
        if (existing) existing.setData(fcLine);
        else map.addSource(srcId, { type: "geojson", data: fcLine } as GeoJSONSourceSpecification);
      } catch (e) {
        console.warn("Fleet circles source setup failed:", e);
        return;
      }

      try {
        const existingFill = map.getSource(fillSrcId) as GeoJSONSource | undefined;
        if (existingFill) existingFill.setData(fcFill);
        else map.addSource(fillSrcId, { type: "geojson", data: fcFill } as GeoJSONSourceSpecification);
      } catch (e) {
        console.warn("Fleet circles source setup failed:", e);
        return;
      }

      const before = undefined;

      if (!map.getLayer(fillLayerId)) {
        try {
          map.addLayer(
            {
              id: fillLayerId,
              type: "fill",
              source: fillSrcId,
              layout: { visibility: "visible" },
              paint: {
                "fill-color": [
                  "case",
                  ["==", ["get", "highlighted"], 1],
                  "rgba(245,158,11,0.16)",
                  "rgba(245,158,11,0.02)",
                ] as never,
                "fill-opacity": ["case", ["==", ["get", "highlighted"], 1], 0.7, 0.36] as never,
              },
            } as unknown as LayerSpecification,
            before,
          );
        } catch (e) {
          console.warn("Fleet circles fill layer add failed:", e);
        }
      } else {
        try {
          map.setLayoutProperty(fillLayerId, "visibility", "visible");
        } catch {
          // ignore
        }
      }

      if (!map.getLayer(layerId)) {
        try {
          map.addLayer(
            {
              id: layerId,
              type: "line",
              source: srcId,
              layout: { "line-cap": "round", "line-join": "round", visibility: "visible" },
              paint: {
                "line-color": ["case", ["==", ["get", "highlighted"], 1], "rgba(245,158,11,0.95)", "rgba(245,158,11,0.65)"] as never,
                "line-width": ["case", ["==", ["get", "highlighted"], 1], 2, 1.1] as never,
                "line-opacity": 0.85,
              },
            } as unknown as LayerSpecification,
            before,
          );
        } catch (e) {
          console.warn("Fleet circles layer add failed:", e);
        }
      } else {
        try {
          map.setLayoutProperty(layerId, "visibility", "visible");
        } catch {
          // ignore
        }
      }

      reorderGlobeFeatureLayers();
      kickRepaint(map);
    };

    const stop = onMapStyleReady(map, ensure);
    ensure();
    return () => {
      stop();
    };
  }, [
    projection,
    overlays.fleetCircles,
    fleetCircles,
    mapSyncEpoch,
    reorderGlobeFeatureLayers,
  ]);

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

    const srcId = "pair-range-ml-src";
    const layerId = "pair-range-ml";

    const remove = () => {
      try {
        if (map.getLayer(layerId)) map.setLayoutProperty(layerId, "visibility", "none");
      } catch {
        // ignore
      }
    };

    const ensure = () => {
      if (projectionBusyRef.current) return;
      if (!map.isStyleLoaded()) return;
      if (projection !== "globe" || !overlays.pairRange) {
        remove();
        return;
      }

      const ranges: PairRangeCircle[] = [];
      for (const p of pairLinks || []) {
        const center: [number, number] = [(p.from[0] + p.to[0]) / 2, (p.from[1] + p.to[1]) / 2];
        ranges.push({
          center,
          radiusNm: Math.max(0.05, p.distanceNm / 2),
          warn: p.warn,
          aMmsi: p.aMmsi,
          bMmsi: p.bMmsi,
          distanceNm: p.distanceNm,
        });
      }
      if (ranges.length === 0) {
        remove();
        return;
      }

      const fc: GeoJSON.FeatureCollection<GeoJSON.LineString> = {
        type: "FeatureCollection",
        features: ranges.map((c) => {
          const ring = circleRingLngLat(c.center, c.radiusNm * 1852);
          return {
            type: "Feature",
            id: makePairLinkFeatureId(c.aMmsi, c.bMmsi),
            geometry: { type: "LineString", coordinates: ring },
            properties: {
              type: "pair-range",
              warn: c.warn,
              aMmsi: c.aMmsi,
              bMmsi: c.bMmsi,
              distanceNm: c.distanceNm,
              // Kept for backward compatibility with existing paint expressions.
              // Actual hover-state highlighting is now handled in
              // updateGlobeOverlayPaintStates.
              highlighted: 0,
            },
          };
        }),
      };

      try {
        const existing = map.getSource(srcId) as GeoJSONSource | undefined;
        if (existing) existing.setData(fc);
        else map.addSource(srcId, { type: "geojson", data: fc } as GeoJSONSourceSpecification);
      } catch (e) {
        console.warn("Pair range source setup failed:", e);
        return;
      }

      const before = undefined;

      if (!map.getLayer(layerId)) {
        try {
          map.addLayer(
            {
              id: layerId,
              type: "line",
              source: srcId,
              layout: { "line-cap": "round", "line-join": "round", visibility: "visible" },
              paint: {
                "line-color": [
                  "case",
                  ["==", ["get", "highlighted"], 1],
                  "rgba(245,158,11,0.92)",
                  ["boolean", ["get", "warn"], false],
                  "rgba(245,158,11,0.75)",
                  "rgba(59,130,246,0.45)",
                ] as never,
                "line-width": ["case", ["==", ["get", "highlighted"], 1], 1.6, 1.0] as never,
                "line-opacity": 0.85,
              },
            } as unknown as LayerSpecification,
            before,
          );
        } catch (e) {
          console.warn("Pair range layer add failed:", e);
        }
      } else {
        try {
          map.setLayoutProperty(layerId, "visibility", "visible");
        } catch {
          // ignore
        }
      }

      kickRepaint(map);
    };

    const stop = onMapStyleReady(map, ensure);
    ensure();
    return () => {
      stop();
    };
  }, [
    projection,
    overlays.pairRange,
    pairLinks,
    mapSyncEpoch,
    reorderGlobeFeatureLayers,
  ]);

  const updateGlobeOverlayPaintStates = useCallback(() => {
    if (projection !== "globe" || projectionBusyRef.current) return;

    const map = mapRef.current;
    if (!map || !map.isStyleLoaded()) return;

    const fleetAwarePairMmsiList = makeUniqueSorted([...hoveredPairMmsiList, ...hoveredFleetMmsiList]);

    const pairHighlightExpr = hoveredPairMmsiList.length >= 2
      ? makeMmsiPairHighlightExpr("aMmsi", "bMmsi", hoveredPairMmsiList)
      : false;

    const fcEndpointHighlightExpr = fleetAwarePairMmsiList.length > 0
      ? makeMmsiAnyEndpointExpr("fcMmsi", "otherMmsi", fleetAwarePairMmsiList)
      : false;

    const fleetOwnerMatchExpr =
      hoveredFleetOwnerKeyList.length > 0 ? makeFleetOwnerMatchExpr(hoveredFleetOwnerKeyList) : false;
    const fleetMemberExpr =
      hoveredFleetMmsiList.length > 0 ? makeFleetMemberMatchExpr(hoveredFleetMmsiList) : false;
    const fleetHighlightExpr =
      hoveredFleetOwnerKeyList.length > 0 || hoveredFleetMmsiList.length > 0
        ? (["any", fleetOwnerMatchExpr, fleetMemberExpr] as never)
        : false;

    try {
      if (map.getLayer("pair-lines-ml")) {
        map.setPaintProperty(
          "pair-lines-ml",
          "line-color",
          ["case", pairHighlightExpr, "rgba(245,158,11,0.98)", ["boolean", ["get", "warn"], false], "rgba(59,130,246,0.55)", "rgba(59,130,246,0.55)"] as never,
        );
        map.setPaintProperty(
          "pair-lines-ml",
          "line-width",
          ["case", pairHighlightExpr, 2.8, ["boolean", ["get", "warn"], false], 2.2, 1.4] as never,
        );
      }
    } catch {
      // ignore
    }

    try {
      if (map.getLayer("fc-lines-ml")) {
        map.setPaintProperty(
          "fc-lines-ml",
          "line-color",
          ["case", fcEndpointHighlightExpr, "rgba(245,158,11,0.98)", ["boolean", ["get", "suspicious"], false], "rgba(239,68,68,0.95)", "rgba(217,119,6,0.92)"] as never,
        );
        map.setPaintProperty(
          "fc-lines-ml",
          "line-width",
          ["case", fcEndpointHighlightExpr, 2.0, 1.3] as never,
        );
      }
    } catch {
      // ignore
    }

    try {
      if (map.getLayer("pair-range-ml")) {
        map.setPaintProperty(
          "pair-range-ml",
          "line-color",
          [
            "case",
            pairHighlightExpr,
            "rgba(245,158,11,0.92)",
            ["boolean", ["get", "warn"], false],
            "rgba(245,158,11,0.75)",
            "rgba(59,130,246,0.45)",
          ] as never,
        );
        map.setPaintProperty(
          "pair-range-ml",
          "line-width",
          ["case", pairHighlightExpr, 1.6, 1.0] as never,
        );
      }
    } catch {
      // ignore
    }

    try {
      if (map.getLayer("fleet-circles-ml-fill")) {
        map.setPaintProperty(
          "fleet-circles-ml-fill",
          "fill-color",
          [
            "case",
            fleetHighlightExpr,
            "rgba(245,158,11,0.24)",
            "rgba(245,158,11,0.02)",
          ] as never,
        );
        map.setPaintProperty(
          "fleet-circles-ml-fill",
          "fill-opacity",
          ["case", fleetHighlightExpr, 0.7, 0.28] as never,
        );
      }
      if (map.getLayer("fleet-circles-ml")) {
        map.setPaintProperty(
          "fleet-circles-ml",
          "line-color",
          ["case", fleetHighlightExpr, "rgba(245,158,11,0.95)", "rgba(245,158,11,0.65)"] as never,
        );
        map.setPaintProperty(
          "fleet-circles-ml",
          "line-width",
          ["case", fleetHighlightExpr, 2, 1.1] as never,
        );
      }
    } catch {
      // ignore
    }
  }, [projection, hoveredFleetMmsiList, hoveredFleetOwnerKeyList, hoveredPairMmsiList]);

  useEffect(() => {
    const map = mapRef.current;
    if (!map) return;
    const stop = onMapStyleReady(map, updateGlobeOverlayPaintStates);
    updateGlobeOverlayPaintStates();
    return () => {
      stop();
    };
  }, [mapSyncEpoch, hoveredFleetMmsiList, hoveredFleetOwnerKeyList, hoveredPairMmsiList, projection, updateGlobeOverlayPaintStates]);

  const shipLayerData = useMemo(() => {
    if (shipData.length === 0) return shipData;
    const layer = [...shipData];
    layer.sort((a, b) => {
      const aPriority = a.mmsi === selectedMmsi ? 3 : isHighlightedMmsi(a.mmsi) ? 2 : 0;
      const bPriority = b.mmsi === selectedMmsi ? 3 : isHighlightedMmsi(b.mmsi) ? 2 : 0;
      if (aPriority !== bPriority) return aPriority - bPriority;
      return a.mmsi - b.mmsi;
    });
    return layer;
  }, [shipData, isHighlightedMmsi, selectedMmsi]);

  const clearGlobeTooltip = useCallback(() => {
    if (!mapTooltipRef.current) return;
    try {
      mapTooltipRef.current.remove();
    } catch {
      // ignore
    }
    mapTooltipRef.current = null;
  }, []);

  const setGlobeTooltip = useCallback((lngLat: maplibregl.LngLatLike, tooltipHtml: string) => {
    const map = mapRef.current;
    if (!map || !map.isStyleLoaded()) return;
    if (!mapTooltipRef.current) {
      mapTooltipRef.current = new maplibregl.Popup({
        closeButton: false,
        closeOnClick: false,
        maxWidth: "360px",
        className: "maplibre-tooltip-popup",
      });
    }

    const container = document.createElement("div");
    container.className = "maplibre-tooltip-popup__content";
    container.innerHTML = tooltipHtml;

    mapTooltipRef.current.setLngLat(lngLat).setDOMContent(container).addTo(map);
  }, []);

  const buildGlobeFeatureTooltip = useCallback(
    (feature: { properties?: Record<string, unknown> | null; layer?: { id?: string } } | null | undefined) => {
      if (!feature) return null;
      const props = feature.properties || {};
      const layerId = feature.layer?.id;

      const maybeMmsi = toIntMmsi(props.mmsi);
      if (maybeMmsi != null && maybeMmsi > 0) {
        return getShipTooltipHtml({ mmsi: maybeMmsi, targetByMmsi: shipByMmsi, legacyHits });
      }

      if (layerId === "pair-lines-ml") {
        const warn = props.warn === true;
        const aMmsi = toIntMmsi(props.aMmsi);
        const bMmsi = toIntMmsi(props.bMmsi);
        if (aMmsi == null || bMmsi == null) return null;
        return getPairLinkTooltipHtml({
          warn,
          distanceNm: toSafeNumber(props.distanceNm),
          aMmsi,
          bMmsi,
          legacyHits,
          targetByMmsi: shipByMmsi,
        });
      }

      if (layerId === "fc-lines-ml") {
        const fcMmsi = toIntMmsi(props.fcMmsi);
        const otherMmsi = toIntMmsi(props.otherMmsi);
        if (fcMmsi == null || otherMmsi == null) return null;
        return getFcLinkTooltipHtml({
          suspicious: props.suspicious === true,
          distanceNm: toSafeNumber(props.distanceNm),
          fcMmsi,
          otherMmsi,
          legacyHits,
          targetByMmsi: shipByMmsi,
        });
      }

      if (layerId === "pair-range-ml") {
        const aMmsi = toIntMmsi(props.aMmsi);
        const bMmsi = toIntMmsi(props.bMmsi);
        if (aMmsi == null || bMmsi == null) return null;
        return getRangeTooltipHtml({
          warn: props.warn === true,
          distanceNm: toSafeNumber(props.distanceNm),
          aMmsi,
          bMmsi,
          legacyHits,
        });
      }

      if (layerId === "fleet-circles-ml" || layerId === "fleet-circles-ml-fill") {
        return getFleetCircleTooltipHtml({
          ownerKey: String(props.ownerKey ?? ""),
          ownerLabel: String(props.ownerLabel ?? props.ownerKey ?? ""),
          count: Number(props.count ?? 0),
        });
      }

      const zoneLabel = getZoneDisplayNameFromProps(props);
      if (zoneLabel) {
        return { html: `<div style="font-size: 12px; font-family: system-ui;">${zoneLabel}</div>` };
      }

      return null;
    },
    [legacyHits, shipByMmsi],
  );

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

    const clearDeckGlobeHoverState = () => {
      clearDeckHoverMmsi();
      clearDeckHoverPairs();
      setHoveredZoneId((prev) => (prev === null ? prev : null));
      clearMapFleetHoverState();
    };

    const resetGlobeHoverStates = () => {
      clearDeckHoverMmsi();
      clearDeckHoverPairs();
      setHoveredZoneId((prev) => (prev === null ? prev : null));
      clearMapFleetHoverState();
    };

    const normalizeMmsiList = (value: unknown): number[] => {
      if (!Array.isArray(value)) return [];
      const out: number[] = [];
      for (const n of value) {
        const m = toIntMmsi(n);
        if (m != null) out.push(m);
      }
      return out;
    };

    const onMouseMove = (e: maplibregl.MapMouseEvent) => {
      if (projection !== "globe") {
        clearGlobeTooltip();
        resetGlobeHoverStates();
        return;
      }
      if (projectionBusyRef.current) {
        resetGlobeHoverStates();
        clearGlobeTooltip();
        return;
      }
      if (!map.isStyleLoaded()) {
        clearDeckGlobeHoverState();
        clearGlobeTooltip();
        return;
      }

      let candidateLayerIds: string[] = [];
      try {
        candidateLayerIds = [
          "ships-globe",
          "ships-globe-halo",
          "ships-globe-outline",
          "pair-lines-ml",
          "fc-lines-ml",
          "fleet-circles-ml",
          "fleet-circles-ml-fill",
          "pair-range-ml",
          "zones-fill",
          "zones-line",
          "zones-label",
        ].filter((id) => map.getLayer(id));
      } catch {
        candidateLayerIds = [];
      }

      if (candidateLayerIds.length === 0) {
        resetGlobeHoverStates();
        clearGlobeTooltip();
        return;
      }

      let rendered: Array<{ properties?: Record<string, unknown> | null; layer?: { id?: string } }> = [];
      try {
        rendered = map.queryRenderedFeatures(e.point, { layers: candidateLayerIds }) as unknown as Array<{
          properties?: Record<string, unknown> | null;
          layer?: { id?: string };
        }>;
      } catch {
        rendered = [];
      }

      const priority = [
        "ships-globe",
        "ships-globe-halo",
        "ships-globe-outline",
        "pair-lines-ml",
        "fc-lines-ml",
        "pair-range-ml",
        "fleet-circles-ml-fill",
        "fleet-circles-ml",
        "zones-fill",
        "zones-line",
        "zones-label",
      ];

      const first = priority.map((id) => rendered.find((r) => r.layer?.id === id)).find(Boolean) as
        | { properties?: Record<string, unknown> | null; layer?: { id?: string } }
        | undefined;

      if (!first) {
        resetGlobeHoverStates();
        clearGlobeTooltip();
        return;
      }

      const layerId = first.layer?.id;
      const props = first.properties || {};
      const isShipLayer = layerId === "ships-globe" || layerId === "ships-globe-halo" || layerId === "ships-globe-outline";
      const isPairLayer = layerId === "pair-lines-ml" || layerId === "pair-range-ml";
      const isFcLayer = layerId === "fc-lines-ml";
      const isFleetLayer = layerId === "fleet-circles-ml" || layerId === "fleet-circles-ml-fill";
      const isZoneLayer = layerId === "zones-fill" || layerId === "zones-line" || layerId === "zones-label";

      if (isShipLayer) {
        const mmsi = toIntMmsi(props.mmsi);
        setDeckHoverMmsi(mmsi == null ? [] : [mmsi]);
        clearDeckHoverPairs();
        clearMapFleetHoverState();
        setHoveredZoneId((prev) => (prev === null ? prev : null));
      } else if (isPairLayer) {
        const aMmsi = toIntMmsi(props.aMmsi);
        const bMmsi = toIntMmsi(props.bMmsi);
        setDeckHoverPairs([...(aMmsi == null ? [] : [aMmsi]), ...(bMmsi == null ? [] : [bMmsi])]);
        clearDeckHoverMmsi();
        clearMapFleetHoverState();
        setHoveredZoneId((prev) => (prev === null ? prev : null));
      } else if (isFcLayer) {
        const from = toIntMmsi(props.fcMmsi);
        const to = toIntMmsi(props.otherMmsi);
        const fromTo = [from, to].filter((v): v is number => v != null);
        setDeckHoverPairs(fromTo);
        setDeckHoverMmsi(fromTo);
        clearMapFleetHoverState();
        setHoveredZoneId((prev) => (prev === null ? prev : null));
      } else if (isFleetLayer) {
        const ownerKey = String(props.ownerKey ?? "");
        const list = normalizeMmsiList(props.vesselMmsis);
        setMapFleetHoverState(ownerKey || null, list);
        clearDeckHoverMmsi();
        clearDeckHoverPairs();
        setHoveredZoneId((prev) => (prev === null ? prev : null));
      } else if (isZoneLayer) {
        clearMapFleetHoverState();
        clearDeckHoverMmsi();
        clearDeckHoverPairs();
        const zoneId = getZoneIdFromProps(props);
        setHoveredZoneId(zoneId || null);
      } else {
        resetGlobeHoverStates();
      }

      const tooltip = buildGlobeFeatureTooltip(first);
      if (!tooltip) {
        if (!isZoneLayer) {
          resetGlobeHoverStates();
        }
        clearGlobeTooltip();
        return;
      }

      const content = tooltip?.html ?? "";
      if (content) {
        setGlobeTooltip(e.lngLat, content);
        return;
      }
      clearGlobeTooltip();
    };

    const onMouseOut = () => {
      resetGlobeHoverStates();
      clearGlobeTooltip();
    };

    map.on("mousemove", onMouseMove);
    map.on("mouseout", onMouseOut);

    return () => {
      map.off("mousemove", onMouseMove);
      map.off("mouseout", onMouseOut);
      clearGlobeTooltip();
    };
  }, [
    projection,
    buildGlobeFeatureTooltip,
    clearGlobeTooltip,
    clearMapFleetHoverState,
    clearDeckHoverPairs,
    clearDeckHoverMmsi,
    setDeckHoverPairs,
    setDeckHoverMmsi,
    setMapFleetHoverState,
    setGlobeTooltip,
  ]);

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

  const legacyTargetsOrdered = useMemo(() => {
    if (legacyTargets.length === 0) return legacyTargets;
    const layer = [...legacyTargets];
    layer.sort((a, b) => {
      const aPriority = a.mmsi === selectedMmsi ? 3 : isHighlightedMmsi(a.mmsi) ? 2 : 0;
      const bPriority = b.mmsi === selectedMmsi ? 3 : isHighlightedMmsi(b.mmsi) ? 2 : 0;
      if (aPriority !== bPriority) return aPriority - bPriority;
      return a.mmsi - b.mmsi;
    });
    return layer;
  }, [legacyTargets, isHighlightedMmsi, selectedMmsi]);

  const fcDashed = useMemo(() => {
    const segs: DashSeg[] = [];
    for (const l of fcLinks || []) {
      segs.push(...dashifyLine(l.from, l.to, l.suspicious, l.distanceNm, l.fcMmsi, l.otherMmsi));
    }
    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,
        aMmsi: p.aMmsi,
        bMmsi: p.bMmsi,
        distanceNm: p.distanceNm,
      });
    }
    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]);

  useEffect(() => {
    const map = mapRef.current;
    if (!map || fleetFocusLon == null || fleetFocusLat == null || !Number.isFinite(fleetFocusLon) || !Number.isFinite(fleetFocusLat))
      return;
    const lon = fleetFocusLon;
    const lat = fleetFocusLat;
    const zoom = fleetFocusZoom ?? 10;

    const apply = () => {
      map.easeTo({
        center: [lon, lat],
        zoom,
        duration: 700,
      });
    };

    if (map.isStyleLoaded()) {
      apply();
      return;
    }

    const stop = onMapStyleReady(map, apply);
    return () => {
      stop();
    };
  }, [fleetFocusId, fleetFocusLon, fleetFocusLat, fleetFocusZoom]);

  // Update Deck.gl layers
  useEffect(() => {
    const map = mapRef.current;
    if (!map) return;
    if (projectionBusyRef.current) return;

    let deckTarget = projection === "globe" ? globeDeckLayerRef.current : overlayRef.current;

    if (projection === "mercator") {
      if (!deckTarget) deckTarget = ensureMercatorOverlay();
      if (!deckTarget) return;
      try {
        deckTarget.setProps({ layers: [] } as never);
      } catch {
        // ignore
      }
    } else if (!deckTarget && projection === "globe") {
      return;
    }
    if (!deckTarget) return;

    const overlayParams = projection === "globe" ? GLOBE_OVERLAY_PARAMS : DEPTH_DISABLED_PARAMS;
    const layers = [];
    const clearDeckHover = () => {
      touchDeckHoverState(false);
    };

    const toFleetMmsiList = (value: unknown) => {
      if (!Array.isArray(value)) return [];
      const out: number[] = [];
      for (const item of value) {
        const v = toIntMmsi(item);
        if (v != null) out.push(v);
      }
      return out;
    };

    const onDeckSelectOrHighlight = (info: unknown, allowMultiSelect = false) => {
      const obj = info as { mmsi?: unknown; srcEvent?: { shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean } | null };
      const mmsi = toIntMmsi(obj.mmsi);
      if (mmsi == null) return;
      const evt = obj.srcEvent ?? null;
      const isAux = hasAuxiliarySelectModifier(evt);
      if (onToggleHighlightMmsi && isAux) {
        onToggleHighlightMmsi(mmsi);
        return;
      }
      if (!allowMultiSelect && selectedMmsi === mmsi) {
        onSelectMmsi(null);
        return;
      }
      onSelectMmsi(mmsi);
    };

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

    if (overlays.pairRange && projection !== "globe" && pairRanges.length > 0) {
      layers.push(
        new ScatterplotLayer<PairRangeCircle>({
          id: "pair-range",
          data: pairRanges,
          pickable: true,
          billboard: false,
          parameters: overlayParams,
          filled: false,
          stroked: true,
          radiusUnits: "meters",
          getRadius: (d) => d.radiusNm * 1852,
          radiusMinPixels: 10,
          lineWidthUnits: "pixels",
          getLineWidth: (d) => (isHighlightedPair(d.aMmsi, d.bMmsi) ? 2.2 : 1),
          getLineColor: (d) => {
            if (isHighlightedPair(d.aMmsi, d.bMmsi)) return [245, 158, 11, 220];
            return d.warn ? [245, 158, 11, 170] : [59, 130, 246, 110];
          },
          getPosition: (d) => d.center,
          onHover: (info) => {
            if (!info.object) {
              clearDeckHover();
              return;
            }
            touchDeckHoverState(true);
            const p = info.object as PairRangeCircle;
            const aMmsi = p.aMmsi;
            const bMmsi = p.bMmsi;
            setDeckHoverPairs([aMmsi, bMmsi]);
            setDeckHoverMmsi([aMmsi, bMmsi]);
            clearMapFleetHoverState();
          },
          onClick: (info) => {
            if (!info.object) {
              onSelectMmsi(null);
              return;
            }
            const obj = info.object as PairRangeCircle;
            const sourceEvent = (info as { srcEvent?: { shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean } | null }).srcEvent;
            if (sourceEvent && hasAuxiliarySelectModifier(sourceEvent)) {
              onToggleHighlightMmsi?.(obj.aMmsi);
              onToggleHighlightMmsi?.(obj.bMmsi);
              return;
            }
            onDeckSelectOrHighlight({ mmsi: obj.aMmsi });
          },
          updateTriggers: {
            getLineWidth: [hoveredPairSignature],
            getLineColor: [hoveredPairSignature],
          },
        }),
      );
    }

    if (overlays.pairLines && projection !== "globe" && (pairLinks?.length ?? 0) > 0) {
      layers.push(
        new LineLayer<PairLink>({
          id: "pair-lines",
          data: pairLinks,
          pickable: true,
          parameters: overlayParams,
          getSourcePosition: (d) => d.from,
          getTargetPosition: (d) => d.to,
          getColor: (d) => {
            if (isHighlightedPair(d.aMmsi, d.bMmsi)) return [245, 158, 11, 245];
            return d.warn ? [245, 158, 11, 220] : [59, 130, 246, 85];
          },
          getWidth: (d) => (isHighlightedPair(d.aMmsi, d.bMmsi) ? 2.6 : d.warn ? 2.2 : 1.4),
          widthUnits: "pixels",
          onHover: (info) => {
            if (!info.object) {
              clearDeckHover();
              return;
            }
            touchDeckHoverState(true);
            const obj = info.object as PairLink;
            setDeckHoverPairs([obj.aMmsi, obj.bMmsi]);
            setDeckHoverMmsi([obj.aMmsi, obj.bMmsi]);
            clearMapFleetHoverState();
          },
          onClick: (info) => {
            if (!info.object) return;
            const obj = info.object as PairLink;
            const sourceEvent = (info as { srcEvent?: { shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean } | null }).srcEvent;
            if (sourceEvent && hasAuxiliarySelectModifier(sourceEvent)) {
              onToggleHighlightMmsi?.(obj.aMmsi);
              onToggleHighlightMmsi?.(obj.bMmsi);
              return;
            }
            onDeckSelectOrHighlight({ mmsi: obj.aMmsi });
          },
          updateTriggers: {
            getColor: [hoveredPairSignature],
            getWidth: [hoveredPairSignature],
          },
        }),
      );
    }

    if (overlays.fcLines && projection !== "globe" && fcDashed.length > 0) {
      layers.push(
        new LineLayer<DashSeg>({
          id: "fc-lines",
          data: fcDashed,
          pickable: true,
          parameters: overlayParams,
          getSourcePosition: (d) => d.from,
          getTargetPosition: (d) => d.to,
          getColor: (d) => {
            const isHighlighted = [d.fromMmsi, d.toMmsi].some((v) => isHighlightedMmsi(v ?? -1));
            if (isHighlighted) return [245, 158, 11, 230];
            return d.suspicious ? [239, 68, 68, 220] : [217, 119, 6, 200];
          },
          getWidth: (d) => {
            const isHighlighted = [d.fromMmsi, d.toMmsi].some((v) => isHighlightedMmsi(v ?? -1));
            return isHighlighted ? 1.9 : 1.3;
          },
          widthUnits: "pixels",
          onHover: (info) => {
            if (!info.object) {
              clearDeckHover();
              return;
            }
            touchDeckHoverState(true);
            const obj = info.object as DashSeg;
            const aMmsi = obj.fromMmsi;
            const bMmsi = obj.toMmsi;
            if (aMmsi == null || bMmsi == null) {
              clearDeckHover();
              return;
            }
            setDeckHoverPairs([aMmsi, bMmsi]);
            setDeckHoverMmsi([aMmsi, bMmsi]);
            clearMapFleetHoverState();
          },
          onClick: (info) => {
            if (!info.object) return;
            const obj = info.object as DashSeg;
            if (obj.fromMmsi == null || obj.toMmsi == null) {
              return;
            }
            const sourceEvent = (info as { srcEvent?: { shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean } | null }).srcEvent;
            if (sourceEvent && hasAuxiliarySelectModifier(sourceEvent)) {
              onToggleHighlightMmsi?.(obj.fromMmsi);
              onToggleHighlightMmsi?.(obj.toMmsi);
              return;
            }
            onDeckSelectOrHighlight({ mmsi: obj.fromMmsi });
          },
          updateTriggers: {
            getColor: [hoveredShipSignature],
            getWidth: [hoveredShipSignature],
          },
        }),
      );
    }

    if (overlays.fleetCircles && projection !== "globe" && (fleetCircles?.length ?? 0) > 0) {
      layers.push(
        new ScatterplotLayer<FleetCircle>({
          id: "fleet-circles",
          data: fleetCircles,
          pickable: true,
          billboard: false,
          parameters: overlayParams,
          filled: false,
          stroked: true,
          radiusUnits: "meters",
          getRadius: (d) => d.radiusNm * 1852,
          lineWidthUnits: "pixels",
          getLineWidth: (d) => (isHighlightedFleet(d.ownerKey, d.vesselMmsis) ? 1.8 : 1.1),
          getLineColor: (d) => {
            const isHighlighted = isHighlightedFleet(d.ownerKey, d.vesselMmsis);
            return isHighlighted ? [245, 158, 11, 220] : [245, 158, 11, 140];
          },
          getPosition: (d) => d.center,
          onHover: (info) => {
            if (!info.object) {
              clearDeckHover();
              return;
            }
            touchDeckHoverState(true);
            const obj = info.object as FleetCircle;
            const list = toFleetMmsiList(obj.vesselMmsis);
            setMapFleetHoverState(obj.ownerKey || null, list);
            setDeckHoverMmsi(list);
            clearDeckHoverPairs();
          },
          onClick: (info) => {
            if (!info.object) return;
            const obj = info.object as FleetCircle;
            const list = toFleetMmsiList(obj.vesselMmsis);
            const sourceEvent = (info as { srcEvent?: { shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean } | null }).srcEvent;
            if (sourceEvent && hasAuxiliarySelectModifier(sourceEvent)) {
              for (const mmsi of list) onToggleHighlightMmsi?.(mmsi);
              return;
            }
            const first = list[0];
            if (first != null) {
              onDeckSelectOrHighlight({ mmsi: first });
            }
          },
          updateTriggers: {
            getLineWidth: [hoveredFleetSignature, hoveredFleetOwnerKey, hoveredShipSignature],
            getLineColor: [hoveredFleetSignature, hoveredFleetOwnerKey, hoveredShipSignature],
          },
        }),
      );
    }

    if (overlays.fleetCircles && projection !== "globe" && (fleetCircles?.length ?? 0) > 0) {
      layers.push(
        new ScatterplotLayer<FleetCircle>({
          id: "fleet-circles-fill",
          data: fleetCircles,
          pickable: false,
          billboard: false,
          parameters: overlayParams,
          filled: true,
          stroked: false,
          radiusUnits: "meters",
          getRadius: (d) => d.radiusNm * 1852,
          getFillColor: (d) => {
            const isHighlighted = isHighlightedFleet(d.ownerKey, d.vesselMmsis);
            return isHighlighted ? [245, 158, 11, 42] : [245, 158, 11, 6];
          },
          getPosition: (d) => d.center,
          updateTriggers: {
            getFillColor: [hoveredFleetSignature, hoveredFleetOwnerKey, hoveredShipSignature],
          },
        }),
      );
    }

    if (settings.showShips && projection !== "globe" && legacyTargets.length > 0) {
      layers.push(
        new ScatterplotLayer<AisTarget>({
          id: "legacy-halo",
          data: legacyTargetsOrdered,
          pickable: false,
          billboard: false,
          // 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) => {
            if (selectedMmsi && d.mmsi === selectedMmsi) return FLAT_LEGACY_HALO_RADIUS_SELECTED;
            if (isHighlightedMmsi(d.mmsi)) return FLAT_LEGACY_HALO_RADIUS_HIGHLIGHTED;
            return FLAT_LEGACY_HALO_RADIUS;
          },
          lineWidthUnits: "pixels",
          getLineWidth: (d) => {
            const isHighlighted = isHighlightedMmsi(d.mmsi);
            return selectedMmsi && d.mmsi === selectedMmsi
              ? 2.5
              : isHighlighted
                ? 2.2
                : 2;
          },
          getLineColor: (d) => {
            if (selectedMmsi && d.mmsi === selectedMmsi) return [14, 234, 255, 230];
            if (isHighlightedMmsi(d.mmsi)) return [245, 158, 11, 210];
            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) => [d.lon, d.lat] as [number, number],
          updateTriggers: {
            getRadius: [selectedMmsi, hoveredShipSignature],
            getLineColor: [selectedMmsi, legacyHits, hoveredShipSignature],
          },
        }),
      );
    }

    if (settings.showShips && projection !== "globe") {
      layers.push(
        new IconLayer<AisTarget>({
          id: "ships",
          data: shipLayerData,
          pickable: true,
          // Keep icons horizontal on the sea surface when view is pitched/rotated.
          billboard: false,
          parameters: overlayParams,
          iconAtlas: "/assets/ship.svg",
          iconMapping: SHIP_ICON_MAPPING,
          getIcon: () => "ship",
          getPosition: (d) => [d.lon, d.lat] as [number, number],
          getAngle: (d) =>
            getDisplayHeading({
              cog: d.cog,
              heading: d.heading,
            }),
          sizeUnits: "pixels",
          getSize: (d) => {
            if (selectedMmsi && d.mmsi === selectedMmsi) return FLAT_SHIP_ICON_SIZE_SELECTED;
            if (isHighlightedMmsi(d.mmsi)) return FLAT_SHIP_ICON_SIZE_HIGHLIGHTED;
            return FLAT_SHIP_ICON_SIZE;
          },
          getColor: (d) =>
            getShipColor(
              d,
              selectedMmsi,
              legacyHits?.get(d.mmsi)?.shipCode ?? null,
              highlightedMmsiSetCombined,
            ),
          onHover: (info) => {
            if (!info.object) {
              clearDeckHover();
              return;
            }
            touchDeckHoverState(true);
            const obj = info.object as AisTarget;
            setDeckHoverMmsi([obj.mmsi]);
            clearDeckHoverPairs();
            clearMapFleetHoverState();
          },
          onClick: (info) => {
            if (!info.object) {
              onSelectMmsi(null);
              return;
            }
            onDeckSelectOrHighlight({
              mmsi: info.object.mmsi,
              srcEvent: (info as { srcEvent?: { shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean } | null }).srcEvent,
            }, true);
          },
          alphaCutoff: 0.05,
          updateTriggers: {
            getSize: [selectedMmsi, hoveredShipSignature],
            getColor: [selectedMmsi, legacyHits, hoveredShipSignature],
          },
        }),
      );
    }

    const normalizedLayers = sanitizeDeckLayerList(layers);

    const deckProps = {
      layers: normalizedLayers,
      getTooltip:
        projection === "globe"
          ? undefined
          : (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}` };
        }
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const obj: any = info.object;
        if (typeof obj.mmsi === "number") {
          return getShipTooltipHtml({ mmsi: obj.mmsi, targetByMmsi: shipByMmsi, legacyHits });
        }

        if (info.layer && info.layer.id === "pair-lines") {
          const aMmsi = toSafeNumber(obj.aMmsi) ?? toSafeNumber(obj.fromMmsi);
          const bMmsi = toSafeNumber(obj.bMmsi) ?? toSafeNumber(obj.toMmsi);
          if (aMmsi == null || bMmsi == null) return null;
          return getPairLinkTooltipHtml({
            warn: !!obj.warn,
            distanceNm: toSafeNumber(obj.distanceNm),
            aMmsi,
            bMmsi,
            legacyHits,
            targetByMmsi: shipByMmsi,
          });
        }

        if (info.layer && info.layer.id === "fc-lines") {
          const fcMmsi = toSafeNumber(obj.fcMmsi) ?? toSafeNumber(obj.fromMmsi);
          const otherMmsi = toSafeNumber(obj.otherMmsi) ?? toSafeNumber(obj.toMmsi);
          if (fcMmsi == null || otherMmsi == null) return null;
          return getFcLinkTooltipHtml({
            suspicious: !!obj.suspicious,
            distanceNm: toSafeNumber(obj.distanceNm),
            fcMmsi,
            otherMmsi,
            legacyHits,
            targetByMmsi: shipByMmsi,
          });
        }

        if (info.layer && info.layer.id === "pair-range") {
          const aMmsi = toSafeNumber(obj.aMmsi) ?? toSafeNumber(obj.fromMmsi);
          const bMmsi = toSafeNumber(obj.bMmsi) ?? toSafeNumber(obj.toMmsi);
          if (aMmsi == null || bMmsi == null) return null;
        return getRangeTooltipHtml({
          warn: !!obj.warn,
          distanceNm: toSafeNumber(obj.distanceNm),
          aMmsi,
          bMmsi,
          legacyHits,
        });
        }

        if (info.layer && info.layer.id === "fleet-circles") {
          return getFleetCircleTooltipHtml({
            ownerKey: String(obj.ownerKey ?? ""),
            ownerLabel: String(obj.ownerLabel ?? obj.ownerKey ?? ""),
            count: Number(obj.count ?? 0),
          });
        }

        const p = obj.properties as Record<string, unknown> | undefined;
        const label = getZoneDisplayNameFromProps(p);
        if (label) return { text: label };
        return null;
      },
      onClick:
        projection === "globe"
          ? undefined
          : (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;
          const sourceEvent = (info as { srcEvent?: { shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean } | null }).srcEvent;
          if (sourceEvent && hasAuxiliarySelectModifier(sourceEvent)) {
            onToggleHighlightMmsi?.(t.mmsi);
            return;
          }
          onSelectMmsi(t.mmsi);
          map.easeTo({ center: [t.lon, t.lat], zoom: Math.max(map.getZoom(), 10), duration: 600 });
        }
      },
    } as const;

    const safeDeckProps = { ...deckProps, layers: normalizedLayers };
    const fallbackDeckProps = { ...safeDeckProps, layers: [] as unknown[] };
    const applyDeckProps = () => {
      if (projection === "globe") {
        const target = globeDeckLayerRef.current;
        if (!target) return;
        try {
          target.setProps(safeDeckProps as never);
        } catch (e) {
          console.error("Failed to apply deck props on globe overlay. Falling back to empty deck layer set.", e);
          try {
            target.setProps(fallbackDeckProps as never);
          } catch {
            // Ignore secondary failure; rendering will recover on next update.
          }
        }
        return;
      }

      const target = overlayRef.current;
      if (!target) return;
      try {
        target.setProps(safeDeckProps as unknown as never);
      } catch (e) {
        console.error("Failed to apply deck props on mercator overlay. Falling back to empty deck layer set.", e);
        try {
          target.setProps(fallbackDeckProps as unknown as never);
        } catch {
          // Ignore secondary failure.
        }
      }
    };

    applyDeckProps();
  }, [
    projection,
    shipLayerData,
    legacyTargetsOrdered,
    baseMap,
    zones,
    selectedMmsi,
    overlays.zones,
    settings.showShips,
    settings.showDensity,
    onSelectMmsi,
    legacyHits,
    legacyTargets,
    overlays.pairLines,
    overlays.pairRange,
    overlays.fcLines,
    overlays.fleetCircles,
    pairLinks,
    pairRanges,
    fcDashed,
    fleetCircles,
    shipByMmsi,
    mapSyncEpoch,
    hoveredShipSignature,
    hoveredFleetSignature,
    hoveredPairSignature,
    hoveredFleetOwnerKey,
    highlightedMmsiSetCombined,
    onToggleHighlightMmsi,
    isHighlightedMmsi,
    isHighlightedFleet,
    isHighlightedPair,
    setDeckHoverMmsi,
    clearDeckHoverMmsi,
    setDeckHoverPairs,
    clearDeckHoverPairs,
    clearMapFleetHoverState,
    setMapFleetHoverState,
    touchDeckHoverState,
    ensureMercatorOverlay,
  ]);

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