import * as THREE from 'three'
import { GLTFLoader, type GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js'
import { clone as cloneSkeleton } from 'three/examples/jsm/utils/SkeletonUtils.js'
import type { PartyRole } from './actionCombatCore'
import { getArenaAbility, getArenaClassKit, type ArenaAbilityId, type ArenaClassId } from './actionClassKits'

export type CombatRenderUnit = {
  classId?: ArenaClassId
  id: string
  role: PartyRole | 'player' | 'boss'
  teamId?: string
}

type CombatModelRuntime = {
  actions: Map<string, THREE.AnimationAction>
  clips: Map<string, THREE.AnimationClip>
  currentAction: string
  mixer: THREE.AnimationMixer
}

type CombatAnimationRequest = string | string[]

const combatModelLoader = new GLTFLoader().setMeshoptDecoder(MeshoptDecoder)
const combatModelCache = new Map<string, Promise<GLTF>>()
const combatWeaponCache = new Map<string, Promise<GLTF>>()
let kayKitMediumAnimationRequest: Promise<THREE.AnimationClip[]> | null = null
const KAYKIT_COMBAT_ANIMATION_URLS = [
  '/action-assets/models/downloaded/kaykit-character-animations/rig-medium/Rig_Medium_General.glb',
  '/action-assets/models/downloaded/kaykit-character-animations/rig-medium/Rig_Medium_MovementBasic.glb',
  '/action-assets/models/downloaded/kaykit-character-animations/rig-medium/Rig_Medium_CombatMelee.glb',
  '/action-assets/models/downloaded/kaykit-character-animations/rig-medium/Rig_Medium_CombatRanged.glb',
]
const IDLE_ANIMATIONS = ['Idle_A', 'Idle_B', 'Skeletons_Idle', 'Idle', 'Idle_Combat']
const RUN_ANIMATIONS = ['Running_A', 'Running_B', 'Walking_A', 'Running_Strafe_Left', 'Walking_Backwards']
const SPELL_CAST_ANIMATIONS = ['Ranged_Magic_Raise', 'Spellcast_Raise', 'Spellcasting', 'Ranged_Magic_Spellcasting']
const SPELL_SHOOT_ANIMATIONS = ['Ranged_Magic_Shoot', 'Spellcast_Shoot', 'Ranged_Magic_Spellcasting', 'Ranged_Magic_Raise', 'Spellcasting']
const RANGED_ATTACK_ANIMATIONS = ['Ranged_Bow_Release', 'Ranged_2H_Shoot', 'Ranged_1H_Shoot', 'Ranged_Magic_Shoot', '2H_Ranged_Shoot']
const MELEE_ATTACK_ANIMATIONS = ['Melee_1H_Attack_Chop', 'Melee_1H_Attack_Slice_Diagonal', 'Melee_1H_Attack_Stab', '1H_Melee_Attack_Chop']
const MELEE_HEAVY_ATTACK_ANIMATIONS = ['Melee_2H_Attack_Chop', 'Melee_2H_Attack_Slice', 'Melee_2H_Attack_Stab', '2H_Melee_Attack_Chop']
const OGA_DRAGON_MODEL_URL = '/action-assets/models/opengameart/dragon-oga/dragon-oga-animated.glb'
const BULLDROME_IDLE_ANIMATIONS = ['Idle1', 'Idle_AnimalArmature', 'Idle_MonsterArmature', 'Idle']
const BULLDROME_WALK_ANIMATIONS = ['Gallop_AnimalArmature', 'Walk_AnimalArmature', 'Walk_MonsterArmature']
const BULLDROME_ATTACK_ANIMATIONS = ['Attack2 (tusks)', 'Attack_Headbutt_AnimalArmature', 'Bite_Front_MonsterArmature']
const BULLDROME_DEATH_ANIMATIONS = ['Death_AnimalArmature', 'Death_MonsterArmature', 'Dying']
const COMBAT_ANIMATION_NAMES = new Set([
  ...IDLE_ANIMATIONS,
  ...RUN_ANIMATIONS,
  ...SPELL_CAST_ANIMATIONS,
  ...SPELL_SHOOT_ANIMATIONS,
  ...RANGED_ATTACK_ANIMATIONS,
  ...MELEE_ATTACK_ANIMATIONS,
  ...MELEE_HEAVY_ATTACK_ANIMATIONS,
  ...BULLDROME_IDLE_ANIMATIONS,
  ...BULLDROME_WALK_ANIMATIONS,
  ...BULLDROME_ATTACK_ANIMATIONS,
  ...BULLDROME_DEATH_ANIMATIONS,
  'Block',
  'Dragon_Attack',
  'Dragon_Death',
  'Dragon_Idle',
  'Dragon_Walk',
  'Dualwield_Melee_Attack_Chop',
  'Flying_Idle',
  'Idle1',
  'Melee_Block',
  'Melee_Blocking',
  'Melee_Dualwield_Attack_Chop',
  'Melee_Dualwield_Attack_Slice',
  'Spellcast_Raise',
  'Spellcast_Shoot',
])
const OGA_DRAGON_FOOT_BONES = [
  'Armature_leg_front_foot_L',
  'Armature_leg_front_foot_R',
  'Armature_leg_back_foot_L',
  'Armature_leg_back_foot_R',
]

export function getEnemyModelYawOffset(kind: string) {
  if (kind === 'bullfango' || kind === 'yian-kut-ku' || kind === 'bird') return Math.PI
  return 0
}

export function addCombatUnitMesh(group: THREE.Group, unit: CombatRenderUnit) {
  group.userData.role = unit.role
  const fallback = new THREE.Group()
  fallback.name = 'proceduralFallback'
  group.add(fallback)

  if (unit.role === 'player') {
    addHunterMesh(fallback)
    addClaudeCraftCombatModel(group, { ...unit, classId: unit.classId ?? 'priest' })
    return
  }
  if (unit.role === 'boss') return
  if (unit.role === 'healer') {
    addHunterMesh(fallback)
    addClaudeCraftCombatModel(group, { ...unit, classId: unit.classId ?? 'priest' })
    return
  }
  addPartyMesh(fallback, unit.role, unit.id)
  addClaudeCraftCombatModel(group, {
    ...unit,
    classId: unit.classId ?? (unit.role === 'tank' ? 'knight' : unit.role === 'melee' ? 'rogue' : unit.id.includes('ranged-2') ? 'ranger' : 'mage'),
  })
}

function addClaudeCraftCombatModel(group: THREE.Group, unit: Exclude<CombatRenderUnit, { role: 'boss' }> & { classId: ArenaClassId }) {
  if (unit.role === 'boss') return

  const kit = getArenaClassKit(unit.classId)
  const loadToken = Symbol(kit.model)
  group.userData.combatModelToken = loadToken
  group.userData.classId = unit.classId

  Promise.all([loadCombatModel(kit.model), loadKayKitMediumAnimationClips()])
    .then(([gltf, kayKitClips]) => {
      if (group.userData.combatModelToken !== loadToken) return

      const model = cloneSkeleton(gltf.scene)
      model.name = 'claudeCraftCombatModel'
      model.scale.setScalar(unit.classId === 'priest' ? 0.5 : 0.56)
      model.traverse((child) => {
        if (!(child instanceof THREE.Mesh)) return
        child.castShadow = true
        child.receiveShadow = true
      })

      const mixer = new THREE.AnimationMixer(model)
      const clips = createCombatClipMap([...kayKitClips, ...gltf.animations])

      const runtime: CombatModelRuntime = {
        actions: new Map<string, THREE.AnimationAction>(),
        clips,
        currentAction: '',
        mixer,
      }
      group.userData.combatModelRuntime = runtime
      group.add(model)
      addClaudeCraftWeapons(model, unit.classId)
      setCombatModelAction(runtime, IDLE_ANIMATIONS, true)

      const fallback = group.getObjectByName('proceduralFallback')
      if (fallback) fallback.visible = false
    })
    .catch(() => {
      const fallback = group.getObjectByName('proceduralFallback')
      if (fallback) fallback.visible = true
    })
}

function loadCombatModel(modelUrl: string) {
  let request = combatModelCache.get(modelUrl)
  if (!request) {
    request = combatModelLoader.loadAsync(modelUrl)
    combatModelCache.set(modelUrl, request)
  }
  return request
}

function loadKayKitMediumAnimationClips() {
  if (!kayKitMediumAnimationRequest) {
    kayKitMediumAnimationRequest = Promise.all(KAYKIT_COMBAT_ANIMATION_URLS.map((url) => combatModelLoader.loadAsync(url)))
      .then((gltfs) => {
        const clips: THREE.AnimationClip[] = []
        const seen = new Set<string>()
        for (const gltf of gltfs) {
          for (const clip of gltf.animations) {
            if (!COMBAT_ANIMATION_NAMES.has(clip.name) || seen.has(clip.name)) continue
            seen.add(clip.name)
            clips.push(clip.clone())
          }
        }
        return clips
      })
  }
  return kayKitMediumAnimationRequest
}

function createCombatClipMap(clips: THREE.AnimationClip[]) {
  const map = new Map<string, THREE.AnimationClip>()
  for (const clip of clips) {
    if (!COMBAT_ANIMATION_NAMES.has(clip.name) || map.has(clip.name)) continue
    map.set(clip.name, clip)
  }
  return map
}

function loadCombatWeapon(weaponUrl: string) {
  let request = combatWeaponCache.get(weaponUrl)
  if (!request) {
    request = combatModelLoader.loadAsync(weaponUrl)
    combatWeaponCache.set(weaponUrl, request)
  }
  return request
}

function addClaudeCraftWeapons(model: THREE.Object3D, classId: ArenaClassId) {
  for (const weapon of getArenaClassKit(classId).weapons) {
    attachClaudeCraftWeapon(model, weapon.hand, weapon.model)
  }
}

function attachClaudeCraftWeapon(model: THREE.Object3D, handSlotName: string, weaponUrl: string) {
  const handSlot = findAttachmentSlot(model, handSlotName)
  if (!handSlot) return

  loadCombatWeapon(weaponUrl)
    .then((gltf) => {
      const weaponModel = cloneSkeleton(gltf.scene)
      weaponModel.name = 'claudeCraftWeapon'
      weaponModel.traverse((child) => {
        if (!(child instanceof THREE.Mesh)) return
        child.castShadow = true
        child.receiveShadow = true
      })
      handSlot.add(weaponModel)
    })
    .catch(() => null)
}

function findAttachmentSlot(model: THREE.Object3D, handSlotName: string) {
  const exact = model.getObjectByName(handSlotName)
  if (exact) return exact

  const normalizedName = normalizeSlotName(handSlotName)
  let match: THREE.Object3D | null = null
  model.traverse((child) => {
    if (!match && normalizeSlotName(child.name) === normalizedName) match = child
  })
  return match
}

function normalizeSlotName(name: string) {
  return name.toLowerCase().replace(/[^a-z0-9]/g, '')
}

function setCombatModelAction(runtime: CombatModelRuntime, requestedAction: CombatAnimationRequest, instant = false) {
  const requestedActions = Array.isArray(requestedAction) ? requestedAction : [requestedAction]
  const nextAction = requestedActions.map((name) => getCombatAction(runtime, name)).find(Boolean)
    ?? IDLE_ANIMATIONS.map((name) => getCombatAction(runtime, name)).find(Boolean)
    ?? getCombatAction(runtime, runtime.clips.keys().next().value)
  if (!nextAction || runtime.currentAction === nextAction.getClip().name) return

  const previousAction = runtime.actions.get(runtime.currentAction)
  const isDeathAction = nextAction.getClip().name === 'Dragon_Death'
    || BULLDROME_DEATH_ANIMATIONS.includes(nextAction.getClip().name)
  nextAction.enabled = true
  nextAction.clampWhenFinished = isDeathAction
  nextAction.setLoop(isDeathAction ? THREE.LoopOnce : THREE.LoopRepeat, Infinity)
  nextAction.reset()
  nextAction.play()
  if (previousAction && !instant) {
    previousAction.crossFadeTo(nextAction, 0.16, false)
  } else {
    nextAction.weight = 1
  }
  runtime.currentAction = nextAction.getClip().name
}

function getCombatAction(runtime: CombatModelRuntime, name: string | undefined) {
  if (!name) return null
  const existing = runtime.actions.get(name)
  if (existing) return existing
  const clip = runtime.clips.get(name)
  if (!clip) return null
  const action = runtime.mixer.clipAction(clip)
  runtime.actions.set(name, action)
  return action
}

export function updateCombatUnitModel(
  mesh: THREE.Object3D,
  deltaSeconds: number,
  options?: { abilityId?: ArenaAbilityId | null, action?: 'idle' | 'attack' | 'run' | 'spell', role?: PartyRole | 'boss' | 'player' },
) {
  const runtime = mesh.userData.combatModelRuntime as CombatModelRuntime | undefined
  if (!runtime) return

  const role = options?.role ?? (mesh.userData.role as PartyRole | 'boss' | 'player' | undefined)
  if (role === 'boss' && mesh.userData.kind === 'bulldrome') {
    const action = options?.action === 'attack' || options?.action === 'spell'
      ? BULLDROME_ATTACK_ANIMATIONS
      : options?.action === 'run'
        ? BULLDROME_WALK_ANIMATIONS
        : BULLDROME_IDLE_ANIMATIONS
    setCombatModelAction(runtime, action)
    runtime.mixer.update(deltaSeconds)
    return
  }

  const action = options?.abilityId
    ? getAnimationForAbility(options.abilityId)
    : options?.action === 'spell'
      ? SPELL_CAST_ANIMATIONS
    : options?.action === 'attack'
    ? role === 'ranged' ? RANGED_ATTACK_ANIMATIONS : role === 'healer' ? SPELL_SHOOT_ANIMATIONS : MELEE_ATTACK_ANIMATIONS
    : options?.action === 'run'
        ? RUN_ANIMATIONS
        : IDLE_ANIMATIONS
  setCombatModelAction(runtime, action)
  runtime.mixer.update(deltaSeconds)
}

export function updateBossModelAnimation(
  mesh: THREE.Object3D,
  deltaSeconds: number,
  action: 'idle' | 'walk' | 'attack' | 'death' = 'idle',
) {
  const runtime = mesh.userData.combatModelRuntime as CombatModelRuntime | undefined
  if (!runtime) return
  const animation: CombatAnimationRequest = mesh.userData.kind === 'bulldrome'
    ? action === 'attack'
      ? BULLDROME_ATTACK_ANIMATIONS
      : action === 'walk'
        ? BULLDROME_WALK_ANIMATIONS
        : action === 'death'
          ? BULLDROME_DEATH_ANIMATIONS
          : BULLDROME_IDLE_ANIMATIONS
    : action === 'attack'
      ? 'Dragon_Attack'
      : action === 'walk'
        ? 'Dragon_Walk'
        : action === 'death'
          ? 'Dragon_Death'
          : 'Dragon_Idle'
  setCombatModelAction(runtime, animation)
  runtime.mixer.update(deltaSeconds)
  if (mesh.userData.kind === 'cyber-dragon') lockDragonFeetToGround(mesh)
}

function getAnimationForAbility(abilityId: ArenaAbilityId) {
  const ability = getArenaAbility(abilityId)
  if (ability.animation === 'block') return ['Melee_Block', 'Melee_Blocking', 'Block', ...MELEE_ATTACK_ANIMATIONS]
  if (ability.animation === 'dual') return ['Melee_Dualwield_Attack_Chop', 'Melee_Dualwield_Attack_Slice', 'Dualwield_Melee_Attack_Chop', ...MELEE_ATTACK_ANIMATIONS]
  if (ability.animation === 'melee') return abilityId === 'thunder_clap' ? MELEE_HEAVY_ATTACK_ANIMATIONS : [...MELEE_ATTACK_ANIMATIONS, ...MELEE_HEAVY_ATTACK_ANIMATIONS]
  if (ability.animation === 'ranged') return RANGED_ATTACK_ANIMATIONS
  return ability.castTime > 0 ? SPELL_CAST_ANIMATIONS : SPELL_SHOOT_ANIMATIONS
}

export function addBoarMesh(group: THREE.Group, options?: { color?: number, kind?: 'bulldrome' | 'bullfango' }) {
  const isBulldrome = options?.kind !== 'bullfango'
  const hide = new THREE.MeshStandardMaterial({ color: options?.color ?? (isBulldrome ? 0x5f4b3e : 0x8a7049), roughness: 0.9 })
  const dark = new THREE.MeshStandardMaterial({ color: 0x2f211b, roughness: 0.85 })
  const hornMaterial = new THREE.MeshStandardMaterial({ color: 0xf4eed8, roughness: 0.5 })

  const bodyMesh = new THREE.Mesh(new THREE.SphereGeometry(isBulldrome ? 0.78 : 0.52, 18, 12), hide)
  bodyMesh.scale.set(isBulldrome ? 1.35 : 1.12, isBulldrome ? 0.68 : 0.58, isBulldrome ? 1.65 : 1.28)
  bodyMesh.position.y = isBulldrome ? 0.62 : 0.48
  bodyMesh.castShadow = true
  group.add(bodyMesh)

  const chest = new THREE.Mesh(new THREE.SphereGeometry(isBulldrome ? 0.52 : 0.34, 14, 10), dark)
  chest.scale.set(1.05, 0.9, 0.9)
  chest.position.set(0, isBulldrome ? 0.76 : 0.55, isBulldrome ? -0.95 : -0.66)
  chest.castShadow = true
  group.add(chest)

  const head = new THREE.Mesh(new THREE.SphereGeometry(isBulldrome ? 0.42 : 0.28, 14, 10), hide)
  head.scale.set(1.05, 0.82, 0.86)
  head.position.set(0, isBulldrome ? 0.86 : 0.62, isBulldrome ? -1.35 : -0.9)
  head.castShadow = true
  group.add(head)

  const snout = new THREE.Mesh(new THREE.SphereGeometry(isBulldrome ? 0.22 : 0.16, 10, 8), dark)
  snout.scale.set(1.2, 0.62, 0.85)
  snout.position.set(0, isBulldrome ? 0.78 : 0.55, isBulldrome ? -1.68 : -1.12)
  snout.castShadow = true
  group.add(snout)

  for (const x of [-0.28, 0.28]) {
    const horn = new THREE.Mesh(new THREE.ConeGeometry(isBulldrome ? 0.1 : 0.065, isBulldrome ? 0.58 : 0.34, 8), hornMaterial)
    horn.position.set(x * (isBulldrome ? 1.5 : 1), isBulldrome ? 1.02 : 0.73, isBulldrome ? -1.5 : -1.0)
    horn.rotation.set(-Math.PI / 2, 0, x < 0 ? -0.45 : 0.45)
    horn.castShadow = true
    group.add(horn)
  }

  for (const x of [-0.42, 0.42]) {
    for (const z of [isBulldrome ? -0.72 : -0.48, isBulldrome ? 0.64 : 0.42]) {
      const leg = new THREE.Mesh(new THREE.CylinderGeometry(isBulldrome ? 0.1 : 0.075, isBulldrome ? 0.12 : 0.09, isBulldrome ? 0.58 : 0.42, 8), dark)
      leg.position.set(x, isBulldrome ? 0.24 : 0.18, z)
      leg.castShadow = true
      group.add(leg)
    }
  }

  const tail = new THREE.Mesh(new THREE.ConeGeometry(isBulldrome ? 0.06 : 0.04, isBulldrome ? 0.5 : 0.3, 8), dark)
  tail.position.set(0, isBulldrome ? 0.74 : 0.52, isBulldrome ? 1.35 : 0.9)
  tail.rotation.x = Math.PI / 2
  tail.castShadow = true
  group.add(tail)
}

export type ClaudeCraftBossModelKind =
  | 'deacon-varric'
  | 'vael-the-mistcaller'
  | 'ysolei'
  | 'korgath-the-bound'
  | 'grand-necromancer-velkhar'
  | 'korzul-the-gravewyrm'
  | 'bulldrome'
  | 'bullfango'
  | 'cyber-dragon'

export function addClaudeCraftBossModel(group: THREE.Group, kind: ClaudeCraftBossModelKind) {
  const modelUrl = getClaudeCraftBossModelUrl(kind)
  const loadToken = Symbol(modelUrl)
  group.userData.bossModelToken = loadToken
  loadCombatModel(modelUrl)
    .then((gltf) => {
      if (group.userData.bossModelToken !== loadToken) return
      const model = cloneSkeleton(gltf.scene)
      model.name = 'claudeCraftBossModel'
      model.scale.setScalar(getClaudeCraftBossModelScale(kind))
      if (kind === 'cyber-dragon') normalizeCyberDragonModel(model)
      if (kind === 'bulldrome') normalizeBulldromeModel(model)
      model.traverse((child) => {
        if (!(child instanceof THREE.Mesh)) return
        child.castShadow = true
        child.frustumCulled = false
        child.receiveShadow = true
      })
      const mixer = new THREE.AnimationMixer(model)
      const runtime: CombatModelRuntime = {
        actions: new Map<string, THREE.AnimationAction>(),
        clips: createCombatClipMap(gltf.animations),
        currentAction: '',
        mixer,
      }
      group.userData.combatModelRuntime = runtime
      group.add(model)
      setCombatModelAction(runtime, getClaudeCraftBossIdleAction(kind), true)
      const fallback = group.getObjectByName('proceduralFallback')
      if (fallback) fallback.visible = false
    })
    .catch(() => {
      const fallback = group.getObjectByName('proceduralFallback')
      if (fallback) fallback.visible = true
    })
}

export function cancelCombatRenderLoads(root: THREE.Object3D) {
  root.traverse((object) => {
    object.userData.combatModelToken = null
    object.userData.bossModelToken = null
  })
}

export function clearCombatRenderCaches() {
  const pending = [...combatModelCache.values(), ...combatWeaponCache.values()]
  combatModelCache.clear()
  combatWeaponCache.clear()
  kayKitMediumAnimationRequest = null
  for (const request of pending) request.then(disposeCachedGltf).catch(() => null)
}

function disposeCachedGltf(gltf: GLTF) {
  gltf.scene.traverse((child) => {
    if (!(child instanceof THREE.Mesh)) return
    child.geometry.dispose()
    const materials = Array.isArray(child.material) ? child.material : [child.material]
    for (const material of materials) disposeMaterial(material)
  })
}

function disposeMaterial(material: THREE.Material) {
  for (const value of Object.values(material)) {
    if (value instanceof THREE.Texture) value.dispose()
  }
  material.dispose()
}

function getClaudeCraftBossModelUrl(kind: ClaudeCraftBossModelKind) {
  if (kind === 'cyber-dragon') return OGA_DRAGON_MODEL_URL
  if (kind === 'ysolei' || kind === 'korzul-the-gravewyrm') {
    return '/action-assets/models/claudecraft/creatures/dragonevolved.glb'
  }
  if (kind === 'bullfango') return '/action-assets/models/claudecraft/creatures/bull.glb'
  if (kind === 'bulldrome') return '/action-assets/models/claudecraft/creatures/wild_boar.glb'
  if (kind === 'korgath-the-bound') return '/action-assets/models/claudecraft/creatures/giant.glb'
  if (kind === 'vael-the-mistcaller') return '/action-assets/models/claudecraft/chars/players/mage_classic.glb'
  return '/action-assets/models/claudecraft/chars/enemies/necromancer.glb'
}

function getClaudeCraftBossModelScale(kind: ClaudeCraftBossModelKind) {
  if (kind === 'cyber-dragon') return 4.5
  if (kind === 'bulldrome') return 0.35
  if (kind === 'korzul-the-gravewyrm') return 1.08
  if (kind === 'ysolei') return 0.92
  if (kind === 'korgath-the-bound') return 1.1
  if (kind === 'deacon-varric' || kind === 'grand-necromancer-velkhar') return 1.05
  return 0.72
}

function getClaudeCraftBossIdleAction(kind: ClaudeCraftBossModelKind) {
  if (kind === 'cyber-dragon') return 'Dragon_Idle'
  if (kind === 'ysolei' || kind === 'korzul-the-gravewyrm') return 'Flying_Idle'
  if (kind === 'bulldrome') return 'Idle1'
  return 'Idle'
}

function normalizeCyberDragonModel(model: THREE.Object3D) {
  liftModelBottomToGround(model)
  model.position.y -= 0.03
}

function normalizeBulldromeModel(model: THREE.Object3D) {
  liftModelBottomToGround(model)
}

function liftModelBottomToGround(model: THREE.Object3D) {
  const box = new THREE.Box3().setFromObject(model)
  if (!Number.isFinite(box.min.y)) return
  model.position.y -= box.min.y
}

function lockDragonFeetToGround(mesh: THREE.Object3D) {
  const model = mesh.getObjectByName('claudeCraftBossModel')
  if (!model) return

  const footPosition = new THREE.Vector3()
  let lowestFootY = Infinity
  for (const boneName of OGA_DRAGON_FOOT_BONES) {
    const bone = model.getObjectByName(boneName)
    if (!bone) continue
    bone.getWorldPosition(footPosition)
    lowestFootY = Math.min(lowestFootY, footPosition.y)
  }

  if (Number.isFinite(lowestFootY)) {
    model.position.y += 0.03 - lowestFootY
    return
  }

  liftModelBottomToGround(model)
}

export function addBirdMesh(group: THREE.Group, kind: 'bird' | 'yian-kut-ku' = 'bird') {
  const isYian = kind === 'yian-kut-ku'
  const feather = new THREE.MeshStandardMaterial({ color: isYian ? 0xd94d2f : 0x7b6ac0, roughness: 0.72 })
  const gold = new THREE.MeshStandardMaterial({ color: 0xffc857, roughness: 0.55, emissive: isYian ? 0x7a2200 : 0x000000, emissiveIntensity: isYian ? 0.25 : 0 })
  const dark = new THREE.MeshStandardMaterial({ color: 0x301015, roughness: 0.82 })

  const torso = new THREE.Mesh(new THREE.SphereGeometry(isYian ? 0.48 : 0.28, 16, 10), feather)
  torso.scale.set(0.9, isYian ? 1.25 : 1.05, isYian ? 1.05 : 1.2)
  torso.position.y = isYian ? 0.82 : 0.55
  torso.castShadow = true
  group.add(torso)

  const head = new THREE.Mesh(new THREE.SphereGeometry(isYian ? 0.24 : 0.16, 12, 8), feather)
  head.position.set(0, isYian ? 1.35 : 0.88, isYian ? -0.34 : -0.22)
  head.castShadow = true
  group.add(head)

  const beak = new THREE.Mesh(new THREE.ConeGeometry(isYian ? 0.09 : 0.06, isYian ? 0.32 : 0.2, 8), gold)
  beak.position.set(0, isYian ? 1.34 : 0.87, isYian ? -0.58 : -0.38)
  beak.rotation.x = -Math.PI / 2
  beak.castShadow = true
  group.add(beak)

  for (const x of [-1, 1]) {
    const wing = new THREE.Mesh(new THREE.ConeGeometry(isYian ? 0.34 : 0.2, isYian ? 1.35 : 0.78, 4), feather)
    wing.position.set(x * (isYian ? 0.68 : 0.38), isYian ? 0.95 : 0.66, 0)
    wing.rotation.set(0.4, 0, x < 0 ? 1.05 : -1.05)
    wing.scale.set(1, 0.38, 1.45)
    wing.castShadow = true
    group.add(wing)
  }

  for (let i = -1; i <= 1; i += 1) {
    const tail = new THREE.Mesh(new THREE.ConeGeometry(isYian ? 0.12 : 0.07, isYian ? 0.9 : 0.48, 4), i === 0 ? gold : feather)
    tail.position.set(i * (isYian ? 0.18 : 0.1), isYian ? 0.62 : 0.42, isYian ? 0.72 : 0.42)
    tail.rotation.set(Math.PI / 2.8, 0, i * 0.28)
    tail.castShadow = true
    group.add(tail)
  }

  for (const x of [-0.13, 0.13]) {
    const leg = new THREE.Mesh(new THREE.CylinderGeometry(isYian ? 0.035 : 0.025, isYian ? 0.045 : 0.03, isYian ? 0.32 : 0.22, 6), dark)
    leg.position.set(x, isYian ? 0.26 : 0.18, -0.04)
    leg.castShadow = true
    group.add(leg)
  }
}

export function addCyberDragonMesh(group: THREE.Group) {
  const shell = new THREE.MeshStandardMaterial({ color: 0x31566f, roughness: 0.42, metalness: 0.55 })
  const glow = new THREE.MeshStandardMaterial({ color: 0x6feeff, roughness: 0.25, metalness: 0.3, emissive: 0x1b7f99, emissiveIntensity: 0.55 })
  const darkMetal = new THREE.MeshStandardMaterial({ color: 0x152432, roughness: 0.48, metalness: 0.72 })

  const bodyMesh = new THREE.Mesh(new THREE.SphereGeometry(0.62, 16, 10), shell)
  bodyMesh.scale.set(1.35, 0.62, 1.55)
  bodyMesh.position.y = 0.72
  bodyMesh.castShadow = true
  group.add(bodyMesh)

  const neck = new THREE.Mesh(new THREE.CylinderGeometry(0.18, 0.24, 0.72, 8), darkMetal)
  neck.position.set(0, 0.88, -0.74)
  neck.rotation.x = -0.75
  neck.castShadow = true
  group.add(neck)

  const head = new THREE.Mesh(new THREE.SphereGeometry(0.34, 12, 8), shell)
  head.scale.set(1.05, 0.78, 1.2)
  head.position.set(0, 1.12, -1.18)
  head.castShadow = true
  group.add(head)

  const snout = new THREE.Mesh(new THREE.ConeGeometry(0.18, 0.42, 4), glow)
  snout.position.set(0, 1.1, -1.53)
  snout.rotation.x = -Math.PI / 2
  snout.castShadow = true
  group.add(snout)

  for (const x of [-0.16, 0.16]) {
    const horn = new THREE.Mesh(new THREE.ConeGeometry(0.045, 0.32, 6), glow)
    horn.position.set(x, 1.36, -1.2)
    horn.rotation.x = -0.55
    horn.castShadow = true
    group.add(horn)
  }

  for (const x of [-1, 1]) {
    const wing = new THREE.Mesh(new THREE.ConeGeometry(0.36, 1.35, 4), glow)
    wing.position.set(x * 0.78, 1.0, 0.02)
    wing.rotation.set(0.25, 0, x < 0 ? 1.08 : -1.08)
    wing.scale.set(1, 0.32, 1.5)
    wing.castShadow = true
    group.add(wing)
  }

  const tail = new THREE.Mesh(new THREE.ConeGeometry(0.16, 1.2, 8), darkMetal)
  tail.position.set(0, 0.68, 1.26)
  tail.rotation.x = Math.PI / 2
  tail.castShadow = true
  group.add(tail)

  for (const x of [-0.42, 0.42]) {
    for (const z of [-0.38, 0.52]) {
      const leg = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.1, 0.46, 8), darkMetal)
      leg.position.set(x, 0.28, z)
      leg.castShadow = true
      group.add(leg)
    }
  }
}

