"use client";

import { useEffect, useRef, useState } from "react";
import * as maplibregl from "maplibre-gl";
import "maplibre-gl/dist/maplibre-gl.css";
import { Protocol } from "pmtiles";
import type { CategoryId, TravelMode, ProfileId } from "@transportationer/shared";

type Weights = Record<CategoryId, number>;

export type BaseOverlay = "accessibility" | "estate-value" | "hidden-gem";

export interface MapViewProps {
  citySlug: string;
  cityBbox: [number, number, number, number];
  profile: ProfileId;
  mode: TravelMode;
  threshold: number;
  activeCategory: CategoryId | "composite";
  weights: Weights;
  pinLocation?: { lat: number; lng: number } | null;
  /** Set in relative mode: each grid cell is colored vs. this reference score. */
  pinCategoryScores?: Record<CategoryId, number> | null;
  /** Set in isochrone mode: GeoJSON FeatureCollection from Valhalla. */
  isochrones?: object | null;
  /** Which base overlay to show (accessibility grid, estate value, or hidden gem). */
  baseOverlay?: BaseOverlay;
  onLocationClick?: (lat: number, lng: number, estateValue: number | null) => void;
}

// Red → yellow → green score ramp
const SCORE_RAMP = [0, "#d73027", 0.25, "#fc8d59", 0.5, "#fee08b", 0.75, "#d9ef8b", 1, "#1a9850"];

function makeColorExpr(cat: CategoryId | "composite", weights: Weights): unknown[] {
  const scoreExpr =
    cat === "composite"
      ? makeCompositeExpr(weights)
      : ["coalesce", ["get", `score_${cat}`], 0];
  return ["interpolate", ["linear"], scoreExpr, ...SCORE_RAMP];
}

function makeCompositeExpr(weights: Weights): unknown[] {
  const entries = Object.entries(weights) as [CategoryId, number][];
  const total = entries.reduce((s, [, w]) => s + w, 0);
  if (total === 0) return [0];
  const terms = entries
    .filter(([, w]) => w > 0)
    .map(([cat, w]) => ["*", ["coalesce", ["get", `score_${cat}`], 0], w]);
  if (terms.length === 0) return [0];
  const sumExpr = terms.length === 1 ? terms[0] : ["+", ...terms];
  return ["/", sumExpr, total];
}

function makeRelativeColorExpr(
  cat: CategoryId | "composite",
  weights: Weights,
  pinCategoryScores: Record<CategoryId, number>,
): unknown[] {
  const scoreExpr =
    cat === "composite"
      ? makeCompositeExpr(weights)
      : ["coalesce", ["get", `score_${cat}`], 0];

  let pinScore: number;
  if (cat === "composite") {
    const entries = Object.entries(weights) as [CategoryId, number][];
    const total = entries.reduce((s, [, w]) => s + w, 0);
    pinScore =
      total === 0
        ? 0
        : entries.reduce((s, [c, w]) => s + (pinCategoryScores[c] ?? 0) * w, 0) / total;
  } else {
    pinScore = pinCategoryScores[cat] ?? 0;
  }

  return [
    "interpolate", ["linear"], ["-", scoreExpr, pinScore],
    -0.5, "#d73027",
    -0.15, "#fc8d59",
    0, "#ffffbf",
    0.15, "#91cf60",
    0.5, "#1a9850",
  ];
}

function tileUrl(city: string, mode: string, threshold: number, profile: string) {
  return `${window.location.origin}/api/tiles/grid/{z}/{x}/{y}?city=${encodeURIComponent(city)}&mode=${mode}&threshold=${threshold}&profile=${profile}`;
}

function removeIsochroneLayers(map: maplibregl.Map) {
  if (map.getLayer("isochrone-fill")) map.removeLayer("isochrone-fill");
  if (map.getSource("isochrone")) map.removeSource("isochrone");
}

