import { Canvas, createPortal, useFrame, useThree } from "@react-three/fiber";
import { useAnimations, useGLTF } from "@react-three/drei";
import { Suspense, useEffect, useMemo, useRef, type MutableRefObject } from "react";
import * as THREE from "three";
import { getControllerMovement } from "../input/controller";
import { clone as cloneSkeleton } from "three/examples/jsm/utils/SkeletonUtils.js";
import { useGameStore } from "../game/store";
import type { MemberId, PulseKind } from "../game/types";
import { BossMechanicIndicators } from "./boss/BossMechanicIndicators";

const BULL_URL = new URL("../../game_assets/models/claudecraft/creatures/bull.glb", import.meta.url).href;
const SPIDER_URL = new URL("../../game_assets/models/downloaded/low-poly-spider/low-poly-spider.glb", import.meta.url).href;
const SPIDER_TEXTURE_URLS: Record<string, string> = {
  "Spinnen_Bein_tex_2.jpg": new URL("../../game_assets/models/downloaded/low-poly-spider/textures/Spinnen_Bein_tex_2.jpg", import.meta.url).href,
  "SH3.png": new URL("../../game_assets/models/downloaded/low-poly-spider/textures/SH3.png", import.meta.url).href,
  "Spinnen_Bein_tex_COLOR_.jpg": new URL("../../game_assets/models/downloaded/low-poly-spider/textures/Spinnen_Bein_tex_COLOR_.jpg", import.meta.url).href,
  "haar_detail_NRM.jpg": new URL("../../game_assets/models/downloaded/low-poly-spider/textures/haar_detail_NRM.jpg", import.meta.url).href,
};
const DRAGON_URL = new URL("../../game_assets/models/claudecraft/creatures/dragonevolved.glb", import.meta.url).href;
const EMBER_MANTIS_URL = new URL("../../game_assets/models/original/bosses/ember-mantis-duelist/ember_mantis_duelist.glb", import.meta.url).href;
const PARTY_MODEL_URLS: Record<MemberId, string> = {
  aelia: new URL("../../game_assets/models/claudecraft/chars/players/druid.glb", import.meta.url).href,
  brann: new URL("../../game_assets/models/claudecraft/chars/players/knight.glb", import.meta.url).href,
  nia: new URL("../../game_assets/models/claudecraft/chars/players/ranger.glb", import.meta.url).href,
  orin: new URL("../../game_assets/models/claudecraft/chars/players/mage.glb", import.meta.url).href,
  vale: new URL("../../game_assets/models/claudecraft/chars/players/rogue.glb", import.meta.url).href,
};
const PARTY_WEAPON_URLS: Record<MemberId, { right: string; left?: string }> = {
  aelia: { right: new URL("../../game_assets/models/claudecraft/weapons/adv_druid_staff.glb", import.meta.url).href },
  brann: {
    right: new URL("../../game_assets/models/claudecraft/weapons/adv_sword_1handed.glb", import.meta.url).href,
    left: new URL("../../game_assets/models/claudecraft/weapons/shield_badge.glb", import.meta.url).href,
  },
  nia: { right: new URL("../../game_assets/models/claudecraft/weapons/crossbow_2handed.glb", import.meta.url).href },
  orin: {
    right: new URL("../../game_assets/models/claudecraft/weapons/adv_wand.glb", import.meta.url).href,
    left: new URL("../../game_assets/models/claudecraft/weapons/spellbook_open.glb", import.meta.url).href,
  },
  vale: {
    right: new URL("../../game_assets/models/claudecraft/weapons/adv_dagger.glb", import.meta.url).href,
    left: new URL("../../game_assets/models/claudecraft/weapons/adv_dagger.glb", import.meta.url).href,
  },
};
const PARTY_MODEL_SCALES: Record<MemberId, number> = { aelia: 0.62, brann: 0.68, nia: 0.7, orin: 0.64, vale: 0.72 };
const PARTY_ATTACK_CLIPS: Record<MemberId, string> = {
  aelia: "2H_Melee_Attack_Chop",
  brann: "1H_Melee_Attack_Chop",
  nia: "2H_Ranged_Shoot",
  orin: "Spellcast_Shoot",
  vale: "Dualwield_Melee_Attack_Chop",
};
const ARENA_COLUMNS = Array.from({ length: 10 }, (_, index) => {
  const angle = (index / 10) * Math.PI * 2;
  return [Math.sin(angle) * 9.3, Math.cos(angle) * 9.3] as const;
});
const ARENA_TORCH_COLORS = [new THREE.Color("#ff9a4f"), new THREE.Color("#77ddce")] as const;
type GameStoreState = ReturnType<typeof useGameStore.getState>;

function encounterBossAt(state: GameStoreState, bossIndex: number) {
  return bossIndex === 0
    ? { boss: state.boss, motion: state.bossMotion }
    : state.additionalBosses[bossIndex - 1];
}

function targetBossMotion(state: GameStoreState) {
  if (state.boss.hp > 0) return state.bossMotion;
  return state.additionalBosses.find((entry) => entry.boss.hp > 0)?.motion ?? state.bossMotion;
}

function targetBossMotionByInstance(state: GameStoreState, instanceId?: string) {
  if (!instanceId || instanceId === `boss-0-${state.boss.id}`) return targetBossMotion(state);
  return state.additionalBosses.find((entry) => entry.instanceId === instanceId)?.motion ?? targetBossMotion(state);
}

const configureSpiderLoader: NonNullable<Parameters<typeof useGLTF>[3]> = (loader) => {
  loader.manager.setURLModifier((url) => {
    const fileName = url.slice(url.lastIndexOf("/") + 1);
    return SPIDER_TEXTURE_URLS[fileName] ?? url;
  });
};

