import { useFrame } from "@react-three/fiber";
import { Html } from "@react-three/drei";
import { useCallback, useEffect, useLayoutEffect, useRef, useState, type ComponentType } from "react";
import * as THREE from "three";
import { BULL_CHARGE, BULL_POUNCE, MEMORY_SEQUENCE, MEMORY_SYMBOLS, SKY_SWEEPER_BREATH } from "../../game/bosses/mechanicPool";
import { bossHazardVfxProfile } from "../../game/bossHazardVisuals";
import { angleTo } from "../../game/geometry";
import { useGameStore } from "../../game/store";
import type { BossMotionMode, BossMotionState, MemorySymbolId, MemoryTile, PoolTelegraph, SoulSiphonState, WorldPosition } from "../../game/types";
import { BreathParticleVfx, CircleMechanicVfx, LaneEnergyVfx } from "./BossAttackVfx";
import { BOSS_INDICATOR_DEATH_FADE_MS, advanceBossIndicatorOpacity } from "./bossDeathVisuals";
import { CircleHazardVfx } from "./CircleHazardVfx";

const CHARGE_MARKERS = [0, 1, 2, 3, 4, 5, 6] as const;
const STACK_DIRECTIONS = Array.from({ length: 8 }, (_, index) => (index / 8) * Math.PI * 2);
const EMPTY_HAZARDS: never[] = [];
const EMPTY_SLASH_LANES: never[] = [];
const EMPTY_POOL_TELEGRAPHS: never[] = [];
const ACTIVE_LANE_MODES = new Set<BossMotionMode>(["charging"]);
const DANGER_WARNING_COLOR = "#ff3b30";
const DANGER_ACTIVE_COLOR = "#d4142a";
const DANGER_HIGHLIGHT_COLOR = "#ff8a80";
const SOAK_DIRECTIONS = Array.from({ length: 8 }, (_, index) => (index / 8) * Math.PI * 2);
const WARD_ARROW_DIRECTIONS = [0, Math.PI / 2, Math.PI, Math.PI * 1.5] as const;
const INWARD_ARROW_SHAPE = new THREE.Shape()
  .moveTo(-0.13, -0.32)
  .lineTo(0.13, -0.32)
  .lineTo(0.13, 0.04)
  .lineTo(0.29, 0.04)
  .lineTo(0, 0.38)
  .lineTo(-0.29, 0.04)
  .lineTo(-0.13, 0.04)
  .lineTo(-0.13, -0.32);
const SOUL_SIPHON_GUIDANCE_HEIGHT = 2.2;
const SOUL_SIPHON_GUIDANCE_COLOR = "#9dff78";
const SOUL_SIPHON_GUIDANCE_OUTLINE = "#2b210b";
type GameStoreState = ReturnType<typeof useGameStore.getState>;

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

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

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

function earliestSoulSiphonInMotion(motion: BossMotionState | undefined, current?: SoulSiphonState) {
  if (!motion) return current;
  let earliest = current;
  for (const telegraph of motion.poolTelegraphs) {
    const siphon = telegraph.kind === "soul-siphon" && !telegraph.resolved ? telegraph.soulSiphon : undefined;
    if (siphon && (!earliest || siphon.nextDamageAt < earliest.nextDamageAt)) earliest = siphon;
  }
  return earliest;
}

function activeSoulSiphonGuidance(state: GameStoreState) {
  if (state.phase !== "combat") return undefined;
  let earliest = state.boss.hp > 0 ? earliestSoulSiphonInMotion(state.bossMotion) : undefined;
  for (const entry of state.additionalBosses) {
    if (entry.boss.hp > 0) earliest = earliestSoulSiphonInMotion(entry.motion, earliest);
  }
  return earliest;
}

function WorldDirectionIndicator({
  origin,
  destination,
  height,
  color,
}: {
  origin: WorldPosition;
  destination: WorldPosition;
  height: number;
  color: string;
}) {
  const group = useRef<THREE.Group>(null);
  const fill = useRef<THREE.MeshBasicMaterial>(null);
  const reducedMotion = document.documentElement.classList.contains("force-reduced-motion")
    || window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  useFrame(({ clock }) => {
    if (!group.current) return;
    const wave = reducedMotion ? 0.5 : (Math.sin(clock.elapsedTime * 5.2) + 1) * 0.5;
    group.current.position.set(origin[0], height + (reducedMotion ? 0 : (wave - 0.5) * 0.12), origin[1]);
    group.current.rotation.y = angleTo(origin, destination) + Math.PI;
    group.current.scale.setScalar(1.55 + wave * 0.16);
    if (fill.current) fill.current.opacity = 0.82 + wave * 0.18;
  });

  return (
    <group ref={group} position={[origin[0], height, origin[1]]} rotation={[0, angleTo(origin, destination) + Math.PI, 0]}>
      <mesh rotation={[-Math.PI / 2, 0, 0]} scale={1.28} renderOrder={30}>
        <shapeGeometry args={[INWARD_ARROW_SHAPE]} />
        <meshBasicMaterial color={SOUL_SIPHON_GUIDANCE_OUTLINE} transparent opacity={0.92} depthTest={false} depthWrite={false} side={THREE.DoubleSide} />
      </mesh>
      <mesh position={[0, 0.012, 0]} rotation={[-Math.PI / 2, 0, 0]} renderOrder={31}>
        <shapeGeometry args={[INWARD_ARROW_SHAPE]} />
        <meshBasicMaterial ref={fill} color={color} transparent opacity={1} depthTest={false} depthWrite={false} side={THREE.DoubleSide} />
      </mesh>
    </group>
  );
}

