import { useEffect, useRef } from "react";
import {
  Cartesian2,
  Cartesian3,
  BingMapsImageryProvider,
  BingMapsStyle,
  Color,
  Credit,
  Cesium3DTileset,
  Cesium3DTileStyle,
  CesiumTerrainProvider,
  CustomDataSource,
  DefaultProxy,
  DistanceDisplayCondition,
  EllipsoidTerrainProvider,
  Entity,
  HeadingPitchRange,
  ImageryLayer,
  JulianDate,
  Matrix4,
  Math as CesiumMath,
  PolygonHierarchy,
  Resource,
  ScreenSpaceEventHandler,
  ScreenSpaceEventType,
  SunLight,
  Viewer,
} from "cesium";
import "cesium/Build/Cesium/Widgets/widgets.css";
import sceneFixture from "../../../registry/fixtures/map/map-operational-v0.1.json";

type Position = [number, number, number?];
type RuntimeConfig = {
  cesiumVersion: string;
  provider: string;
  ionReady: boolean;
  gatewayReady: boolean;
  osmBuildingsReady: boolean;
  gaussianSplatsReady: boolean;
  gaussianAssetId?: string | null;
  assetEndpointBase: string;
  resourceProxyBase?: string | null;
  gatewayHealthUrl?: string | null;
};

export type MapGatewayHealth = {
  cache?: { mode?: string; entries?: number; bytes?: number; maxBytes?: number; persistent?: boolean };
  ionConfigured?: boolean;
};

type IonAssetEndpoint = {
  assetId: string;
  type: "TERRAIN" | "3DTILES" | "IMAGERY";
  url?: string;
  accessToken?: string;
  externalType?: "BING";
  options?: { url?: string; key?: string; mapStyle?: string };
  attributions: Array<{ html?: string; collapsible?: boolean }>;
};

export type MapPresentation = {
  imagerySource: "cesium-live";
  imageryVisible: boolean;
  cacheEnabled: boolean;
  terrainEnabled: boolean;
  terrainExaggeration: number;
  monochrome: boolean;
  monochromeColor: string;
  imageryGamma: number;
  imageryHue: number;
  imageryAlpha: number;
  globeColor: string;
  backgroundColor: string;
  atmosphereEnabled: boolean;
  atmosphereHue: number;
  atmosphereSaturation: number;
  atmosphereBrightness: number;
  fogEnabled: boolean;
  fogDensity: number;
  sunEnabled: boolean;
  sunHour: number;
  sunIntensity: number;
  shadowsEnabled: boolean;
  buildingsVisible: boolean;
  buildingsColor: string;
  buildingsOpacity: number;
  buildingsDetail: number;
  imageryBrightness: number;
  imageryContrast: number;
  imagerySaturation: number;
  gridVisible: boolean;
  gridLodEnabled: boolean;
  gridHeightMeters: number;
  gridLod1MaxHeightKm: number;
  gridLod1StepKm: number;
  gridLod2MaxHeightKm: number;
  gridLod2StepKm: number;
  gridLod3StepKm: number;
  gridRadiusKm: number;
  gridLineWidth: number;
  gridColor: string;
  gridOpacity: number;
  gridDotsEnabled: boolean;
  gridDotsSize: number;
  gridDotsColor: string;
  gridDotsOpacity: number;
  cacheRefresh: boolean;
};

export type MapCameraView = {
  longitude: number;
  latitude: number;
  height: number;
  heading: number;
  pitch: number;
  roll: number;
};

const toCartesian = ([longitude, latitude, height = 0]: Position) => Cartesian3.fromDegrees(longitude, latitude, height);
const toCartesianArray = (positions: Position[]) => positions.map(toCartesian);
const accent = Color.fromCssColorString("#ff2f92");
const violet = Color.fromCssColorString("#8f72dc");
const clamp = (value: number, minimum: number, maximum: number) => Math.max(minimum, Math.min(maximum, value));

function getCameraView(viewer: Viewer): MapCameraView {
  const position = viewer.camera.positionCartographic;
  return {
    longitude: CesiumMath.toDegrees(position.longitude),
    latitude: CesiumMath.toDegrees(position.latitude),
    height: position.height,
    heading: viewer.camera.heading,
    pitch: viewer.camera.pitch,
    roll: viewer.camera.roll,
  };
}