type ActorAnimationState = "idle" | "walk" | "run" | "attack" | "cast" | "hit" | "death";

type WeaponGrip = "staff" | "sword" | "crossbow" | "wand" | "dagger" | "prop";

const PARTY_WEAPON_GRIPS: Record<MemberId, { right: WeaponGrip; left?: WeaponGrip }> = {
  aelia: { right: "staff" },
  brann: { right: "sword", left: "prop" },
  nia: { right: "crossbow" },
  orin: { right: "wand", left: "prop" },
  vale: { right: "dagger", left: "dagger" },
};

const VARIANT_GRIPS: Record<Exclude<WeaponGrip, "crossbow" | "prop">, { lift: number; maxHeight: number }> = {
  sword: { lift: 0.04, maxHeight: 2 },
  dagger: { lift: 0.04, maxHeight: 1.4 },
  staff: { lift: 0.18, maxHeight: 2.4 },
  wand: { lift: 0.04, maxHeight: 1.2 },
};

function resolveRigNode(root: THREE.Object3D, authoredName: string) {
  return root.getObjectByName(authoredName)
    ?? root.getObjectByName(authoredName.replace(/[[\].:/]/g, ""));
}

function flattenWeaponScene(scene: THREE.Object3D) {
  if (scene.children.length !== 1) return scene;
  const holder = new THREE.Group();
  const child = scene.children[0];
  holder.scale.copy(child.scale);
  child.position.set(0, 0, 0);
  child.rotation.set(0, 0, 0);
  child.scale.set(1, 1, 1);
  scene.remove(child);
  holder.add(child);
  return holder;
}

function prepareHeldWeapon(scene: THREE.Object3D, grip: WeaponGrip, side: "r" | "l") {
  // Shields and spellbooks carry useful authored offsets, so keep their scene transform.
  if (grip === "prop") return scene;

  const weapon = flattenWeaponScene(scene);
  if (grip === "crossbow") {
    weapon.position.set(0.3381, 0.058, 0);
    weapon.quaternion.set(0, 0.7071068, 0, 0.7071067);
    weapon.scale.setScalar(0.7204);
    return weapon;
  }

  const { lift, maxHeight } = VARIANT_GRIPS[grip];
  const bounds = new THREE.Box3().setFromObject(weapon);
  const height = bounds.max.y - bounds.min.y;
  const scale = height > 0.001 ? Math.min(1, maxHeight / height) : 1;
  weapon.position.set(0, lift, 0);
  weapon.quaternion.set(0, side === "l" ? 0 : 1, 0, side === "l" ? 1 : 0);
  weapon.scale.setScalar(scale);
  return weapon;
}

function PartyCharacterModel({
  memberId,
  animationState,
}: {
  memberId: MemberId;
  animationState: MutableRefObject<ActorAnimationState>;
}) {
  const gltf = useGLTF(PARTY_MODEL_URLS[memberId], false, true);
  const actorScene = useMemo(() => cloneSkeleton(gltf.scene), [gltf.scene]);
  const loadout = PARTY_WEAPON_URLS[memberId];
  const grips = PARTY_WEAPON_GRIPS[memberId];
  const rightWeapon = useGLTF(loadout.right, false, true);
  const leftWeapon = useGLTF(loadout.left ?? loadout.right, false, true);
  const rightHandSlot = resolveRigNode(actorScene, "handslot.r");
  const leftHandSlot = resolveRigNode(actorScene, "handslot.l");
  const rightWeaponScene = useMemo(
    () => prepareHeldWeapon(rightWeapon.scene.clone(true), grips.right, "r"),
    [grips.right, rightWeapon.scene],
  );
  const leftWeaponScene = useMemo(
    () => loadout.left ? prepareHeldWeapon(leftWeapon.scene.clone(true), grips.left ?? grips.right, "l") : null,
    [grips.left, grips.right, leftWeapon.scene, loadout.left],
  );
  const { actions } = useAnimations(gltf.animations, actorScene);
  const activeClip = useRef<string | undefined>(undefined);

  useEffect(() => {
    actorScene.traverse((object) => {
      if (object instanceof THREE.Mesh) {
        object.castShadow = true;
        object.receiveShadow = true;
      }
    });
  }, [actorScene]);

  useEffect(() => {
    for (const weaponScene of [rightWeaponScene, leftWeaponScene]) {
      if (!weaponScene) continue;
      weaponScene.traverse((object) => {
        if (object instanceof THREE.Mesh) {
          object.castShadow = true;
          object.receiveShadow = true;
          // Animated hand sockets can cross a mesh's original root-space frustum.
          object.frustumCulled = false;
        }
      });
    }
  }, [leftWeaponScene, rightWeaponScene]);

  useFrame(() => {
    const state = animationState.current;
    const clipName = state === "death"
      ? "Death_A"
      : state === "hit"
        ? "Hit_A"
        : state === "run"
          ? "Running_A"
          : state === "walk"
            ? "Walking_A"
            : state === "cast"
              ? "Spellcasting"
              : state === "attack"
                ? PARTY_ATTACK_CLIPS[memberId]
                : "Idle";
    if (activeClip.current === clipName) return;
    const next = actions[clipName];
    if (!next) return;
    if (activeClip.current) actions[activeClip.current]?.fadeOut(0.16);
    next.reset().setEffectiveWeight(1).setEffectiveTimeScale(state === "run" ? 1.1 : 1).fadeIn(0.16);
    if (state === "death" || state === "hit" || state === "attack" || state === "cast") {
      next.setLoop(THREE.LoopOnce, 1);
      next.clampWhenFinished = state === "death";
    } else {
      next.setLoop(THREE.LoopRepeat, Number.POSITIVE_INFINITY);
      next.clampWhenFinished = false;
    }
    next.play();
    activeClip.current = clipName;
  });

  return (
    <>
      <primitive object={actorScene} scale={PARTY_MODEL_SCALES[memberId]} />
      {rightHandSlot && createPortal(<primitive object={rightWeaponScene} />, rightHandSlot)}
      {leftWeaponScene && leftHandSlot && createPortal(<primitive object={leftWeaponScene} />, leftHandSlot)}
    </>
  );
}