function SoulSiphonGuidanceIndicator() {
  const guidance = useGameStore(activeSoulSiphonGuidance);
  const playerPosition = useGameStore((state) => state.partyPositions.aelia);
  if (!guidance) return null;
  return (
    <WorldDirectionIndicator
      origin={playerPosition}
      destination={guidance.wardPosition}
      height={SOUL_SIPHON_GUIDANCE_HEIGHT}
      color={SOUL_SIPHON_GUIDANCE_COLOR}
    />
  );
}

export function ChargeLaneIndicator({ bossIndex = 0 }: { bossIndex?: number }) {
  const phase = useGameStore((state) => state.phase);
  const motionMode = useGameStore((state) => motionAt(state, bossIndex)?.mode);
  const motion = motionAt(useGameStore.getState(), bossIndex);
  const warningMaterial = useRef<THREE.MeshBasicMaterial>(null);
  useFrame(({ clock }) => {
    if (!warningMaterial.current) return;
    warningMaterial.current.opacity = motionAt(useGameStore.getState(), bossIndex)?.mode === "charging"
      ? 0.34
      : 0.16 + (Math.sin(clock.elapsedTime * 9) + 1) * 0.09;
  });
  if (!motion || phase !== "combat" || (motionMode !== "telegraph" && motionMode !== "charging")) return null;

  const dx = motion.chargeEnd[0] - motion.chargeStart[0];
  const dz = motion.chargeEnd[1] - motion.chargeStart[1];
  const length = Math.hypot(dx, dz);
  const angle = Math.atan2(dx, dz);
  const midpoint: [number, number, number] = [
    (motion.chargeStart[0] + motion.chargeEnd[0]) / 2,
    0.045,
    (motion.chargeStart[1] + motion.chargeEnd[1]) / 2,
  ];
  const color = motionMode === "charging" ? DANGER_ACTIVE_COLOR : DANGER_WARNING_COLOR;
  const laneWidth = BULL_CHARGE.hitRadius * 2;
  return (
    <>
      <group position={midpoint} rotation={[0, angle, 0]}>
        <mesh rotation={[-Math.PI / 2, 0, 0]}>
          <planeGeometry args={[laneWidth, length]} />
          <meshBasicMaterial ref={warningMaterial} color={color} transparent depthWrite={false} />
        </mesh>
        <mesh position={[-BULL_CHARGE.hitRadius, 0.018, 0]}>
          <boxGeometry args={[0.055, 0.025, length]} />
          <meshBasicMaterial color={color} transparent opacity={0.9} />
        </mesh>
        <mesh position={[BULL_CHARGE.hitRadius, 0.018, 0]}>
          <boxGeometry args={[0.055, 0.025, length]} />
          <meshBasicMaterial color={color} transparent opacity={0.9} />
        </mesh>
        {CHARGE_MARKERS.map((index) => (
          <mesh key={index} position={[0, 0.025, -length / 2 + ((index + 0.55) / 7) * length]}>
            <boxGeometry args={[BULL_CHARGE.hitRadius, 0.028, 0.12]} />
            <meshBasicMaterial color={color} transparent opacity={0.62} />
          </mesh>
        ))}
      </group>
      <mesh position={[motion.chargeEnd[0], 0.055, motion.chargeEnd[1]]} rotation={[-Math.PI / 2, 0, 0]}>
        <ringGeometry args={[1.05, BULL_CHARGE.hitRadius, 36]} />
        <meshBasicMaterial color={color} transparent opacity={0.9} depthWrite={false} />
      </mesh>
    </>
  );
}

function SlashLaneIndicator({ laneId, bossIndex }: { laneId: string; bossIndex: number }) {
  const phase = useGameStore((state) => state.phase);
  const motion = useGameStore((state) => motionAt(state, bossIndex));
  const material = useRef<THREE.MeshBasicMaterial>(null);
  const edgeMaterial = useRef<THREE.MeshBasicMaterial>(null);
  useFrame(({ clock }) => {
    if (!material.current || !edgeMaterial.current) return;
    const current = motionAt(useGameStore.getState(), bossIndex);
    const active = current ? ACTIVE_LANE_MODES.has(current.mode) : false;
    material.current.opacity = active ? 0.5 : 0.14 + (Math.sin(clock.elapsedTime * 12) + 1) * 0.09;
    edgeMaterial.current.opacity = active ? 1 : 0.62 + (Math.sin(clock.elapsedTime * 12) + 1) * 0.15;
  });
  const lane = motion?.slashLanes.find((candidate) => candidate.id === laneId);
  if (!lane || phase !== "combat") return null;

  const dx = lane.end[0] - lane.start[0];
  const dz = lane.end[1] - lane.start[1];
  const length = Math.hypot(dx, dz);
  const angle = Math.atan2(dx, dz);
  const midpoint: [number, number, number] = [
    (lane.start[0] + lane.end[0]) * 0.5,
    0.052,
    (lane.start[1] + lane.end[1]) * 0.5,
  ];
  const active = ACTIVE_LANE_MODES.has(motion.mode);
  const chargeEffect = motion.mode === "telegraph" || motion.mode === "charging";
  const color = active ? DANGER_ACTIVE_COLOR : DANGER_WARNING_COLOR;
  return (
    <>
      <group position={midpoint} rotation={[0, angle, 0]}>
        <mesh rotation={[-Math.PI / 2, 0, 0]}>
          <planeGeometry args={[lane.width, length]} />
          <meshBasicMaterial ref={material} color={color} transparent opacity={0.22} depthWrite={false} />
        </mesh>
        {[-1, 1].map((side) => (
          <mesh key={side} position={[side * lane.width * 0.5, 0.018, 0]}>
            <boxGeometry args={[0.07, 0.03, length]} />
            <meshBasicMaterial ref={side < 0 ? edgeMaterial : undefined} color={color} transparent opacity={0.8} depthWrite={false} />
          </mesh>
        ))}
        {active && (
          <mesh position={[0, 0.055, 0]}>
            <boxGeometry args={[0.16, 0.08, length]} />
            <meshBasicMaterial color={DANGER_HIGHLIGHT_COLOR} transparent opacity={0.92} depthWrite={false} />
          </mesh>
        )}
      </group>
      <LaneEnergyVfx
        start={lane.start}
        end={lane.end}
        width={lane.width}
        color={active ? DANGER_HIGHLIGHT_COLOR : color}
        active={active}
        variant={chargeEffect ? "charge" : "slash"}
      />
    </>
  );
}

