"use client";

/**
 * PticaMapInner — the rich dark cockpit map (client-only; loaded via
 * dynamic(ssr:false) from PticaMap). Prototype parity (ptica-redesign/app.js):
 *
 *  - Two base layers, toggled by the «Спутник | Схема» segmented control: Esri
 *    World Imagery (default, `.baseSat` → darker --map-filter-sat) and CartoDB
 *    dark vector tiles. The parcel polygon reads clearly over both.
 *  - REAL data layers, reusing the existing analysis-map components so logic is
 *    not duplicated:
 *      · parcel polygon (cyan, geom_geojson)
 *      · POI markers from score_breakdown — glyph divIcon pins (prototype
 *        pinIcon + POI_GLYPH) bucketed Школы·Детсады / Парки·Метро, one glyph
 *        per OSM category (school / kindergarten / park / metro / tram / bus)
 *      · competitors / pipeline (ЖК + будущие проекты) — жилой-дом glyph pins
 *        in the market colors (viz-2 / viz-5)
 *      · ЗОУИТ охранные зоны (ZouitLayer)
 *      · connection points — resource glyph pins (RES_GLYPH: Электро/Вода/
 *        Канализация/Тепло/Газ/…) + dashed colored polylines from the centroid
 *      · custom POI (CustomPoiLayer + add/edit/delete via useCustomPois)
 *      · static compass (.mapCompass) bottom-right
 *  - The map-overlay legend rows TOGGLE each layer; the «Точки подключения
 *    ресурсов» list is driven by real connection-points data (nearest distance
 *    per resource category).
 *  - Isochrones: the only real source (useIsochrones) is an on-demand ORS
 *    mutation, not part of /analyze — per the task we DO NOT fake it; the legend
 *    row stays «скоро».
 *  - The «3D-масса застройки» legend row + map tool open the future3d drawer.
 */

import { useEffect, useMemo, useRef, useState } from "react";
import {
  MapContainer,
  TileLayer,
  GeoJSON,
  Marker,
  Polyline,
  Tooltip,
  AttributionControl,
  ZoomControl,
  ScaleControl,
  useMap,
  useMapEvents,
} from "react-leaflet";
import L from "leaflet";
import type { Geometry, Position } from "geojson";
import "leaflet/dist/leaflet.css";

import styles from "@/app/site-finder/analysis/[cad]/ptica/ptica.module.css";
import { EKB_CENTER } from "@/components/site-finder/ptica/ptica-adapt";
import type {
  ParcelAnalysis,
  ParcelAnalysisCompetitor,
  PipelineObject,
} from "@/types/site-finder";
import type { ConnectionPointsResponse } from "@/types/nspd";
import type { CustomPoi } from "@/types/customPoi";
import { MARKET_COLORS } from "@/components/site-finder/MarketLayers";
import {
  CP_CATEGORY_STYLES,
  CP_ALL_CATEGORIES,
  classifyStructure,
  type CpCategory,
} from "@/components/site-finder/ConnectionPointsLayer";
import {
  ZouitLayer,
  ZOUIT_ALL_SEVERITIES,
  groupZouitBySeverity,
  type ZouitSeverity,
} from "@/components/site-finder/ZouitLayer";
import { CustomPoiLayer } from "@/components/site-finder/CustomPoiLayer";
import { CustomPoiAddModal } from "@/components/site-finder/CustomPoiAddModal";
import { CustomPoiEditModal } from "@/components/site-finder/CustomPoiEditModal";
import {
  useAddCustomPoi,
  useUpdateCustomPoi,
  useDeleteCustomPoi,
} from "@/hooks/useCustomPois";
import type { DrawerKey } from "@/components/site-finder/ptica/drawers/drawer-keys";

// ── divIcon glyph pins (prototype app.js pinIcon / POI_GLYPH / RES_GLYPH) ───────
// Inline SVG glyphs reused from the prototype's schematic markers. The glyph
// stroke/fill is dark (#08131c) so it reads on the bright colored disc. The
// `.map-pin` / `.map-pin.poi-dot` CSS is already ported into ptica.module.css.

