// -----------------------------------------------------------------------------
// Lou-Ka — Agrégateur de logements à louer (province de Québec)
// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
// components/MapView.tsx : carte interactive MapLibre GL
//   · chargée paresseusement (React.lazy) — le bundle carte ne pèse rien
//     tant que l'utilisateur reste en vue liste
//   · données GeoJSON (/api/listings.geojson) synchronisées avec les filtres
//   · clustering natif, marqueurs-prix, fiche popup, tuiles OpenFreeMap
// -----------------------------------------------------------------------------
import { useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom";
import * as maplibregl from "maplibre-gl";
import "maplibre-gl/dist/maplibre-gl.css";
import { API_BASE, ListingFilters, sourceName } from "../api";

// MapLibre v6 : worker en fichier séparé. En production il est copié
// (avec son module partagé) dans dist/maplibre/ par le plugin Vite
// « louka-copy-maplibre-worker » ; en dev, la résolution par défaut
// (node_modules servi par Vite) fonctionne déjà.
if (import.meta.env.PROD) {
  maplibregl.setWorkerUrl(`${import.meta.env.BASE_URL}maplibre/maplibre-gl-worker.mjs`);
}

const STYLE_URL = "https://tiles.openfreemap.org/styles/positron";
const QUEBEC_CENTER: [number, number] = [-71.25, 46.82];

function geojsonUrl(f: ListingFilters): string {
  const params = new URLSearchParams();
  for (const [k, v] of Object.entries(f)) if (v) params.set(k, v);
  return `${API_BASE}/api/listings.geojson?${params}`;
}

const fmtDispo = (iso: string | null): string =>
  !iso ? "" : iso === "now" ? "Libre maintenant"
    : `Dispo ${new Date(iso + "T12:00:00").toLocaleDateString("fr-CA", { day: "numeric", month: "short", year: "numeric" })}`;

function popupHTML(p: Record<string, unknown>): string {
  const prix = p.price != null
    ? `${Number(p.price).toLocaleString("fr-CA", { maximumFractionDigits: 0 })} $`
    : (p.price_label as string) || "Prix sur demande";
  const img = p.image
    ? `<img src="${p.image}" alt="" loading="lazy"/>`
    : `<div class="mv-noimg">🏠</div>`;
  const meta = [p.unit_type, p.area_sqft ? `${Math.round(Number(p.area_sqft))} pi²` : "",
                fmtDispo(p.availability_date as string | null)]
    .filter(Boolean).join(" · ");
  return `
    <div class="mv-pop">
      ${img}
      <div class="mv-pop-body">
        <div class="mv-pop-price">${prix}<small>${p.price != null ? " /mois" : ""}</small></div>
        <div class="mv-pop-title">${p.title ?? ""}</div>
        <div class="mv-pop-meta">${meta}</div>
        <div class="mv-pop-src">${sourceName(String(p.source ?? ""))}</div>
        <a class="mv-pop-cta" href="${API_BASE}/logement/${encodeURIComponent(String(p.uid))}">Voir la fiche →</a>
      </div>
    </div>`;
}

export default function MapView({ filters }: { filters: ListingFilters }) {
  const div = useRef<HTMLDivElement>(null);
  const mapRef = useRef<maplibregl.Map | null>(null);
  const loadedRef = useRef(false);
  const navigate = useNavigate();

  // création de la carte (une seule fois)
  useEffect(() => {
    if (!div.current || mapRef.current) return;
    const map = new maplibregl.Map({
      container: div.current,
      style: STYLE_URL,
      center: QUEBEC_CENTER,
      zoom: 11,
      attributionControl: { compact: true },
    });
    mapRef.current = map;
    (window as unknown as { _loukaMap?: maplibregl.Map })._loukaMap = map; // hook de test/débogage
    map.addControl(new maplibregl.NavigationControl({ showCompass: false }), "top-right");
    map.addControl(new maplibregl.GeolocateControl({ trackUserLocation: false }), "top-right");
    map.touchZoomRotate.disableRotation();

    map.on("load", () => {
      map.addSource("logements", {
        type: "geojson",
        data: { type: "FeatureCollection", features: [] },
        cluster: true,
        clusterMaxZoom: 15,
        clusterRadius: 46,
      });

      // grappes : cercle encre + halo lime, compte au centre
      map.addLayer({
        id: "clusters", type: "circle", source: "logements",
        filter: ["has", "point_count"],
        paint: {
          "circle-color": "#141814",
          "circle-radius": ["step", ["get", "point_count"], 15, 10, 19, 50, 24, 200, 30],
          "circle-stroke-width": 3,
          "circle-stroke-color": "#d9f26b",
        },
      });
      map.addLayer({
        id: "cluster-count", type: "symbol", source: "logements",
        filter: ["has", "point_count"],
        layout: {
          "text-field": ["get", "point_count_abbreviated"],
          "text-font": ["Noto Sans Bold"],
          "text-size": 13,
        },
        paint: { "text-color": "#d9f26b" },
      });

      // marqueurs individuels : point vert + étiquette-prix au-dessus
      map.addLayer({
        id: "point", type: "circle", source: "logements",
        filter: ["!", ["has", "point_count"]],
        paint: {
          "circle-color": "#1c5c41",
          "circle-radius": 5,
          "circle-stroke-width": 1.5,
          "circle-stroke-color": "#f5f3ee",
        },
      });
      map.addLayer({
        id: "point-price", type: "symbol", source: "logements",
        filter: ["!", ["has", "point_count"]],
        layout: {
          "text-field": [
            "case",
            ["!=", ["get", "price"], null],
            ["concat", ["number-format", ["get", "price"], { locale: "fr-CA", "max-fraction-digits": 0 }], " $"],
            "?",
          ],
          "text-font": ["Noto Sans Bold"],
          "text-size": 12,
          "text-offset": [0, -1.1],
          "text-allow-overlap": false,
          "text-optional": true,
        },
        paint: {
          "text-color": "#141814",
          "text-halo-color": "#d9f26b",
          "text-halo-width": 2,
        },
      });

      // interactions
      map.on("click", "clusters", async (e: maplibregl.MapLayerMouseEvent) => {
        const f = map.queryRenderedFeatures(e.point, { layers: ["clusters"] })[0];
        const src = map.getSource("logements") as maplibregl.GeoJSONSource;
        const zoom = await src.getClusterExpansionZoom(f.properties!.cluster_id);
        map.easeTo({ center: (f.geometry as GeoJSON.Point).coordinates as [number, number], zoom });
      });
      const openPopup = (e: maplibregl.MapLayerMouseEvent) => {
        const f = e.features?.[0];
        if (!f) return;
        const coords = (f.geometry as GeoJSON.Point).coordinates.slice() as [number, number];
        const pop = new maplibregl.Popup({ offset: 14, maxWidth: "290px", closeButton: true })
          .setLngLat(coords).setHTML(popupHTML(f.properties as Record<string, unknown>)).addTo(map);
        // navigation SPA (sans rechargement) sur le lien de la fiche
        pop.getElement().querySelector(".mv-pop-cta")?.addEventListener("click", (ev: Event) => {
          ev.preventDefault();
          navigate(`/logement/${encodeURIComponent(String((f.properties as any).uid))}`);
        });
      };
      map.on("click", "point", openPopup);
      map.on("click", "point-price", openPopup);
      for (const layer of ["clusters", "point", "point-price"]) {
        map.on("mouseenter", layer, () => { map.getCanvas().style.cursor = "pointer"; });
        map.on("mouseleave", layer, () => { map.getCanvas().style.cursor = ""; });
      }
      loadedRef.current = true;
    });

    return () => { map.remove(); mapRef.current = null; loadedRef.current = false; };
  }, [navigate]);

  // synchronisation avec les filtres
  useEffect(() => {
    const map = mapRef.current;
    if (!map) return;
    let cancelled = false;
    const apply = async () => {
      try {
        const res = await fetch(geojsonUrl(filters));
        const data = (await res.json()) as GeoJSON.FeatureCollection;
        if (cancelled || !mapRef.current) return;
        (map.getSource("logements") as maplibregl.GeoJSONSource | undefined)?.setData(data);
        // cadrer sur les résultats (avec limites raisonnables)
        if (data.features.length > 0) {
          const b = new maplibregl.LngLatBounds();
          for (const f of data.features)
            b.extend((f.geometry as GeoJSON.Point).coordinates as [number, number]);
          map.fitBounds(b, { padding: 56, maxZoom: 15, duration: 500 });
        }
      } catch { /* réseau : la carte garde ses données précédentes */ }
    };
    if (loadedRef.current) apply();
    else map.once("load", () => apply());   // la source est créée par le
    // gestionnaire « load » de l'effet de création, enregistré avant celui-ci
    return () => { cancelled = true; };
  }, [filters]);

  return <div ref={div} className="mapview" role="application" aria-label="Carte des logements" />;
}