function Arena() {
  const pillarInstances = useRef<THREE.InstancedMesh>(null);
  const torchInstances = useRef<THREE.InstancedMesh>(null);

  useEffect(() => {
    const pillars = pillarInstances.current;
    const torches = torchInstances.current;
    if (!pillars || !torches) return;
    const matrix = new THREE.Matrix4();
    ARENA_COLUMNS.forEach(([x, z], index) => {
      matrix.makeTranslation(x, 1.1, z - 1);
      pillars.setMatrixAt(index, matrix);
      matrix.makeTranslation(x, 2.6, z - 1);
      torches.setMatrixAt(index, matrix);
      torches.setColorAt(index, ARENA_TORCH_COLORS[index % ARENA_TORCH_COLORS.length]);
    });
    pillars.instanceMatrix.needsUpdate = true;
    torches.instanceMatrix.needsUpdate = true;
    if (torches.instanceColor) torches.instanceColor.needsUpdate = true;
  }, []);

  return (
    <group>
      <mesh position={[0, -0.45, -1]} receiveShadow>
        <cylinderGeometry args={[10.5, 11.2, 0.8, 48]} />
        <meshStandardMaterial color="#182420" roughness={0.92} />
      </mesh>
      <mesh position={[0, -0.015, -1]} rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
        <ringGeometry args={[4.8, 5.05, 64]} />
        <meshBasicMaterial color="#765c32" transparent opacity={0.55} />
      </mesh>
      <mesh position={[0, -0.01, -1]} rotation={[-Math.PI / 2, 0, 0]}>
        <circleGeometry args={[2.1, 48]} />
        <meshStandardMaterial color="#27342d" roughness={1} />
      </mesh>
      <instancedMesh ref={pillarInstances} args={[undefined, undefined, ARENA_COLUMNS.length]} castShadow receiveShadow>
        <cylinderGeometry args={[0.38, 0.5, 2.4, 6]} />
        <meshStandardMaterial color="#26342f" roughness={0.8} />
      </instancedMesh>
      <instancedMesh ref={torchInstances} args={[undefined, undefined, ARENA_COLUMNS.length]}>
        <octahedronGeometry args={[0.2, 0]} />
        <meshBasicMaterial />
      </instancedMesh>
      <pointLight color="#dd7b38" intensity={2.2} distance={7} position={[-6, 2.7, -1]} />
      <pointLight color="#6fc9ba" intensity={2.2} distance={7} position={[6, 2.7, -1]} />
      <gridHelper args={[22, 22, "#2c4039", "#1b2925"]} position={[0, 0.01, -1]} />
    </group>
  );
}

function Character({ memberId, selected = false }: { memberId: Exclude<MemberId, "aelia">; selected?: boolean }) {
  const group = useRef<THREE.Group>(null);
  const animationState = useRef<ActorAnimationState>("idle");
  useEffect(() => {
    const start = useGameStore.getState().partyPositions[memberId];
    group.current?.position.set(start[0], 0.025, start[1]);
  }, [memberId]);

  useFrame((_, delta) => {
    if (!group.current) return;
    const state = useGameStore.getState();
    const target = state.partyPositions[memberId];
    const dx = target[0] - group.current.position.x;
    const dz = target[1] - group.current.position.z;
    const moving = Math.hypot(dx, dz) > 0.015;
    const movementBlend = 1 - Math.pow(0.002, delta);
    group.current.position.x = THREE.MathUtils.lerp(group.current.position.x, target[0], movementBlend);
    group.current.position.z = THREE.MathUtils.lerp(group.current.position.z, target[1], movementBlend);

    const member = state.party.find((entry) => entry.id === memberId)!;
    const knocked = member.knockedUntil > state.time;
    const visualAction = state.partyCombat.combatants[memberId].visualAction;
    const targetMotion = targetBossMotionByInstance(state, visualAction?.targetInstanceId);
    const attacking = state.phase === "combat"
      && visualAction !== null
      && visualAction.endsAt > state.time;
    animationState.current = member.hp <= 0
      ? "death"
      : knocked
        ? "hit"
        : attacking
          ? "attack"
          : moving
            ? memberId === "vale" ? "run" : "walk"
            : "idle";
    if (!knocked && member.hp > 0 && (state.phase === "combat" || moving)) {
      const faceBoss = state.phase === "combat";
      const facingX = faceBoss ? targetMotion.position[0] - group.current.position.x : dx;
      const facingZ = faceBoss ? targetMotion.position[1] - group.current.position.z : dz;
      const targetAngle = Math.atan2(facingX, facingZ);
      const angleDelta = Math.atan2(
        Math.sin(targetAngle - group.current.rotation.y),
        Math.cos(targetAngle - group.current.rotation.y),
      );
      group.current.rotation.y += angleDelta * (1 - Math.pow(0.0008, delta));
    }
    group.current.position.y = THREE.MathUtils.lerp(group.current.position.y, 0.025, 0.16);
  });

  return (
    <group ref={group} rotation={[0, Math.PI, 0]}>
      <PartyCharacterModel memberId={memberId} animationState={animationState} />
      {selected && (
        <mesh position={[0, 0.018, 0]} rotation={[-Math.PI / 2, 0, 0]}>
          <ringGeometry args={[0.55, 0.66, 32]} />
          <meshBasicMaterial color="#f6d477" transparent opacity={0.95} />
        </mesh>
      )}
    </group>
  );
}