function addFixtureEntities(viewer: Viewer) {
  for (const place of sceneFixture.scene.places) {
    viewer.entities.add({
      id: place.id,
      position: toCartesian(place.position as Position),
      label: {
        text: place.label.text,
        font: "700 28px Arial",
        fillColor: Color.WHITE,
        outlineColor: Color.BLACK.withAlpha(0.8),
        outlineWidth: 4,
        style: 2,
        distanceDisplayCondition: new DistanceDisplayCondition(100_000, 50_000_000),
      },
    });
  }

  for (const route of sceneFixture.scene.routes) {
    viewer.entities.add({
      id: route.id,
      polyline: {
        positions: toCartesianArray(route.coordinates as Position[]),
        width: 4,
        material: accent.withAlpha(0.92),
        clampToGround: true,
      },
    });
  }

  for (const track of sceneFixture.scene.tracks) {
    viewer.entities.add({
      id: track.id,
      polyline: {
        positions: toCartesianArray(track.coordinates as Position[]),
        width: 2,
        material: violet.withAlpha(0.88),
        clampToGround: true,
      },
    });
  }

  for (const zone of sceneFixture.scene.zones) {
    const ring = zone.geometry.type === "Polygon" ? zone.geometry.coordinates[0] : zone.geometry.coordinates[0][0];
    viewer.entities.add({
      id: zone.id,
      polygon: {
        hierarchy: new PolygonHierarchy(toCartesianArray(ring as Position[])),
        material: accent.withAlpha(0.2),
      },
    });
    viewer.entities.add({
      id: `${zone.id}:boundary`,
      polyline: {
        positions: toCartesianArray(ring as Position[]),
        width: 2,
        material: accent.withAlpha(0.78),
        clampToGround: true,
      },
    });
  }

  for (const object of sceneFixture.scene.movingObjects) {
    if (object.trace) {
      viewer.entities.add({
        id: `${object.id}:trace`,
        polyline: {
          positions: toCartesianArray(object.trace as Position[]),
          width: 2,
          material: accent.withAlpha(0.52),
          clampToGround: true,
        },
      });
    }
    viewer.entities.add({
      id: object.id,
      position: toCartesian(object.position as Position),
      point: { pixelSize: 18, color: accent, outlineColor: Color.WHITE, outlineWidth: 3 },
      label: {
        text: object.label.text,
        font: "700 13px Arial",
        fillColor: Color.WHITE,
        showBackground: true,
        backgroundColor: Color.BLACK.withAlpha(0.72),
        backgroundPadding: new Cartesian2(10, 7),
        pixelOffset: new Cartesian2(0, -31),
      },
    });
  }

  for (const station of sceneFixture.scene.stations) {
    viewer.entities.add({
      id: station.id,
      position: toCartesian(station.position as Position),
      point: { pixelSize: station.stationType === "metro" ? 21 : 18, color: violet, outlineColor: Color.WHITE, outlineWidth: 3 },
      label: {
        text: station.label.text,
        font: "700 14px Arial",
        fillColor: Color.WHITE,
        showBackground: true,
        backgroundColor: Color.BLACK.withAlpha(0.72),
        backgroundPadding: new Cartesian2(10, 7),
        pixelOffset: new Cartesian2(0, -33),
      },
    });
  }
}