function removeEstateValueLayers(map: maplibregl.Map) {
  if (map.getLayer("estate-value-outline")) map.removeLayer("estate-value-outline");
  if (map.getLayer("estate-value-fill")) map.removeLayer("estate-value-fill");
  if (map.getSource("estate-value-zones")) map.removeSource("estate-value-zones");
}

function removeHiddenGemLayers(map: maplibregl.Map) {
  if (map.getLayer("hidden-gems-fill")) map.removeLayer("hidden-gems-fill");
  if (map.getSource("hidden-gems-tiles")) map.removeSource("hidden-gems-tiles");
}

function buildEstateValuePopupHtml(props: Record<string, unknown>): string {
  const value = props.value != null ? `${props.value} €/m²` : "–";
  const usageLabels: Record<string, string> = {
    W: "Residential", G: "Commercial", LF: "Agricultural",
    SF: "Special Use", B: "Mixed", GIF: "Mixed Infill",
  };
  const detailLabels: Record<string, string> = {
    EFH: "Single-family", ZFH: "Two-family", MFH: "Multi-family",
    RH: "Terraced", GW: "Mixed-use",
  };
  const usage = props.usageType
    ? (usageLabels[props.usageType as string] ?? props.usageType as string)
    : null;
  const detail = props.usageDetail
    ? (detailLabels[props.usageDetail as string] ?? props.usageDetail as string)
    : null;
  const zone = props.zoneName as string | null;
  const stichtag = props.stichtag as string | null;

  return `<div style="font:13px/1.5 system-ui,sans-serif;min-width:140px">
    <div style="font-size:18px;font-weight:700;color:#c0392b;margin-bottom:2px">${value}</div>
    ${usage ? `<div style="color:#444">${usage}${detail ? ` · ${detail}` : ""}</div>` : ""}
    ${zone ? `<div style="color:#777;font-size:11px;margin-top:2px">${zone}</div>` : ""}
    ${stichtag ? `<div style="color:#aaa;font-size:10px">${stichtag}</div>` : ""}
  </div>`;
}