function PlayerCharacter() {
  const group = useRef<THREE.Group>(null);
  const animationState = useRef<ActorAnimationState>("idle");
  const keys = useRef(new Set<string>());
  const scenePulse = useGameStore((state) => state.scenePulse);
  const selected = useGameStore((state) => state.selectedMemberId === "aelia");
  const setPlayerPosition = useGameStore((state) => state.setPlayerPosition);
  const { camera } = useThree();
  const broadcastTimer = useRef(0);
  const castingUntil = useRef(0);
  const desiredCameraPosition = useMemo(() => new THREE.Vector3(), []);

  useEffect(() => {
    const start = useGameStore.getState().partyPositions.aelia;
    group.current?.position.set(start[0], 0.025, start[1]);
  }, []);

  useEffect(() => {
    if (["renew", "shield", "purify", "radiance", "barrier"].includes(scenePulse.kind)) {
      castingUntil.current = performance.now() + 700;
    }
  }, [scenePulse]);

  useEffect(() => {
    const down = (event: KeyboardEvent) => {
      const key = event.key.toLowerCase();
      keys.current.add(key);
      if (event.repeat || !group.current) return;
      const state = useGameStore.getState();
      if (state.phase !== "combat" || state.paused || state.activeCast || state.party[0].hp <= 0 || state.party[0].knockedUntil > state.time) return;
      const nudgeX = Number(key === "d") - Number(key === "a");
      const nudgeZ = Number(key === "s") - Number(key === "w");
      if (!nudgeX && !nudgeZ) return;
      group.current.position.x = THREE.MathUtils.clamp(group.current.position.x + nudgeX * 0.18, -7.2, 7.2);
      group.current.position.z = THREE.MathUtils.clamp(group.current.position.z + nudgeZ * 0.18, -4.8, 7.2);
      setPlayerPosition([group.current.position.x, group.current.position.z]);
    };
    const up = (event: KeyboardEvent) => keys.current.delete(event.key.toLowerCase());
    window.addEventListener("keydown", down);
    window.addEventListener("keyup", up);
    return () => {
      window.removeEventListener("keydown", down);
      window.removeEventListener("keyup", up);
    };
  }, [setPlayerPosition]);

  useFrame((_, delta) => {
    if (!group.current) return;
    let inputX = 0;
    let inputZ = 0;
    const state = useGameStore.getState();
    const knocked = state.party[0].knockedUntil > state.time;
    const player = state.party[0];
    if (state.phase === "combat" && !state.paused && !state.activeCast && player.hp > 0 && !knocked) {
      inputX = Number(keys.current.has("d")) - Number(keys.current.has("a"));
      inputZ = Number(keys.current.has("s")) - Number(keys.current.has("w"));
      const controller = getControllerMovement();
      inputX += controller.x;
      inputZ += controller.y;
    }
    const length = Math.hypot(inputX, inputZ);
    if (length > 0.05) {
      const speed = 4.6 * delta / Math.max(1, length);
      group.current.position.x = THREE.MathUtils.clamp(group.current.position.x + inputX * speed, -7.2, 7.2);
      group.current.position.z = THREE.MathUtils.clamp(group.current.position.z + inputZ * speed, -4.8, 7.2);
      group.current.rotation.y = Math.atan2(inputX, inputZ);
    } else if (state.phase === "combat" && player.hp > 0 && !knocked) {
      const boss = targetBossMotion(state).position;
      const targetAngle = Math.atan2(boss[0] - group.current.position.x, boss[1] - group.current.position.z);
      const angleDelta = Math.atan2(
        Math.sin(targetAngle - group.current.rotation.y),
        Math.cos(targetAngle - group.current.rotation.y),
      );
      group.current.rotation.y += angleDelta * (1 - Math.pow(0.0008, delta));
    }
    animationState.current = player.hp <= 0
      ? "death"
      : knocked
        ? "hit"
        : state.activeCast
          ? "cast"
          : length > 0.05
            ? "run"
            : performance.now() < castingUntil.current
            ? "cast"
            : "idle";
    group.current.position.y = THREE.MathUtils.lerp(group.current.position.y, 0.025, 0.16);

    desiredCameraPosition.set(group.current.position.x * 0.45, 5.1, group.current.position.z + 7.7);
    camera.position.lerp(desiredCameraPosition, 1 - Math.pow(0.002, delta));
    camera.lookAt(group.current.position.x * 0.55, 0.65, group.current.position.z - 2.8);

    broadcastTimer.current += delta;
    if (broadcastTimer.current > 0.15) {
      setPlayerPosition([group.current.position.x, group.current.position.z]);
      broadcastTimer.current = 0;
    }
  });

  return (
    <group ref={group} rotation={[0, Math.PI, 0]}>
      <PartyCharacterModel memberId="aelia" animationState={animationState} />
      {selected && (
        <mesh position={[0, 0.018, 0]} rotation={[-Math.PI / 2, 0, 0]}>
          <ringGeometry args={[0.55, 0.66, 32]} />
          <meshBasicMaterial color="#f6d477" transparent opacity={0.95} />
        </mesh>
      )}
      <pointLight color="#f7d873" intensity={0.8} distance={2.5} position={[0.45, 1.12, 0]} />
    </group>
  );
}