function rebuildElevatedGrid(viewer: Viewer, dataSource: CustomDataSource, presentation: MapPresentation) {
  const entities = dataSource.entities;
  entities.removeAll();
  if (!presentation.gridVisible) return;

  const cameraHeightKm = Math.max(0, Number(viewer.camera.positionCartographic?.height || 0) / 1000);
  const stepKm = !presentation.gridLodEnabled || cameraHeightKm <= presentation.gridLod1MaxHeightKm
    ? presentation.gridLod1StepKm
    : cameraHeightKm <= presentation.gridLod2MaxHeightKm
      ? presentation.gridLod2StepKm
      : presentation.gridLod3StepKm;
  const safeStepKm = clamp(stepKm, 0.25, 100);
  const safeRadiusKm = clamp(presentation.gridRadiusKm, safeStepKm, 150);
  const stepsPerSide = Math.min(32, Math.max(1, Math.floor(safeRadiusKm / safeStepKm)));
  const center = sceneFixture.viewport.center as Position;
  const latitude = center[1];
  const longitude = center[0];
  const metersPerLatitudeDegree = 110_574;
  const metersPerLongitudeDegree = Math.max(1, 111_320 * Math.cos(CesiumMath.toRadians(latitude)));
  const stepMeters = safeStepKm * 1000;
  const radiusMeters = stepsPerSide * stepMeters;
  const deltaLatitude = stepMeters / metersPerLatitudeDegree;
  const deltaLongitude = stepMeters / metersPerLongitudeDegree;
  const radiusLatitude = radiusMeters / metersPerLatitudeDegree;
  const radiusLongitude = radiusMeters / metersPerLongitudeDegree;
  const lineColor = Color.fromCssColorString(presentation.gridColor).withAlpha(clamp(presentation.gridOpacity / 100, 0, 1));
  const dotColor = Color.fromCssColorString(presentation.gridDotsColor).withAlpha(clamp(presentation.gridDotsOpacity / 100, 0, 1));
  const elevation = Math.max(0, presentation.gridHeightMeters);

  for (let index = -stepsPerSide; index <= stepsPerSide; index += 1) {
    const nextLatitude = latitude + index * deltaLatitude;
    const nextLongitude = longitude + index * deltaLongitude;
    entities.add({
      polyline: {
        positions: [
          Cartesian3.fromDegrees(longitude - radiusLongitude, nextLatitude, elevation),
          Cartesian3.fromDegrees(longitude + radiusLongitude, nextLatitude, elevation),
        ],
        width: clamp(presentation.gridLineWidth, 1, 8),
        material: lineColor,
      },
    });
    entities.add({
      polyline: {
        positions: [
          Cartesian3.fromDegrees(nextLongitude, latitude - radiusLatitude, elevation),
          Cartesian3.fromDegrees(nextLongitude, latitude + radiusLatitude, elevation),
        ],
        width: clamp(presentation.gridLineWidth, 1, 8),
        material: lineColor,
      },
    });
  }

  if (!presentation.gridDotsEnabled) return;
  const dotStride = Math.max(1, Math.ceil((stepsPerSide * 2 + 1) / 25));
  for (let row = -stepsPerSide; row <= stepsPerSide; row += dotStride) {
    for (let column = -stepsPerSide; column <= stepsPerSide; column += dotStride) {
      entities.add({
        position: Cartesian3.fromDegrees(longitude + column * deltaLongitude, latitude + row * deltaLatitude, elevation),
        point: {
          pixelSize: clamp(presentation.gridDotsSize, 2, 28),
          color: dotColor,
          disableDepthTestDistance: Number.POSITIVE_INFINITY,
        },
      });
    }
  }
}