export function SlashLaneIndicators({ bossIndex = 0 }: { bossIndex?: number }) {
  const lanes = useGameStore((state) => motionAt(state, bossIndex)?.slashLanes ?? EMPTY_SLASH_LANES);
  return <>{lanes.map((lane) => <SlashLaneIndicator key={lane.id} laneId={lane.id} bossIndex={bossIndex} />)}</>;
}

export function PounceStackIndicator({ bossIndex = 0 }: { bossIndex?: number }) {
  const phase = useGameStore((state) => state.phase);
  const motionMode = useGameStore((state) => motionAt(state, bossIndex)?.mode);
  const group = useRef<THREE.Group>(null);
  const warningMaterial = useRef<THREE.MeshBasicMaterial>(null);
  useFrame(({ clock }) => {
    const motion = motionAt(useGameStore.getState(), bossIndex);
    if (!motion) return;
    if (group.current) group.current.position.set(motion.pounceCenter[0], 0.052, motion.pounceCenter[1]);
    if (!warningMaterial.current) return;
    warningMaterial.current.opacity = 0.14 + (Math.sin(clock.elapsedTime * 8) + 1) * 0.08;
  });
  if (phase !== "combat" || (motionMode !== "stacking" && motionMode !== "pouncing")) return null;

  const radius = BULL_POUNCE.stackRadius;
  const active = motionMode === "pouncing";
  return (
    <group ref={group}>
      <mesh rotation={[-Math.PI / 2, 0, 0]}>
        <circleGeometry args={[radius, 48]} />
        <meshBasicMaterial ref={warningMaterial} color="#ff2f37" transparent opacity={0.2} depthWrite={false} />
      </mesh>
      <mesh rotation={[-Math.PI / 2, 0, 0]}>
        <ringGeometry args={[radius - 0.12, radius + 0.02, 48]} />
        <meshBasicMaterial color="#ff4450" transparent opacity={0.95} depthWrite={false} />
      </mesh>
      {STACK_DIRECTIONS.map((angle, index) => (
          <group
            key={index}
            position={[Math.sin(angle) * (radius - 0.32), 0.045, Math.cos(angle) * (radius - 0.32)]}
            rotation={[0, angle, 0]}
          >
            <mesh position={[0, 0, -0.18]}>
              <boxGeometry args={[0.09, 0.035, 0.38]} />
              <meshBasicMaterial color="#ff7278" />
            </mesh>
            <mesh position={[0, 0, -0.48]} rotation={[-Math.PI / 2, 0, 0]}>
              <coneGeometry args={[0.19, 0.38, 4]} />
              <meshBasicMaterial color="#ff7278" />
            </mesh>
          </group>
      ))}
      <CircleMechanicVfx radius={radius} kind="pounce" color={active ? "#ffe0a8" : "#ff7278"} active={active} />
    </group>
  );
}

export function BindingWebIndicator({ bossIndex = 0 }: { bossIndex?: number }) {
  const phase = useGameStore((state) => state.phase);
  const motion = useGameStore((state) => motionAt(state, bossIndex));
  const positions = useGameStore((state) => state.partyPositions);
  if (!motion || phase !== "combat" || motion.mode !== "tethering" || motion.tetherIds.length !== 2) return null;
  const [firstId, secondId] = motion.tetherIds;
  const first = positions[firstId];
  const second = positions[secondId];
  const dx = second[0] - first[0];
  const dz = second[1] - first[1];
  const length = Math.hypot(dx, dz);
  const angle = Math.atan2(dx, dz);
  const stretched = length >= motion.tetherBreakDistance * 0.8;
  const color = stretched ? "#f2b7ff" : "#ae52ef";
  return (
    <group>
      <mesh position={[(first[0] + second[0]) / 2, 0.12, (first[1] + second[1]) / 2]} rotation={[0, angle, 0]}>
        <boxGeometry args={[0.1, 0.07, length]} />
        <meshBasicMaterial color={color} transparent opacity={0.9} />
      </mesh>
      {[first, second].map((position, index) => (
        <mesh key={index} position={[position[0], 0.06, position[1]]} rotation={[-Math.PI / 2, 0, 0]}>
          <ringGeometry args={[0.55, 0.68, 32]} />
          <meshBasicMaterial color={color} transparent opacity={0.92} depthWrite={false} />
        </mesh>
      ))}
      <LaneEnergyVfx start={first} end={second} width={0.34} color={color} active={stretched} variant="web" height={0.2} />
    </group>
  );
}