function Party() {
  const party = useGameStore((state) => state.party);
  const selected = useGameStore((state) => state.selectedMemberId);
  return (
    <>
      <PlayerCharacter />
      {party.slice(1).map((member) => (
        <Character
          key={member.id}
          memberId={member.id as Exclude<MemberId, "aelia">}
          selected={selected === member.id}
        />
      ))}
    </>
  );
}

function PartyFallback() {
  const positions = useGameStore((state) => state.partyPositions);
  return (
    <>
      {(Object.keys(positions) as MemberId[]).map((memberId) => (
        <mesh key={memberId} castShadow position={[positions[memberId][0], 0.8, positions[memberId][1]]}>
          <capsuleGeometry args={[0.3, 0.75, 4, 8]} />
          <meshStandardMaterial color="#79998c" roughness={0.8} />
        </mesh>
      ))}
    </>
  );
}

function BossFallback({ bossIndex }: { bossIndex: number }) {
  const boss = useGameStore((state) => bossIndex === 0 ? state.boss : state.additionalBosses[bossIndex - 1]?.boss);
  const motion = useGameStore((state) => bossIndex === 0 ? state.bossMotion : state.additionalBosses[bossIndex - 1]?.motion);
  if (!boss || !motion) return null;
  const position = motion.position;
  const bossId = boss.id;
  return (
    <mesh castShadow position={[position[0], 1.1, position[1]]}>
      <dodecahedronGeometry args={[1.1, 0]} />
      <meshStandardMaterial color={bossId === "vexa" ? "#56306f" : bossId === "cindermaw" ? "#9d4c24" : bossId === "ember-mantis-duelist" ? "#a42d18" : "#7b3928"} emissive="#3a100c" emissiveIntensity={0.5} />
    </mesh>
  );
}

function BullBoss({ bossIndex }: { bossIndex: number }) {
  const phase = useGameStore((state) => state.phase);
  const motionMode = useGameStore((state) => (bossIndex === 0 ? state.bossMotion : state.additionalBosses[bossIndex - 1]?.motion)?.mode ?? "holding");
  const bossHp = useGameStore((state) => (bossIndex === 0 ? state.boss : state.additionalBosses[bossIndex - 1]?.boss)?.hp ?? 0);
  const defeated = bossHp <= 0;
  const group = useRef<THREE.Group>(null);
  const gltf = useGLTF(BULL_URL, false, true);
  const bullScene = useMemo(() => cloneSkeleton(gltf.scene), [gltf.scene]);
  const { actions } = useAnimations(gltf.animations, bullScene);
  const targetPosition = useMemo(() => new THREE.Vector3(), []);

  useEffect(() => {
    bullScene.traverse((object) => {
      if (object instanceof THREE.Mesh) {
        object.castShadow = true;
        object.receiveShadow = true;
      }
    });
  }, [bullScene]);

  const clipName = phase === "victory" || defeated
    ? "Death"
    : motionMode === "telegraph"
      ? "Idle_Headlow"
      : motionMode === "pouncing"
        ? "Gallop_Jump"
        : motionMode === "charging" || motionMode === "returning"
        ? "Gallop"
          : motionMode === "stacking"
            ? "Idle_Headlow"
            : "Idle";

  useEffect(() => {
    const next = actions[clipName];
    if (!next) return;
    for (const action of Object.values(actions)) action?.fadeOut(0.18);
    next.reset().setEffectiveWeight(1).setEffectiveTimeScale(motionMode === "charging" ? 1.3 : 1).fadeIn(0.18).play();
    if (clipName === "Death") {
      next.setLoop(THREE.LoopOnce, 1);
      next.clampWhenFinished = true;
    } else {
      next.setLoop(THREE.LoopRepeat, Number.POSITIVE_INFINITY);
    }
    return () => { next.fadeOut(0.18); };
  }, [actions, clipName, motionMode]);

  useFrame((_, delta) => {
    if (!group.current) return;
    const state = useGameStore.getState();
    const current = encounterBossAt(state, bossIndex);
    if (!current) return;
    const motion = current.motion;
    targetPosition.set(motion.position[0], 0.03, motion.position[1]);
    group.current.position.lerp(targetPosition, 1 - Math.pow(0.00001, delta));

    if (motion.mode === "stacking") {
      group.current.rotation.y += (Math.PI * 2 / 5) * delta;
      return;
    }

    let facingX = state.partyPositions.brann[0] - motion.position[0];
    let facingZ = state.partyPositions.brann[1] - motion.position[1];
    if (motion.mode === "telegraph" || motion.mode === "charging" || motion.mode === "pouncing") {
      facingX = motion.chargeEnd[0] - motion.chargeStart[0];
      facingZ = motion.chargeEnd[1] - motion.chargeStart[1];
    } else if (motion.mode === "returning") {
      facingX = state.partyPositions.brann[0] + motion.formationOffsetX - motion.position[0];
      facingZ = state.partyPositions.brann[1] - 4.25 - motion.position[1];
    }
    if (Math.hypot(facingX, facingZ) > 0.01) {
      const targetAngle = Math.atan2(facingX, facingZ);
      const difference = Math.atan2(Math.sin(targetAngle - group.current.rotation.y), Math.cos(targetAngle - group.current.rotation.y));
      group.current.rotation.y += difference * (1 - Math.pow(0.001, delta));
    }
  });
  if (phase === "briefing") return null;
  return (
    <group ref={group}>
      <primitive object={bullScene} scale={0.81} />
      <pointLight color="#ff9b5c" intensity={2.8} distance={7} position={[0, 2.3, 0.8]} />
    </group>
  );
}

type AlternateBossKind = "vexa" | "cindermaw" | "ember-mantis-duelist";

