import { useEffect, useMemo, useRef, useState } from "react";
import { useFrame, useThree } from "@react-three/fiber";
import {
  AdditiveBlending,
  Color,
  DoubleSide,
  Group,
  MeshBasicMaterial,
  PointsMaterial,
  Vector3,
} from "three";
import { useShellStore } from "../app/shellStore";
import { useCombatStore } from "../game/combatStore";
import {
  combatEffectStyle,
  preloadCombatEffectManifest,
  subscribeCombatPresentation,
  type CombatPresentationEvent,
  type CombatVfxQuality,
} from "../game/combatPresentation";
import {
  configureCombatAudio,
  playCombatAudio,
  resetCombatAudio,
  setCombatAudioListener,
  stopCombatAudioGroup,
} from "../game/combatAudio";

interface ActiveCombatEffect {
  readonly key: string;
  readonly event: CombatPresentationEvent;
}

export interface CombatEffectsProps {
  readonly active: boolean;
  readonly safeGraphics?: boolean;
}

const QUALITY_SYSTEM_LIMIT: Readonly<Record<CombatVfxQuality, number>> = {
  off: 0,
  low: 24,
  high: 64,
};

const QUALITY_PARTICLE_COUNT: Readonly<Record<Exclude<CombatVfxQuality, "off">, number>> = {
  low: 48,
  high: 128,
};

function effectDurationMs(event: CombatPresentationEvent): number {
  if (event.phase === "cast-start") return Math.max(280, Math.min(4_500, event.durationMs ?? 900));
  if (event.phase === "tick") return 320;
  if (event.phase === "impact") return 420;
  if (event.delivery === "melee") return 280;
  if (event.delivery === "area") return 850;
  if (event.delivery === "aura") return 950;
  if (event.delivery === "channel") return 620;
  if (event.delivery === "projectile" && event.targetPosition) {
    const dx = event.targetPosition[0] - event.origin[0];
    const dy = event.targetPosition[1] - event.origin[1];
    const dz = event.targetPosition[2] - event.origin[2];
    return Math.max(180, Math.min(550, Math.hypot(dx, dy, dz) / 28 * 1_000));
  }
  return 460;
}

function deterministicParticles(seed: number, count: number): Float32Array {
  const result = new Float32Array(count * 3);
  for (let index = 0; index < count; index += 1) {
    const angle = index * 2.399963229728653 + seed * 0.17;
    const normalized = (index + 0.5) / count;
    const radius = 0.12 + Math.sqrt(normalized) * 0.72;
    result[index * 3] = Math.cos(angle) * radius;
    result[index * 3 + 1] = (normalized - 0.45) * 1.4 + Math.sin(seed + index * 1.73) * 0.16;
    result[index * 3 + 2] = Math.sin(angle) * radius;
  }
  return result;
}