export function BreathConeIndicator({ bossIndex = 0 }: { bossIndex?: number }) {
  const phase = useGameStore((state) => state.phase);
  const motion = useGameStore((state) => motionAt(state, bossIndex));
  const material = useRef<THREE.MeshBasicMaterial>(null);
  useFrame(({ clock }) => {
    if (material.current) material.current.opacity = 0.2 + (Math.sin(clock.elapsedTime * 8) + 1) * 0.07;
  });
  if (!motion || phase !== "combat" || (motion.mode !== "breath_telegraph" && motion.mode !== "breath_sweeping")) return null;
  const color = motion.mode === "breath_sweeping" ? DANGER_ACTIVE_COLOR : DANGER_WARNING_COLOR;
  return (
    <>
      <group position={[motion.position[0], 0.07, motion.position[1]]} rotation={[0, motion.breathAngle, 0]}>
        <mesh rotation={[-Math.PI / 2, 0, -Math.PI / 2 - SKY_SWEEPER_BREATH.halfAngle]}>
          <circleGeometry args={[SKY_SWEEPER_BREATH.range, 64, 0, SKY_SWEEPER_BREATH.halfAngle * 2]} />
          <meshBasicMaterial ref={material} color={color} transparent opacity={0.25} depthWrite={false} />
        </mesh>
        <mesh position={[0, 0.02, SKY_SWEEPER_BREATH.range * 0.48]} rotation={[-Math.PI / 2, 0, 0]}>
          <ringGeometry args={[SKY_SWEEPER_BREATH.range * 0.47, SKY_SWEEPER_BREATH.range * 0.49, 48, 1, -SKY_SWEEPER_BREATH.halfAngle, SKY_SWEEPER_BREATH.halfAngle * 2]} />
          <meshBasicMaterial color={color} transparent opacity={0.85} depthWrite={false} />
        </mesh>
      </group>
      <BreathParticleVfx
        position={motion.position}
        angle={motion.breathAngle}
        range={SKY_SWEEPER_BREATH.range}
        halfAngle={SKY_SWEEPER_BREATH.halfAngle}
        active={motion.mode === "breath_sweeping"}
      />
    </>
  );
}

function CircleHazardIndicator({ hazardId, bossIndex }: { hazardId: string; bossIndex: number }) {
  const hazards = useGameStore((state) => motionAt(state, bossIndex)?.hazards ?? EMPTY_HAZARDS);
  const hazard = hazards.find((entry) => entry.id === hazardId);
  const time = useGameStore((state) => state.time);
  const material = useRef<THREE.MeshBasicMaterial>(null);
  useFrame(({ clock }) => {
    if (material.current) material.current.opacity = 0.16 + (Math.sin(clock.elapsedTime * 7) + 1) * 0.06;
  });
  if (!hazard) return null;
  const active = time >= hazard.activatesAt;
  const visual = bossHazardVfxProfile(hazard.kind);
  const color = active ? visual.activeColor : visual.warningColor;
  return (
    <group position={[hazard.center[0], 0.065, hazard.center[1]]}>
      <mesh rotation={[-Math.PI / 2, 0, 0]}>
        {hazard.innerRadius ? (
          <ringGeometry args={[hazard.innerRadius, hazard.radius, 48]} />
        ) : (
          <circleGeometry args={[hazard.radius, 40]} />
        )}
        <meshBasicMaterial ref={material} color={color} transparent opacity={active ? 0.24 : 0.16} depthWrite={false} />
      </mesh>
      <mesh position={[0, 0.012, 0]} rotation={[-Math.PI / 2, 0, 0]}>
        <ringGeometry args={[hazard.radius - 0.1, hazard.radius + 0.04, 40]} />
        <meshBasicMaterial color={color} transparent opacity={0.92} depthWrite={false} />
      </mesh>
      {!active && (
        <mesh position={[0, 0.03, 0]} rotation={[-Math.PI / 2, 0, 0]}>
          <ringGeometry args={[0.22, 0.34, 24]} />
          <meshBasicMaterial color={visual.highlightColor} transparent opacity={0.95} depthWrite={false} />
        </mesh>
      )}
      <CircleHazardVfx hazard={hazard} active={active} />
    </group>
  );
}

export function CircleHazardIndicators({ bossIndex = 0 }: { bossIndex?: number }) {
  const hazards = useGameStore((state) => motionAt(state, bossIndex)?.hazards ?? EMPTY_HAZARDS);
  return <>{hazards.map((hazard) => <CircleHazardIndicator key={hazard.id} hazardId={hazard.id} bossIndex={bossIndex} />)}</>;
}