const ALTERNATE_BOSS_CONFIG = {
  vexa: {
    url: SPIDER_URL,
    scale: 0.022,
    idle: "Spider_Armature|warte_pose",
    move: "Spider_Armature|run_ani_vor",
    attack: "Spider_Armature|Attack",
    special: "Spider_Armature|Jump",
    death: "Spider_Armature|die",
    light: "#bb67ff",
    rotationOffset: Math.PI,
  },
  cindermaw: {
    url: DRAGON_URL,
    scale: 1.15,
    idle: "Flying_Idle",
    move: "Fast_Flying",
    attack: "Headbutt",
    special: "Punch",
    death: "Death",
    light: "#ff8742",
    rotationOffset: 0,
  },
  "ember-mantis-duelist": {
    url: EMBER_MANTIS_URL,
    scale: 0.78,
    idle: "Idle",
    move: "Sidestep",
    attack: "LineSlash",
    special: "CrossSlash",
    death: "Death",
    light: "#ff5a24",
    rotationOffset: 0,
  },
} as const;

function AlternateBoss({ kind, bossIndex }: { kind: AlternateBossKind; bossIndex: number }) {
  const config = ALTERNATE_BOSS_CONFIG[kind];
  const phase = useGameStore((state) => state.phase);
  const motionMode = useGameStore((state) => (bossIndex === 0 ? state.bossMotion : state.additionalBosses[bossIndex - 1]?.motion)?.mode ?? "holding");
  const bossHp = useGameStore((state) => (bossIndex === 0 ? state.boss : state.additionalBosses[bossIndex - 1]?.boss)?.hp ?? 0);
  const defeated = bossHp <= 0;
  const group = useRef<THREE.Group>(null);
  const gltf = useGLTF(config.url, false, true, kind === "vexa" ? configureSpiderLoader : undefined);
  const model = useMemo(() => cloneSkeleton(gltf.scene), [gltf.scene]);
  const { actions } = useAnimations(gltf.animations, model);
  const targetPosition = useMemo(() => new THREE.Vector3(), []);

  useEffect(() => {
    model.traverse((object) => {
      if (object instanceof THREE.Mesh) {
        object.castShadow = true;
        object.receiveShadow = true;
      }
    });
    if (kind === "vexa") {
      const authoredHelperBox = model.getObjectByName("Box");
      if (authoredHelperBox) authoredHelperBox.visible = false;
    }
  }, [kind, model]);

  const clipName = phase === "victory" || defeated
    ? config.death
    : kind === "ember-mantis-duelist"
      ? motionMode === "mantis_sidestep"
        ? config.move
        : motionMode === "mantis_line_telegraph"
          ? config.attack
          : motionMode === "mantis_cross_telegraph"
            ? config.special
            : motionMode === "mantis_recover"
              ? "Recover"
              : config.idle
      : motionMode === "skyfall"
        ? config.move
        : motionMode === "breath_telegraph" || motionMode === "breath_sweeping"
          ? config.special
          : motionMode === "tethering" || motionMode === "venom_cast"
            ? config.attack
            : config.idle;

  useEffect(() => {
    const next = actions[clipName];
    if (!next) return;
    for (const action of Object.values(actions)) action?.fadeOut(0.16);
    const timeScale = kind === "ember-mantis-duelist" && motionMode === "mantis_line_telegraph"
      ? 0.55
      : kind === "ember-mantis-duelist" && motionMode === "mantis_cross_telegraph"
        ? 0.6
        : 1;
    next.reset().setEffectiveWeight(1).setEffectiveTimeScale(timeScale).fadeIn(0.16).play();
    const emberOneShot = kind === "ember-mantis-duelist" && clipName !== config.idle;
    if (phase === "victory" || defeated || emberOneShot) {
      next.setLoop(THREE.LoopOnce, 1);
      next.clampWhenFinished = true;
    } else {
      next.setLoop(THREE.LoopRepeat, Number.POSITIVE_INFINITY);
    }
    return () => { next.fadeOut(0.16); };
  }, [actions, clipName, config.idle, defeated, kind, motionMode, phase]);

  useFrame((_, delta) => {
    if (!group.current) return;
    const state = useGameStore.getState();
    const current = encounterBossAt(state, bossIndex);
    if (!current) return;
    const motion = current.motion;
    const airborne = kind === "cindermaw" && motion.mode === "skyfall";
    targetPosition.set(motion.position[0], airborne ? 3.2 : 0.03, motion.position[1]);
    group.current.position.lerp(targetPosition, 1 - Math.pow(0.00001, delta));

    let targetAngle = Math.atan2(
      state.partyPositions.brann[0] - motion.position[0],
      state.partyPositions.brann[1] - motion.position[1],
    );
    if (kind === "cindermaw" && (motion.mode === "breath_telegraph" || motion.mode === "breath_sweeping")) {
      targetAngle = motion.breathAngle;
    } else if (kind === "ember-mantis-duelist" && (
      motion.mode === "mantis_sidestep"
      || motion.mode === "mantis_line_telegraph"
      || motion.mode === "mantis_cross_telegraph"
    )) {
      const target = state.partyPositions[motion.chargeTargetId];
      targetAngle = Math.atan2(target[0] - motion.position[0], target[1] - motion.position[1]);
    }
    const difference = Math.atan2(
      Math.sin(targetAngle - group.current.rotation.y),
      Math.cos(targetAngle - group.current.rotation.y),
    );
    group.current.rotation.y += difference * (1 - Math.pow(0.001, delta));
  });

  if (phase === "briefing") return null;
  return (
    <group ref={group}>
      <primitive object={model} scale={config.scale} rotation={[0, config.rotationOffset, 0]} />
      <pointLight color={config.light} intensity={2.5} distance={7} position={[0, 2.2, 0.5]} />
    </group>
  );
}

