// WoW-grade upper/lower body animation & locomotion controller for Frostcall Arena.
// Fixes the 6 root causes identified in WHY.md without touching src/lib/*:
// 1. Run speed matching + stride amplification + FootLock release while moving (eliminates foot skating & ankle snapping).
// 2. Real strafe left/right: yaws pelvis & legs into the travel vector while counter-rotating spine_01..03 so chest & weapons face the target.
// 3. Real backpedal: keeps character facing forward while driving a reversed-stride backpedal cycle on thighs, calves, and feet.
// 4. Turn & strafe banking roll + athletic forward run/dash/charge lean + hip/shoulder counter-twist.
// 5. Upper-body layered spell casting, skyward Blizzard channeling, Whirlwind spin, and instant-release thrusts while legs keep running.
// 6. Three-phase jump (take-off drive, live mid-air float pose, and landing knee/pelvis absorption at any speed) + non-blocking hit flinch.
import * as THREE from "three";
import type { Walker } from "@/lib/cast/actors/walker";
import type { CastState } from "../combat/sim";
import type { GarmentRole } from "./garments";

export interface UnitAnimInput {
  /** World-space velocity X (m/s). */
  vx: number;
  /** World-space velocity Z (m/s). */
  vz: number;
  /** Character forward facing yaw in world space (radians). */
  yaw: number;
  /** True while jumping or leaping in the air. */
  airborne: boolean;
  /** Normalized jump progress 0..1 when airborne (optional). */
  jumpProgress?: number;
  /** Active cast/channel state from sim. */
  cast: CastState | null;
  /** True when dashing (Mage Dash) or charging (Warrior Charge). */
  dashing?: boolean;
  /** True when dead. */
  dead: boolean;
  /** True when stunned, polymorphed, or frozen in Ice Block. */
   immobilizedPose?: "iceblock" | "stun" | null;
}

export interface UnitAnimController {
  /** Trigger an upper-body cast windup. */
  onCastStart(spellId: string, kind: CastState["kind"]): void;
  /** Trigger an upper-body spell release or melee strike without freezing leg locomotion. */
  onRelease(spellId: string): void;
  /** Trigger a short non-blocking upper-body hit flinch. */
  onHit(fromX?: number, fromZ?: number): void;
  /** Update per-frame state from Game.ts before or alongside walker.update(dt). */
  setInput(input: UnitAnimInput): void;
}

interface RigBones {
  pelvis: THREE.Object3D | null;
  spine1: THREE.Object3D | null;
  spine2: THREE.Object3D | null;
  spine3: THREE.Object3D | null;
  neck: THREE.Object3D | null;
  head: THREE.Object3D | null;
  clavL: THREE.Object3D | null;
  clavR: THREE.Object3D | null;
  uarmL: THREE.Object3D | null;
  larmL: THREE.Object3D | null;
  handL: THREE.Object3D | null;
  uarmR: THREE.Object3D | null;
  larmR: THREE.Object3D | null;
  handR: THREE.Object3D | null;
  thighL: THREE.Object3D | null;
  calfL: THREE.Object3D | null;
  footL: THREE.Object3D | null;
  thighR: THREE.Object3D | null;
  calfR: THREE.Object3D | null;
  footR: THREE.Object3D | null;
}