/** Tall gold ward plus a lightweight spectral pursuer for the shared Soul Siphon mechanic. */
export function SoulSiphonIndicator({ bossIndex = 0 }: { bossIndex?: number }) {
  const phase = useGameStore((state) => state.phase);
  const motion = useGameStore((state) => motionAt(state, bossIndex));
  const wardGroup = useRef<THREE.Group>(null);
  const ghostGroup = useRef<THREE.Group>(null);
  const outerColumn = useRef<THREE.MeshBasicMaterial>(null);
  const innerColumn = useRef<THREE.MeshBasicMaterial>(null);
  const groundRing = useRef<THREE.MeshBasicMaterial>(null);

  useFrame(({ clock }) => {
    const current = motionAt(useGameStore.getState(), bossIndex);
    const siphon = current?.poolTelegraphs.find((telegraph) => telegraph.kind === "soul-siphon")?.soulSiphon;
    const active = useGameStore.getState().phase === "combat" && siphon;
    if (!active || !siphon) return;
    const pulse = (Math.sin(clock.elapsedTime * 5.5) + 1) * 0.5;
    if (wardGroup.current) {
      wardGroup.current.position.set(siphon.wardPosition[0], 0, siphon.wardPosition[1]);
      wardGroup.current.rotation.y += 0.003;
    }
    if (ghostGroup.current) {
      ghostGroup.current.position.set(siphon.ghostPosition[0], 0.85 + Math.sin(clock.elapsedTime * 4.2) * 0.12, siphon.ghostPosition[1]);
      ghostGroup.current.rotation.y += 0.018;
    }
    if (outerColumn.current) outerColumn.current.opacity = 0.15 + pulse * 0.09;
    if (innerColumn.current) innerColumn.current.opacity = 0.2 + pulse * 0.16;
    if (groundRing.current) groundRing.current.opacity = 0.62 + pulse * 0.28;
  });

  const siphon = motion?.poolTelegraphs.find((telegraph) => telegraph.kind === "soul-siphon")?.soulSiphon;
  if (!siphon || phase !== "combat") return null;

  return (
    <>
      <group ref={wardGroup} position={[siphon.wardPosition[0], 0, siphon.wardPosition[1]]}>
        <mesh position={[0, 2.45, 0]}>
          <cylinderGeometry args={[0.78, 1.05, 4.9, 28, 1, true]} />
          <meshBasicMaterial ref={outerColumn} color="#f8c94e" transparent opacity={0.2} depthWrite={false} side={THREE.DoubleSide} />
        </mesh>
        <mesh position={[0, 2.45, 0]}>
          <cylinderGeometry args={[0.38, 0.58, 4.9, 24, 1, true]} />
          <meshBasicMaterial ref={innerColumn} color="#fff0a3" transparent opacity={0.32} depthWrite={false} side={THREE.DoubleSide} />
        </mesh>
        <mesh position={[0, 0.045, 0]} rotation={[-Math.PI / 2, 0, 0]}>
          <ringGeometry args={[siphon.wardRadius - 0.13, siphon.wardRadius, 40]} />
          <meshBasicMaterial ref={groundRing} color="#ffe47d" transparent opacity={0.9} depthWrite={false} />
        </mesh>
        {WARD_ARROW_DIRECTIONS.map((angle) => (
          <group
            key={angle}
            position={[Math.sin(angle) * 1.58, 0.062, Math.cos(angle) * 1.58]}
            rotation={[0, angle, 0]}
          >
            <mesh rotation={[-Math.PI / 2, 0, 0]} renderOrder={2}>
              <shapeGeometry args={[INWARD_ARROW_SHAPE]} />
              <meshBasicMaterial color="#54ff90" transparent opacity={1} depthWrite={false} depthTest={false} />
            </mesh>
          </group>
        ))}
      </group>
      <group ref={ghostGroup} position={[siphon.ghostPosition[0], 0.85, siphon.ghostPosition[1]]}>
        <mesh>
          <sphereGeometry args={[0.34, 16, 12]} />
          <meshBasicMaterial color="#b48aff" transparent opacity={0.68} depthWrite={false} />
        </mesh>
        <mesh position={[0, -0.52, 0]} scale={[0.82, 1.45, 0.82]}>
          <coneGeometry args={[0.34, 0.78, 12, 1, true]} />
          <meshBasicMaterial color="#8d62d2" transparent opacity={0.36} depthWrite={false} side={THREE.DoubleSide} />
        </mesh>
        <mesh rotation={[Math.PI / 2, 0, 0]}>
          <torusGeometry args={[0.48, 0.026, 8, 24]} />
          <meshBasicMaterial color="#e1c4ff" transparent opacity={0.72} depthWrite={false} />
        </mesh>
      </group>
    </>
  );
}

function MemorySymbolMark({ symbol, size = 1, opacity = 1 }: { symbol: MemorySymbolId; size?: number; opacity?: number }) {
  const color = MEMORY_SYMBOLS[symbol].color;
  if (symbol === "cross") {
    return (
      <group position={[0, 0.012, 0]}>
        <mesh>
          <boxGeometry args={[size * 0.22, 0.035, size]} />
          <meshBasicMaterial color={color} transparent opacity={opacity} depthWrite={false} />
        </mesh>
        <mesh>
          <boxGeometry args={[size, 0.035, size * 0.22]} />
          <meshBasicMaterial color={color} transparent opacity={opacity} depthWrite={false} />
        </mesh>
      </group>
    );
  }
  return (
    <mesh position={[0, 0.02, 0]} rotation={[-Math.PI / 2, 0, symbol === "square" ? Math.PI / 4 : -Math.PI / 2]}>
      <ringGeometry args={[size * 0.34, size * 0.54, symbol === "circle" ? 36 : symbol === "triangle" ? 3 : 4]} />
      <meshBasicMaterial color={color} transparent opacity={opacity} depthWrite={false} />
    </mesh>
  );
}

const MEMORY_SYMBOL_GLYPHS: Record<MemorySymbolId, string> = {
  triangle: "△",
  cross: "+",
  circle: "○",
  square: "◇",
};