// POI glyphs — one per OSM category bucket present in score_breakdown.
const POI_GLYPH: Record<string, string> = {
  // школа — академическая шапочка
  school:
    '<svg viewBox="0 0 24 24" fill="none"><path d="M3 9l9-4 9 4-9 4-9-4z" stroke="#08131c" stroke-width="2" stroke-linejoin="round"/><path d="M7 11v4c0 1.5 2.5 3 5 3s5-1.5 5-3v-4" stroke="#08131c" stroke-width="2"/></svg>',
  // детский сад — домик с сердечком (мягче школьной шапочки)
  kindergarten:
    '<svg viewBox="0 0 24 24" fill="none"><path d="M5 21V10l7-5 7 5v11" stroke="#08131c" stroke-width="2" stroke-linejoin="round"/><path d="M12 18c-1.6-1.4-3-2.4-3-3.8a1.5 1.5 0 013-0.4 1.5 1.5 0 013 0.4c0 1.4-1.4 2.4-3 3.8z" fill="#08131c"/></svg>',
  // парк — дерево
  park: '<svg viewBox="0 0 24 24" fill="none"><path d="M12 3l5 8h-3l3 5H7l3-5H7l5-8zM12 16v5" stroke="#08131c" stroke-width="2" stroke-linejoin="round"/></svg>',
  // метро — арочный вход в метро
  metro_stop:
    '<svg viewBox="0 0 24 24" fill="none"><path d="M4 18L12 6l8 12M4 18h16M9 18v-4l3-4 3 4v4" stroke="#08131c" stroke-width="2" stroke-linejoin="round"/></svg>',
  // трамвай — вагон на рельсах
  tram_stop:
    '<svg viewBox="0 0 24 24" fill="none"><rect x="6" y="4" width="12" height="13" rx="2" stroke="#08131c" stroke-width="1.8"/><path d="M6 10h12M9 21l-1.5-3M15 21l1.5-3" stroke="#08131c" stroke-width="1.8" stroke-linecap="round"/></svg>',
  // остановка автобуса — корпус автобуса
  bus_stop:
    '<svg viewBox="0 0 24 24" fill="none"><rect x="5" y="5" width="14" height="11" rx="2" stroke="#08131c" stroke-width="1.8"/><path d="M5 11h14M8 20l1-4M16 20l-1-4" stroke="#08131c" stroke-width="1.8" stroke-linecap="round"/></svg>',
};

// ЖК / pipeline (competitor + future project) — жилой дом glyph.
const ZHK_GLYPH =
  '<svg viewBox="0 0 24 24" fill="none"><path d="M5 21V8l7-4 7 4v13M9 21v-5h6v5" stroke="#08131c" stroke-width="2" stroke-linejoin="round"/></svg>';

// Connection-point resource glyphs — keyed by CpCategory.
const RES_GLYPH: Record<CpCategory, string> = {
  electricity:
    '<svg viewBox="0 0 24 24" fill="none"><path d="M13 3l-7 10h5l-1 8 7-11h-5l1-7z" fill="#08131c"/></svg>',
  water:
    '<svg viewBox="0 0 24 24" fill="none"><path d="M12 4c4 5 6 8 6 11a6 6 0 11-12 0c0-3 2-6 6-11z" fill="#08131c"/></svg>',
  sewage:
    '<svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="7" stroke="#08131c" stroke-width="2"/><path d="M9 12h6" stroke="#08131c" stroke-width="2"/></svg>',
  heat: '<svg viewBox="0 0 24 24" fill="none"><path d="M12 3c2 3-1 4 0 7 1 2 4 3 4 6a6 6 0 11-9-5" stroke="#08131c" stroke-width="2" stroke-linecap="round"/></svg>',
  gas: '<svg viewBox="0 0 24 24" fill="none"><path d="M9 3v4M15 3v4M6 7h12v9a6 6 0 11-12 0V7z" stroke="#08131c" stroke-width="1.8" stroke-linejoin="round"/></svg>',
  telecom:
    '<svg viewBox="0 0 24 24" fill="none"><path d="M12 18v-7M7 9a7 7 0 0110 0M9.5 11.5a3.5 3.5 0 015 0" stroke="#08131c" stroke-width="1.8" stroke-linecap="round"/><circle cx="12" cy="19" r="1.4" fill="#08131c"/></svg>',
  other:
    '<svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="4" fill="#08131c"/></svg>',
};

// Build a Leaflet divIcon pin (colored disc + dark glyph). `poiDot` renders the
// smaller flat POI variant (prototype `.map-pin.poi-dot`).
function pinIcon(color: string, glyph: string, poiDot = false): L.DivIcon {
  const size = poiDot ? 14 : 22;
  return L.divIcon({
    className: "",
    iconSize: [size, size],
    iconAnchor: [size / 2, size / 2],
    html: `<div class="map-pin${poiDot ? " poi-dot" : ""}" style="background:${color};color:${color}">${glyph}</div>`,
  });
}