export function animateCombatWeapon(
  mesh: THREE.Object3D,
  role: PartyRole,
  options: { elapsed: number, inMelee: boolean },
) {
  if (role !== 'tank' && role !== 'melee') return
  const swing = options.inMelee ? 0.5 - Math.cos(options.elapsed * (role === 'tank' ? 12 : 10.5)) * 0.5 : 0

  if (role === 'tank') {
    const sword = mesh.getObjectByName('tankSword')
    if (sword) {
      sword.rotation.x = 0.18 - swing * 0.35
      sword.rotation.y = 0.12 + swing * 0.18
      sword.rotation.z = -0.55 + swing * 0.72
    }
    return
  }

  const sword = mesh.getObjectByName('twoHandSword')
  if (sword) {
    sword.rotation.x = 0.26 - swing * 0.42
    sword.rotation.y = 0.08 + swing * 0.28
    sword.rotation.z = -0.82 + swing * 1.35
  }
}

export function syncRangedCombatProjectiles(
  scene: THREE.Scene,
  effects: Map<string, THREE.Group>,
  options: {
    elapsed: number
    teamKey: string
    toWorld: (x: number, y: number) => { x: number, z: number }
    target: { x: number, y: number } | null
    staffCaster?: { x: number, y: number } | null
    archer?: { x: number, y: number } | null
  },
) {
  const active = new Set<string>()
  if (!options.target) {
    pruneCombatEffects(effects, active)
    return
  }

  if (options.staffCaster) {
    const id = `${options.teamKey}-staff-spell`
    active.add(id)
    const effect = getOrCreateStaffSpell(scene, effects, id)
    const t = (options.elapsed * 1.45) % 1
    const start = options.toWorld(options.staffCaster.x, options.staffCaster.y)
    const end = options.toWorld(options.target.x, options.target.y)
    effect.position.set(
      THREE.MathUtils.lerp(start.x, end.x, t),
      0.82 + Math.sin(t * Math.PI) * 0.58,
      THREE.MathUtils.lerp(start.z, end.z, t),
    )
    effect.scale.setScalar(0.78 + Math.sin(options.elapsed * 11) * 0.18)
    effect.visible = true
  }

  if (options.archer) {
    const id = `${options.teamKey}-arrow-shot`
    active.add(id)
    const effect = getOrCreateArrowShot(scene, effects, id)
    const t = (options.elapsed * 2.2) % 1
    const start = options.toWorld(options.archer.x, options.archer.y)
    const end = options.toWorld(options.target.x, options.target.y)
    effect.position.set(
      THREE.MathUtils.lerp(start.x, end.x, t),
      0.62 + Math.sin(t * Math.PI) * 0.22,
      THREE.MathUtils.lerp(start.z, end.z, t),
    )
    effect.rotation.y = Math.atan2(end.x - start.x, end.z - start.z)
    effect.visible = true
  }

  pruneCombatEffects(effects, active)
}