function applyPresentation(
  viewer: Viewer,
  imageryLayer: ImageryLayer | null,
  buildings: Cesium3DTileset | null,
  terrain: { world: CesiumTerrainProvider | null; ellipsoid: EllipsoidTerrainProvider },
  presentation: MapPresentation,
) {
  if (imageryLayer) {
    imageryLayer.show = presentation.imageryVisible && !presentation.monochrome;
    imageryLayer.brightness = presentation.imageryBrightness / 100;
    imageryLayer.contrast = presentation.imageryContrast / 100;
    imageryLayer.saturation = presentation.imagerySaturation / 100;
    imageryLayer.gamma = presentation.imageryGamma / 100;
    imageryLayer.hue = CesiumMath.toRadians(presentation.imageryHue);
    imageryLayer.alpha = presentation.imageryAlpha / 100;
  }
  if (buildings) {
    buildings.show = presentation.buildingsVisible;
    buildings.maximumScreenSpaceError = presentation.buildingsDetail;
    buildings.style = new Cesium3DTileStyle({
      color: `color('${presentation.buildingsColor}', ${presentation.buildingsOpacity})`,
    });
  }
  viewer.terrainProvider = presentation.terrainEnabled && terrain.world ? terrain.world : terrain.ellipsoid;
  viewer.scene.globe.show = true;
  viewer.scene.globe.baseColor = Color.fromCssColorString(presentation.monochrome ? presentation.monochromeColor : presentation.globeColor);
  viewer.scene.globe.enableLighting = presentation.sunEnabled;
  (viewer.scene as unknown as { verticalExaggeration?: number }).verticalExaggeration = clamp(presentation.terrainExaggeration, 0.25, 3);
  viewer.scene.backgroundColor = Color.fromCssColorString(presentation.backgroundColor);
  viewer.scene.fog.enabled = presentation.fogEnabled;
  viewer.scene.fog.density = clamp(presentation.fogDensity / 10_000, 0, 0.01);
  const atmosphere = viewer.scene.skyAtmosphere;
  if (atmosphere) {
    atmosphere.show = presentation.atmosphereEnabled;
    atmosphere.hueShift = clamp(presentation.atmosphereHue / 100, -1, 1);
    atmosphere.saturationShift = clamp(presentation.atmosphereSaturation / 100, -1, 1);
    atmosphere.brightnessShift = clamp(presentation.atmosphereBrightness / 100, -1, 1);
  }
  viewer.shadows = presentation.shadowsEnabled;
  const sunDate = JulianDate.toDate(JulianDate.now());
  sunDate.setUTCHours(clamp(Math.round(presentation.sunHour), 0, 24), 0, 0, 0);
  viewer.clock.currentTime = JulianDate.fromDate(sunDate);
  viewer.clock.shouldAnimate = false;
  const sun = new SunLight();
  (sun as unknown as { intensity?: number }).intensity = clamp(presentation.sunIntensity / 100, 0, 2);
  viewer.scene.light = sun;
  viewer.scene.requestRender();
}

