"use client";

import { useEffect, useRef, useState } from "react";
import {
  MapContainer,
  TileLayer,
  GeoJSON,
  CircleMarker,
  Popup,
  useMapEvents,
} from "react-leaflet";
import type { Feature, FeatureCollection, Geometry, Position } from "geojson";
import "leaflet/dist/leaflet.css";

import type {
  ParcelAnalysis,
  ParcelAnalysisCompetitor,
  PipelineObject,
} from "@/types/site-finder";
import type {
  ConnectionPointsResponse,
  EngineeringStructure,
  OpportunityParcel,
  RedLine,
  RiskZone,
} from "@/types/nspd";
import type { UtilityInfrastructureResponse } from "@/hooks/useUtilityInfrastructure";
import type { CustomPoi } from "@/types/customPoi";
import {
  MarketLayers,
  MARKET_COLORS,
} from "@/components/site-finder/MarketLayers";
import {
  ConnectionPointsLayer,
  CP_ALL_CATEGORIES,
  groupStructuresByCategory,
  type CpCategory,
} from "@/components/site-finder/ConnectionPointsLayer";
import { CpLayerControlPanel } from "@/components/site-finder/CpLayerControlPanel";
import {
  ZouitLayer,
  ZOUIT_ALL_SEVERITIES,
  groupZouitBySeverity,
  type ZouitSeverity,
} from "@/components/site-finder/ZouitLayer";
import { ZouitLayerControlPanel } from "@/components/site-finder/ZouitLayerControlPanel";
import {
  UtilityInfrastructureLayer,
  UTILITY_ALL_KINDS,
  groupUtilityByKind,
  type UtilityKind,
  type UtilityMappable,
} from "@/components/site-finder/UtilityInfrastructureLayer";
import { UtilityLayerControlPanel } from "@/components/site-finder/UtilityLayerControlPanel";
import {
  PowerConnectionPointsLayer,
  countMappablePowerPoints,
} from "@/components/site-finder/PowerConnectionPointsLayer";
import {
  GasOutletPointsLayer,
  countMappableGasOutlets,
} from "@/components/site-finder/GasOutletPointsLayer";
import {
  GAS_OUTLET_MARKER_COLOR,
  LOAD_INDEX_MARKER_COLOR,
  LOAD_INDEX_NULL_COLOR,
  pluralizeOutletPoints,
} from "@/components/site-finder/connection-capacity";
import type {
  GasOutletPoint,
  PowerConnectionPoint,
} from "@/hooks/useConnectionCapacity";
import { CustomPoiLayer } from "@/components/site-finder/CustomPoiLayer";
import { CustomPoiToggleButton } from "@/components/site-finder/CustomPoiToggleButton";
import { CustomPoiAddModal } from "@/components/site-finder/CustomPoiAddModal";
import { CustomPoiEditModal } from "@/components/site-finder/CustomPoiEditModal";
import {
  useAddCustomPoi,
  useUpdateCustomPoi,
  useDeleteCustomPoi,
} from "@/hooks/useCustomPois";

// ---------------------------------------------------------------------------
// POI legend config (for the legend row below the map)
// ---------------------------------------------------------------------------

interface CategoryStyle {
  color: string;
  label: string;
  radius: number;
}

export const CATEGORY_STYLES: Record<string, CategoryStyle> = {
  school: { color: "#1d4ed8", label: "Школа", radius: 8 },
  kindergarten: { color: "#1d4ed8", label: "Детский сад", radius: 8 },
  pharmacy: { color: "#10b981", label: "Аптека", radius: 6 },
  hospital: { color: "#f59e0b", label: "Больница", radius: 8 },
  shop_mall: { color: "#a855f7", label: "ТЦ", radius: 8 },
  shop_supermarket: { color: "#a855f7", label: "Супермаркет", radius: 6 },
  shop_small: { color: "#c084fc", label: "Магазин", radius: 6 },
  park: { color: "#16a34a", label: "Парк", radius: 6 },
  tram_stop: { color: "#dc2626", label: "Трамвай", radius: 6 },
  bus_stop: { color: "#6b7280", label: "Автобус", radius: 6 },
  metro_stop: { color: "#1e40af", label: "Метро", radius: 8 },
};