function addHunterMesh(group: THREE.Group) {
  const armor = new THREE.MeshStandardMaterial({ color: 0x2f8fd1, roughness: 0.58, metalness: 0.16 })
  const robe = new THREE.MeshStandardMaterial({ color: 0xf4eed8, roughness: 0.78 })
  const skin = new THREE.MeshStandardMaterial({ color: 0xf4c9a0, roughness: 0.72 })
  const metal = new THREE.MeshStandardMaterial({ color: 0xd7d9de, roughness: 0.32, metalness: 0.8 })
  const glow = new THREE.MeshStandardMaterial({ color: 0x7dff9d, roughness: 0.25, emissive: 0x1e8f45, emissiveIntensity: 0.55 })
  group.add(body(armor, 0.34, 0.82))
  const skirt = new THREE.Mesh(new THREE.ConeGeometry(0.42, 0.46, 10), robe)
  skirt.position.y = 0.25
  skirt.castShadow = true
  group.add(skirt)
  const head = new THREE.Mesh(new THREE.SphereGeometry(0.22, 12, 8), skin)
  head.position.y = 1.18
  head.castShadow = true
  group.add(head)
  const staff = new THREE.Group()
  staff.name = 'weapon'
  const shaft = new THREE.Mesh(new THREE.CylinderGeometry(0.035, 0.035, 1.48, 8), metal)
  shaft.rotation.x = 0.22
  shaft.castShadow = true
  staff.add(shaft)
  const headRing = new THREE.Mesh(new THREE.TorusGeometry(0.18, 0.018, 8, 20), metal)
  headRing.position.y = 0.84
  headRing.rotation.x = Math.PI / 2
  headRing.castShadow = true
  staff.add(headRing)
  const crystal = new THREE.Mesh(new THREE.OctahedronGeometry(0.14), glow)
  crystal.position.y = 0.84
  crystal.castShadow = true
  staff.add(crystal)
  const butt = new THREE.Mesh(new THREE.SphereGeometry(0.07, 8, 6), metal)
  butt.position.y = -0.76
  butt.castShadow = true
  staff.add(butt)
  staff.position.set(0.42, 0.76, -0.22)
  staff.rotation.z = -0.22
  group.add(staff)
}