export function CesiumMapRenderer({
  onSelect,
  onGatewayHealth,
  onCameraChange,
  initialCamera,
  presentation,
}: {
  onSelect?: (entityId: string) => void;
  onGatewayHealth?: (health: MapGatewayHealth | null) => void;
  onCameraChange?: (camera: MapCameraView) => void;
  initialCamera?: MapCameraView;
  presentation: MapPresentation;
}) {
  const containerRef = useRef<HTMLDivElement>(null);
  const creditContainerRef = useRef<HTMLDivElement>(null);
  const viewerRef = useRef<Viewer | null>(null);
  const imageryLayerRef = useRef<ImageryLayer | null>(null);
  const buildingsRef = useRef<Cesium3DTileset | null>(null);
  const terrainRef = useRef<{ world: CesiumTerrainProvider | null; ellipsoid: EllipsoidTerrainProvider } | null>(null);
  const rebuildGridRef = useRef<(() => void) | null>(null);
  const presentationRef = useRef(presentation);
  const onCameraChangeRef = useRef(onCameraChange);

  useEffect(() => {
    onCameraChangeRef.current = onCameraChange;
  }, [onCameraChange]);

  useEffect(() => {
    presentationRef.current = presentation;
    if (viewerRef.current && terrainRef.current) applyPresentation(viewerRef.current, imageryLayerRef.current, buildingsRef.current, terrainRef.current, presentation);
    rebuildGridRef.current?.();
  }, [presentation]);

  useEffect(() => {
    let viewer: Viewer | undefined;
    let handler: ScreenSpaceEventHandler | undefined;
    let resizeObserver: ResizeObserver | undefined;
    let removeGridCameraListener: (() => void) | undefined;
    let cancelled = false;

    const start = async () => {
      try {
        const response = await fetch("/api/map/runtime-config");
        const config = response.ok ? await response.json() as RuntimeConfig : null;
        if (config?.gatewayHealthUrl) {
          void fetch(config.gatewayHealthUrl)
            .then((healthResponse) => healthResponse.ok ? healthResponse.json() as Promise<MapGatewayHealth> : null)
            .then((health) => { if (!cancelled) onGatewayHealth?.(health); })
            .catch(() => { if (!cancelled) onGatewayHealth?.(null); });
        }
        if (!containerRef.current || cancelled) return;

        viewer = new Viewer(containerRef.current, {
          animation: false,
          baseLayer: false,
          baseLayerPicker: false,
          fullscreenButton: false,
          geocoder: false,
          homeButton: false,
          infoBox: false,
          navigationHelpButton: false,
          sceneModePicker: false,
          selectionIndicator: false,
          timeline: false,
          requestRenderMode: true,
          maximumRenderTimeChange: Number.POSITIVE_INFINITY,
          // Sandbox-only: Cesium writes credits into a dedicated, visually
          // suppressed container. Runtime attribution metadata is preserved;
          // external and commercial surfaces must provide visible credits.
          creditContainer: creditContainerRef.current ?? undefined,
        });
        const resourceProxy = config?.resourceProxyBase ? new DefaultProxy(config.resourceProxyBase) : undefined;
        const buildResource = (url: string, accessToken?: string) => {
          // Put cache intent into the upstream URL itself. Cesium providers
          // derive child resources (metadata, imagery tiles, terrain and 3D
          // tiles) from this URL; DefaultProxy then forwards the exact intent
          // to Gateway for every derived request.
          const routedUrl = new URL(url);
          if (!presentationRef.current.cacheEnabled) routedUrl.searchParams.set("nodedc_cache_mode", "passthrough");
          if (presentationRef.current.cacheRefresh) routedUrl.searchParams.set("nodedc_cache_refresh", "1");
          return new Resource({
          url: routedUrl.toString(),
          queryParameters: {
            ...(accessToken ? { access_token: accessToken } : {}),
          },
          proxy: resourceProxy,
        });
        };
        const loadEndpoint = async (assetId: string) => {
          if (!config?.gatewayReady) throw new Error("map_gateway_not_ready");
          const endpointResponse = await fetch(`${config.assetEndpointBase}/${assetId}/endpoint`);
          if (!endpointResponse.ok) throw new Error(`Map Gateway asset ${assetId}: ${endpointResponse.status}`);
          return endpointResponse.json() as Promise<IonAssetEndpoint>;
        };
        const endpoint = await loadEndpoint("2");
        if (endpoint.externalType !== "BING" || !endpoint.options?.url || !endpoint.options?.key) throw new Error("cesium_live_imagery_endpoint_invalid");
        const imageryProvider = await BingMapsImageryProvider.fromUrl(buildResource(endpoint.options.url), {
          key: endpoint.options.key,
          mapStyle: (endpoint.options.mapStyle || "Aerial") as BingMapsStyle,
          tileProtocol: "https",
        });
        for (const attribution of endpoint.attributions) if (attribution.html) viewer.creditDisplay.addStaticCredit(new Credit(attribution.html, attribution.collapsible));
        const imageryLayer = viewer.imageryLayers.addImageryProvider(imageryProvider);
        const gridDataSource = new CustomDataSource("nodedc-map-grid");
        viewer.dataSources.add(gridDataSource);
        const terrain = { world: null as CesiumTerrainProvider | null, ellipsoid: new EllipsoidTerrainProvider() };
        viewer.terrainProvider = terrain.ellipsoid;
        viewer.scene.globe.depthTestAgainstTerrain = true;
        addFixtureEntities(viewer);
        viewerRef.current = viewer;
        imageryLayerRef.current = imageryLayer;
        terrainRef.current = terrain;
        const rebuildGrid = () => rebuildElevatedGrid(viewer!, gridDataSource, presentationRef.current);
        rebuildGridRef.current = rebuildGrid;
        removeGridCameraListener = viewer.camera.moveEnd.addEventListener(() => {
          rebuildGrid();
          onCameraChangeRef.current?.(getCameraView(viewer!));
        });

        if (config?.gatewayReady) {
          void loadEndpoint("1").then(async (terrainEndpoint) => {
            if (!terrainEndpoint.url || !terrainEndpoint.accessToken) throw new Error("terrain_endpoint_invalid");
            const terrainResource = buildResource(terrainEndpoint.url, terrainEndpoint.accessToken);
            const world = await CesiumTerrainProvider.fromUrl(terrainResource, { requestVertexNormals: true, requestWaterMask: true });
            if (cancelled || !viewer || viewer.isDestroyed()) return;
            terrain.world = world;
            for (const attribution of terrainEndpoint.attributions) if (attribution.html) viewer.creditDisplay.addStaticCredit(new Credit(attribution.html, attribution.collapsible));
            applyPresentation(viewer, imageryLayer, buildingsRef.current, terrain, presentationRef.current);
          }).catch(() => undefined);
          void loadEndpoint("96188").then(async (buildingsEndpoint) => {
            if (!buildingsEndpoint.url || !buildingsEndpoint.accessToken) throw new Error("buildings_endpoint_invalid");
            const buildingsResource = buildResource(buildingsEndpoint.url, buildingsEndpoint.accessToken);
            const buildings = await Cesium3DTileset.fromUrl(buildingsResource);
            if (cancelled || !viewer || viewer.isDestroyed()) return;
            viewer.scene.primitives.add(buildings);
            buildingsRef.current = buildings;
            for (const attribution of buildingsEndpoint.attributions) if (attribution.html) viewer.creditDisplay.addStaticCredit(new Credit(attribution.html, attribution.collapsible));
            applyPresentation(viewer, imageryLayer, buildings, terrain, presentationRef.current);
          }).catch(() => undefined);
          if (config.gaussianSplatsReady && config.gaussianAssetId) {
            const gaussianEndpoint = await loadEndpoint(config.gaussianAssetId);
            if (!gaussianEndpoint.url || !gaussianEndpoint.accessToken) throw new Error("gaussian_endpoint_invalid");
            const gaussianResource = buildResource(gaussianEndpoint.url, gaussianEndpoint.accessToken);
            viewer.scene.primitives.add(await Cesium3DTileset.fromUrl(gaussianResource));
          }
        }

        applyPresentation(viewer, imageryLayer, buildingsRef.current, terrain, presentationRef.current);

        if (initialCamera) {
          viewer.camera.setView({
            destination: Cartesian3.fromDegrees(initialCamera.longitude, initialCamera.latitude, initialCamera.height),
            orientation: {
              heading: initialCamera.heading,
              pitch: initialCamera.pitch,
              roll: initialCamera.roll,
            },
          });
        } else {
          const center = toCartesian(sceneFixture.viewport.center as Position);
          viewer.camera.lookAt(center, new HeadingPitchRange(
            CesiumMath.toRadians(sceneFixture.viewport.heading),
            CesiumMath.toRadians(sceneFixture.viewport.pitch),
            sceneFixture.viewport.range,
          ));
          viewer.camera.lookAtTransform(Matrix4.IDENTITY);
        }
        rebuildGrid();
        onCameraChangeRef.current?.(getCameraView(viewer));

        handler = new ScreenSpaceEventHandler(viewer.scene.canvas);
        handler.setInputAction((movement: { position: Cartesian2 }) => {
          const picked = viewer?.scene.pick(movement.position);
          const entity = picked?.id instanceof Entity ? picked.id : undefined;
          if (entity?.id) onSelect?.(entity.id);
        }, ScreenSpaceEventType.LEFT_CLICK);
        resizeObserver = new ResizeObserver(() => {
          if (!viewer || viewer.isDestroyed()) return;
          viewer.resize();
          viewer.scene.requestRender();
        });
        resizeObserver.observe(containerRef.current);
      } catch {
        // The canvas stays available even if an optional provider fails. Its
        // detailed state belongs to the Inspector, not to a map overlay label.
      }
    };

    void start();
    return () => {
      cancelled = true;
      resizeObserver?.disconnect();
      removeGridCameraListener?.();
      handler?.destroy();
      if (viewer && !viewer.isDestroyed()) viewer.destroy();
      viewerRef.current = null;
      imageryLayerRef.current = null;
      buildingsRef.current = null;
      terrainRef.current = null;
      rebuildGridRef.current = null;
    };
  }, [onGatewayHealth, onSelect]);

  return (
    <div className="catalog-cesium-map">
      <div ref={containerRef} className="catalog-cesium-map__canvas" />
      <div ref={creditContainerRef} className="catalog-cesium-map__credits" aria-hidden="true" />
    </div>
  );
}