// Allowlist http(s) перед записью в href (XSS-защита, frontend.md). Возвращает
// null если схема не http/https — тогда показываем сайт как текст, не ссылку.
function safeHttpUrl(url: string | null | undefined): string | null {
  if (!url) return null;
  const t = url.trim();
  return t.startsWith("https://") || t.startsWith("http://") ? t : null;
}

// ---------------------------------------------------------------------------
// Geometry centroid (bounding-box center — sufficient for zoom-to fit)
// ---------------------------------------------------------------------------

function flattenCoords(geom: Geometry): Position[] {
  switch (geom.type) {
    case "Point":
      return [geom.coordinates];
    case "MultiPoint":
    case "LineString":
      return geom.coordinates as Position[];
    case "MultiLineString":
    case "Polygon":
      return (geom.coordinates as Position[][]).flat();
    case "MultiPolygon":
      return (geom.coordinates as Position[][][]).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 [56.838, 60.6];
  let minLat = Infinity,
    maxLat = -Infinity,
    minLon = Infinity,
    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];
}

// ---------------------------------------------------------------------------
// Props + Component
// ---------------------------------------------------------------------------

interface Props {
  data: ParcelAnalysis;
  isochrones?: FeatureCollection;
  connectionPoints?: ConnectionPointsResponse;
  // #1746 — инженерная инфраструктура из OSM (доп. слои на той же карте).
  utilityInfrastructure?: UtilityInfrastructureResponse;
  // Центры питания Россетей со свободной мощностью для ТП (#connection-capacity).
  // Отдельный слой (не OSM): цвет по индексу загрузки.
  powerConnectionPoints?: PowerConnectionPoint[];
  // Точки выхода газосети (форма 6 Екатеринбурггаза) в радиусе от участка
  // (#2119 B2 PR-4). Отдельный слой: цвет по знаку свободной мощности.
  gasOutletPoints?: GasOutletPoint[];
  customPois?: CustomPoi[];
  parcelCad?: string;
  // #999 (958-B4) — рыночные слои (опциональны: SiteMap используется и без них).
  competitors?: ParcelAnalysisCompetitor[];
  pipelineObjects?: PipelineObject[];
  riskZones?: RiskZone[];
  // §12.1-13 (#958) — opportunity-ЗУ + красные линии застройки (geom_wkt).
  opportunityParcels?: OpportunityParcel[];
  redLines?: RedLine[];
  // #1218 — высота тайла карты (px). Default 420 — режим обычной странички
  // /site-finder/[cad]. MiniMap на /analysis передаёт меньше (≈280), чтобы
  // карта была компактнее. На высоту НЕ влияет на flow-контролы ниже
  // (POI add button, легенда, CpLayerControlPanel) — они идут потоком.
  mapHeight?: number;
}

// Ключи переключаемых рыночных слоёв (#999 + §12.1-13 #958).
export type MarketLayerKey =
  "competitors" | "pipeline" | "risk" | "opportunity" | "redlines";

// ---------------------------------------------------------------------------
// Map click handler sub-component (must be inside MapContainer)
// ---------------------------------------------------------------------------

interface MapClickHandlerProps {
  editMode: boolean;
  onMapClick: (lat: number, lon: number) => void;
}

function MapClickHandler({ editMode, onMapClick }: MapClickHandlerProps) {
  useMapEvents({
    click(e) {
      if (editMode) {
        onMapClick(e.latlng.lat, e.latlng.lng);
      }
    },
  });
  return null;
}