function CombatEffectPrimitive({
  event,
  quality,
  active,
  onDone,
}: {
  readonly event: CombatPresentationEvent;
  readonly quality: Exclude<CombatVfxQuality, "off">;
  readonly active: boolean;
  readonly onDone: () => void;
}) {
  const groupRef = useRef<Group>(null);
  const pointsMaterialRef = useRef<PointsMaterial>(null);
  const primaryMaterialRef = useRef<MeshBasicMaterial>(null);
  const secondaryMaterialRef = useRef<MeshBasicMaterial>(null);
  const elapsedRef = useRef(0);
  const doneRef = useRef(false);
  const style = useMemo(() => combatEffectStyle(event), [event]);
  const particleCount = QUALITY_PARTICLE_COUNT[quality];
  const positions = useMemo(
    () => deterministicParticles(event.id, particleCount),
    [event.id, particleCount],
  );
  const origin = useMemo(() => new Vector3(...event.origin), [event.origin]);
  const target = useMemo(
    () => new Vector3(...(event.targetPosition ?? event.origin)),
    [event.origin, event.targetPosition],
  );
  const durationMs = effectDurationMs(event);
  const projectile = event.phase === "release"
    && event.delivery === "projectile"
    && event.targetPosition !== undefined;
  const effectPosition = projectile
    ? origin
    : event.phase === "cast-start"
      ? origin
      : target;
  const isGround = event.delivery === "area";
  const initialY = effectPosition.y + (isGround ? 0.08 : event.delivery === "melee" ? 1 : 1.15);

  useFrame((_state, delta) => {
    if (!active || doneRef.current || !groupRef.current) return;
    elapsedRef.current += Math.min(0.05, delta) * 1_000;
    const progress = Math.min(1, elapsedRef.current / durationMs);
    const eased = 1 - (1 - progress) ** 3;
    if (projectile) {
      groupRef.current.position.lerpVectors(origin, target, eased);
      groupRef.current.position.y += 1.05 + Math.sin(progress * Math.PI) * 0.28;
      groupRef.current.scale.setScalar(0.72 + Math.sin(progress * Math.PI) * 0.32);
    } else {
      groupRef.current.rotation.y += delta * (event.school === "physical" ? 4 : 1.8);
      const scale = event.phase === "cast-start"
        ? 0.85 + Math.sin(progress * Math.PI * 3) * 0.08
        : 0.35 + eased * (event.delivery === "area" ? Math.max(1.2, event.radius ?? 2.4) : 1.75);
      groupRef.current.scale.setScalar(scale);
      if (!isGround && event.delivery !== "melee") groupRef.current.position.y = initialY + progress * 0.55;
    }
    const opacity = event.phase === "cast-start"
      ? Math.min(1, (1 - progress) * 1.8)
      : Math.max(0, 1 - progress);
    if (pointsMaterialRef.current) opacity >= 0 && (pointsMaterialRef.current.opacity = opacity * 0.9);
    if (primaryMaterialRef.current) primaryMaterialRef.current.opacity = opacity * 0.72;
    if (secondaryMaterialRef.current) secondaryMaterialRef.current.opacity = opacity * 0.55;
    if (progress >= 1) {
      doneRef.current = true;
      onDone();
    }
  });

  return (
    <group
      ref={groupRef}
      position={[effectPosition.x, initialY, effectPosition.z]}
      rotation={isGround ? [-Math.PI / 2, 0, 0] : [0, 0, event.delivery === "melee" ? -0.55 : 0]}
    >
      <points>
        <bufferGeometry>
          <bufferAttribute attach="attributes-position" args={[positions, 3]} />
        </bufferGeometry>
        <pointsMaterial
          ref={pointsMaterialRef}
          color={style.primary}
          size={quality === "high" ? 0.15 : 0.19}
          sizeAttenuation
          transparent
          opacity={0.9}
          depthWrite={false}
          blending={AdditiveBlending}
        />
      </points>
      {event.delivery === "melee" ? (
        <mesh rotation={[0, 0.2, Math.PI / 2]}>
          <torusGeometry args={[0.7, 0.075, 8, 32, Math.PI * 1.35]} />
          <meshBasicMaterial
            ref={primaryMaterialRef}
            color={style.primary}
            transparent
            opacity={0.75}
            depthWrite={false}
            side={DoubleSide}
            blending={AdditiveBlending}
          />
        </mesh>
      ) : event.delivery === "area" ? (
        <>
          <mesh>
            <ringGeometry args={[0.72, 1, 48]} />
            <meshBasicMaterial
              ref={primaryMaterialRef}
              color={style.primary}
              transparent
              opacity={0.62}
              depthWrite={false}
              side={DoubleSide}
              blending={AdditiveBlending}
            />
          </mesh>
          <mesh scale={0.68}>
            <ringGeometry args={[0.82, 1, 32]} />
            <meshBasicMaterial
              ref={secondaryMaterialRef}
              color={style.secondary}
              transparent
              opacity={0.5}
              depthWrite={false}
              side={DoubleSide}
              blending={AdditiveBlending}
            />
          </mesh>
        </>
      ) : (
        <>
          <mesh>
            <sphereGeometry args={[projectile ? 0.28 : 0.44, 12, 8]} />
            <meshBasicMaterial
              ref={primaryMaterialRef}
              color={style.primary}
              transparent
              opacity={0.7}
              depthWrite={false}
              blending={AdditiveBlending}
            />
          </mesh>
          <mesh scale={projectile ? 0.52 : 1.18}>
            <sphereGeometry args={[0.32, 10, 7]} />
            <meshBasicMaterial
              ref={secondaryMaterialRef}
              color={style.secondary}
              transparent
              opacity={0.45}
              wireframe={!projectile}
              depthWrite={false}
              blending={AdditiveBlending}
            />
          </mesh>
        </>
      )}
      {quality === "high" && event.school !== "physical" ? (
        <pointLight color={new Color(style.primary)} intensity={style.emissive * 1.5} distance={5} decay={2} />
      ) : null}
    </group>
  );
}