// ── POI legend buckets (prototype: ЖК / Школы·Детсады / Парки·Метро) ───────────

type PoiBucket = "edu" | "greenmetro";

interface PoiBucketStyle {
  color: string;
  label: string;
}

// token-hex (map exception per ui-tokens.md) — match prototype poiColor()
const POI_BUCKET_STYLES: Record<PoiBucket, PoiBucketStyle> = {
  edu: { color: "#3fa77d", label: "Школы · Дет.сады" }, // --res-poi
  greenmetro: { color: "#6aa15f", label: "Парки · Метро" },
};

const EDU_CATEGORIES = new Set(["school", "kindergarten"]);
const GREENMETRO_CATEGORIES = new Set([
  "park",
  "metro_stop",
  "tram_stop",
  "bus_stop",
]);

function poiBucket(category: string): PoiBucket | null {
  if (EDU_CATEGORIES.has(category)) return "edu";
  if (GREENMETRO_CATEGORIES.has(category)) return "greenmetro";
  return null;
}

// Fallback POI glyph (small filled dot) for any category without a dedicated
// glyph — keeps the pin readable instead of an empty disc.
const POI_GLYPH_FALLBACK =
  '<svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="4" fill="#08131c"/></svg>';

// ── Narrowing: unknown → GeoJSON Geometry ─────────────────────────────────────

const GEOMETRY_TYPES = new Set([
  "Point",
  "MultiPoint",
  "LineString",
  "MultiLineString",
  "Polygon",
  "MultiPolygon",
  "GeometryCollection",
]);

function asGeometry(value: unknown): Geometry | null {
  if (value == null || typeof value !== "object") return null;
  if (!("type" in value)) return null;
  const type = (value as { type: unknown }).type;
  if (typeof type !== "string" || !GEOMETRY_TYPES.has(type)) return null;
  return value as Geometry;
}

// ── Centroid (bounding-box center — sufficient for connection-line origin) ─────

function flattenCoords(geom: Geometry): Position[] {
  switch (geom.type) {
    case "Point":
      return [geom.coordinates];
    case "MultiPoint":
    case "LineString":
      return geom.coordinates;
    case "MultiLineString":
    case "Polygon":
      return geom.coordinates.flat();
    case "MultiPolygon":
      return geom.coordinates.flat(2);
    case "GeometryCollection":
      return geom.geometries.flatMap(flattenCoords);
    default:
      return [];
  }
}

function geomCenter(geom: Geometry): [number, number] {
  const coords = flattenCoords(geom);
  if (!coords.length) return EKB_CENTER;
  let minLat = Infinity;
  let maxLat = -Infinity;
  let minLon = Infinity;
  let maxLon = -Infinity;
  for (const [lon, lat] of coords) {
    if (lat < minLat) minLat = lat;
    if (lat > maxLat) maxLat = lat;
    if (lon < minLon) minLon = lon;
    if (lon > maxLon) maxLon = lon;
  }
  return [(minLat + maxLat) / 2, (minLon + maxLon) / 2];
}

// Pull [lat, lon] out of an EngineeringStructure point geometry (GeoJSON
// [lon, lat]). Non-points / missing coords → null (skip in lines).
function structureLatLon(
  geom: Record<string, unknown>,
): [number, number] | null {
  if (geom.type !== "Point") return null;
  const coords = geom.coordinates as number[] | undefined;
  if (!coords || coords.length < 2) return null;
  return [coords[1], coords[0]];
}

function formatMeters(m: number): string {
  if (m >= 1000) return `${(m / 1000).toFixed(2)} км`;
  return `${Math.round(m)} м`;
}

// ── Layer keys ────────────────────────────────────────────────────────────────

type LayerKey =
  | "parcel"
  | "competitors" // ЖК
  | "pipeline" // будущие проекты
  | "edu"
  | "greenmetro"
  | "connections"
  | "zouit"
  | "custompoi";

// ── Map controllers (must live inside MapContainer) ───────────────────────────