export function MapView({
  citySlug,
  cityBbox,
  profile,
  mode,
  threshold,
  activeCategory,
  weights,
  pinLocation,
  pinCategoryScores,
  isochrones,
  baseOverlay = "accessibility",
  onLocationClick,
}: MapViewProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const mapRef = useRef<maplibregl.Map | null>(null);
  const markerRef = useRef<maplibregl.Marker | null>(null);
  const estateValuePopupRef = useRef<maplibregl.Popup | null>(null);
  const hiddenGemPopupRef = useRef<maplibregl.Popup | null>(null);
  const mountedRef = useRef(false);
  // Ref for estate-value event handlers so cleanup can call map.off() even after async fetch
  const evHandlersRef = useRef<{
    map: maplibregl.Map;
    onEnter: () => void;
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    onMove: (e: any) => void;
    onLeave: () => void;
  } | null>(null);

  // Tracked as state so effects re-run once the map style finishes loading.
  // Without this, effects that check isStyleLoaded() return early on first render
  // and never reapply state after the async on("load") fires.
  const [mapLoaded, setMapLoaded] = useState(false);

  // Derive which layers should be visible
  const showGrid = !isochrones && (baseOverlay === "accessibility" || !!pinCategoryScores);
  const showEstateValue = !isochrones && !pinCategoryScores && baseOverlay === "estate-value";
  const showHiddenGem = !isochrones && !pinCategoryScores && baseOverlay === "hidden-gem";

  const stateRef = useRef({
    citySlug, cityBbox, profile, mode, threshold, activeCategory, weights, onLocationClick,
  });
  stateRef.current = { citySlug, cityBbox, profile, mode, threshold, activeCategory, weights, onLocationClick };

  // ── Update heatmap paint when category / weights / pin scores change ─────
  useEffect(() => {
    if (!mapLoaded) return;
    const map = mapRef.current;
    if (!map?.getLayer("grid-fill")) return;
    const colorExpr = pinCategoryScores
      ? makeRelativeColorExpr(activeCategory, weights, pinCategoryScores)
      : makeColorExpr(activeCategory, weights);
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    map.setPaintProperty("grid-fill", "fill-color", colorExpr as any);
  }, [mapLoaded, activeCategory, weights, pinCategoryScores]);

  // ── Update tile source when city / mode / threshold / profile change ──────
  useEffect(() => {
    if (!mapLoaded) return;
    const map = mapRef.current;
    if (!map) return;
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const src = map.getSource("grid-tiles") as any;
    if (src?.setTiles) src.setTiles([tileUrl(citySlug, mode, threshold, profile)]);
  }, [mapLoaded, citySlug, mode, threshold, profile]);

  // ── Pin marker ─────────────────────────────────────────────────────────────
  // Markers are DOM elements (not style layers), but mapRef.current is only
  // set inside the mount effect which runs after all earlier effects. Adding
  // mapLoaded as a dep guarantees mapRef.current is non-null when this runs.
  useEffect(() => {
    if (!mapLoaded) return;
    const map = mapRef.current;
    if (!map) return;
    markerRef.current?.remove();
    markerRef.current = null;
    if (pinLocation) {
      markerRef.current = new maplibregl.Marker({ color: "#2563eb" })
        .setLngLat([pinLocation.lng, pinLocation.lat])
        .addTo(map);
    }
  }, [mapLoaded, pinLocation]);

  // ── Grid visibility ───────────────────────────────────────────────────────
  useEffect(() => {
    if (!mapLoaded) return;
    const map = mapRef.current;
    if (!map?.getLayer("grid-fill")) return;
    map.setLayoutProperty("grid-fill", "visibility", showGrid ? "visible" : "none");
  }, [mapLoaded, showGrid]);

  // ── Isochrone layer ───────────────────────────────────────────────────────
  useEffect(() => {
    if (!mapLoaded) return;
    const map = mapRef.current!;

    removeIsochroneLayers(map);

    if (!isochrones) return;

    if (map.getLayer("grid-fill")) {
      map.setLayoutProperty("grid-fill", "visibility", "none");
    }

    const geojson = isochrones as { type: string; features: { properties: { contour: number } }[] };
    if (!Array.isArray(geojson.features) || geojson.features.length === 0) return;

    const contourValues = geojson.features.map((f) => f.properties.contour);
    const maxContour = Math.max(...contourValues);
    const sorted = {
      ...geojson,
      features: [...geojson.features].sort((a, b) => b.properties.contour - a.properties.contour),
    };

    map.addSource("isochrone", { type: "geojson", data: sorted as never });
    map.addLayer({
      id: "isochrone-fill",
      type: "fill",
      source: "isochrone",
      paint: {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        "fill-color": ["interpolate", ["linear"], ["get", "contour"],
          0,               "#1a9850",
          maxContour * 0.5, "#fee08b",
          maxContour,      "#d73027",
        ] as any,
        "fill-opacity": 0.5,
        "fill-outline-color": "rgba(0,0,0,0.1)",
      },
    });

    return () => { removeIsochroneLayers(map); };
  }, [mapLoaded, isochrones]);

  // ── Hidden gem overlay ────────────────────────────────────────────────────
  useEffect(() => {
    if (!mapLoaded) return;
    const map = mapRef.current!;

    removeHiddenGemLayers(map);
    hiddenGemPopupRef.current?.remove();
    hiddenGemPopupRef.current = null;

    if (!showHiddenGem) return;

    const tileBase = `${window.location.origin}/api/tiles/hidden-gems/{z}/{x}/{y}?city=${encodeURIComponent(citySlug)}`;

    map.addSource("hidden-gems-tiles", {
      type: "vector",
      tiles: [tileBase],
      minzoom: 0,
      maxzoom: 16,
    });
    map.addLayer({
      id: "hidden-gems-fill",
      type: "fill",
      source: "hidden-gems-tiles",
      "source-layer": "hidden-gems",
      paint: {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        "fill-color": ["interpolate", ["linear"], ["coalesce", ["get", "score"], 0],
          0,   "#d73027",
          40,  "#fee08b",
          70,  "#d9ef8b",
          100, "#1a9850",
        ] as any,
        "fill-opacity": 0.5,
        "fill-outline-color": "rgba(0,0,0,0.1)",
      },
    });

    const popup = new maplibregl.Popup({ closeButton: false, closeOnClick: false, maxWidth: "200px" });
    hiddenGemPopupRef.current = popup;

    const onEnter = () => { map.getCanvas().style.cursor = "pointer"; };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const onMove = (e: any) => {
      const feature = e.features?.[0];
      if (!feature) return;
      const score = feature.properties?.score as number | null;
      popup.setLngLat(e.lngLat).setHTML(
        `<div style="font:13px/1.5 system-ui,sans-serif">
          <div style="font-size:16px;font-weight:700;color:#1a9850">${score != null ? score + "%" : "–"}</div>
          <div style="color:#555;font-size:11px">Hidden gem score</div>
        </div>`
      ).addTo(map);
    };
    const onLeave = () => { map.getCanvas().style.cursor = "crosshair"; popup.remove(); };

    map.on("mouseenter", "hidden-gems-fill", onEnter);
    map.on("mousemove",  "hidden-gems-fill", onMove);
    map.on("mouseleave", "hidden-gems-fill", onLeave);

    return () => {
      map.off("mouseenter", "hidden-gems-fill", onEnter);
      map.off("mousemove",  "hidden-gems-fill", onMove);
      map.off("mouseleave", "hidden-gems-fill", onLeave);
      removeHiddenGemLayers(map);
      try { map.getCanvas().style.cursor = "crosshair"; } catch { /* map removed */ }
      hiddenGemPopupRef.current?.remove();
      hiddenGemPopupRef.current = null;
    };
  }, [mapLoaded, showHiddenGem, citySlug]); // eslint-disable-line react-hooks/exhaustive-deps

  // ── Estate value overlay ──────────────────────────────────────────────────
  useEffect(() => {
    if (!mapLoaded) return;
    const map = mapRef.current!;

    // Clean up any previously registered handlers before tearing down layers
    if (evHandlersRef.current) {
      const { map: m, onEnter, onMove, onLeave } = evHandlersRef.current;
      m.off("mouseenter", "estate-value-fill", onEnter);
      m.off("mousemove",  "estate-value-fill", onMove);
      m.off("mouseleave", "estate-value-fill", onLeave);
      evHandlersRef.current = null;
    }
    removeEstateValueLayers(map);
    estateValuePopupRef.current?.remove();
    estateValuePopupRef.current = null;

    if (!showEstateValue) return;

    const [minLng, minLat, maxLng, maxLat] = stateRef.current.cityBbox;
    let cancelled = false;

    fetch(`/api/estate-value?bbox=${minLng},${minLat},${maxLng},${maxLat}`)
      .then((r) => {
        if (!r.ok) throw new Error(`estate-value API ${r.status}`);
        return r.json();
      })
      .then((geojson) => {
        if (cancelled || !map.isStyleLoaded()) return;

        map.addSource("estate-value-zones", { type: "geojson", data: geojson });
        map.addLayer({
          id: "estate-value-fill",
          type: "fill",
          source: "estate-value-zones",
          paint: {
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            "fill-color": ["interpolate", ["linear"], ["coalesce", ["get", "value"], 0],
              0,    "#ffffb2",
              100,  "#fecc5c",
              300,  "#fd8d3c",
              700,  "#f03b20",
              1500, "#bd0026",
            ] as any,
            "fill-opacity": 0.5,
          },
        });
        map.addLayer({
          id: "estate-value-outline",
          type: "line",
          source: "estate-value-zones",
          paint: { "line-color": "rgba(0,0,0,0.25)", "line-width": 0.6 },
        });

        const popup = new maplibregl.Popup({ closeButton: false, closeOnClick: false, maxWidth: "240px" });
        estateValuePopupRef.current = popup;

        const onEnter = () => { map.getCanvas().style.cursor = "pointer"; };
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const onMove = (e: any) => {
          const feature = e.features?.[0];
          if (!feature) return;
          popup.setLngLat(e.lngLat)
            .setHTML(buildEstateValuePopupHtml(feature.properties as Record<string, unknown>))
            .addTo(map);
        };
        const onLeave = () => { map.getCanvas().style.cursor = "crosshair"; popup.remove(); };

        map.on("mouseenter", "estate-value-fill", onEnter);
        map.on("mousemove",  "estate-value-fill", onMove);
        map.on("mouseleave", "estate-value-fill", onLeave);
        evHandlersRef.current = { map, onEnter, onMove, onLeave };
      })
      .catch((err) => {
        if (!cancelled) console.warn("[map-view] estate-value fetch failed:", err);
      });

    return () => {
      cancelled = true;
      if (evHandlersRef.current) {
        const { map: m, onEnter, onMove, onLeave } = evHandlersRef.current;
        m.off("mouseenter", "estate-value-fill", onEnter);
        m.off("mousemove",  "estate-value-fill", onMove);
        m.off("mouseleave", "estate-value-fill", onLeave);
        evHandlersRef.current = null;
      }
      try {
        if (map.isStyleLoaded()) {
          removeEstateValueLayers(map);
          map.getCanvas().style.cursor = "crosshair";
        }
      } catch { /* map removed */ }
      estateValuePopupRef.current?.remove();
      estateValuePopupRef.current = null;
    };
  }, [mapLoaded, showEstateValue, citySlug]); // eslint-disable-line react-hooks/exhaustive-deps

  // ── Initialize map (runs once on mount) ───────────────────────────────────
  useEffect(() => {
    if (mountedRef.current || !containerRef.current) return;
    mountedRef.current = true;

    const protocol = new Protocol();
    maplibregl.addProtocol("pmtiles", protocol.tile);

    const map = new maplibregl.Map({
      container: containerRef.current,
      style: "/tiles/style.json",
      bounds: cityBbox,
      fitBoundsOptions: { padding: 40 },
    });

    mapRef.current = map;

    map.on("load", () => {
      const { citySlug: city, profile: prof, mode: m, threshold: t, activeCategory: cat, weights: w } = stateRef.current;

      map.addSource("grid-tiles", {
        type: "vector",
        tiles: [tileUrl(city, m, t, prof)],
        minzoom: 0,
        maxzoom: 16,
      });

      map.addLayer({
        id: "grid-fill",
        type: "fill",
        source: "grid-tiles",
        "source-layer": "grid",
        paint: {
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          "fill-color": makeColorExpr(cat, w) as any,
          "fill-opacity": 0.5,
          "fill-outline-color": "rgba(0,0,0,0.06)",
        },
      });

      map.on("click", (e) => {
        const evFeatures = map.getLayer("estate-value-fill")
          ? map.queryRenderedFeatures(e.point, { layers: ["estate-value-fill"] })
          : [];
        const estateValue = (evFeatures[0]?.properties?.value as number | null) ?? null;
        stateRef.current.onLocationClick?.(e.lngLat.lat, e.lngLat.lng, estateValue);
      });

      map.getCanvas().style.cursor = "crosshair";

      // Signal all reactive effects that the map is ready.
      setMapLoaded(true);
    });

    return () => {
      // Reset mapLoaded so effects re-run if the map is recreated (e.g. StrictMode).
      setMapLoaded(false);
      markerRef.current?.remove();
      markerRef.current = null;
      mapRef.current?.remove();
      mapRef.current = null;
      mountedRef.current = false;
    };
  }, []); // eslint-disable-line react-hooks/exhaustive-deps

  return <div ref={containerRef} className="w-full h-full" />;
}