function addPartyMesh(group: THREE.Group, role: PartyRole, id: string) {
  const color = role === 'tank' ? 0xe5b95f : role === 'melee' ? 0xdc5162 : 0x8e68c4
  const armor = new THREE.MeshStandardMaterial({ color, roughness: 0.62, metalness: role === 'tank' ? 0.36 : 0.18 })
  const skin = new THREE.MeshStandardMaterial({ color: 0xf4c9a0, roughness: 0.72 })
  const leather = new THREE.MeshStandardMaterial({ color: 0x65412a, roughness: 0.78 })
  const metal = new THREE.MeshStandardMaterial({ color: 0xd2d5db, roughness: 0.36, metalness: 0.72 })
  const wood = new THREE.MeshStandardMaterial({ color: 0x8a5a2f, roughness: 0.82 })
  const magic = new THREE.MeshStandardMaterial({ color: 0x89f7ff, roughness: 0.26, emissive: 0x147d92, emissiveIntensity: 0.5 })

  group.add(body(armor, role === 'tank' ? 0.34 : 0.29, 0.78))
  const head = new THREE.Mesh(new THREE.SphereGeometry(0.18, 12, 8), skin)
  head.position.y = 1.05
  head.castShadow = true
  group.add(head)

  const leftArm = limb(armor, 0.08, 0.48)
  leftArm.position.set(-0.28, 0.72, -0.03)
  leftArm.rotation.z = -0.24
  group.add(leftArm)
  const rightArm = limb(armor, 0.08, 0.48)
  rightArm.position.set(0.28, 0.72, -0.03)
  rightArm.rotation.z = 0.24
  group.add(rightArm)

  const leftLeg = limb(leather, 0.08, 0.48)
  leftLeg.position.set(-0.13, 0.22, 0)
  group.add(leftLeg)
  const rightLeg = limb(leather, 0.08, 0.48)
  rightLeg.position.set(0.13, 0.22, 0)
  group.add(rightLeg)

  if (role === 'tank') {
    addTankWeapons(group, metal, wood)
    return
  }
  if (role === 'melee') {
    addMeleeWeapon(group, metal, wood)
    return
  }
  if (id.includes('ranged-1')) {
    addCasterStaff(group, metal, wood, magic)
    return
  }
  addBow(group, metal, wood)
}