export function attachUnitAnimation(walker: Walker, role: GarmentRole): UnitAnimController {
  const root = walker.root;
  let rig: RigBones | null = null;
  let footLockRef: { reset(): void } | null = null;

  // Persistent animation state (zero per-frame allocations)
  let time = Math.random() * 3;
  let backPhase = 0;
  let runPhase = 0;
  let whirlYaw = 0;
  let prevYaw = root.rotation.y;
  let smoothedTurnRate = 0;
  let smoothedStrafeYaw = 0;
  let smoothedBankRoll = 0;
  let smoothedFwdLean = 0;
  let smoothedBackBlend = 0;
  let smoothedRunBlend = 0;
  let smoothedCastBlend = 0;
  let smoothedChannelBlend = 0;
  let wasAirborne = false;
  let landTimer = -1;
  const landDuration = 0.24;
  let releaseTimer = -1;
  let releaseDuration = 0.42;
  let releaseSpell = "";
  let hitTimer = -1;
  const hitDuration = 0.22;
  let hitDirX = 0;
  let hitDirZ = 1;

  const state: UnitAnimInput = {
    vx: 0,
    vz: 0,
    yaw: root.rotation.y,
    airborne: false,
    jumpProgress: 0,
    cast: null,
    dashing: false,
    dead: false,
    immobilizedPose: null,
  };

  // Pre-allocated scratch math objects
  const qRoot = new THREE.Quaternion();
  const qRootInv = new THREE.Quaternion();
  const qBoneWorld = new THREE.Quaternion();
  const qParentInv = new THREE.Quaternion();
  const qDeltaRoot = new THREE.Quaternion();
  const qIdentity = new THREE.Quaternion();
  const eDeltaRoot = new THREE.Euler(0, 0, 0, "YXZ");
  const vCurY = new THREE.Vector3();
  const vWantY = new THREE.Vector3();
  const vPelvisLocal = new THREE.Vector3();

  // Baseline thigh rest quaternions to amplify forward stride angle
  const qThighLBase = new THREE.Quaternion();
  const qThighRBase = new THREE.Quaternion();
  let hasThighBase = false;

  function ensureRig(): RigBones | null {
    if (rig && rig.pelvis) return rig;
    const pelvis = root.getObjectByName("pelvis") ?? null;
    if (!pelvis) return null;
    rig = {
      pelvis,
      spine1: root.getObjectByName("spine_01") ?? null,
      spine2: root.getObjectByName("spine_02") ?? null,
      spine3: root.getObjectByName("spine_03") ?? null,
      neck: root.getObjectByName("neck_01") ?? null,
      head: root.getObjectByName("Head") ?? null,
      clavL: root.getObjectByName("clavicle_l") ?? null,
      clavR: root.getObjectByName("clavicle_r") ?? null,
      uarmL: root.getObjectByName("upperarm_l") ?? null,
      larmL: root.getObjectByName("lowerarm_l") ?? null,
      handL: root.getObjectByName("hand_l") ?? null,
      uarmR: root.getObjectByName("upperarm_r") ?? null,
      larmR: root.getObjectByName("lowerarm_r") ?? null,
      handR: root.getObjectByName("hand_r") ?? null,
      thighL: root.getObjectByName("thigh_l") ?? null,
      calfL: root.getObjectByName("calf_l") ?? null,
      footL: root.getObjectByName("foot_l") ?? null,
      thighR: root.getObjectByName("thigh_r") ?? null,
      calfR: root.getObjectByName("calf_r") ?? null,
      footR: root.getObjectByName("foot_r") ?? null,
    };
    if (rig.thighL && rig.thighR && !hasThighBase) {
      qThighLBase.copy(rig.thighL.quaternion);
      qThighRBase.copy(rig.thighR.quaternion);
      hasThighBase = true;
    }
    footLockRef = (root.userData as { footLock?: { reset(): void } }).footLock ?? null;
    return rig;
  }

  /**
   * Apply an additive rotation (pitchX, yawY, rollZ) expressed in character-root space
   * (+X left, +Y up, +Z forward) onto `bone`.
   */
  function rotateBoneInRootSpace(
    bone: THREE.Object3D | null,
    pitchX: number,
    yawY: number,
    rollZ: number,
  ) {
    if (!bone || !bone.parent) return;
    if (Math.abs(pitchX) < 1e-4 && Math.abs(yawY) < 1e-4 && Math.abs(rollZ) < 1e-4) return;
    bone.parent.updateWorldMatrix(true, false);
    bone.updateMatrix();
    bone.matrixWorld.multiplyMatrices(bone.parent.matrixWorld, bone.matrix);
    bone.getWorldQuaternion(qBoneWorld);

    eDeltaRoot.set(pitchX, yawY, rollZ, "YXZ");
    qDeltaRoot.setFromEuler(eDeltaRoot);

    // qNewWorld = qRoot * qDeltaRoot * qRootInv * qBoneWorld
    qBoneWorld.premultiply(qRootInv).premultiply(qDeltaRoot).premultiply(qRoot);
    bone.parent.getWorldQuaternion(qParentInv).invert();
    bone.quaternion.copy(qParentInv).multiply(qBoneWorld).normalize();
    bone.updateMatrix();
    bone.matrixWorld.multiplyMatrices(bone.parent.matrixWorld, bone.matrix);
  }

  /**
   * Steer `bone`'s +Y shaft axis in character-root space toward `(wx, wy, wz)` with blend `weight` (0..1).
   */
  function aimBoneYInRootSpace(
    bone: THREE.Object3D | null,
    wx: number,
    wy: number,
    wz: number,
    weight: number,
  ) {
    if (!bone || !bone.parent || weight <= 0.001) return;
    bone.parent.updateWorldMatrix(true, false);
    bone.updateMatrix();
    bone.matrixWorld.multiplyMatrices(bone.parent.matrixWorld, bone.matrix);
    bone.getWorldQuaternion(qBoneWorld);

    qBoneWorld.premultiply(qRootInv);
    vCurY.set(0, 1, 0).applyQuaternion(qBoneWorld).normalize();
    vWantY.set(wx, wy, wz).normalize();
    qDeltaRoot.setFromUnitVectors(vCurY, vWantY);
    if (weight < 0.999) qDeltaRoot.slerp(qIdentity, 1 - weight);

    qBoneWorld.premultiply(qDeltaRoot).premultiply(qRoot);
    bone.parent.getWorldQuaternion(qParentInv).invert();
    bone.quaternion.copy(qParentInv).multiply(qBoneWorld).normalize();
    bone.updateMatrix();
    bone.matrixWorld.multiplyMatrices(bone.parent.matrixWorld, bone.matrix);
  }

  // Intercept walker.act so spell/melee releases while running do NOT fade out the leg locomotion clip!
  const origAct = walker.act.bind(walker);
  walker.act = (name) => {
    const speed = Math.hypot(state.vx, state.vz);
    const isUpperAction = name === "throw" || name === "interact" || name === "punch" || name === "hook";
    if (isUpperAction && (speed > 0.35 || state.airborne || state.cast != null)) {
      if (releaseTimer < 0) {
        releaseTimer = 0;
        releaseDuration = 0.42;
        releaseSpell = name;
      }
      return 0.42;
    }
    return origAct(name);
  };

  // Hook walker.update: runs BEFORE and AFTER walker's internal mixer + FootLock update
  const origUpdate = walker.update.bind(walker);
  walker.update = (dt: number) => {
    const step = Math.min(Math.max(dt, 0), 0.1);
    time += step;

    const speed = Math.hypot(state.vx, state.vz);
    const sinY = Math.sin(state.yaw);
    const cosY = Math.cos(state.yaw);
    const localFwd = state.vx * sinY + state.vz * cosY;
    const localLat = state.vx * cosY - state.vz * sinY; // +1 = moving right, -1 = moving left

    // Ensure walker locomotion state and clip selection match actual instantaneous ground speed before mixer ticks
    if (!state.dead && !state.airborne && state.immobilizedPose == null) {
      if (speed > 0.15) walker.go(speed);
    }

    // Run original mixer + inertializer + FootLock + attachMageStaff
    origUpdate(dt);

    const b = ensureRig();
    if (!b || !b.pelvis) return;

    // Release FootLock whenever moving, strafing, backpedalling, jumping, or dead so ankles never snag/skate
    if (footLockRef && (speed > 0.2 || state.airborne || state.dead)) {
      footLockRef.reset();
    }

    // If dead, let Death01 play cleanly without procedural overrides
    if (state.dead) {
      return;
    }

    // Detect landing transition (airborne -> ground) to trigger landing absorption even at 8 m/s run
    if (wasAirborne && !state.airborne) {
      landTimer = 0;
    }
    wasAirborne = state.airborne;

    // Compute angular turn velocity (rad/s) wrapped to [-pi, pi]
    let dYaw = state.yaw - prevYaw;
    while (dYaw > Math.PI) dYaw -= Math.PI * 2;
    while (dYaw < -Math.PI) dYaw += Math.PI * 2;
    prevYaw = state.yaw;
    const rawTurnRate = step > 1e-4 ? Math.max(-10, Math.min(10, dYaw / step)) : 0;
    const kSmooth = step > 0 ? Math.min(1, step * 14) : 1;
    smoothedTurnRate += (rawTurnRate - smoothedTurnRate) * kSmooth;

    // Whirlwind spin override for Gladiator / Warrior
    const isWhirlwind = state.cast?.id === "whirl";
    if (isWhirlwind) {
      whirlYaw = (whirlYaw + step * 18.0) % (Math.PI * 2);
      root.rotation.y = state.yaw + whirlYaw;
    } else {
      whirlYaw = 0;
    }

    root.updateWorldMatrix(true, false);
    root.getWorldQuaternion(qRoot);
    qRootInv.copy(qRoot).invert();

    // -----------------------------------------------------------------------
    // 1. Strafe Hip/Leg Yaw & Backpedal Blend
    // -----------------------------------------------------------------------
    const moving = speed > 0.25 && !state.airborne;
    const isBacking = moving && localFwd < -0.25;
    let targetStrafeYaw = 0;
    if (moving && Math.abs(localLat) > 0.2) {
      if (Math.abs(localFwd) < 0.35) {
        // Pure left/right strafe: hips & legs angle +-56 deg into the strafe
        targetStrafeYaw = -Math.sign(localLat) * 0.98;
      } else if (localFwd > 0) {
        // Forward diagonal strafe: hips & legs angle +-34 deg
        targetStrafeYaw = -Math.sign(localLat) * 0.60;
      } else {
        // Backward diagonal strafe: hips angle +-32 deg while legs backpedal
        targetStrafeYaw = Math.sign(localLat) * 0.56;
      }
    }
    smoothedStrafeYaw += (targetStrafeYaw - smoothedStrafeYaw) * kSmooth;

    const targetBackBlend = isBacking ? Math.min(1, Math.abs(localFwd) / 3.2) : 0;
    smoothedBackBlend += (targetBackBlend - smoothedBackBlend) * kSmooth;

    const targetRunBlend = moving && !isBacking ? Math.min(1, speed / 6.5) : 0;
    smoothedRunBlend += (targetRunBlend - smoothedRunBlend) * kSmooth;

    // Banking roll into turns and strafes + athletic forward run/dash/charge lean
    const strafeNorm = Math.max(-1, Math.min(1, localLat / 7.5));
    const targetBankRoll = moving
      ? Math.max(-0.22, Math.min(0.22, -strafeNorm * 0.13 - smoothedTurnRate * 0.025))
      : Math.max(-0.1, Math.min(0.1, -smoothedTurnRate * 0.018));
    smoothedBankRoll += (targetBankRoll - smoothedBankRoll) * kSmooth;

    const dashBoost = state.dashing ? 0.24 : 0;
    const targetFwdLean = state.airborne
      ? 0.06
      : isBacking
        ? 0.12 * smoothedBackBlend
        : smoothedRunBlend * 0.19 + dashBoost;
    smoothedFwdLean += (targetFwdLean - smoothedFwdLean) * kSmooth;

    // Advance rhythmic stride phases
    if (moving) {
      runPhase = (runPhase + step * speed * 1.95) % (Math.PI * 2);
      backPhase = (backPhase + step * speed * 2.45) % (Math.PI * 2);
    }

    // -----------------------------------------------------------------------
    // 2. Lower Body: Pelvis Yaw/Roll/Bounce + Stride Amplification + Backpedal
    // -----------------------------------------------------------------------
    const strideBounce = smoothedRunBlend * Math.abs(Math.sin(runPhase * 2)) * 0.032;
    const backCrouch = smoothedBackBlend * (-0.042 + Math.abs(Math.sin(backPhase)) * 0.024);

    // Landing absorption curve (0..1..0 over 0.24s)
    let landDip = 0;
    if (landTimer >= 0) {
      landTimer += step;
      if (landTimer >= landDuration) {
        landTimer = -1;
      } else {
        const u = landTimer / landDuration;
        landDip = Math.sin(u * Math.PI) * (1 - u * 0.35);
      }
    }

    // Apply vertical pelvis offset in root space (bounce, backpedal crouch, landing dip)
    const pelvisYDelta = strideBounce + backCrouch - landDip * 0.095;
    if (Math.abs(pelvisYDelta) > 1e-4 && b.pelvis.parent) {
      vPelvisLocal.set(0, pelvisYDelta, 0);
      b.pelvis.position.y += vPelvisLocal.y;
      b.pelvis.updateMatrix();
    }

    // Rotate pelvis in root space: yaws entire lower body into strafe, banks into turn, adds subtle hip counter-swing
    const hipSwingYaw = smoothedRunBlend * Math.sin(runPhase) * 0.08;
    rotateBoneInRootSpace(
      b.pelvis,
      smoothedFwdLean * 0.35 + landDip * 0.12,
      smoothedStrafeYaw + hipSwingYaw,
      smoothedBankRoll,
    );

    // Amplify forward run leg stride angle (+18% thigh swing) so 8 m/s ground speed never looks choppy
    if (smoothedRunBlend > 0.05 && !state.airborne && hasThighBase && b.thighL && b.thighR) {
      const amp = smoothedRunBlend * (state.dashing ? 0.32 : 0.18);
      qDeltaRoot.copy(qThighLBase).invert().multiply(b.thighL.quaternion);
      b.thighL.quaternion.multiply(qIdentity.clone().slerp(qDeltaRoot, amp)).normalize();
      b.thighL.updateMatrix();

      qDeltaRoot.copy(qThighRBase).invert().multiply(b.thighR.quaternion);
      b.thighR.quaternion.multiply(qIdentity.clone().slerp(qDeltaRoot, amp)).normalize();
      b.thighR.updateMatrix();
    }

    // True reversed-stride backpedal on legs when moving backward (S)
    if (smoothedBackBlend > 0.02 && !state.airborne) {
      const wBack = smoothedBackBlend;
      const sYaw = smoothedStrafeYaw;
      const sinS = Math.sin(sYaw);
      const cosS = Math.cos(sYaw);

      for (let leg = 0; leg < 2; leg++) {
        const isL = leg === 0;
        const side = isL ? 1 : -1;
        const ph = isL ? backPhase : backPhase + Math.PI;
        const s = Math.sin(ph);
        const c = Math.cos(ph);

        // Thigh reaches backward (-Z) on recovery (c > 0) and pushes from front (+Z) to back (-Z)
        const localX = side * 0.09;
        const thighZ = -0.06 + s * 0.44;
        const tx = localX * cosS + thighZ * sinS;
        const tz = -localX * sinS + thighZ * cosS;
        aimBoneYInRootSpace(isL ? b.thighL : b.thighR, tx, -0.90, tz, wBack * 0.92);

        // Calf bends backward at the knee as the foot lifts to reach behind
        const kneeBend = 0.26 + Math.max(0, c) * 0.52;
        const calfZ = thighZ - kneeBend;
        const cx = localX * cosS + calfZ * sinS;
        const cz = -localX * sinS + calfZ * cosS;
        aimBoneYInRootSpace(isL ? b.calfL : b.calfR, cx, -0.84, cz, wBack * 0.92);

        // Foot plantarflexes as it reaches back for toe-down contact
        rotateBoneInRootSpace(isL ? b.footL : b.footR, (0.14 + c * 0.22) * wBack, 0, 0);
      }
    }

    // Landing knee absorption (flexes thighs forward and calves back on touchdown even while running)
    if (landDip > 0.01 && !state.airborne) {
      const kBend = landDip * 0.34;
      rotateBoneInRootSpace(b.thighL, -kBend, 0, 0);
      rotateBoneInRootSpace(b.thighR, -kBend, 0, 0);
      rotateBoneInRootSpace(b.calfL, kBend * 1.45, 0, 0);
      rotateBoneInRootSpace(b.calfR, kBend * 1.45, 0, 0);
    }

    // -----------------------------------------------------------------------
    // 3. Three-Phase Jump Pose (Take-Off -> Live Mid-Air Float -> Pre-Land)
    // -----------------------------------------------------------------------
    if (state.airborne) {
      const u = Math.max(0, Math.min(1, state.jumpProgress ?? 0.45));
      // Smooth bell curve at apex (u = 0.45) + subtle aerial balance motion so pose is never frozen
      const apex = Math.sin(u * Math.PI);
      const drift = Math.sin(u * Math.PI * 2.2) * 0.06;
      const isEnemyLeap = role === "warrior" || role === "gladiator";

      if (isEnemyLeap) {
        // Heroic Gladiator Leap Strike: high tucked front knee, trail leg back, weapon raised overhead
        aimBoneYInRootSpace(b.thighL, 0.12, -0.62, 0.72, 0.92);
        aimBoneYInRootSpace(b.calfL, 0.12, -0.58, -0.74, 0.92);
        aimBoneYInRootSpace(b.thighR, -0.12, -0.82, -0.48, 0.92);
        aimBoneYInRootSpace(b.calfR, -0.12, -0.68, -0.68, 0.92);
      } else if (u < 0.24) {
        // Phase 1: Take-off drive (0..0.24) - lead knee drives high, trail leg pushes off ground
        const p = u / 0.24;
        aimBoneYInRootSpace(b.thighL, 0.11, -0.66, 0.68 * p, 0.88);
        aimBoneYInRootSpace(b.calfL, 0.11, -0.62, -0.66 * p, 0.88);
        aimBoneYInRootSpace(b.thighR, -0.11, -0.92, -0.38 * p, 0.88);
        aimBoneYInRootSpace(b.calfR, -0.11, -0.82, -0.52 * p, 0.88);
      } else if (u < 0.74) {
        // Phase 2: Mid-air heroic float (0.24..0.74) - athletic split knee tuck + live aerial sway
        aimBoneYInRootSpace(b.thighL, 0.12, -0.64, 0.68 + drift, 0.92);
        aimBoneYInRootSpace(b.calfL, 0.12, -0.56, -0.74 + drift, 0.92);
        aimBoneYInRootSpace(b.thighR, -0.12, -0.78, 0.18 - drift, 0.92);
        aimBoneYInRootSpace(b.calfR, -0.12, -0.62, -0.76 - drift, 0.92);
        rotateBoneInRootSpace(b.footL, 0.28 * apex, 0, 0);
        rotateBoneInRootSpace(b.footR, 0.34 * apex, 0, 0);
      } else {
        // Phase 3: Pre-landing reach (0.74..1.0) - legs extend downward to catch the ground
        const p = (u - 0.74) / 0.26;
        const reach = 1 - p;
        aimBoneYInRootSpace(b.thighL, 0.10, -0.88, 0.32 * reach + 0.14, 0.88);
        aimBoneYInRootSpace(b.calfL, 0.10, -0.86, -0.45 * reach - 0.12, 0.88);
        aimBoneYInRootSpace(b.thighR, -0.10, -0.90, 0.12 * reach + 0.08, 0.88);
        aimBoneYInRootSpace(b.calfR, -0.10, -0.88, -0.48 * reach - 0.14, 0.88);
      }
    }

    // -----------------------------------------------------------------------
    // 4. Upper-Body Spine Counter-Twist, Forward Lean & Hit Flinch
    // -----------------------------------------------------------------------
    // Counter-rotate spine_01..03 by -smoothedStrafeYaw so chest, head, and staff face straight at the target!
    const counterYaw = -smoothedStrafeYaw - hipSwingYaw * 1.35;
    const spinePitch = smoothedFwdLean * 0.65 + landDip * 0.14;
    const spineRoll = -smoothedBankRoll * 0.28;

    rotateBoneInRootSpace(b.spine1, spinePitch * 0.35, counterYaw * 0.36, spineRoll * 0.35);
    rotateBoneInRootSpace(b.spine2, spinePitch * 0.35, counterYaw * 0.36, spineRoll * 0.35);
    rotateBoneInRootSpace(b.spine3, spinePitch * 0.30, counterYaw * 0.28, spineRoll * 0.30);

    // Keep head & neck looking forward toward the target
    rotateBoneInRootSpace(b.neck, -spinePitch * 0.35, smoothedTurnRate * 0.025, 0);
    rotateBoneInRootSpace(b.head, -spinePitch * 0.35, smoothedTurnRate * 0.03, -smoothedBankRoll * 0.25);

    // Non-blocking upper-body hit flinch impulse (0.22s)
    if (hitTimer >= 0) {
      hitTimer += step;
      if (hitTimer >= hitDuration) {
        hitTimer = -1;
      } else {
        const u = hitTimer / hitDuration;
        const flinch = Math.sin(u * Math.PI) * (1 - u * 0.4);
        // Convert hit direction into root space
        const localHitZ = hitDirX * sinY + hitDirZ * cosY;
        const localHitX = hitDirX * cosY - hitDirZ * sinY;
        const flinchPitch = -Math.sign(localHitZ || 1) * 0.18 * flinch;
        const flinchRoll = Math.sign(localHitX || 1) * 0.14 * flinch;
        rotateBoneInRootSpace(b.spine2, flinchPitch * 0.5, 0, flinchRoll * 0.5);
        rotateBoneInRootSpace(b.spine3, flinchPitch * 0.6, flinchRoll * 0.4, flinchRoll * 0.5);
        rotateBoneInRootSpace(b.neck, flinchPitch * 0.4, 0, 0);
      }
    }

    // -----------------------------------------------------------------------
    // 5. Layered Upper-Body Cast, Channel, Release & Combat Arm Poses
    // -----------------------------------------------------------------------
    const isCasting = state.cast != null && state.cast.kind === "cast";
    const isChanneling = state.cast != null && state.cast.kind === "channel";
    smoothedCastBlend += ((isCasting ? 1 : 0) - smoothedCastBlend) * kSmooth;
    smoothedChannelBlend += ((isChanneling ? 1 : 0) - smoothedChannelBlend) * kSmooth;

    let releaseWeight = 0;
    if (releaseTimer >= 0) {
      releaseTimer += step;
      if (releaseTimer >= releaseDuration) {
        releaseTimer = -1;
      } else {
        const u = releaseTimer / releaseDuration;
        releaseWeight = u < 0.22 ? u / 0.22 : 1 - (u - 0.22) / 0.78;
      }
    }

    // A. Mage / Caster Upper-Body Spellweaving, Channeling & Release
    if (role === "hero" || role === "pyromancer" || role === "priestess") {
      // Airborne balance pose for left arm when not casting
      if (state.airborne && smoothedCastBlend < 0.1 && smoothedChannelBlend < 0.1 && releaseWeight < 0.1) {
        aimBoneYInRootSpace(b.uarmL, 0.48, -0.42, 0.32, 0.78);
        aimBoneYInRootSpace(b.larmL, 0.34, 0.18, 0.82, 0.78);
      }

      // 1) Cast-bar spellweaving pose (e.g. Frostbolt, Fireball, Polymorph, Flash Heal)
      if (smoothedCastBlend > 0.01) {
        const w = smoothedCastBlend;
        const weaveX = Math.sin(time * 7.5) * 0.12;
        const weaveY = Math.cos(time * 3.8) * 0.10;
        // Turn upper chest slightly into the spell weave
        rotateBoneInRootSpace(b.spine2, 0.06 * w, 0.12 * w, 0);
        rotateBoneInRootSpace(b.spine3, 0.06 * w, 0.14 * w, 0);
        // Left off-hand weaves arcane energy in front of the chest while legs keep running
        aimBoneYInRootSpace(b.uarmL, 0.32 + weaveX * 0.4, -0.34 + weaveY * 0.4, 0.88, w * 0.94);
        aimBoneYInRootSpace(b.larmL, -0.06 + weaveX, 0.28 + weaveY, 0.94, w * 0.94);
        if (role !== "hero") {
          // Enemy caster right wand/hand also channels forward
          aimBoneYInRootSpace(b.uarmR, -0.32 - weaveX * 0.4, -0.32 - weaveY * 0.4, 0.89, w * 0.92);
          aimBoneYInRootSpace(b.larmR, 0.04 - weaveX, 0.26 - weaveY, 0.95, w * 0.92);
        }
      }

      // 2) Channeling pose (Mage Blizzard skyward invocation)
      if (smoothedChannelBlend > 0.01) {
        const w = smoothedChannelBlend;
        const pulse = Math.sin(time * 6.2) * 0.08;
        // Arch chest slightly back toward the sky
        rotateBoneInRootSpace(b.spine2, -0.07 * w, 0, 0);
        rotateBoneInRootSpace(b.spine3, -0.09 * w, 0, 0);
        rotateBoneInRootSpace(b.head, -0.14 * w, 0, 0);
        // Left hand raised high forward-skyward calling down the blizzard
        aimBoneYInRootSpace(b.uarmL, 0.34, 0.08 + pulse, 0.92, w * 0.96);
        aimBoneYInRootSpace(b.larmL, 0.14, 0.64 + pulse, 0.75, w * 0.96);
        // Right staff arm raised high in invocation
        aimBoneYInRootSpace(b.uarmR, -0.32, 0.14 - pulse * 0.5, 0.93, w * 0.92);
        aimBoneYInRootSpace(b.larmR, -0.14, 0.58 - pulse * 0.5, 0.80, w * 0.92);
      }

      // 3) Instant / Release forward thrust (Ice Lance, Frost Nova, Counterspell, Orb, Frostbolt release)
      if (releaseWeight > 0.01) {
        const w = releaseWeight;
        const isRadial = releaseSpell === "interact"; // Nova, Barrier, Counterspell, Blink
        rotateBoneInRootSpace(b.spine2, 0.11 * w, isRadial ? 0 : -0.12 * w, 0);
        rotateBoneInRootSpace(b.spine3, 0.13 * w, isRadial ? 0 : -0.16 * w, 0);
        if (isRadial) {
          // Outward radial arcane burst with left arm
          aimBoneYInRootSpace(b.uarmL, 0.72, 0.08, 0.68, w * 0.95);
          aimBoneYInRootSpace(b.larmL, 0.58, 0.34, 0.74, w * 0.95);
        } else {
          // Forward spell-hurl thrust with left hand + right staff arm
          aimBoneYInRootSpace(b.uarmL, 0.22, -0.12, 0.96, w * 0.95);
          aimBoneYInRootSpace(b.larmL, 0.08, 0.14, 0.98, w * 0.95);
        }
      }
    }

    // B. Warrior & Gladiator Combat Guard, Charge, Leap Slam & Whirlwind Poses
    if (role === "warrior" || role === "gladiator") {
      if (isWhirlwind) {
        // Spinning cyclone pose: sword arm extended wide horizontally, torso pitched forward
        rotateBoneInRootSpace(b.spine2, 0.16, 0, 0);
        rotateBoneInRootSpace(b.spine3, 0.18, -0.25, 0);
        aimBoneYInRootSpace(b.uarmR, -0.92, -0.08, 0.38, 0.96);
        aimBoneYInRootSpace(b.larmR, -0.84, 0.04, 0.54, 0.96);
        aimBoneYInRootSpace(b.uarmL, 0.82, -0.16, -0.54, 0.92);
        aimBoneYInRootSpace(b.larmL, 0.68, 0.08, -0.72, 0.92);
      } else if (state.airborne) {
        // Overhead Leap Strike slam pose
        rotateBoneInRootSpace(b.spine2, -0.12, 0, 0);
        rotateBoneInRootSpace(b.spine3, -0.14, 0.18, 0);
        aimBoneYInRootSpace(b.uarmR, -0.32, 0.68, 0.65, 0.95);
        aimBoneYInRootSpace(b.larmR, -0.12, 0.82, 0.55, 0.95);
        aimBoneYInRootSpace(b.uarmL, 0.45, -0.22, 0.86, 0.90);
        aimBoneYInRootSpace(b.larmL, 0.18, 0.24, 0.95, 0.90);
      } else if (state.dashing || state.cast?.id === "charge") {
        // Shield-forward Charge rush pose
        rotateBoneInRootSpace(b.spine2, 0.18, 0.22, 0);
        rotateBoneInRootSpace(b.spine3, 0.22, 0.26, 0);
        aimBoneYInRootSpace(b.uarmL, 0.34, -0.28, 0.89, 0.95);
        aimBoneYInRootSpace(b.larmL, -0.12, 0.18, 0.97, 0.95);
        aimBoneYInRootSpace(b.uarmR, -0.48, -0.42, -0.76, 0.92);
        aimBoneYInRootSpace(b.larmR, -0.36, -0.12, -0.92, 0.92);
      } else if (role === "warrior") {
        // Standard Gladiator shield-and-sword guard so the bulwark shield sits cleanly in front-left
        const guardW = 1 - releaseWeight * 0.5;
        aimBoneYInRootSpace(b.uarmL, 0.44, -0.68, 0.58, 0.82 * guardW);
        aimBoneYInRootSpace(b.larmL, 0.12, -0.14, 0.98, 0.86 * guardW);
        if (releaseWeight > 0.01) {
          // Melee slash with right sword arm while moving
          rotateBoneInRootSpace(b.spine3, 0.12 * releaseWeight, -0.32 * releaseWeight, 0);
          aimBoneYInRootSpace(b.uarmR, -0.36, -0.18, 0.91, releaseWeight * 0.94);
          aimBoneYInRootSpace(b.larmR, 0.12, -0.08, 0.98, releaseWeight * 0.94);
        }
      }
    }
  };

  return {
    onCastStart(_spellId, _kind) {
      // Cast state is tracked continuously via state.cast
    },
    onRelease(spellId) {
      releaseTimer = 0;
      releaseDuration = 0.42;
      releaseSpell = spellId === "nova" || spellId === "barrier" || spellId === "counter" || spellId === "blink"
        ? "interact"
        : "throw";
    },
    onHit(fromX, fromZ) {
      hitTimer = 0;
      if (fromX !== undefined && fromZ !== undefined) {
        const dx = root.position.x - fromX;
        const dz = root.position.z - fromZ;
        const len = Math.hypot(dx, dz) || 1;
        hitDirX = dx / len;
        hitDirZ = dz / len;
      } else {
        hitDirX = Math.sin(state.yaw);
        hitDirZ = Math.cos(state.yaw);
      }
    },
    setInput(inp) {
      state.vx = inp.vx;
      state.vz = inp.vz;
      state.yaw = inp.yaw;
      state.airborne = inp.airborne;
      state.jumpProgress = inp.jumpProgress ?? 0;
      state.cast = inp.cast;
      state.dashing = inp.dashing ?? false;
      state.dead = inp.dead;
      state.immobilizedPose = inp.immobilizedPose ?? null;
    },
  };
}