export function CombatEffects({ active, safeGraphics = false }: CombatEffectsProps) {
  const settings = useShellStore((state) => state.combatPresentationSettings);
  const masterVolume = useCombatStore((state) => state.settings.masterVolume);
  const [effects, setEffects] = useState<readonly ActiveCombatEffect[]>([]);
  const settingsRef = useRef(settings);
  const activeRef = useRef(active);
  const effectiveQuality: CombatVfxQuality = safeGraphics && settings.vfxQuality === "high"
    ? "low"
    : settings.vfxQuality;
  const qualityRef = useRef(effectiveQuality);
  const camera = useThree((state) => state.camera);
  const forward = useMemo(() => new Vector3(), []);

  useEffect(() => {
    settingsRef.current = settings;
    qualityRef.current = safeGraphics && settings.vfxQuality === "high" ? "low" : settings.vfxQuality;
    configureCombatAudio(settings, masterVolume);
    if (
      qualityRef.current !== "off"
      || Object.values(settings.audioEnabled).some(Boolean)
    ) void preloadCombatEffectManifest();
    setEffects((current) => current.filter(({ event }) => (
      qualityRef.current !== "off" && settings.vfxEnabled[event.actorGroup]
    )));
    for (const group of ["player", "party", "enemy"] as const) {
      if (!settings.audioEnabled[group] || settings.audioVolume[group] <= 0) stopCombatAudioGroup(group);
    }
  }, [masterVolume, safeGraphics, settings]);

  useEffect(() => {
    activeRef.current = active;
  }, [active]);

  useEffect(() => subscribeCombatPresentation((message) => {
    if (message.kind === "reset") {
      setEffects([]);
      resetCombatAudio();
      return;
    }
    const event = message.event;
    if (activeRef.current) playCombatAudio(event);
    setEffects((current) => {
      const withoutCast = event.phase === "release" || event.phase === "cast-cancel"
        ? current.filter((effect) => !(
          effect.event.phase === "cast-start"
          && effect.event.sourceActorId === event.sourceActorId
          && effect.event.abilityId === event.abilityId
        ))
        : current;
      const presentationSettings = settingsRef.current;
      const quality = qualityRef.current;
      if (
        event.phase === "cast-cancel"
        || !activeRef.current
        || quality === "off"
        || !presentationSettings.vfxEnabled[event.actorGroup]
      ) return withoutCast;
      const limit = QUALITY_SYSTEM_LIMIT[quality];
      return [...withoutCast, { key: `${event.sessionId}:${event.id}`, event }].slice(-limit);
    });
  }), []);

  useEffect(() => () => resetCombatAudio(), []);

  useFrame(() => {
    camera.getWorldDirection(forward);
    setCombatAudioListener(
      [camera.position.x, camera.position.y, camera.position.z],
      [forward.x, forward.y, forward.z],
    );
  });

  if (effectiveQuality === "off") return null;
  return (
    <group name="combat-effects">
      {effects.map((effect) => (
        <CombatEffectPrimitive
          key={effect.key}
          event={effect.event}
          quality={effectiveQuality}
          active={active}
          onDone={() => setEffects((current) => current.filter((candidate) => candidate.key !== effect.key))}
        />
      ))}
    </group>
  );
}
