import { useEffect, useMemo, useState } from "react";
import type { MapRuntimeBinding, MapRuntimeFact } from "./useMapDataProductRuntime.js";
import {
  TRANSPORT_STATION_REFERENCE_PROFILE_ID,
  type MapReferenceLayer,
  type MapReferenceStationCategory,
} from "./mapReferenceStations.js";

type ReferenceSnapshot = {
  schemaVersion: "nodedc.map-reference.snapshot/v1";
  profileId: typeof TRANSPORT_STATION_REFERENCE_PROFILE_ID;
  sourceRevision: string;
  facts: MapRuntimeFact[];
};

const identifier = /^[A-Za-z0-9._:-]{1,160}$/;
const categories = new Set<MapReferenceStationCategory>(["metro", "railway_terminal", "railway_station"]);

export function useMapReferenceRuntime(
  layers: MapReferenceLayer[],
  camera: { longitude: number; latitude: number; height: number } | null,
  enabled = true,
) {
  const [snapshot, setSnapshot] = useState<ReferenceSnapshot | null>(null);
  const signature = useMemo(() => layers.map((layer) => `${layer.id}:${layer.category}:${layer.presentationProfileId}`).join("|"), [layers]);
  const bbox = useMemo(() => referenceBbox(camera), [camera?.height, camera?.latitude, camera?.longitude]);
  const bboxSignature = bbox?.join(",") ?? "";

  useEffect(() => {
    if (!enabled || !layers.length) {
      setSnapshot(null);
      return;
    }
    const controller = new AbortController();
    const debounce = window.setTimeout(() => {
      const query = bbox ? `?bbox=${encodeURIComponent(bbox.join(","))}` : "";
      void fetch(`/api/map-gateway/api/map/reference-sources/v1/profiles/${TRANSPORT_STATION_REFERENCE_PROFILE_ID}/current${query}`, {
        cache: "no-store",
        signal: controller.signal,
      })
        .then(async (response) => {
          if (!response.ok) throw new Error(`map_reference_http_${response.status}`);
          const parsed = asSnapshot(await response.json());
          if (!parsed) throw new Error("map_reference_snapshot_invalid");
          setSnapshot(parsed);
        })
        .catch(() => {
          if (!controller.signal.aborted) setSnapshot(null);
        });
    }, 450);
    return () => {
      window.clearTimeout(debounce);
      controller.abort();
    };
  }, [bboxSignature, enabled, signature]);

  return useMemo<MapRuntimeBinding[]>(() => layers.map((layer) => ({
    bindingId: layer.id,
    dataProductId: `platform-reference.${layer.referenceProfileId}`,
    slotId: "reference-points",
    presentationProfileId: layer.presentationProfileId,
    facts: snapshot?.facts.filter((fact) => fact.attributes.category === layer.category) ?? [],
    cursor: snapshot?.sourceRevision ?? null,
    state: snapshot ? "ready" : "loading",
  })), [layers, snapshot]);
}

function referenceBbox(camera: { longitude: number; latitude: number; height: number } | null) {
  if (!camera || camera.height > 150_000 || !Number.isFinite(camera.height)) return null;
  const radiusKm = Math.max(10, Math.min(40, camera.height / 1_000 * 0.8));
  const latitudeDelta = radiusKm / 111.32;
  const longitudeScale = Math.max(0.2, Math.cos(camera.latitude * Math.PI / 180));
  const longitudeDelta = radiusKm / (111.32 * longitudeScale);
  return [
    Math.max(-180, camera.longitude - longitudeDelta),
    Math.max(-90, camera.latitude - latitudeDelta),
    Math.min(180, camera.longitude + longitudeDelta),
    Math.min(90, camera.latitude + latitudeDelta),
  ].map((value) => Number(value.toFixed(5)));
}

function asSnapshot(value: unknown): ReferenceSnapshot | null {
  if (!isObject(value) || value.schemaVersion !== "nodedc.map-reference.snapshot/v1"
    || value.profileId !== TRANSPORT_STATION_REFERENCE_PROFILE_ID
    || typeof value.sourceRevision !== "string" || !Array.isArray(value.facts)) return null;
  const facts = value.facts.map(asFact).filter((fact): fact is MapRuntimeFact => Boolean(fact));
  return {
    schemaVersion: "nodedc.map-reference.snapshot/v1",
    profileId: TRANSPORT_STATION_REFERENCE_PROFILE_ID,
    sourceRevision: value.sourceRevision,
    facts,
  };
}

function asFact(value: unknown): MapRuntimeFact | null {
  if (!isObject(value) || typeof value.sourceId !== "string" || !identifier.test(value.sourceId)
    || !["map.station", "map.terminal"].includes(String(value.semanticType))
    || !isIso(value.observedAt) || !isIso(value.receivedAt)
    || !isObject(value.attributes) || !categories.has(value.attributes.category as MapReferenceStationCategory)
    || !isPoint(value.geometry)) return null;
  const allowedAttributes = new Set(["name", "category", "network", "operator", "official_name", "local_name", "uic_ref", "wheelchair"]);
  return {
    sourceId: value.sourceId,
    semanticType: String(value.semanticType),
    observedAt: value.observedAt,
    receivedAt: value.receivedAt,
    attributes: Object.fromEntries(Object.entries(value.attributes).filter(([key]) => allowedAttributes.has(key))),
    geometry: { type: "Point", coordinates: [...value.geometry.coordinates] as [number, number] },
    presentationStatus: "active",
  };
}

function isPoint(value: unknown): value is { type: "Point"; coordinates: [number, number] } {
  if (!isObject(value) || value.type !== "Point" || !Array.isArray(value.coordinates) || value.coordinates.length !== 2) return false;
  const [longitude, latitude] = value.coordinates;
  return typeof longitude === "number" && Number.isFinite(longitude) && longitude >= -180 && longitude <= 180
    && typeof latitude === "number" && Number.isFinite(latitude) && latitude >= -90 && latitude <= 90;
}

function isObject(value: unknown): value is Record<string, any> {
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}

function isIso(value: unknown): value is string {
  return typeof value === "string" && !Number.isNaN(Date.parse(value));
}
