import { useCallback, useEffect, useRef } from "react";
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import L from "leaflet";
import type { Feature, FeatureCollection, Geometry, MultiPoint, Point, GeoJsonObject, GeoJsonProperties } from "geojson";
import { activeLayersAtom } from "../state/active-layers";
import { leafletLayerInstancesAtom } from "../state/leaflet-layer-instances";
import { featureInformationAtom, FeatureInformationType } from "../state/feature-information";
import { constructWfsUrl } from "@/shared/utils/wms";
import colorScaleApi from "@/shared/services/color-scale";
import { appConfig } from "@/shared/config/app-config";
import { mergeDataToGeoJSON } from "@/shared/utils/mege-data-geojson";
import jatimGeojson from "@/public/jatim.json";

// -------- Helpers (module scope) --------
const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === "object" && v !== null;

const looksLikeUrl = (s: string): boolean => {
  const t = s.trim();
  if (/^(data:)/i.test(t)) return true;
  if (/^https?:\/\//i.test(t)) return true;
  if (/\.(svg|png|jpg|jpeg|gif)(\?|#|$)/i.test(t)) return true;
  return false;
};

const pickUrl = (node: unknown): string | null => {
  if (typeof node === "string" && looksLikeUrl(node)) return node;
  if (!isRecord(node)) return null;
  const candidates: unknown[] = [node["url"], node["href"], node["externalGraphic"], node["xlink:href"], node["@xlink:href"], node["@href"]];
  for (const c of candidates) {
    if (typeof c === "string" && looksLikeUrl(c)) return c;
  }
  return null;
};

const deepFindUrl = (obj: unknown, depth = 0): string | null => {
  if (!obj || depth > 4) return null;
  const u = pickUrl(obj);
  if (u) return u;
  if (isRecord(obj)) {
    for (const k of Object.keys(obj)) {
      const r = deepFindUrl(obj[k], depth + 1);
      if (r) return r;
    }
  } else if (Array.isArray(obj)) {
    for (const el of obj) {
      const r = deepFindUrl(el, depth + 1);
      if (r) return r;
    }
  }
  return null;
};

const extractLegendRulesFlexible = (json: unknown): unknown[] => {
  if (!json) return [];
  if (isRecord(json)) {
    const rules = json["rules"];
    if (Array.isArray(rules)) return rules as unknown[];

    const legendObj = json["legend"];
    if (isRecord(legendObj) && Array.isArray(legendObj["rules"])) return (legendObj["rules"] as unknown[]) ?? [];
    const legendArr = json["legend"];
    if (Array.isArray(legendArr)) {
      const all = legendArr.flatMap((e) => (isRecord(e) && Array.isArray(e["rules"]) ? (e["rules"] as unknown[]) : []));
      if (all.length) return all;
    }

    const LegendObj = json["Legend"];
    if (isRecord(LegendObj) && Array.isArray(LegendObj["rules"])) return (LegendObj["rules"] as unknown[]) ?? [];
    const LegendArr = json["Legend"];
    if (Array.isArray(LegendArr)) {
      const all = LegendArr.flatMap((e) => (isRecord(e) && Array.isArray(e["rules"]) ? (e["rules"] as unknown[]) : []));
      if (all.length) return all;
    }

    const classesArr = json["classes"];
    if (Array.isArray(classesArr)) return classesArr as unknown[];
  }
  return [];
};

const findIconUrlInRule = (rule: unknown): string | null => {
  if (isRecord(rule)) {
    const sym = rule["symbolizers"];
    if (Array.isArray(sym) && sym.length) {
      for (const s of sym) {
        if (!isRecord(s)) continue;
        const point = (s["pointSymbolizer"] ?? s["PointSymbolizer"] ?? s["Point"]) as unknown;
        const g = isRecord(point) ? point["graphic"] ?? point["Graphic"] ?? point["graphics"] ?? point : point;
        const url = deepFindUrl(g) || deepFindUrl(point) || deepFindUrl(s);
        if (url) return url;
      }
    }
    return deepFindUrl(rule);
  }
  return null;
};

export const LayerManager = ({ map }: { map: L.Map | null }) => {
  const activeLayers = useAtomValue(activeLayersAtom);
  const [layerInstances, setLayerInstances] = useAtom(leafletLayerInstancesAtom);
  const setFeatureInformation = useSetAtom(featureInformationAtom);
  const featureInformation = useAtomValue(featureInformationAtom);
  const previousLayersRef = useRef<Map<string, { visible: boolean; opacity: number; zIndex: number }>>(new Map());
  const highlightRef = useRef<L.Layer | null>(null);
  const backdropRef = useRef<L.Layer | null>(null);

  const clearHighlight = useCallback(() => {
    if (map && highlightRef.current) {
      map.removeLayer(highlightRef.current);
      highlightRef.current = null;
    }
    if (map && backdropRef.current) {
      map.removeLayer(backdropRef.current);
      backdropRef.current = null;
    }
  }, [map]);

  // Ensure custom panes for proper stacking and non-blocking UI
  useEffect(() => {
    if (!map) return;
    if (!map.getPane("backdropPane")) {
      const p = map.createPane("backdropPane");
      p.style.zIndex = "450";
      p.style.pointerEvents = "none";
    }
    if (!map.getPane("highlightPane")) {
      const p = map.createPane("highlightPane");
      p.style.zIndex = "650";
      p.style.pointerEvents = "none";
    }
  }, [map]);

  // Handle map click for feature info
  useEffect(() => {
    if (!map) return;

    const handleMapClick = async (e: L.LeafletMouseEvent) => {
      setFeatureInformation([]);
      clearHighlight();

      // Find all visible WMS layers
      const wmsLayers = activeLayers.filter((layer) => layer.layer.type === "wms" && layer.settings.visible);

      if (wmsLayers.length === 0) return;

      try {
        const allFeatureInfo: FeatureInformationType[] = [];
        const highlightCandidates: Array<{ layer: (typeof wmsLayers)[number]; features: Feature<Geometry>[] }> = [];

        for (const layer of wmsLayers) {
          if (layer.layer.type !== "wms") continue;

          const url = layer.layer.url;
          const layers = layer.layer.layers;

          if (!url || !layers) continue;

          const bounds = map.getBounds();
          const size = map.getSize();

          // Construct GetFeatureInfo URL
          const params = new URLSearchParams({
            service: "WMS",
            version: "1.1.1",
            request: "GetFeatureInfo",
            layers: layers,
            query_layers: layers,
            info_format: "application/json",
            feature_count: "20",
            x: Math.floor(e.containerPoint.x).toString(),
            y: Math.floor(e.containerPoint.y).toString(),
            width: Math.floor(size.x).toString(),
            height: Math.floor(size.y).toString(),
            srs: "EPSG:4326",
            bbox: `${bounds.getSouthWest().lng},${bounds.getSouthWest().lat},${bounds.getNorthEast().lng},${bounds.getNorthEast().lat}`,
          });

          // Respect active CQL_FILTER (if set on the Leaflet WMS instance)
          try {
            const instance = (layerInstances as Map<string, L.Layer>).get(layer.id) as L.TileLayer.WMS | undefined;
            const cql = (instance?.wmsParams as unknown as Record<string, unknown>)?.["CQL_FILTER"] as string | undefined;
            if (cql) params.set("CQL_FILTER", cql);
          } catch {
            // ignore
          }

          const featureInfoUrl = `${url}?${params.toString()}`;

          // Fetch the feature info
          const response = await fetch(featureInfoUrl);
          if (!response.ok) continue;

          const featureData = await response.json();

          if (featureData && featureData.features && featureData.features.length > 0) {
            const layerFeatureInfo = {
              id: layer.id,
              name: layer.name || layer.id,
              info: featureData.features,
            };

            allFeatureInfo.push(layerFeatureInfo);
            highlightCandidates.push({ layer, features: featureData.features as Feature<Geometry>[] });
          }
        }

        // Update state with all found features
        setFeatureInformation(allFeatureInfo.length > 0 ? allFeatureInfo : null);

        // Add geometry-based highlight for the returned features
        if (highlightCandidates.length > 0) {
          try {
            const group = L.layerGroup();
            // selected rule label (if any) from URL (?filter=["label"]) to resolve icon for this layer
            const getSelectedFilterLabelFromUrl = (layerId: string): string | null => {
              try {
                const sp = new URLSearchParams(window.location.search);
                const filterKey = `filter__${String(layerId).replace(/[^a-zA-Z0-9_-]/g, "_")}`;
                const raw = sp.get(filterKey) ?? sp.get("filter");
                if (!raw) return null;
                const arr = JSON.parse(raw);
                return Array.isArray(arr) && arr.length === 1 && typeof arr[0] === "string" ? arr[0] : null;
              } catch {
                return null;
              }
            };
            // selected label for each layer will be resolved within loop

            // Helper within effect: try get legend icon URL for a given layer and selected rule label
            const tryGetIconForLayerRule = (baseUrl: string, layerName: string, ruleLabel: string | null): string | null => {
              if (!baseUrl || !layerName || !ruleLabel) return null;
              try {
                const cacheKey = `legend-json:${baseUrl}:${layerName}`;
                const cached = sessionStorage.getItem(cacheKey);
                const json: unknown = cached ? JSON.parse(cached) : null;
                if (!json) return null;
                const rules = extractLegendRulesFlexible(json);
                const rule = rules.find((r) => {
                  if (!isRecord(r)) return false;
                  const labelVal = r["title"] ?? r["name"];
                  return typeof labelVal === "string" && labelVal.trim() === ruleLabel;
                });
                if (!rule) return null;
                return findIconUrlInRule(rule);
              } catch {
                return null;
              }
            };

            for (const hc of highlightCandidates) {
              const selectedLabel = getSelectedFilterLabelFromUrl(hc.layer.id);
              const fc: FeatureCollection<Geometry> = {
                type: "FeatureCollection",
                features: hc.features as Feature<Geometry>[],
              };
              const lg = L.geoJSON(fc, {
                pane: "highlightPane",
                pointToLayer: (feature: Feature<Point | MultiPoint>, latlng: L.LatLng) => {
                  // Try use icon for selected rule; else circle marker
                  const iconUrl = tryGetIconForLayerRule(hc.layer.layer.url ?? "", hc.layer.layer.layers ?? "", selectedLabel);
                  if (iconUrl) {
                    const icon = L.icon({ iconUrl, iconSize: [24, 24], iconAnchor: [12, 12] });
                    const m = L.marker(latlng, { icon, pane: "highlightPane" });
                    const halo = L.circleMarker(latlng, {
                      radius: 14,
                      color: "#ffcc00",
                      weight: 3,
                      opacity: 1,
                      fillColor: "#ffcc00",
                      fillOpacity: 0.25,
                      pane: "highlightPane",
                    });
                    return L.layerGroup([halo, m]);
                  }
                  return L.circleMarker(latlng, {
                    radius: 10,
                    color: "#ffcc00",
                    weight: 3,
                    opacity: 1,
                    fillColor: "#ffcc00",
                    fillOpacity: 0.5,
                    pane: "highlightPane",
                  });
                },
                style: (feature?: Feature<Geometry, GeoJsonProperties>): L.PathOptions => {
                  const geomType = feature?.geometry?.type;
                  if (geomType && /LineString/i.test(geomType)) {
                    return {
                      color: "#ffcc00",
                      weight: 5,
                      opacity: 1,
                    } as L.PathOptions;
                  }
                  if (geomType && /Polygon/i.test(geomType)) {
                    return {
                      color: "#ffcc00",
                      weight: 3,
                      opacity: 1,
                      fillColor: "#fff2a8",
                      fillOpacity: 0.35,
                    } as L.PathOptions;
                  }
                  // default
                  return {
                    color: "#ffcc00",
                    weight: 3,
                    opacity: 1,
                  } as L.PathOptions;
                },
                interactive: false,
              });
              lg.addTo(group);
            }
            group.addTo(map);
            // bring to front if possible
            (group as unknown as { bringToFront?: () => void }).bringToFront?.();
            highlightRef.current = group;

            // Add backdrop rectangle with current viewport bounds, below highlight
            try {
              const b = map.getBounds();
              const rect = L.rectangle(b, {
                pane: "backdropPane",
                interactive: false,
                stroke: false,
                fillColor: "#000000",
                fillOpacity: 0.25,
              });
              rect.addTo(map);
              (rect as unknown as { bringToBack?: () => void }).bringToBack?.();
              backdropRef.current = rect;
            } catch {}
          } catch (err) {
            console.warn("Failed to add geometry highlight", err);
            // fallback: mark click location
            try {
              const marker = L.circleMarker(e.latlng, {
                radius: 10,
                color: "#ffcc00",
                weight: 3,
                opacity: 1,
                fillColor: "#ffcc00",
                fillOpacity: 0.6,
              });
              marker.addTo(map);
              (marker as unknown as { bringToFront?: () => void }).bringToFront?.();
              highlightRef.current = marker;
            } catch {}
          }
        }
      } catch (error) {
        console.error("Error fetching feature info:", error);
        setFeatureInformation(null);
        clearHighlight();
      }
    };

    map.on("click", handleMapClick);

    return () => {
      map.off("click", handleMapClick);
    };
  }, [map, activeLayers, setFeatureInformation, layerInstances, clearHighlight]);

  // Remove highlight when feature information panel is closed (null or empty array)
  useEffect(() => {
    const empty = !featureInformation || featureInformation.length === 0;
    if (empty) {
      clearHighlight();
    }
  }, [featureInformation, clearHighlight]);

  // Keep backdrop following the viewport while the panel is open
  useEffect(() => {
    if (!map) return;

    const hasInfo = !!featureInformation && featureInformation.length > 0;
    if (!hasInfo) return;

    const updateBackdrop = () => {
      try {
        if (backdropRef.current instanceof L.Rectangle) {
          const b = map.getBounds();
          backdropRef.current.setBounds(b);
        }
      } catch {
        // ignore
      }
    };

    // Initial sync
    updateBackdrop();

    map.on("move", updateBackdrop);
    map.on("zoom", updateBackdrop);
    map.on("moveend", updateBackdrop);
    map.on("zoomend", updateBackdrop);

    return () => {
      map.off("move", updateBackdrop);
      map.off("zoom", updateBackdrop);
      map.off("moveend", updateBackdrop);
      map.off("zoomend", updateBackdrop);
    };
  }, [map, featureInformation]);

  // (helpers moved to module scope)

  // Handle layer creation and removal
  useEffect(() => {
    if (!map) return;

    const instances = layerInstances;
    const currentLayerIds = new Set(activeLayers.map((layer) => layer.id));

    // Remove layers that are no longer in activeLayers
    instances.forEach((layer, id) => {
      if (!currentLayerIds.has(id)) {
        layer.remove();
        instances.delete(id);
      }
    });

    // Create new layers
    const layerPromises: Promise<{
      id: string;
      zIndex: number;
      leafletLayer: L.Layer;
      bounds?: L.LatLngBoundsExpression;
      visible: boolean;
    } | null>[] = [];

    activeLayers.forEach((layer) => {
      if (instances.has(layer.id)) return; // Skip if layer already exists

      const mode = layer.mode || "basic";

      if (mode === "basic" && layer.layer.type === "wms") {
        const leafletLayer = L.tileLayer.wms(layer.layer.url ?? "", {
          layers: layer.layer.layers,
          format: "image/png",
          transparent: true,
          opacity: layer.settings.opacity,
          zIndex: layer.settings.zIndex,
        });

        layerPromises.push(
          Promise.resolve({
            id: layer.id,
            zIndex: layer.settings.zIndex ?? 0,
            leafletLayer,
            bounds: layer.layer.bounds ?? undefined,
            visible: layer.settings.visible,
          })
        );
      } else if (mode === "choropleth") {
        const promise = (async () => {
          try {
            const sourceUrl = constructWfsUrl(layer.layer) as string;
            const colorScale = await colorScaleApi.getColorScale({
              source_url: sourceUrl,
              boundary_file_id: appConfig.boundaryFileId,
            });
            if (!colorScale?.data) return null;

            const enrichedGeoJSON = mergeDataToGeoJSON(jatimGeojson as unknown as FeatureCollection<Geometry>, colorScale.data);

            const leafletLayer = L.geoJSON(enrichedGeoJSON, {
              style: (feature?: Feature<Geometry, GeoJsonProperties>) => ({
                fillColor: String((feature?.properties as Record<string, unknown> | null)?.["color"] ?? "#cccccc"),
                weight: 2,
                color: "#333333",
                // Initialize with current layer opacity
                fillOpacity: layer.settings.opacity,
                opacity: layer.settings.opacity,
              }),
              onEachFeature: (feature: Feature<Geometry, GeoJsonProperties>, layerInstance) => {
                const props = (feature.properties ?? {}) as Record<string, unknown>;
                const wadmkk = String(props["WADMKK"] ?? "");
                const wadmpr = String(props["WADMPR"] ?? "");
                const value = String(props["value"] ?? "");
                layerInstance.bindPopup(`<strong>${wadmkk}, ${wadmpr}</strong><br>Value: ${value}`);
              },
            });

            return {
              id: layer.id,
              zIndex: layer.settings.zIndex ?? 0,
              leafletLayer,
              visible: layer.settings.visible,
            };
          } catch (err) {
            console.error("Failed to add choropleth layer", err);
            return null;
          }
        })();
        layerPromises.push(promise);
      }
    });

    // Add new layers to map and instances
    Promise.all(layerPromises).then((results) => {
      results
        .filter((r): r is NonNullable<typeof r> => !!r)
        .sort((a, b) => a.zIndex - b.zIndex)
        .forEach(({ id, leafletLayer, visible, bounds }) => {
          instances.set(id, leafletLayer);

          // Only add to map if visible
          if (visible) {
            leafletLayer.addTo(map);
            // Only fit bounds for newly added layers
            if (bounds && !previousLayersRef.current.has(id)) {
              map.fitBounds(bounds);
            }
          }
        });

      setLayerInstances(instances);
    });
  }, [activeLayers, map, setLayerInstances, layerInstances]);

  // Handle visibility, opacity, and zIndex changes separately
  useEffect(() => {
    if (!map) return;

    const instances = layerInstances;

    activeLayers.forEach((layer) => {
      const leafletLayer = instances.get(layer.id);
      if (!leafletLayer) return;

      const previousLayer = previousLayersRef.current.get(layer.id);
      const currentSettings = layer.settings;

      // Handle visibility changes
      if (!previousLayer || previousLayer.visible !== currentSettings.visible) {
        if (currentSettings.visible) {
          if (!map.hasLayer(leafletLayer)) {
            leafletLayer.addTo(map);
          }
        } else {
          if (map.hasLayer(leafletLayer)) {
            map.removeLayer(leafletLayer);
          }
        }
      }

      // Handle opacity changes
      if (!previousLayer || previousLayer.opacity !== currentSettings.opacity) {
        // Try WMS/Tile-like layers
        const asTile = leafletLayer as unknown as { setOpacity?: (n: number) => void };
        if (asTile.setOpacity) {
          asTile.setOpacity(currentSettings.opacity);
        } else if ((leafletLayer as L.GeoJSON<GeoJsonObject>).setStyle) {
          // Fallback for GeoJSON/vector layers
          try {
            (leafletLayer as L.GeoJSON<GeoJsonObject>).setStyle({
              fillOpacity: currentSettings.opacity,
              opacity: currentSettings.opacity,
            });
          } catch {
            // ignore
          }
        }
      }

      // Handle zIndex changes
      if (!previousLayer || previousLayer.zIndex !== currentSettings.zIndex) {
        (leafletLayer as unknown as { setZIndex?: (n: number) => void }).setZIndex?.(currentSettings.zIndex);
      }

      // Update previous layer state
      previousLayersRef.current.set(layer.id, {
        visible: currentSettings.visible,
        opacity: currentSettings.opacity,
        zIndex: currentSettings.zIndex,
      });
    });

    // Clean up previous layer references for removed layers
    const currentLayerIds = new Set(activeLayers.map((l) => l.id));
    previousLayersRef.current.forEach((_, id) => {
      if (!currentLayerIds.has(id)) {
        previousLayersRef.current.delete(id);
      }
    });
  }, [activeLayers, map, layerInstances]);

  return null;
};