function BarrierField() {
  const group = useRef<THREE.Group>(null);
  const fill = useRef<THREE.MeshBasicMaterial>(null);
  const innerRing = useRef<THREE.MeshBasicMaterial>(null);

  useFrame(({ clock }) => {
    if (!group.current) return;
    const state = useGameStore.getState();
    const active = state.phase === "combat" && state.barrier.expiresAt > state.time;
    group.current.visible = active;
    if (!active) return;
    group.current.position.set(state.barrier.center[0], 0.058, state.barrier.center[1]);
    group.current.rotation.y = clock.elapsedTime * 0.08;
    if (fill.current) fill.current.opacity = 0.16 + (Math.sin(clock.elapsedTime * 2.6) + 1) * 0.035;
    if (innerRing.current) innerRing.current.opacity = 0.38 + (Math.sin(clock.elapsedTime * 3.2) + 1) * 0.12;
  });

  return (
    <group ref={group} visible={false}>
      <mesh rotation={[-Math.PI / 2, 0, 0]}>
        <circleGeometry args={[3, 64]} />
        <meshBasicMaterial ref={fill} color="#e7bf46" transparent opacity={0.2} depthWrite={false} />
      </mesh>
      <mesh position={[0, 0.012, 0]} rotation={[-Math.PI / 2, 0, 0]}>
        <ringGeometry args={[2.92, 3.05, 64]} />
        <meshBasicMaterial color="#ffd968" transparent opacity={0.88} depthWrite={false} />
      </mesh>
      <mesh position={[0, 0.016, 0]} rotation={[-Math.PI / 2, 0, 0]}>
        <ringGeometry args={[1.82, 1.9, 48]} />
        <meshBasicMaterial ref={innerRing} color="#ffe89a" transparent opacity={0.55} depthWrite={false} />
      </mesh>
      {Array.from({ length: 8 }, (_, index) => {
        const angle = (index / 8) * Math.PI * 2;
        return (
          <mesh
            key={index}
            position={[Math.sin(angle) * 2.35, 0.02, Math.cos(angle) * 2.35]}
            rotation={[-Math.PI / 2, 0, angle]}
          >
            <ringGeometry args={[0.09, 0.16, 6]} />
            <meshBasicMaterial color="#ffe89a" transparent opacity={0.62} depthWrite={false} />
          </mesh>
        );
      })}
    </group>
  );
}

function TankAuraField() {
  const group = useRef<THREE.Group>(null);
  const material = useRef<THREE.MeshBasicMaterial>(null);
  useFrame(({ clock }) => {
    if (!group.current) return;
    const state = useGameStore.getState();
    const active = state.phase === "combat" && state.partyCombat.tankAura.expiresAt > state.time && state.party[1].hp > 0;
    group.current.visible = active;
    if (!active) return;
    const brann = state.partyPositions.brann;
    group.current.position.set(brann[0], 0.06, brann[1]);
    group.current.rotation.y = clock.elapsedTime * -0.22;
    if (material.current) material.current.opacity = 0.13 + (Math.sin(clock.elapsedTime * 5) + 1) * 0.05;
  });
  return (
    <group ref={group} visible={false}>
      <mesh rotation={[-Math.PI / 2, 0, 0]}>
        <circleGeometry args={[3, 48]} />
        <meshBasicMaterial ref={material} color="#67bfff" transparent opacity={0.18} depthWrite={false} />
      </mesh>
      <mesh position={[0, 0.012, 0]} rotation={[-Math.PI / 2, 0, 0]}>
        <ringGeometry args={[2.88, 3.04, 48]} />
        <meshBasicMaterial color="#8fd5ff" transparent opacity={0.92} depthWrite={false} />
      </mesh>
      {Array.from({ length: 6 }, (_, index) => {
        const angle = (index / 6) * Math.PI * 2;
        return <mesh key={index} position={[Math.sin(angle) * 2.35, 0.025, Math.cos(angle) * 2.35]} rotation={[-Math.PI / 2, 0, angle]}>
          <ringGeometry args={[0.08, 0.15, 4]} />
          <meshBasicMaterial color="#d2efff" transparent opacity={0.8} depthWrite={false} />
        </mesh>;
      })}
    </group>
  );
}