function MemoryTileIndicator({ tile, inputActive, completed }: { tile: MemoryTile; inputActive: boolean; completed: boolean }) {
  const color = MEMORY_SYMBOLS[tile.symbol].color;
  const halfSize = MEMORY_SEQUENCE.tileSize * 0.5;
  const gridOffsets = [-0.48, 0, 0.48] as const;
  const group = useRef<THREE.Group>(null);
  const fade = useRef(1);
  const materials = useRef<{ material: THREE.MeshBasicMaterial; baseOpacity: number }[]>([]);

  useLayoutEffect(() => {
    const nextMaterials: { material: THREE.MeshBasicMaterial; baseOpacity: number }[] = [];
    group.current?.traverse((child) => {
      if (!(child instanceof THREE.Mesh)) return;
      const childMaterials = Array.isArray(child.material) ? child.material : [child.material];
      for (const material of childMaterials) {
        if (material instanceof THREE.MeshBasicMaterial) {
          nextMaterials.push({ material, baseOpacity: material.opacity });
        }
      }
    });
    materials.current = nextMaterials;
  }, [inputActive]);

  useFrame((_, delta) => {
    if (!completed && fade.current >= 0.999) return;
    if (completed && fade.current <= 0.01) {
      fade.current = 0;
      if (group.current) group.current.visible = false;
      return;
    }
    if (group.current) group.current.visible = true;
    fade.current = THREE.MathUtils.damp(fade.current, completed ? 0 : 1, 18, delta);
    for (const entry of materials.current) entry.material.opacity = entry.baseOpacity * fade.current;
  });

  return (
    <group ref={group} position={[tile.center[0], 0.075, tile.center[1]]}>
      <mesh>
        <boxGeometry args={[MEMORY_SEQUENCE.tileSize, 0.04, MEMORY_SEQUENCE.tileSize]} />
        <meshBasicMaterial color={color} transparent opacity={inputActive ? 0.28 : 0.16} depthWrite={false} />
      </mesh>
      {[-1, 1].map((side) => (
        <group key={side}>
          <mesh position={[side * halfSize, 0.03, 0]}>
            <boxGeometry args={[0.08, 0.06, MEMORY_SEQUENCE.tileSize + 0.08]} />
            <meshBasicMaterial color={color} transparent opacity={0.96} depthWrite={false} />
          </mesh>
          <mesh position={[0, 0.03, side * halfSize]}>
            <boxGeometry args={[MEMORY_SEQUENCE.tileSize + 0.08, 0.06, 0.08]} />
            <meshBasicMaterial color={color} transparent opacity={0.96} depthWrite={false} />
          </mesh>
        </group>
      ))}
      {gridOffsets.map((offset) => (
        <group key={offset}>
          <mesh position={[0, 0.026, offset * MEMORY_SEQUENCE.tileSize]}>
            <boxGeometry args={[MEMORY_SEQUENCE.tileSize * 0.84, 0.018, 0.026]} />
            <meshBasicMaterial color="#fff6df" transparent opacity={0.38} depthWrite={false} />
          </mesh>
          <mesh position={[offset * MEMORY_SEQUENCE.tileSize, 0.027, 0]}>
            <boxGeometry args={[0.026, 0.018, MEMORY_SEQUENCE.tileSize * 0.84]} />
            <meshBasicMaterial color="#fff6df" transparent opacity={0.38} depthWrite={false} />
          </mesh>
        </group>
      ))}
      <MemorySymbolMark symbol={tile.symbol} size={0.78} opacity={1} />
    </group>
  );
}

function MemorySequenceIndicator({ telegraph, bossPosition, time }: { telegraph: PoolTelegraph; bossPosition?: [number, number]; time: number }) {
  if (!telegraph.sequence || !telegraph.tiles || telegraph.inputStartsAt === undefined) return null;
  const showingSequence = time < telegraph.inputStartsAt;
  const flashIndex = Math.min(
    telegraph.sequence.length - 1,
    Math.max(0, Math.floor((time - telegraph.activatesAt) / MEMORY_SEQUENCE.flashDuration)),
  );
  const flashSymbol = telegraph.sequence[flashIndex];
  const source = bossPosition ?? telegraph.center;
  const completedCount = showingSequence ? 0 : telegraph.inputIndex ?? 0;
  return (
    <>
      {telegraph.tiles.map((tile) => {
        const sequenceIndex = telegraph.sequence!.indexOf(tile.symbol);
        return (
          <MemoryTileIndicator
            key={tile.symbol}
            tile={tile}
            inputActive={!showingSequence}
            completed={sequenceIndex >= 0 && sequenceIndex < completedCount}
          />
        );
      })}
      {showingSequence && (
        <Html position={[source[0], 3.1, source[1] + 0.3]} center zIndexRange={[30, 20]} style={{ pointerEvents: "none" }}>
          <div style={{ display: "grid", justifyItems: "center", gap: 7 }}>
            <div
              style={{
                width: 64,
                height: 64,
                display: "grid",
                placeItems: "center",
                borderRadius: "50%",
                border: `4px solid ${MEMORY_SYMBOLS[flashSymbol].color}`,
                background: "rgba(17, 24, 39, 0.96)",
                boxShadow: "0 0 0 4px rgba(216, 184, 92, 0.92), 0 0 18px rgba(255, 244, 199, 0.8)",
                color: MEMORY_SYMBOLS[flashSymbol].color,
                fontSize: 47,
                fontWeight: 900,
                lineHeight: 1,
                textShadow: "0 0 8px currentColor",
              }}
            >
              {MEMORY_SYMBOL_GLYPHS[flashSymbol]}
            </div>
            <div style={{ display: "flex", gap: 6 }}>
              {telegraph.sequence.map((_, index) => (
                <span
                  key={index}
                  style={{
                    width: index === flashIndex ? 9 : 7,
                    height: index === flashIndex ? 9 : 7,
                    borderRadius: "50%",
                    background: index === flashIndex
                      ? MEMORY_SYMBOLS[flashSymbol].color
                      : index < flashIndex ? "#fff4c7" : "#64748b",
                    boxShadow: index === flashIndex ? "0 0 7px currentColor" : "none",
                  }}
                />
              ))}
            </div>
          </div>
        </Html>
      )}
    </>
  );
}