function FitToGeometry({ geom }: { geom: Geometry | null }) {
  const map = useMap();
  useEffect(() => {
    if (!geom) return;
    const coords = flattenCoords(geom);
    if (coords.length < 2) return;
    let minLat = Infinity;
    let maxLat = -Infinity;
    let minLon = Infinity;
    let maxLon = -Infinity;
    for (const [lon, lat] of coords) {
      if (lat < minLat) minLat = lat;
      if (lat > maxLat) maxLat = lat;
      if (lon < minLon) minLon = lon;
      if (lon > maxLon) maxLon = lon;
    }
    map.fitBounds(
      [
        [minLat, minLon],
        [maxLat, maxLon],
      ],
      { padding: [48, 48], maxZoom: 16, animate: false },
    );
    map.setZoom(Math.max(map.getZoom() - 1, 13), { animate: false });
  }, [map, geom]);
  return null;
}

function MapClickHandler({
  editMode,
  onMapClick,
}: {
  editMode: boolean;
  onMapClick: (lat: number, lon: number) => void;
}) {
  useMapEvents({
    click(e) {
      if (editMode) onMapClick(e.latlng.lat, e.latlng.lng);
    },
  });
  return null;
}

// ── Props ─────────────────────────────────────────────────────────────────────

interface Props {
  analysis: ParcelAnalysis;
  cad: string;
  connectionPoints?: ConnectionPointsResponse;
  customPois?: CustomPoi[];
  onOpenDrawer?: (key: DrawerKey) => void;
}