function addTankWeapons(group: THREE.Group, metal: THREE.Material, wood: THREE.Material) {
  const shield = new THREE.Group()
  const shieldFace = new THREE.Mesh(new THREE.CylinderGeometry(0.3, 0.24, 0.08, 6), metal)
  shieldFace.rotation.set(Math.PI / 2, 0, 0)
  shieldFace.castShadow = true
  shield.add(shieldFace)
  const boss = new THREE.Mesh(new THREE.SphereGeometry(0.08, 8, 6), wood)
  boss.position.z = -0.05
  boss.castShadow = true
  shield.add(boss)
  const stripe = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.46, 0.025), wood)
  stripe.position.z = -0.08
  shield.add(stripe)
  shield.position.set(-0.43, 0.72, -0.26)
  shield.rotation.z = 0.25
  group.add(shield)

  const sword = new THREE.Group()
  sword.name = 'tankSword'
  const blade = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.82, 0.055), metal)
  blade.position.y = 0.24
  blade.castShadow = true
  sword.add(blade)
  const tip = new THREE.Mesh(new THREE.ConeGeometry(0.075, 0.2, 4), metal)
  tip.position.y = 0.75
  tip.castShadow = true
  sword.add(tip)
  const guard = new THREE.Mesh(new THREE.BoxGeometry(0.36, 0.07, 0.06), metal)
  guard.position.y = -0.2
  sword.add(guard)
  const grip = new THREE.Mesh(new THREE.CylinderGeometry(0.04, 0.04, 0.34, 8), wood)
  grip.position.y = -0.42
  sword.add(grip)
  const pommel = new THREE.Mesh(new THREE.SphereGeometry(0.07, 8, 6), metal)
  pommel.position.y = -0.63
  sword.add(pommel)
  sword.position.set(0.42, 0.62, -0.38)
  sword.rotation.set(0.18, 0.12, -0.55)
  group.add(sword)
}