export function SiteMap({
  data,
  isochrones,
  connectionPoints,
  utilityInfrastructure,
  powerConnectionPoints,
  gasOutletPoints,
  customPois,
  parcelCad,
  competitors,
  pipelineObjects,
  riskZones,
  opportunityParcels,
  redLines,
  mapHeight = 420,
}: Props) {
  // Fix Leaflet default icon paths broken by webpack bundler
  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",
      });
    });
  }, []);

  // Connection-points layer toggle state (all categories visible by default)
  const [visibleCategories, setVisibleCategories] = useState<Set<CpCategory>>(
    new Set(CP_ALL_CATEGORIES),
  );

  // #999 (958-B4) — рыночные слои. Карта — MiniMap (компактная), поэтому по
  // умолчанию включены только Конкуренты; Будущие проекты + Зоны риска OFF
  // (пользователь включает тумблером в CpLayerControlPanel).
  const [visibleMarketLayers, setVisibleMarketLayers] = useState<
    Set<MarketLayerKey>
  >(new Set<MarketLayerKey>(["competitors"]));

  function toggleMarketLayer(key: MarketLayerKey) {
    setVisibleMarketLayers((prev) => {
      const next = new Set(prev);
      if (next.has(key)) {
        next.delete(key);
      } else {
        next.add(key);
      }
      return next;
    });
  }

  // #255 — ЗОУИТ-слой. По умолчанию показываем blocker + warning (важные
  // охранные зоны), info (зоны сервитута) OFF, чтобы не зашумлять карту —
  // пользователь включает тумблером в ZouitLayerControlPanel.
  const [visibleZouit, setVisibleZouit] = useState<Set<ZouitSeverity>>(
    () => new Set<ZouitSeverity>(["blocker", "warning"]),
  );

  function toggleZouit(sev: ZouitSeverity) {
    setVisibleZouit((prev) => {
      const next = new Set(prev);
      if (next.has(sev)) {
        next.delete(sev);
      } else {
        next.add(sev);
      }
      return next;
    });
  }

  // #1746 — слой инж. инфраструктуры OSM. Все виды видимы по умолчанию (как CP);
  // пользователь отключает шумные слои тумблером в UtilityLayerControlPanel.
  const [visibleUtilityKinds, setVisibleUtilityKinds] = useState<
    Set<UtilityKind>
  >(() => new Set(UTILITY_ALL_KINDS));

  function toggleUtilityKind(kind: UtilityKind) {
    setVisibleUtilityKinds((prev) => {
      const next = new Set(prev);
      if (next.has(kind)) {
        next.delete(kind);
      } else {
        next.add(kind);
      }
      return next;
    });
  }

  function toggleAllUtilityKinds() {
    setVisibleUtilityKinds((prev) =>
      prev.size === UTILITY_ALL_KINDS.length
        ? new Set()
        : new Set(UTILITY_ALL_KINDS),
    );
  }

  // Точки выхода газосети (#2119 B2 PR-4) — единичный слой, тумблер-чекбокс
  // под картой. По умолчанию виден (как ЦП Россетей): точек мало (LIMIT 40,
  // только геокодированные), карту не зашумляет.
  const [showGasOutlets, setShowGasOutlets] = useState(true);

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

  // Mutations
  const addMutation = useAddCustomPoi(parcelCad);
  const updateMutation = useUpdateCustomPoi(parcelCad);
  const deleteMutation = useDeleteCustomPoi(parcelCad);

  // Keyboard 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 toggleCategory(cat: CpCategory) {
    setVisibleCategories((prev) => {
      const next = new Set(prev);
      if (next.has(cat)) {
        next.delete(cat);
      } else {
        next.add(cat);
      }
      return next;
    });
  }

  function toggleAll() {
    setVisibleCategories((prev) =>
      prev.size === CP_ALL_CATEGORIES.length
        ? new Set()
        : new Set(CP_ALL_CATEGORIES),
    );
  }

  function handleMapClick(lat: number, lon: number) {
    setPendingCoords({ lat, lon });
    setAddMode(false); // toggle off click mode; modal opens
  }

  const center: [number, number] = data.geom_geojson
    ? geomCenter(data.geom_geojson)
    : [56.838, 60.6];

  // Build legend from categories present in score_breakdown
  const presentCategories = Object.keys(data.score_breakdown);

  // Pre-group structures for both the map layer and the control panel
  const cpGrouped = connectionPoints
    ? groupStructuresByCategory(connectionPoints.engineering_structures)
    : new Map<CpCategory, EngineeringStructure[]>();

  // #999 — нормализуем рыночные данные (тонкий ответ → пустые массивы).
  const competitorList = competitors ?? data.competitors ?? [];
  const pipelineList = pipelineObjects ?? data.pipeline_24mo?.top_objects ?? [];
  const riskZoneList = riskZones ?? data.nspd_risk_zones ?? [];
  // §12.1-13 (#958) — opportunity-ЗУ + красные линии застройки.
  const opportunityList =
    opportunityParcels ?? data.nspd_opportunity_parcels ?? [];
  const redLineList = redLines ?? data.nspd_red_lines ?? [];
  // Слой рисуется только если у фичи есть геометрия (geom_wkt) — счётчик панели
  // должен это отражать (пустой WKT не даёт полигон/линию).
  const opportunityMappable = opportunityList.filter(
    (o) => typeof o.geom_wkt === "string" && o.geom_wkt.length > 0,
  ).length;
  const redLineMappable = redLineList.filter(
    (l) => typeof l.geom_wkt === "string" && l.geom_wkt.length > 0,
  ).length;
  const hasMarketData =
    competitorList.length > 0 ||
    pipelineList.length > 0 ||
    riskZoneList.length > 0 ||
    opportunityMappable > 0 ||
    redLineMappable > 0;
  // Кол-во объектов с координатами — для подписи в панели (без координат не
  // рисуются, поэтому счётчик панели должен это отражать).
  const competitorMappable = competitorList.filter(
    (c) => typeof c.lat === "number" && typeof c.lon === "number",
  ).length;
  const pipelineMappable = pipelineList.filter(
    (p) => typeof p.lat === "number" && typeof p.lon === "number",
  ).length;

  // #255 — ЗОУИТ-полигоны из analyze payload (geom_geojson). Группируем по
  // severity; в карту/панель идут только overlaps с валидной геометрией.
  const zouitOverlaps = data.nspd_zouit_overlaps ?? [];
  const zouitGrouped = groupZouitBySeverity(zouitOverlaps);
  const zouitMappableCount = ZOUIT_ALL_SEVERITIES.reduce(
    (sum, sev) => sum + (zouitGrouped.get(sev)?.length ?? 0),
    0,
  );
  const hasZouitGeom = zouitMappableCount > 0;

  // #1746 — инж. инфраструктура OSM. Группируем по виду; в карту/панель идут
  // только features с валидной геометрией (Point/Line/Polygon).
  const utilityGrouped = utilityInfrastructure
    ? groupUtilityByKind(utilityInfrastructure.features)
    : new Map<UtilityKind, UtilityMappable[]>();
  const utilityMappableCount = UTILITY_ALL_KINDS.reduce(
    (sum, kind) => sum + (utilityGrouped.get(kind)?.length ?? 0),
    0,
  );
  const hasUtilityGeom = utilityMappableCount > 0;

  // Центры питания Россетей со свободной мощностью (#connection-capacity).
  // Рисуем только точки с валидными координатами (Decimal→str приводим в слое).
  const powerPointList = powerConnectionPoints ?? [];
  const powerPointMappable = countMappablePowerPoints(powerPointList);
  const hasPowerPoints = powerPointMappable > 0;

  // Точки выхода газосети (#2119 B2 PR-4). Только ~19% строк источника
  // геокодированы → список часто пуст (норма). Считаем рисуемые точки.
  const gasOutletList = gasOutletPoints ?? [];
  const gasOutletMappable = countMappableGasOutlets(gasOutletList);
  const hasGasOutlets = gasOutletMappable > 0;

  return (
    <div>
      {/* Map */}
      <div
        style={{
          border: "1px solid #e5e7eb",
          borderRadius: 12,
          overflow: "hidden",
          height: mapHeight,
          cursor: addMode ? "crosshair" : "grab",
        }}
      >
        <MapContainer
          center={center}
          zoom={15}
          style={{ height: "100%", width: "100%" }}
          scrollWheelZoom
        >
          <MapClickHandler editMode={addMode} onMapClick={handleMapClick} />
          <TileLayer
            attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OSM</a>'
            url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
          />

          {/* Parcel / quarter polygon */}
          {data.geom_geojson && (
            <GeoJSON
              key={data.cad_num}
              data={data.geom_geojson}
              style={{
                color: "#1d4ed8",
                weight: 2.5,
                fillColor: "#3b82f6",
                fillOpacity: 0.2,
              }}
            />
          )}

          {/* #255 — ЗОУИТ-полигоны (охранные зоны). Рисуем рано (под точками POI
              и рыночными маркерами) как заливку-фон, поверх полигона участка. */}
          {hasZouitGeom && (
            <ZouitLayer
              grouped={zouitGrouped}
              visibleSeverities={visibleZouit}
            />
          )}

          {/* Isochrones — render in reverse order (largest range at bottom).
              NB: react-leaflet GeoJSON кэширует слой, не реагирует на смену
              `data` prop. Включаем в key первую координату feature (она
              различается между mode=foot/cycle/car и временем) — это
              форсирует remount при пересчёте от ORS. */}
          {isochrones &&
            [...isochrones.features].reverse().map((feature, idx) => {
              const seconds =
                typeof feature?.properties?.value === "number"
                  ? (feature.properties.value as number)
                  : 0;
              const minutes = seconds / 60;
              const opacity = Math.max(0.2, 0.5 - minutes * 0.02);
              const color =
                minutes <= 10
                  ? "#16a34a"
                  : minutes <= 20
                    ? "#3b82f6"
                    : "#a855f7";
              // Hash из первой координаты — различает foot/cycle/car полигоны
              const firstCoord = (
                feature.geometry as { coordinates?: number[][][] }
              )?.coordinates?.[0]?.[0];
              const coordHash = firstCoord
                ? `${firstCoord[0].toFixed(4)},${firstCoord[1].toFixed(4)}`
                : `${idx}`;
              return (
                <GeoJSON
                  key={`iso-${seconds}-${coordHash}`}
                  data={feature as Feature}
                  style={{
                    color,
                    fillColor: color,
                    fillOpacity: opacity,
                    weight: 1,
                  }}
                />
              );
            })}

          {/* #999 (958-B4) — рыночные слои (конкуренты / pipeline / зоны риска).
              Зоны риска рисуются как фон-заливка внутри MarketLayers, точки —
              поверх. Видимость каждого слоя — из visibleMarketLayers. */}
          {hasMarketData && (
            <MarketLayers
              competitors={competitorList}
              pipelineObjects={pipelineList}
              riskZones={riskZoneList}
              opportunityParcels={opportunityList}
              redLines={redLineList}
              showCompetitors={visibleMarketLayers.has("competitors")}
              showPipeline={visibleMarketLayers.has("pipeline")}
              showRiskZones={visibleMarketLayers.has("risk")}
              showOpportunity={visibleMarketLayers.has("opportunity")}
              showRedLines={visibleMarketLayers.has("redlines")}
            />
          )}

          {/* POI markers */}
          {Object.entries(data.score_breakdown).flatMap(([cat, pois]) => {
            const style = CATEGORY_STYLES[cat];
            if (!style) return [];
            return pois
              .filter((poi) => poi.lat !== 0 && poi.lon !== 0)
              .map((poi, idx) => {
                const websiteUrl = safeHttpUrl(poi.website);
                return (
                  <CircleMarker
                    key={`${cat}-${idx}`}
                    center={[poi.lat, poi.lon]}
                    radius={style.radius}
                    pathOptions={{
                      color: style.color,
                      fillColor: style.color,
                      fillOpacity: 0.85,
                      weight: 1.5,
                    }}
                  >
                    <Popup>
                      <div
                        style={{ fontSize: 13, lineHeight: 1.5, minWidth: 160 }}
                      >
                        <strong>{style.label}</strong>
                        <br />
                        {poi.name ?? <em>без названия</em>}
                        <br />
                        <span style={{ color: "#374151" }}>
                          {poi.distance_m.toFixed(0)} м от участка
                        </span>
                        {poi.address && (
                          <>
                            <br />
                            <span style={{ color: "#374151" }}>
                              Адрес: {poi.address}
                            </span>
                          </>
                        )}
                        {poi.operator && (
                          <>
                            <br />
                            <span style={{ color: "#374151" }}>
                              Оператор: {poi.operator}
                            </span>
                          </>
                        )}
                        {poi.opening_hours && (
                          <>
                            <br />
                            <span style={{ color: "#374151" }}>
                              Часы: {poi.opening_hours}
                            </span>
                          </>
                        )}
                        {poi.phone && (
                          <>
                            <br />
                            <a href={`tel:${poi.phone.replace(/[^\d+]/g, "")}`}>
                              {poi.phone}
                            </a>
                          </>
                        )}
                        {websiteUrl ? (
                          <>
                            <br />
                            <a
                              href={websiteUrl}
                              target="_blank"
                              rel="noopener noreferrer"
                            >
                              Сайт
                            </a>
                          </>
                        ) : (
                          poi.website && (
                            <>
                              <br />
                              <span style={{ color: "#374151" }}>
                                {poi.website}
                              </span>
                            </>
                          )
                        )}
                        {poi.last_edit && (
                          <>
                            <br />
                            <span style={{ color: "#6b7280", fontSize: 11 }}>
                              OSM: {poi.last_edit}
                            </span>
                          </>
                        )}
                      </div>
                    </Popup>
                  </CircleMarker>
                );
              });
          })}

          {/* Connection points layer — rendered on top of POI markers */}
          {connectionPoints && (
            <ConnectionPointsLayer
              grouped={cpGrouped}
              visibleCategories={visibleCategories}
            />
          )}

          {/* #1746 — инж. инфраструктура OSM (ЛЭП-линии, подстанции, узлы).
              Линии/полигоны как <GeoJSON>, точки как CircleMarker. */}
          {hasUtilityGeom && (
            <UtilityInfrastructureLayer
              grouped={utilityGrouped}
              visibleKinds={visibleUtilityKinds}
            />
          )}

          {/* Центры питания Россетей со свободной мощностью для ТП
              (#connection-capacity). Отдельный слой поверх OSM: цвет по индексу
              загрузки, отличается от оранжевых точек НСПД/инж.сетей OSM. */}
          {hasPowerPoints && (
            <PowerConnectionPointsLayer points={powerPointList} />
          )}

          {/* Точки выхода газосети (форма 6 Екатеринбурггаза, #2119 B2 PR-4).
              Отдельный слой поверх ЦП: цвет по знаку свободной мощности,
              меньший радиус (низовой слой газосети). Тумблер-чекбокс под картой. */}
          {hasGasOutlets && showGasOutlets && (
            <GasOutletPointsLayer points={gasOutletList} />
          )}

          {/* Custom POI markers — topmost layer */}
          {customPois && customPois.length > 0 && (
            <CustomPoiLayer
              pois={customPois}
              onEdit={(poi) => setEditingPoi(poi)}
              onDelete={(id) => deleteMutation.mutate(id)}
            />
          )}
        </MapContainer>
      </div>

      {/* Custom POI add-mode toggle button */}
      <div
        style={{ marginTop: 8, display: "flex", gap: 10, alignItems: "center" }}
      >
        <CustomPoiToggleButton
          active={addMode}
          onClick={() => {
            setAddMode((v) => !v);
            setPendingCoords(null);
          }}
        />
        {addMode && (
          <span style={{ fontSize: 12, color: "#6b7280" }}>
            Кликните на карте для добавления точки
          </span>
        )}
        {customPois && customPois.length > 0 && (
          <span style={{ fontSize: 12, color: "#374151", marginLeft: "auto" }}>
            {customPois.length} польз.{" "}
            {customPois.length === 1
              ? "точка"
              : customPois.length < 5
                ? "точки"
                : "точек"}
          </span>
        )}
      </div>

      {/* Add modal */}
      {pendingCoords && (
        <CustomPoiAddModal
          lat={pendingCoords.lat}
          lon={pendingCoords.lon}
          parcelCad={parcelCad ?? null}
          isLoading={addMutation.isPending}
          onSubmit={(payload) => {
            addMutation.mutate(payload, {
              onSuccess: () => setPendingCoords(null),
            });
          }}
          onCancel={() => setPendingCoords(null)}
        />
      )}

      {/* Edit modal */}
      {editingPoi && (
        <CustomPoiEditModal
          poi={editingPoi}
          isLoading={updateMutation.isPending}
          onSubmit={(updateData) => {
            updateMutation.mutate(
              { id: editingPoi.id, data: updateData },
              { onSuccess: () => setEditingPoi(null) },
            );
          }}
          onCancel={() => setEditingPoi(null)}
        />
      )}

      {/* POI category legend */}
      {presentCategories.length > 0 && (
        <div
          style={{
            display: "flex",
            flexWrap: "wrap",
            gap: "6px 12px",
            padding: "10px 4px 0",
          }}
        >
          {presentCategories.map((cat) => {
            const s = CATEGORY_STYLES[cat];
            if (!s) return null;
            const count = data.score_breakdown[cat]?.length ?? 0;
            return (
              <div
                key={cat}
                style={{
                  display: "flex",
                  alignItems: "center",
                  gap: 5,
                  fontSize: 12,
                  color: "#374151",
                }}
              >
                <span
                  style={{
                    width: 10,
                    height: 10,
                    borderRadius: "50%",
                    background: s.color,
                    flexShrink: 0,
                  }}
                />
                {s.label} ({count})
              </div>
            );
          })}
        </div>
      )}

      {/* Легенда слоя центров питания (#connection-capacity). Слой не тумблерный
          (небольшое число точек), поэтому подпись рендерим в легенде под картой:
          цвет маркера кодирует индекс загрузки ЦП. */}
      {hasPowerPoints && (
        <div
          style={{
            display: "flex",
            flexWrap: "wrap",
            alignItems: "center",
            gap: "6px 12px",
            padding: "10px 4px 0",
            fontSize: 12,
            color: "#374151",
          }}
        >
          <span style={{ fontWeight: 600 }}>
            Центры питания (резерв): {powerPointMappable}
          </span>
          {(
            [
              ["open", "открыт для ТП"],
              ["limited", "ограничен"],
              ["closed", "закрыт"],
            ] as const
          ).map(([idx, label]) => (
            <span
              key={idx}
              style={{ display: "flex", alignItems: "center", gap: 5 }}
            >
              <span
                style={{
                  width: 12,
                  height: 12,
                  borderRadius: "50%",
                  background: LOAD_INDEX_MARKER_COLOR[idx],
                  border: "2px solid #ffffff",
                  boxShadow: "0 0 0 1px #d1d5db",
                  flexShrink: 0,
                }}
              />
              {label}
            </span>
          ))}
          <span style={{ display: "flex", alignItems: "center", gap: 5 }}>
            <span
              style={{
                width: 12,
                height: 12,
                borderRadius: "50%",
                background: LOAD_INDEX_NULL_COLOR,
                border: "2px solid #ffffff",
                boxShadow: "0 0 0 1px #d1d5db",
                flexShrink: 0,
              }}
            />
            нет данных
          </span>
        </div>
      )}

      {/* Точки выхода газосети (#2119 B2 PR-4). Чекбокс-тумблер + легенда цветов
          по знаку свободной мощности. Показываем только при непустом списке
          рисуемых точек (LIMIT 40, ~19% геокодировано). */}
      {hasGasOutlets && (
        <div
          style={{
            display: "flex",
            flexWrap: "wrap",
            alignItems: "center",
            gap: "6px 12px",
            padding: "10px 4px 0",
            fontSize: 12,
            color: "#374151",
          }}
        >
          <label
            style={{
              display: "flex",
              alignItems: "center",
              gap: 6,
              fontWeight: 600,
              cursor: "pointer",
            }}
          >
            <input
              type="checkbox"
              checked={showGasOutlets}
              onChange={(e) => setShowGasOutlets(e.target.checked)}
            />
            Точки выхода газа ({pluralizeOutletPoints(gasOutletMappable)})
          </label>
          {(
            [
              ["profit", "профицит"],
              ["deficit", "дефицит"],
              ["calc", "нужен гидрорасчёт"],
            ] as const
          ).map(([state, label]) => (
            <span
              key={state}
              style={{ display: "flex", alignItems: "center", gap: 5 }}
            >
              <span
                style={{
                  width: 12,
                  height: 12,
                  borderRadius: "50%",
                  background: GAS_OUTLET_MARKER_COLOR[state],
                  border: "2px solid #ffffff",
                  boxShadow: "0 0 0 1px #d1d5db",
                  flexShrink: 0,
                }}
              />
              {label}
            </span>
          ))}
          <span style={{ color: "#9ca3af" }}>
            Форма 6 АО «Екатеринбурггаз» · млн м³/мес
          </span>
        </div>
      )}

      {!data.geom_geojson && (
        <p
          style={{
            marginTop: 8,
            fontSize: 12,
            color: "#9ca3af",
            textAlign: "center",
          }}
        >
          Геометрия участка не найдена — на карте нет полигона
        </p>
      )}

      {/* Layer control panel — CP-слои + рыночные слои (#999). Рендерится,
          если есть данные точек подключения ИЛИ рыночные данные. */}
      {(connectionPoints || hasMarketData) && (
        <CpLayerControlPanel
          data={connectionPoints ?? null}
          grouped={cpGrouped}
          visibleCategories={visibleCategories}
          onToggleCategory={toggleCategory}
          onToggleAll={toggleAll}
          marketLayers={
            hasMarketData
              ? [
                  {
                    key: "competitors",
                    label: "Конкуренты",
                    color: MARKET_COLORS.competitor,
                    count: competitorMappable,
                  },
                  {
                    key: "pipeline",
                    label: "Будущие проекты",
                    color: MARKET_COLORS.pipeline,
                    count: pipelineMappable,
                  },
                  {
                    key: "risk",
                    label: "Зоны риска",
                    color: MARKET_COLORS.risk,
                    count: riskZoneList.length,
                  },
                  {
                    key: "opportunity",
                    label: "Перспективные ЗУ",
                    color: MARKET_COLORS.opportunity,
                    count: opportunityMappable,
                  },
                  {
                    key: "redlines",
                    label: "Красные линии",
                    color: MARKET_COLORS.redline,
                    count: redLineMappable,
                  },
                ]
              : undefined
          }
          visibleMarketLayers={visibleMarketLayers}
          onToggleMarketLayer={toggleMarketLayer}
        />
      )}

      {/* #255 — ЗОУИТ-слой control panel. Рендерится только при наличии
          полигональной геометрии охранных зон в payload. */}
      {hasZouitGeom && (
        <ZouitLayerControlPanel
          grouped={zouitGrouped}
          visibleSeverities={visibleZouit}
          onToggleSeverity={toggleZouit}
        />
      )}

      {/* #1746 — control panel слоя инж. инфраструктуры OSM. Рендерится только
          при наличии рисуемой геометрии. */}
      {hasUtilityGeom && (
        <UtilityLayerControlPanel
          grouped={utilityGrouped}
          visibleKinds={visibleUtilityKinds}
          onToggleKind={toggleUtilityKind}
          onToggleAll={toggleAllUtilityKinds}
        />
      )}
    </div>
  );
}
