"use client";

import { useEffect, useRef } from "react";
import type { CategoryId, TravelMode, ProfileId } from "@transportationer/shared";

type Weights = Record<CategoryId, number>;

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;
  onLocationClick?: (lat: number, lng: number) => 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;
  }

  // Diverging: negative = worse than pin (red), positive = better (green)
  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) {
  const origin = typeof window !== "undefined" ? window.location.origin : "";
  return `${origin}/api/tiles/grid/{z}/{x}/{y}?city=${encodeURIComponent(city)}&mode=${mode}&threshold=${threshold}&profile=${profile}`;
}

/** Remove isochrone layer/source if they exist. */
function removeIsochroneLayers(map: import("maplibre-gl").Map) {
  if (map.getLayer("isochrone-fill")) map.removeLayer("isochrone-fill");
  if (map.getSource("isochrone")) map.removeSource("isochrone");
}

export function MapView({
  citySlug,
  cityBbox,
  profile,
  mode,
  threshold,
  activeCategory,
  weights,
  pinLocation,
  pinCategoryScores,
  isochrones,
  onLocationClick,
}: MapViewProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const mapRef = useRef<import("maplibre-gl").Map | null>(null);
  const markerRef = useRef<import("maplibre-gl").Marker | null>(null);
  const mountedRef = useRef(false);

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

  // Update heatmap paint when category, weights, or pin scores change
  useEffect(() => {
    const map = mapRef.current;
    if (!map?.isStyleLoaded() || !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);
  }, [activeCategory, weights, pinCategoryScores]);

  // Update tile source when city/mode/threshold/profile change
  useEffect(() => {
    const map = mapRef.current;
    if (!map?.isStyleLoaded()) 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)]);
  }, [citySlug, mode, threshold, profile]);

  // Add/remove pin marker when pin location changes
  useEffect(() => {
    const map = mapRef.current;
    if (!map) return;
    markerRef.current?.remove();
    markerRef.current = null;
    if (pinLocation) {
      import("maplibre-gl").then(({ Marker }) => {
        const marker = new Marker({ color: "#2563eb" })
          .setLngLat([pinLocation.lng, pinLocation.lat])
          .addTo(map);
        markerRef.current = marker;
      });
    }
  }, [pinLocation]);

  // Add/remove isochrone layer when isochrones data changes.
  // The grid-fill layer is hidden while isochrones are shown so only one
  // overlay is visible at a time.
  useEffect(() => {
    const map = mapRef.current;
    if (!map?.isStyleLoaded()) return;

    removeIsochroneLayers(map);

    if (!isochrones) {
      // Restore grid when leaving isochrone mode.
      if (map.getLayer("grid-fill")) {
        map.setLayoutProperty("grid-fill", "visibility", "visible");
      }
      return;
    }

    // Hide the grid heatmap — the isochrone replaces it visually.
    if (map.getLayer("grid-fill")) {
      map.setLayoutProperty("grid-fill", "visibility", "none");
    }

    // Sort largest contour first — smaller (inner, more accessible) polygons
    // are drawn on top, so each pixel shows the color of the smallest contour
    // that covers it (i.e. the fastest reachable zone wins visually).
    const geojson = isochrones as { type: string; features: { properties: { contour: number } }[] };
    if (!Array.isArray(geojson.features) || geojson.features.length === 0) {
      // Malformed response (e.g. Valhalla error body with no features) — restore grid.
      if (map.getLayer("grid-fill")) map.setLayoutProperty("grid-fill", "visibility", "visible");
      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,
      ),
    };

    try {
      map.addSource("isochrone", { type: "geojson", data: sorted as never });
      // Color each zone using the same green→red ramp:
      // small contour (close) = green, large contour (far) = red.
      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.65,
          "fill-outline-color": "rgba(0,0,0,0.15)",
        },
      });
    } catch (err) {
      console.warn("[map-view] Error adding isochrone layer:", err);
    }

    return () => {
      const m = mapRef.current;
      if (!m?.isStyleLoaded()) return;
      removeIsochroneLayers(m);
      if (m.getLayer("grid-fill")) {
        m.setLayoutProperty("grid-fill", "visibility", "visible");
      }
    };
  }, [isochrones]);

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

    (async () => {
      const mgl = await import("maplibre-gl");
      const { Protocol } = await import("pmtiles");

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

      const map = new mgl.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.8,
            "fill-outline-color": "rgba(0,0,0,0.06)",
          },
        });

        map.on("click", (e) => {
          stateRef.current.onLocationClick?.(e.lngLat.lat, e.lngLat.lng);
        });

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

    return () => {
      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" />;
}