function addMeleeWeapon(group: THREE.Group, metal: THREE.Material, wood: THREE.Material) {
  const sword = new THREE.Group()
  sword.name = 'twoHandSword'
  const blade = new THREE.Mesh(new THREE.BoxGeometry(0.18, 1.24, 0.075), metal)
  blade.position.y = 0.28
  blade.castShadow = true
  sword.add(blade)
  const fuller = new THREE.Mesh(new THREE.BoxGeometry(0.045, 0.88, 0.082), wood)
  fuller.position.y = 0.24
  sword.add(fuller)
  const tip = new THREE.Mesh(new THREE.ConeGeometry(0.13, 0.28, 4), metal)
  tip.position.y = 1.03
  sword.add(tip)
  const guard = new THREE.Mesh(new THREE.BoxGeometry(0.62, 0.1, 0.1), metal)
  guard.position.y = -0.4
  sword.add(guard)
  const grip = new THREE.Mesh(new THREE.CylinderGeometry(0.055, 0.055, 0.56, 8), wood)
  grip.position.y = -0.74
  sword.add(grip)
  const pommel = new THREE.Mesh(new THREE.SphereGeometry(0.1, 8, 6), metal)
  pommel.position.y = -1.07
  sword.add(pommel)
  sword.position.set(0.42, 0.66, -0.24)
  sword.rotation.set(0.26, 0.08, -0.82)
  group.add(sword)
}