export default function PticaMapInner({
  analysis,
  cad,
  connectionPoints,
  customPois,
  onOpenDrawer,
}: Props) {
  // Fix Leaflet default icon paths broken by webpack bundler (mirror SiteMap).
  useEffect(() => {
    void import("leaflet").then((L) => {
      // @ts-expect-error — _getIconUrl is internal Leaflet property
      delete L.Icon.Default.prototype._getIconUrl;
      L.Icon.Default.mergeOptions({
        iconRetinaUrl:
          "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png",
        iconUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png",
        shadowUrl:
          "https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png",
      });
    });
  }, []);

  const geom = useMemo(
    () => asGeometry(analysis.geom_geojson),
    [analysis.geom_geojson],
  );
  const center = geom ? geomCenter(geom) : EKB_CENTER;

  // ── Base layer (satellite default — matches prototype + header) ──────────────
  const [base, setBase] = useState<"sat" | "vec">("sat");

  // ── Layer visibility (parcel always on; isochrones is «скоро», not wired) ────
  const [visible, setVisible] = useState<Set<LayerKey>>(
    () =>
      new Set<LayerKey>([
        "parcel",
        "competitors",
        "pipeline",
        "edu",
        "greenmetro",
        "connections",
        "zouit",
        "custompoi",
      ]),
  );
  function toggleLayer(key: LayerKey) {
    setVisible((prev) => {
      const next = new Set(prev);
      if (next.has(key)) next.delete(key);
      else next.add(key);
      return next;
    });
  }

  // ── Custom POI add/edit state + mutations ────────────────────────────────────
  const [addMode, setAddMode] = useState(false);
  const [pendingCoords, setPendingCoords] = useState<{
    lat: number;
    lon: number;
  } | null>(null);
  const [editingPoi, setEditingPoi] = useState<CustomPoi | null>(null);

  const addMutation = useAddCustomPoi(cad);
  const updateMutation = useUpdateCustomPoi(cad);
  const deleteMutation = useDeleteCustomPoi(cad);

  // ESC cancels add mode
  const addModeRef = useRef(addMode);
  addModeRef.current = addMode;
  useEffect(() => {
    function onKeyDown(e: KeyboardEvent) {
      if (e.key === "Escape" && addModeRef.current) {
        setAddMode(false);
        setPendingCoords(null);
      }
    }
    window.addEventListener("keydown", onKeyDown);
    return () => window.removeEventListener("keydown", onKeyDown);
  }, []);

  function handleMapClick(lat: number, lon: number) {
    setPendingCoords({ lat, lon });
    setAddMode(false);
  }

  // ── POI markers from score_breakdown, bucketed (category kept for glyph) ──────
  const poiByBucket = useMemo(() => {
    const buckets: Record<
      PoiBucket,
      {
        lat: number;
        lon: number;
        name: string | null;
        distance_m: number;
        category: string;
      }[]
    > = {
      edu: [],
      greenmetro: [],
    };
    for (const [cat, pois] of Object.entries(analysis.score_breakdown)) {
      const bucket = poiBucket(cat);
      if (!bucket) continue;
      for (const p of pois) {
        if (p.lat === 0 && p.lon === 0) continue;
        buckets[bucket].push({
          lat: p.lat,
          lon: p.lon,
          name: p.name,
          distance_m: p.distance_m,
          category: cat,
        });
      }
    }
    return buckets;
  }, [analysis.score_breakdown]);

  // ── Market data (competitors = ЖК, pipeline = будущие проекты) ───────────────
  // Filtered to objects with usable coords (drop (0,0) Атлантика sentinels) so
  // the glyph pins below render only real points; counts drive the legend rows.
  const competitorList = useMemo<
    (ParcelAnalysisCompetitor & { lat: number; lon: number })[]
  >(
    () =>
      (analysis.competitors ?? []).filter(
        (c): c is ParcelAnalysisCompetitor & { lat: number; lon: number } =>
          typeof c.lat === "number" &&
          typeof c.lon === "number" &&
          Number.isFinite(c.lat) &&
          Number.isFinite(c.lon) &&
          !(c.lat === 0 && c.lon === 0),
      ),
    [analysis.competitors],
  );
  const pipelineList = useMemo<
    (PipelineObject & { lat: number; lon: number })[]
  >(
    () =>
      (analysis.pipeline_24mo?.top_objects ?? []).filter(
        (p): p is PipelineObject & { lat: number; lon: number } =>
          typeof p.lat === "number" &&
          typeof p.lon === "number" &&
          Number.isFinite(p.lat) &&
          Number.isFinite(p.lon) &&
          !(p.lat === 0 && p.lon === 0),
      ),
    [analysis.pipeline_24mo],
  );
  const competitorCount = competitorList.length;
  const pipelineCount = pipelineList.length;

  // ── Connection points → glyph-pin markers (prototype pinIcon + RES_GLYPH) ─────
  const cpMarkers = useMemo(() => {
    if (!connectionPoints) return [];
    const out: {
      latLon: [number, number];
      category: CpCategory;
      name: string | null;
      distance_m: number;
    }[] = [];
    for (const s of connectionPoints.engineering_structures) {
      const latLon = structureLatLon(s.geometry_geojson);
      if (!latLon) continue;
      out.push({
        latLon,
        category: classifyStructure(s),
        name: s.name ?? s.type ?? null,
        distance_m: s.distance_to_boundary_m,
      });
    }
    return out;
  }, [connectionPoints]);
  const cpCount = connectionPoints?.engineering_structures.length ?? 0;

  // Nearest distance per category — drives the «Точки подключения» list.
  const cpNearest = useMemo(() => {
    const nearest = new Map<CpCategory, number>();
    if (!connectionPoints) return nearest;
    for (const s of connectionPoints.engineering_structures) {
      const cat = classifyStructure(s);
      const cur = nearest.get(cat);
      if (cur === undefined || s.distance_to_boundary_m < cur) {
        nearest.set(cat, s.distance_to_boundary_m);
      }
    }
    return nearest;
  }, [connectionPoints]);

  // Connection polylines from the parcel centroid to each structure point.
  const connectionLines = useMemo(() => {
    if (!connectionPoints || !geom) return [];
    const origin = geomCenter(geom);
    const lines: {
      from: [number, number];
      to: [number, number];
      color: string;
    }[] = [];
    for (const s of connectionPoints.engineering_structures) {
      const latLon = structureLatLon(s.geometry_geojson);
      if (!latLon) continue;
      const cat = classifyStructure(s);
      lines.push({
        from: origin,
        to: latLon,
        color: CP_CATEGORY_STYLES[cat].color,
      });
    }
    return lines;
  }, [connectionPoints, geom]);

  // ── ЗОУИТ overlaps grouped by severity ───────────────────────────────────────
  const zouitGrouped = useMemo(
    () => groupZouitBySeverity(analysis.nspd_zouit_overlaps ?? []),
    [analysis.nspd_zouit_overlaps],
  );
  const zouitCount = ZOUIT_ALL_SEVERITIES.reduce(
    (sum, sev) => sum + (zouitGrouped.get(sev)?.length ?? 0),
    0,
  );
  const allZouitVisible: Set<ZouitSeverity> = useMemo(
    () => new Set(ZOUIT_ALL_SEVERITIES),
    [],
  );

  const customPoiList = customPois ?? [];

  return (
    <div
      className={`${styles.mapMount} ${base === "sat" ? styles.baseSat : ""}`}
    >
      <MapContainer
        center={center}
        zoom={geom ? 15 : 11}
        zoomControl={false}
        attributionControl={false}
        scrollWheelZoom
        style={{ cursor: addMode ? "crosshair" : "grab" }}
      >
        {/* Drop the Leaflet 1.9 flag/«Leaflet» prefix but KEEP the per-tile data
            attribution (© Esri · Maxar / CARTO · OSM) required for licensing. */}
        <AttributionControl prefix={false} position="bottomright" />
        {/* Dark-styled zoom +/- (single instance — MapContainer zoomControl is
            false so this is the only one) and a metric scale bar. */}
        <ZoomControl position="topright" />
        <ScaleControl position="bottomleft" imperial={false} />
        <FitToGeometry geom={geom} />
        <MapClickHandler editMode={addMode} onMapClick={handleMapClick} />

        {/* Base layers — only one mounted at a time (key forces tile swap). */}
        {base === "sat" ? (
          <TileLayer
            key="sat"
            attribution="&copy; Esri · Maxar"
            url="https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"
            maxZoom={19}
          />
        ) : (
          <TileLayer
            key="vec"
            attribution='&copy; <a href="https://carto.com/attributions">CARTO</a> · &copy; <a href="https://www.openstreetmap.org/copyright">OSM</a>'
            url="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png"
            maxZoom={19}
          />
        )}

        {/* ЗОУИТ охранные зоны — background fill (under markers). */}
        {visible.has("zouit") && zouitCount > 0 && (
          <ZouitLayer
            grouped={zouitGrouped}
            visibleSeverities={allZouitVisible}
          />
        )}

        {/* Competitors (ЖК) — glyph pins (prototype жилой-дом glyph, viz-2). */}
        {visible.has("competitors") &&
          competitorList.map((c, idx) => (
            <Marker
              key={`comp-${c.obj_id}-${idx}`}
              position={[c.lat, c.lon]}
              icon={pinIcon(MARKET_COLORS.competitor, ZHK_GLYPH)}
            >
              <Tooltip direction="top">
                {c.comm_name ?? "ЖК"}
                {typeof c.distance_m === "number"
                  ? ` · ${formatMeters(c.distance_m)}`
                  : ""}
              </Tooltip>
            </Marker>
          ))}

        {/* Будущие проекты (pipeline) — glyph pins (жилой-дом glyph, viz-5). */}
        {visible.has("pipeline") &&
          pipelineList.map((p, idx) => (
            <Marker
              key={`pipe-${p.obj_id}-${idx}`}
              position={[p.lat, p.lon]}
              icon={pinIcon(MARKET_COLORS.pipeline, ZHK_GLYPH)}
            >
              <Tooltip direction="top">
                {p.comm_name ?? "Будущий проект"}
                {typeof p.distance_m === "number"
                  ? ` · ${formatMeters(p.distance_m)}`
                  : ""}
              </Tooltip>
            </Marker>
          ))}

        {/* POI markers (Школы·Детсады / Парки·Метро) — glyph divIcon pins. */}
        {(["edu", "greenmetro"] as PoiBucket[]).flatMap((bucket) => {
          if (!visible.has(bucket)) return [];
          const style = POI_BUCKET_STYLES[bucket];
          return poiByBucket[bucket].map((poi, idx) => (
            <Marker
              key={`${bucket}-${idx}`}
              position={[poi.lat, poi.lon]}
              icon={pinIcon(
                style.color,
                POI_GLYPH[poi.category] ?? POI_GLYPH_FALLBACK,
                true,
              )}
            >
              <Tooltip direction="top">
                {poi.name ?? style.label} · {formatMeters(poi.distance_m)}
              </Tooltip>
            </Marker>
          ));
        })}

        {/* Connection points: dashed polylines from the centroid + glyph pins
            per resource (prototype RES_GLYPH disc + conn-glow lines). */}
        {visible.has("connections") && connectionPoints && (
          <>
            {connectionLines.map((line, idx) => (
              <Polyline
                key={`cp-line-${idx}`}
                positions={[line.from, line.to]}
                pathOptions={{
                  color: line.color,
                  weight: 2.2,
                  opacity: 0.85,
                  dashArray: "4 6",
                  // global ported class (`.mapMount :global(.conn-glow)` adds a
                  // currentColor drop-shadow to the dashed connection lines).
                  className: "conn-glow",
                }}
              />
            ))}
            {cpMarkers.map((m, idx) => {
              const cpStyle = CP_CATEGORY_STYLES[m.category];
              return (
                <Marker
                  key={`cp-${m.category}-${idx}`}
                  position={m.latLon}
                  icon={pinIcon(cpStyle.color, RES_GLYPH[m.category])}
                >
                  <Tooltip direction="top">
                    {m.name ?? cpStyle.label} · {formatMeters(m.distance_m)}
                  </Tooltip>
                </Marker>
              );
            })}
          </>
        )}

        {/* Parcel polygon — cyan, on top of fills so it always reads. */}
        {visible.has("parcel") && geom && (
          <GeoJSON
            key={analysis.cad_num}
            data={geom}
            style={{
              // = --accent-cyan-strong (Leaflet styles can't read CSS vars)
              color: "#8ed3ff",
              weight: 2.5,
              fillColor: "#8ed3ff",
              fillOpacity: 0.15,
            }}
          />
        )}

        {/* Custom POI — topmost; add/edit/delete wired to useCustomPois. */}
        {visible.has("custompoi") && customPoiList.length > 0 && (
          <CustomPoiLayer
            pois={customPoiList}
            onEdit={(poi) => setEditingPoi(poi)}
            onDelete={(id) => deleteMutation.mutate(id)}
          />
        )}
      </MapContainer>

      {/* ── Legend / layer-toggle overlay ────────────────────────────────────── */}
      <div className={styles.mapOverlay}>
        <h4>
          <span className={styles.live} />
          {base === "sat" ? "Спутник · свежий снимок" : "Схема · тёмная карта"}
        </h4>

        <div className={styles.baseToggle}>
          <button
            type="button"
            className={base === "sat" ? styles.baseToggleActive : ""}
            onClick={() => setBase("sat")}
          >
            Спутник
          </button>
          <button
            type="button"
            className={base === "vec" ? styles.baseToggleActive : ""}
            onClick={() => setBase("vec")}
          >
            Схема
          </button>
        </div>

        <div className={styles.layerList}>
          <LegendRow
            color="#8ed3ff"
            label="Участок"
            count={geom ? 1 : 0}
            on={visible.has("parcel")}
            onToggle={() => toggleLayer("parcel")}
          />
          <LegendRow
            color={MARKET_COLORS.competitor}
            label="ЖК"
            count={competitorCount}
            on={visible.has("competitors")}
            onToggle={() => toggleLayer("competitors")}
          />
          <LegendRow
            color={MARKET_COLORS.pipeline}
            label="Будущие проекты"
            count={pipelineCount}
            on={visible.has("pipeline")}
            onToggle={() => toggleLayer("pipeline")}
          />
          <LegendRow
            color={POI_BUCKET_STYLES.edu.color}
            label="Школы · Дет.сады"
            count={poiByBucket.edu.length}
            on={visible.has("edu")}
            onToggle={() => toggleLayer("edu")}
          />
          <LegendRow
            color={POI_BUCKET_STYLES.greenmetro.color}
            label="Парки · Метро"
            count={poiByBucket.greenmetro.length}
            on={visible.has("greenmetro")}
            onToggle={() => toggleLayer("greenmetro")}
          />
          {zouitCount > 0 && (
            <LegendRow
              color="#d2655b"
              label="ЗОУИТ"
              count={zouitCount}
              on={visible.has("zouit")}
              onToggle={() => toggleLayer("zouit")}
            />
          )}
          <LegendRow
            color="#7fd0ee"
            label="Точки подключения"
            count={cpCount}
            on={visible.has("connections")}
            onToggle={() => toggleLayer("connections")}
          />
          {/* Изохроны — реальный источник (ORS) пока вне /analyze: «скоро». */}
          <div className={`${styles.layerRow} ${styles.layerRowSoon}`}>
            <span className={styles.swatch} />
            <span>Изохроны доступности</span>
            <span className={styles.cnt}>скоро</span>
          </div>
          <LegendRow
            color="#e0a93b"
            label="Кастомные POI"
            count={customPoiList.length}
            on={visible.has("custompoi")}
            onToggle={() => toggleLayer("custompoi")}
          />
          {onOpenDrawer && (
            <button
              type="button"
              className={`${styles.layerRow} ${styles.layerRow3d}`}
              onClick={() => onOpenDrawer("future3d")}
              title="Открыть 3D-массу застройки"
            >
              <span className={`${styles.swatch} ${styles.swatch3d}`} />
              <span>3D-масса застройки</span>
              <span className={styles.openTag}>3D</span>
            </button>
          )}
        </div>

        {cpCount > 0 && (
          <>
            <hr className={styles.overlayDivider} />
            <div className={styles.overlaySub}>Точки подключения ресурсов</div>
            {CP_ALL_CATEGORIES.filter((c) => cpNearest.has(c)).map((catKey) => {
              const dist = cpNearest.get(catKey);
              if (dist === undefined) return null;
              const style = CP_CATEGORY_STYLES[catKey];
              return (
                <div key={catKey} className={styles.resRow}>
                  <span
                    className={styles.dot}
                    style={{ background: style.color }}
                  />
                  <span>{style.label}</span>
                  <b>{formatMeters(dist)}</b>
                </div>
              );
            })}
          </>
        )}
      </div>

      {/* ── Map tools (add-POI · 3D) ─────────────────────────────────────────── */}
      <div className={styles.mapTools}>
        <button
          type="button"
          className={`${styles.tool} ${addMode ? styles.toolActive : ""}`}
          title={addMode ? "Кликните на карте" : "Добавить POI"}
          aria-label="Добавить пользовательскую точку"
          onClick={() => {
            setAddMode((v) => !v);
            setPendingCoords(null);
          }}
        >
          <svg viewBox="0 0 24 24" fill="none">
            <path
              d="M12 21s7-6.2 7-11a7 7 0 10-14 0c0 4.8 7 11 7 11z"
              stroke="currentColor"
              strokeWidth="1.6"
              strokeLinejoin="round"
            />
            <path
              d="M12 7v6M9 10h6"
              stroke="currentColor"
              strokeWidth="1.6"
              strokeLinecap="round"
            />
          </svg>
        </button>
        {onOpenDrawer && (
          <button
            type="button"
            className={styles.tool}
            title="3D-масса застройки"
            aria-label="Открыть 3D-массу застройки"
            onClick={() => onOpenDrawer("future3d")}
          >
            <svg viewBox="0 0 24 24" fill="none">
              <path
                d="M12 3l8 4.5v9L12 21l-8-4.5v-9L12 3z"
                stroke="currentColor"
                strokeWidth="1.4"
                strokeLinejoin="round"
              />
            </svg>
          </button>
        )}
      </div>

      {/* Static compass (prototype .map-compass) — bottom-right, above the
          Leaflet panes; non-interactive so the map stays pannable beneath it. */}
      <svg
        className={styles.mapCompass}
        viewBox="0 0 46 46"
        fill="none"
        aria-hidden="true"
      >
        <circle
          cx="23"
          cy="23"
          r="20"
          stroke="currentColor"
          strokeWidth="1"
          opacity="0.5"
        />
        <path d="M23 7l4 16-4-3-4 3 4-16z" fill="var(--accent-cyan)" />
        <text
          x="23"
          y="42"
          textAnchor="middle"
          fontSize="8"
          fill="currentColor"
          fontFamily="monospace"
        >
          N
        </text>
      </svg>

      {addMode && (
        <div className={styles.mapHint}>Кликните на карте для точки</div>
      )}

      {/* Add / edit modals (reuse the analysis-map custom-POI flow). */}
      {pendingCoords && (
        <CustomPoiAddModal
          lat={pendingCoords.lat}
          lon={pendingCoords.lon}
          parcelCad={cad}
          isLoading={addMutation.isPending}
          onSubmit={(payload) => {
            addMutation.mutate(payload, {
              onSuccess: () => setPendingCoords(null),
            });
          }}
          onCancel={() => setPendingCoords(null)}
        />
      )}
      {editingPoi && (
        <CustomPoiEditModal
          poi={editingPoi}
          isLoading={updateMutation.isPending}
          onSubmit={(updateData) => {
            updateMutation.mutate(
              { id: editingPoi.id, data: updateData },
              { onSuccess: () => setEditingPoi(null) },
            );
          }}
          onCancel={() => setEditingPoi(null)}
        />
      )}
    </div>
  );
}

// ── Legend row (toggle button) ─────────────────────────────────────────────────

function LegendRow({
  color,
  label,
  count,
  on,
  onToggle,
}: {
  color: string;
  label: string;
  count: number;
  on: boolean;
  onToggle: () => void;
}) {
  return (
    <button
      type="button"
      className={`${styles.layerRow} ${on ? "" : styles.layerRowOff}`}
      onClick={onToggle}
      aria-pressed={on}
    >
      <span className={styles.swatch} style={{ background: color }} />
      <span>{label}</span>
      <span className={styles.cnt}>{count}</span>
    </button>
  );
}