function PooledTelegraphIndicator({ telegraphId, bossIndex }: { telegraphId: string; bossIndex: number }) {
  const phase = useGameStore((state) => state.phase);
  const time = useGameStore((state) => state.time);
  const telegraph = useGameStore((state) => motionAt(state, bossIndex)?.poolTelegraphs.find((entry) => entry.id === telegraphId));
  const bossPosition = useGameStore((state) => motionAt(state, bossIndex)?.position);
  const fillMaterial = useRef<THREE.MeshBasicMaterial>(null);
  useFrame(({ clock }) => {
    if (!fillMaterial.current) return;
    const current = motionAt(useGameStore.getState(), bossIndex)?.poolTelegraphs.find((entry) => entry.id === telegraphId);
    if (!current) return;
    const active = useGameStore.getState().time >= current.activatesAt;
    fillMaterial.current.opacity = active ? 0.46 : 0.14 + (Math.sin(clock.elapsedTime * 10) + 1) * 0.08;
  });
  if (!telegraph || phase !== "combat") return null;

  if (telegraph.kind === "memory") return <MemorySequenceIndicator telegraph={telegraph} bossPosition={bossPosition} time={time} />;
  if (telegraph.kind === "soul-siphon") return null;

  const active = time >= telegraph.activatesAt;
  const color = telegraph.kind === "soak" ? "#57e8ff"
    : telegraph.kind === "spread" ? "#e869ff"
      : active ? DANGER_ACTIVE_COLOR : DANGER_WARNING_COLOR;

  if (telegraph.kind === "beam") {
    if (!telegraph.start || !telegraph.end) return null;
    const dx = telegraph.end[0] - telegraph.start[0];
    const dz = telegraph.end[1] - telegraph.start[1];
    const length = Math.hypot(dx, dz);
    const angle = Math.atan2(dx, dz);
    return (
      <>
        <group position={[(telegraph.start[0] + telegraph.end[0]) * 0.5, 0.07, (telegraph.start[1] + telegraph.end[1]) * 0.5]} rotation={[0, angle, 0]}>
          <mesh rotation={[-Math.PI / 2, 0, 0]}>
            <planeGeometry args={[telegraph.width ?? 1, length]} />
            <meshBasicMaterial ref={fillMaterial} color={color} transparent depthWrite={false} />
          </mesh>
          {[-1, 1].map((side) => (
            <mesh key={side} position={[side * (telegraph.width ?? 1) * 0.5, 0.022, 0]}>
              <boxGeometry args={[0.06, 0.035, length]} />
              <meshBasicMaterial color={color} transparent opacity={0.95} depthWrite={false} />
            </mesh>
          ))}
          {CHARGE_MARKERS.map((index) => (
            <mesh key={index} position={[0, 0.034, -length * 0.44 + (index / 6) * length * 0.88]}>
              <boxGeometry args={[(telegraph.width ?? 1) * 0.56, 0.035, 0.13]} />
              <meshBasicMaterial color="#ffe1b3" transparent opacity={active ? 1 : 0.7} />
            </mesh>
          ))}
        </group>
        <LaneEnergyVfx
          start={telegraph.start}
          end={telegraph.end}
          width={telegraph.width ?? 1}
          color={active ? "#ffe1b3" : color}
          active={active}
          variant="beam"
        />
      </>
    );
  }

  const innerRadius = telegraph.innerRadius ?? 0;
  return (
    <group position={[telegraph.center[0], 0.07, telegraph.center[1]]}>
      <mesh rotation={[-Math.PI / 2, 0, 0]}>
        {telegraph.kind === "donut"
          ? <ringGeometry args={[innerRadius, telegraph.radius, 56]} />
          : <circleGeometry args={[telegraph.radius, 48]} />}
        <meshBasicMaterial ref={fillMaterial} color={color} transparent depthWrite={false} />
      </mesh>
      <mesh position={[0, 0.018, 0]} rotation={[-Math.PI / 2, 0, 0]}>
        <ringGeometry args={[telegraph.radius - 0.1, telegraph.radius + 0.04, 48]} />
        <meshBasicMaterial color={color} transparent opacity={0.94} depthWrite={false} />
      </mesh>
      {telegraph.kind === "donut" && (
        <mesh position={[0, 0.021, 0]} rotation={[-Math.PI / 2, 0, 0]}>
          <ringGeometry args={[innerRadius - 0.05, innerRadius + 0.05, 40]} />
          <meshBasicMaterial color="#f9efcf" transparent opacity={0.8} depthWrite={false} />
        </mesh>
      )}
      {telegraph.kind === "spread" && (
        <>
          <mesh position={[0, 0.028, 0]}>
            <boxGeometry args={[telegraph.radius * 1.35, 0.032, 0.07]} />
            <meshBasicMaterial color="#ffd9ff" transparent opacity={0.9} />
          </mesh>
          <mesh position={[0, 0.03, 0]}>
            <boxGeometry args={[0.07, 0.032, telegraph.radius * 1.35]} />
            <meshBasicMaterial color="#ffd9ff" transparent opacity={0.9} />
          </mesh>
        </>
      )}
      {telegraph.kind === "soak" && SOAK_DIRECTIONS.map((angle, index) => (
        <group key={index} rotation={[0, angle, 0]} position={[0, 0.035, 0]}>
          <mesh position={[0, 0, -telegraph.radius + 0.32]} rotation={[-Math.PI / 2, 0, 0]}>
            <coneGeometry args={[0.16, 0.34, 3]} />
            <meshBasicMaterial color="#c9fbff" transparent opacity={0.96} />
          </mesh>
        </group>
      ))}
      <CircleMechanicVfx
        radius={telegraph.radius}
        innerRadius={innerRadius}
        kind={telegraph.kind}
        color={color}
        active={active}
      />
    </group>
  );
}

export function PooledMechanicIndicators({ bossIndex = 0 }: { bossIndex?: number }) {
  const telegraphs = useGameStore((state) => motionAt(state, bossIndex)?.poolTelegraphs ?? EMPTY_POOL_TELEGRAPHS);
  return <>{telegraphs.map((telegraph) => <PooledTelegraphIndicator key={telegraph.id} telegraphId={telegraph.id} bossIndex={bossIndex} />)}</>;
}