function addCasterStaff(group: THREE.Group, metal: THREE.Material, wood: THREE.Material, magic: THREE.Material) {
  const staff = new THREE.Group()
  const shaft = new THREE.Mesh(new THREE.CylinderGeometry(0.035, 0.035, 1.35, 8), wood)
  shaft.castShadow = true
  staff.add(shaft)
  const crescent = new THREE.Mesh(new THREE.TorusGeometry(0.18, 0.018, 8, 24, Math.PI * 1.3), magic)
  crescent.position.y = 0.76
  crescent.rotation.x = Math.PI / 2
  staff.add(crescent)
  const orb = new THREE.Mesh(new THREE.SphereGeometry(0.11, 12, 8), magic)
  orb.position.y = 0.76
  staff.add(orb)
  const cap = new THREE.Mesh(new THREE.SphereGeometry(0.06, 8, 6), metal)
  cap.position.y = -0.7
  staff.add(cap)
  staff.position.set(0.38, 0.74, -0.18)
  staff.rotation.set(0.18, 0, -0.24)
  group.add(staff)
}

function addBow(group: THREE.Group, metal: THREE.Material, wood: THREE.Material) {
  const bow = new THREE.Group()
  for (const y of [-0.2, 0.2]) {
    const limbMesh = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.42, 0.045), wood)
    limbMesh.position.y = y
    limbMesh.position.x = y < 0 ? -0.08 : 0.08
    limbMesh.rotation.z = y < 0 ? -0.35 : 0.35
    bow.add(limbMesh)
  }
  const grip = new THREE.Mesh(new THREE.CylinderGeometry(0.045, 0.045, 0.24, 8), wood)
  grip.rotation.z = Math.PI / 2
  bow.add(grip)
  const string = new THREE.Mesh(new THREE.BoxGeometry(0.025, 0.78, 0.025), metal)
  string.position.x = 0.24
  bow.add(string)
  const arrow = new THREE.Group()
  const arrowShaft = new THREE.Mesh(new THREE.BoxGeometry(0.035, 0.035, 0.72), metal)
  arrow.add(arrowShaft)
  const arrowTip = new THREE.Mesh(new THREE.ConeGeometry(0.045, 0.12, 6), metal)
  arrowTip.position.z = -0.42
  arrowTip.rotation.x = -Math.PI / 2
  arrow.add(arrowTip)
  arrow.position.set(0.08, 0, -0.05)
  bow.add(arrow)
  bow.position.set(0.4, 0.75, -0.32)
  bow.rotation.y = 0.18
  group.add(bow)
}