function RangedProjectile({ memberId }: { memberId: "nia" | "orin" }) {
  const group = useRef<THREE.Group>(null);
  const start = useMemo(() => new THREE.Vector3(), []);
  const end = useMemo(() => new THREE.Vector3(), []);
  const current = useMemo(() => new THREE.Vector3(), []);
  const direction = useMemo(() => new THREE.Vector3(), []);
  const up = useMemo(() => new THREE.Vector3(0, 1, 0), []);

  useFrame(({ clock }) => {
    if (!group.current) return;
    const state = useGameStore.getState();
    const action = state.partyCombat.combatants[memberId].visualAction;
    const member = state.party.find((entry) => entry.id === memberId)!;
    const rapid = action?.abilityId === "rapid_fire";
    const active = action !== null
      && state.phase === "combat"
      && member.hp > 0
      && action.abilityId !== "overcharge"
      && state.time >= action.startedAt
      && (rapid ? state.time <= action.endsAt : state.time <= action.impactAt);
    group.current.visible = active;
    if (!active || !action) return;

    const targetMotion = targetBossMotionByInstance(state, action.targetInstanceId);
    const projectileDuration = Math.max(0.12, action.impactAt - action.startedAt);
    const progress = rapid
      ? ((state.time - action.startedAt) % 0.4) / 0.4
      : Math.min(1, (state.time - action.startedAt) / projectileDuration);
    const source = state.partyPositions[memberId];
    const target = targetMotion.position;
    start.set(source[0], 1.18, source[1]);
    end.set(target[0], 1.12, target[1]);
    current.copy(start).lerp(end, progress);
    current.y += Math.sin(progress * Math.PI) * (memberId === "orin" ? 0.95 : 0.34);
    group.current.position.copy(current);
    direction.subVectors(end, start).normalize();
    group.current.quaternion.setFromUnitVectors(up, direction);
    if (memberId === "orin") group.current.scale.setScalar(0.9 + Math.sin(clock.elapsedTime * 14) * 0.12);
  });

  return (
    <group ref={group} visible={false}>
      {memberId === "nia" ? (
        <>
          <mesh>
            <cylinderGeometry args={[0.026, 0.026, 0.82, 6]} />
            <meshBasicMaterial color="#d7b477" />
          </mesh>
          <mesh position={[0, 0.5, 0]}>
            <coneGeometry args={[0.085, 0.2, 6]} />
            <meshBasicMaterial color="#f1ddaa" />
          </mesh>
          <mesh position={[0, -0.4, 0]}>
            <coneGeometry args={[0.1, 0.18, 4]} />
            <meshBasicMaterial color="#70cf8e" />
          </mesh>
        </>
      ) : (
        <>
          <mesh>
            <sphereGeometry args={[0.19, 12, 10]} />
            <meshBasicMaterial color="#bc8cff" />
          </mesh>
          <mesh rotation={[Math.PI / 2, 0, 0]}>
            <torusGeometry args={[0.25, 0.025, 6, 20]} />
            <meshBasicMaterial color="#ead8ff" transparent opacity={0.8} />
          </mesh>
        </>
      )}
    </group>
  );
}

function RangedProjectiles() {
  return (
    <>
      <RangedProjectile memberId="nia" />
      <RangedProjectile memberId="orin" />
    </>
  );
}

function BossActor() {
  const phase = useGameStore((state) => state.phase);
  const primaryBoss = useGameStore((state) => state.boss);
  const additionalBosses = useGameStore((state) => state.additionalBosses);
  if (phase === "briefing") return null;
  const bosses = [primaryBoss, ...additionalBosses.map((entry) => entry.boss)];
  return (
    <>{bosses.map((boss, bossIndex) => (
      <Suspense key={`${boss.id}-${bossIndex}`} fallback={<BossFallback bossIndex={bossIndex} />}>
        {boss.id === "bulldrome" ? <BullBoss bossIndex={bossIndex} /> : <AlternateBoss kind={boss.id} bossIndex={bossIndex} />}
      </Suspense>
    ))}</>
  );
}

function FxBurst({ kind, targetId }: { kind: PulseKind; targetId?: MemberId }) {
  const ring = useRef<THREE.Mesh>(null);
  const material = useRef<THREE.MeshBasicMaterial>(null);
  const age = useRef(0);
  const isBossFx = kind === "boss";
  const state = useGameStore.getState();
  const worldPosition = isBossFx ? targetBossMotion(state).position : targetId ? state.partyPositions[targetId] : [0, 0];
  const position: [number, number, number] = [worldPosition[0], 0.15, worldPosition[1]];
  const color = kind === "boss" || kind === "debuff" || kind === "charge" || kind === "pounce" || kind === "slash" ? "#ff643c" : kind === "shield" ? "#62bdff" : kind === "purify" ? "#c39bff" : "#ffe087";
  useFrame((_, delta) => {
    age.current += delta;
    if (!ring.current || !material.current) return;
    const progress = Math.min(1, age.current / 0.7);
    ring.current.scale.setScalar(0.5 + progress * 3.6);
    material.current.opacity = (1 - progress) * 0.85;
  });
  return (
    <mesh ref={ring} position={position} rotation={[-Math.PI / 2, 0, 0]}>
      <ringGeometry args={[0.38, 0.5, 32]} />
      <meshBasicMaterial ref={material} color={color} transparent depthWrite={false} />
    </mesh>
  );
}

function CombatFx() {
  const pulse = useGameStore((state) => state.scenePulse);
  if (!pulse.id) return null;
  return <FxBurst key={pulse.id} kind={pulse.kind} targetId={pulse.targetId} />;
}

export function GameScene() {
  return (
    <Canvas
      shadows
      dpr={[1, 1.5]}
      camera={{ position: [0, 5.2, 12], fov: 48, near: 0.1, far: 70 }}
      gl={{ antialias: true, powerPreference: "high-performance" }}
    >
      <color attach="background" args={["#07110f"]} />
      <fog attach="fog" args={["#07110f", 17, 32]} />
      <hemisphereLight args={["#8ac4b5", "#15100b", 1.25]} />
      <directionalLight castShadow position={[5, 10, 8]} intensity={2.2} color="#ffe2a9" shadow-mapSize={[1024, 1024]} />
      <Arena />
      <BossMechanicIndicators />
      <BarrierField />
      <TankAuraField />
      <Suspense fallback={<PartyFallback />}>
        <Party />
      </Suspense>
      <BossActor />
      <RangedProjectiles />
      <CombatFx />
    </Canvas>
  );
}

useGLTF.preload(BULL_URL, false, true);
useGLTF.preload(EMBER_MANTIS_URL, false, true);
for (const modelUrl of Object.values(PARTY_MODEL_URLS)) useGLTF.preload(modelUrl, false, true);
for (const loadout of Object.values(PARTY_WEAPON_URLS)) {
  useGLTF.preload(loadout.right, false, true);
  if (loadout.left) useGLTF.preload(loadout.left, false, true);
}