export function BasicMeleeVfx({ bossIndex = 0 }: { bossIndex?: number }) {
  const group = useRef<THREE.Group>(null);
  const slashMaterial = useRef<THREE.MeshBasicMaterial>(null);
  const impactMaterial = useRef<THREE.MeshBasicMaterial>(null);
  const initialBoss = bossAt(useGameStore.getState(), bossIndex);
  const previousNextMeleeAt = useRef(initialBoss?.nextMeleeAt ?? 0);
  const age = useRef(Number.POSITIVE_INFINITY);

  useFrame((_, delta) => {
    const state = useGameStore.getState();
    const boss = bossAt(state, bossIndex);
    const motion = motionAt(state, bossIndex);
    if (!group.current || !boss || !motion) return;
    if (boss.nextMeleeAt !== previousNextMeleeAt.current) {
      if (state.phase === "combat" && motion.mode === "holding") age.current = 0;
      previousNextMeleeAt.current = boss.nextMeleeAt;
    }
    age.current += delta;
    const visible = state.phase === "combat" && age.current < 0.42 && state.party[1].hp > 0;
    group.current.visible = visible;
    if (!visible) return;
    const target = state.partyPositions.brann;
    const progress = THREE.MathUtils.clamp(age.current / 0.42, 0, 1);
    group.current.position.set(target[0], 1.02, target[1]);
    group.current.rotation.y = Math.atan2(target[0] - motion.position[0], target[1] - motion.position[1]);
    group.current.scale.setScalar(0.68 + progress * 0.72);
    if (slashMaterial.current) slashMaterial.current.opacity = Math.sin(progress * Math.PI) * 0.92;
    if (impactMaterial.current) impactMaterial.current.opacity = (1 - progress) * 0.68;
  });

  return (
    <group ref={group} visible={false}>
      <mesh rotation={[0, 0, -Math.PI * 0.72]}>
        <torusGeometry args={[0.7, 0.055, 6, 28, Math.PI * 1.35]} />
        <meshBasicMaterial
          ref={slashMaterial}
          color="#ffd58a"
          transparent
          opacity={0}
          depthWrite={false}
          blending={THREE.AdditiveBlending}
          toneMapped={false}
        />
      </mesh>
      <mesh position={[0, -0.94, 0]} rotation={[-Math.PI / 2, 0, 0]}>
        <ringGeometry args={[0.32, 0.5, 24]} />
        <meshBasicMaterial ref={impactMaterial} color="#ff8d5c" transparent opacity={0} depthWrite={false} />
      </mesh>
    </group>
  );
}

const BOSS_MECHANIC_INDICATORS: readonly ComponentType<{ bossIndex?: number }>[] = [
  BasicMeleeVfx,
  ChargeLaneIndicator,
  SlashLaneIndicators,
  PounceStackIndicator,
  BindingWebIndicator,
  BreathConeIndicator,
  CircleHazardIndicators,
  SoulSiphonIndicator,
  PooledMechanicIndicators,
];

function BossMechanicIndicatorSet({ bossIndex }: { bossIndex: number }) {
  const defeated = useGameStore((state) => bossHpAt(state, bossIndex) <= 0);
  const group = useRef<THREE.Group>(null);
  const opacity = useRef(defeated ? 0 : 1);
  const patchedMeshes = useRef(new WeakSet<THREE.Mesh>());
  const [retainIndicators, setRetainIndicators] = useState(!defeated);

  const patchMeshOpacity = useCallback((child: THREE.Object3D) => {
    if (!(child instanceof THREE.Mesh) || patchedMeshes.current.has(child)) return;
    patchedMeshes.current.add(child);
    const previousBeforeRender = child.onBeforeRender;
    const previousAfterRender = child.onAfterRender;
    const materials = Array.isArray(child.material) ? child.material : [child.material];
    const sourceOpacities = new Float32Array(materials.length);
    child.onBeforeRender = (renderer, scene, camera, geometry, material, renderGroup) => {
      previousBeforeRender.call(child, renderer, scene, camera, geometry, material, renderGroup);
      for (let index = 0; index < materials.length; index += 1) {
        sourceOpacities[index] = materials[index].opacity;
        materials[index].opacity *= opacity.current;
      }
    };
    child.onAfterRender = (renderer, scene, camera, geometry, material, renderGroup) => {
      previousAfterRender.call(child, renderer, scene, camera, geometry, material, renderGroup);
      for (let index = 0; index < materials.length; index += 1) materials[index].opacity = sourceOpacities[index];
    };
  }, []);

  useEffect(() => {
    if (!defeated) {
      setRetainIndicators(true);
      return;
    }
    const timeout = window.setTimeout(() => setRetainIndicators(false), BOSS_INDICATOR_DEATH_FADE_MS);
    return () => window.clearTimeout(timeout);
  }, [defeated]);

  useFrame((_, delta) => {
    if (!group.current) return;
    opacity.current = advanceBossIndicatorOpacity(opacity.current, defeated, delta);
    group.current.visible = opacity.current > 0;
    group.current.traverse(patchMeshOpacity);
  });

  return (
    <group ref={group}>
      {retainIndicators && BOSS_MECHANIC_INDICATORS.map((Indicator) => <Indicator key={Indicator.name} bossIndex={bossIndex} />)}
    </group>
  );
}

export function BossMechanicIndicators() {
  const bossCount = useGameStore((state) => state.additionalBosses.length + 1);
  return (
    <>
      <SoulSiphonGuidanceIndicator />
      {Array.from({ length: bossCount }, (_, bossIndex) => (
        <BossMechanicIndicatorSet key={bossIndex} bossIndex={bossIndex} />
      ))}
    </>
  );
}