function body(material: THREE.Material, radius: number, height: number) {
  const mesh = new THREE.Mesh(new THREE.CylinderGeometry(radius, radius * 0.86, height, 12), material)
  mesh.position.y = height / 2
  mesh.castShadow = true
  return mesh
}

function limb(material: THREE.Material, radius: number, height: number) {
  const mesh = new THREE.Mesh(new THREE.CylinderGeometry(radius, radius * 0.82, height, 8), material)
  mesh.castShadow = true
  return mesh
}

function getOrCreateStaffSpell(scene: THREE.Scene, effects: Map<string, THREE.Group>, id: string) {
  let group = effects.get(id)
  if (!group) {
    group = new THREE.Group()
    const material = new THREE.MeshBasicMaterial({ color: 0x89f7ff, transparent: true, opacity: 0.86 })
    group.add(new THREE.Mesh(new THREE.SphereGeometry(0.11, 12, 8), material))
    const ring = new THREE.Mesh(new THREE.TorusGeometry(0.19, 0.013, 6, 20), material)
    ring.rotation.x = Math.PI / 2
    group.add(ring)
    scene.add(group)
    effects.set(id, group)
  }
  return group
}

function getOrCreateArrowShot(scene: THREE.Scene, effects: Map<string, THREE.Group>, id: string) {
  let group = effects.get(id)
  if (!group) {
    group = new THREE.Group()
    const wood = new THREE.MeshStandardMaterial({ color: 0x8a5a2f, roughness: 0.82 })
    const metal = new THREE.MeshStandardMaterial({ color: 0xd2d5db, roughness: 0.36, metalness: 0.72 })
    const shaft = new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.04, 0.58), wood)
    group.add(shaft)
    const tip = new THREE.Mesh(new THREE.ConeGeometry(0.055, 0.14, 6), metal)
    tip.position.z = -0.36
    tip.rotation.x = -Math.PI / 2
    group.add(tip)
    scene.add(group)
    effects.set(id, group)
  }
  return group
}

function pruneCombatEffects(map: Map<string, THREE.Group>, active: Set<string>) {
  for (const [id, object] of map) {
    if (active.has(id)) continue
    object.removeFromParent()
    map.delete(id)
  }
}
