import {
  Suspense,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import { useFrame, useThree } from "@react-three/fiber";
import { CHUNK_CONFIG } from "@/data/world/chunkStreamingConfig";
import { selectMapModelPathByDistance } from "@/data/world/mapLodConfig";
import { useCameraMode } from "@/hooks/debug/useCameraMode";
import { useSceneMode } from "@/hooks/debug/useSceneMode";
import {
  useGraphicsPreset,
  useGraphicsPresetConfig,
} from "@/hooks/world/useGraphicsSettings";
import { useVisibleWorldChunks } from "@/hooks/world/useVisibleWorldChunks";
import {
  isMapModelVisible,
  useMapPerformanceStore,
} from "@/managers/stores/useMapPerformanceStore";
import { useGameStore } from "@/managers/stores/useGameStore";
import { InstancedMapAsset } from "@/world/map-instancing/InstancedMapAsset";
import {
  MAP_INSTANCING_ASSETS,
  MAP_INSTANCING_ASSET_TYPES,
  type MapInstancingAssetConfig,
  type MapInstancingAssetType,
} from "@/data/world/mapInstancingConfig";
import { REPAIR_MISSION_ANCHOR_IDS } from "@/data/gameplay/repairMissionAnchors";
import { isRepairGameStep } from "@/types/gameplay/repairMission";
import { useMapInstancingData } from "@/hooks/world/useMapInstancingData";
import type { MapAssetInstance } from "@/types/map/mapScene";
import type { GraphicsPreset } from "@/data/world/graphicsConfig";
import { createWorldInstanceChunks } from "@/utils/world/chunkInstances";

interface MapInstancingSystemProps {
  onlyMapName?: string | null;
  streaming?: boolean;
}

interface MapAssetChunk {
  key: string;
  config: MapInstancingAssetConfig;
  centerX: number;
  centerZ: number;
  instances: MapAssetInstance[];
}

function createMapAssetChunks(
  type: MapInstancingAssetType,
  config: MapInstancingAssetConfig,
  instances: MapAssetInstance[],
): MapAssetChunk[] {
  return createWorldInstanceChunks(instances).map((chunk) => {
    return {
      key: `${type}:${chunk.chunkKey}`,
      config,
      centerX: chunk.centerX,
      centerZ: chunk.centerZ,
      instances: chunk.instances,
    };
  });
}

function areChunkModelPathsEqual(
  a: ReadonlyMap<string, string>,
  b: ReadonlyMap<string, string>,
): boolean {
  return (
    a.size === b.size && [...a].every(([key, value]) => b.get(key) === value)
  );
}

function getNearestChunkInstanceDistance(
  chunk: MapAssetChunk,
  cameraX: number,
  cameraZ: number,
): number {
  return chunk.instances.reduce((nearestDistance, instance) => {
    const distance = Math.hypot(
      instance.position[0] - cameraX,
      instance.position[2] - cameraZ,
    );

    return Math.min(nearestDistance, distance);
  }, Number.POSITIVE_INFINITY);
}

function useChunkModelPaths(
  chunks: readonly MapAssetChunk[],
  preset: GraphicsPreset,
): ReadonlyMap<string, string> {
  const camera = useThree((state) => state.camera);
  const lastUpdateRef = useRef(-CHUNK_CONFIG.updateInterval);
  const modelPathsRef = useRef<Map<string, string>>(new Map());
  const [modelPaths, setModelPaths] = useState<ReadonlyMap<string, string>>(
    () => new Map(),
  );

  const updateModelPaths = useCallback(() => {
    const cameraX = camera.position.x;
    const cameraZ = camera.position.z;
    const nextModelPaths = new Map<string, string>();

    for (const chunk of chunks) {
      const distance = getNearestChunkInstanceDistance(chunk, cameraX, cameraZ);
      const modelPath = selectMapModelPathByDistance({
        distance,
        modelName: chunk.config.mapName,
        modelPath: chunk.config.modelPath,
        preset,
      });

      nextModelPaths.set(chunk.key, modelPath);
    }

    if (areChunkModelPathsEqual(nextModelPaths, modelPathsRef.current)) return;

    modelPathsRef.current = nextModelPaths;
    setModelPaths(nextModelPaths);
  }, [camera, chunks, preset]);

  useEffect(() => {
    updateModelPaths();
  }, [updateModelPaths]);

  useFrame(({ clock }) => {
    const now = clock.elapsedTime * 1000;
    if (now - lastUpdateRef.current < CHUNK_CONFIG.updateInterval) return;
    lastUpdateRef.current = now;

    updateModelPaths();
  });

  return modelPaths;
}

export function MapInstancingSystem({
  onlyMapName = null,
  streaming = true,
}: MapInstancingSystemProps): React.JSX.Element | null {
  const camera = useThree((state) => state.camera);
  const cameraMode = useCameraMode();
  const sceneMode = useSceneMode();
  const graphicsPreset = useGraphicsPreset();
  const graphicsPresetConfig = useGraphicsPresetConfig();
  const groups = useMapPerformanceStore((state) => state.groups);
  const models = useMapPerformanceStore((state) => state.models);
  const { data, isLoading } = useMapInstancingData();
  const mainState = useGameStore((state) => state.mainState);
  const pylonStep = useGameStore((state) => state.pylon.currentStep);
  const streamingEnabled =
    streaming &&
    CHUNK_CONFIG.enabled &&
    graphicsPresetConfig.chunkStreamingEnabled &&
    sceneMode === "game" &&
    cameraMode === "player";

  // During the pylon narrative phase (before the pylon is raised), hide the
  // repair:pylon instanced mesh so the PylonDownedPylon component takes its place.
  // Once the pylon is raised (repair-game steps), restore it so the normal model
  // appears upright in the world while the repair mini-game runs.
  const hidePylonAnchorId =
    mainState === "pylon" && !isRepairGameStep(pylonStep)
      ? REPAIR_MISSION_ANCHOR_IDS.pylon
      : undefined;

  const chunks = useMemo(() => {
    if (!data) return [];

    return MAP_INSTANCING_ASSET_TYPES.flatMap((type) => {
      const config = MAP_INSTANCING_ASSETS[type];

      if (onlyMapName && config.mapName !== onlyMapName) return [];

      if (
        !config.enabled ||
        !isMapModelVisible(config.mapName, { groups, models })
      ) {
        return [];
      }

      let instances = data.get(type);
      if (!instances || instances.length === 0) return [];

      // Filter out the repair-mission pylon instance during the narrative phase
      if (hidePylonAnchorId && config.mapName === "pylone") {
        instances = instances.filter((inst) => inst.id !== hidePylonAnchorId);
        if (instances.length === 0) return [];
      }

      return createMapAssetChunks(type, config, instances);
    });
  }, [data, groups, models, onlyMapName, hidePylonAnchorId]);

  const visibleChunks = useVisibleWorldChunks(chunks, streamingEnabled, {
    loadRadius: graphicsPresetConfig.chunkLoadRadius,
    unloadRadius: graphicsPresetConfig.chunkUnloadRadius,
  });
  const chunkModelPaths = useChunkModelPaths(visibleChunks, graphicsPreset);
  const getChunkModelPath = useCallback(
    (chunk: MapAssetChunk): string => {
      const cachedModelPath = chunkModelPaths.get(chunk.key);
      if (cachedModelPath) return cachedModelPath;

      return selectMapModelPathByDistance({
        distance: getNearestChunkInstanceDistance(
          chunk,
          camera.position.x,
          camera.position.z,
        ),
        modelName: chunk.config.mapName,
        modelPath: chunk.config.modelPath,
        preset: graphicsPreset,
      });
    },
    [camera, chunkModelPaths, graphicsPreset],
  );

  if (isLoading || !data) {
    return null;
  }

  return (
    <group name="map-instancing-system">
      {visibleChunks.map((chunk) => {
        const modelPath = getChunkModelPath(chunk);

        return (
          <Suspense key={`${chunk.key}:${modelPath}`} fallback={null}>
            <InstancedMapAsset
              modelPath={modelPath}
              instances={chunk.instances}
              scaleMultiplier={chunk.config.scaleMultiplier}
              castShadow={chunk.config.castShadow}
              receiveShadow={chunk.config.receiveShadow}
            />
          </Suspense>
        );
      })}
    </group>
  );
}
