import { useEffect, useMemo, useRef, useState } from 'react'
import * as THREE from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js'
import { getArenaClassKit } from '../actionClassKits'
import type { ArenaClassId } from '../actionClassKits'
import { ACTION_RENDER_QUALITY } from '../action3dSceneKit'
import {
  ACTION_CLASS_SKIN_COUNTS,
  HEALER_ACTION_CLASSES,
  type ActionCharacter,
  type ActionCharacterCreateInput,
  type ActionSkinCatalog,
} from '../actionMode'
import type { PlayerClass } from '../claudeCraftTypes'

type CharacterPreviewRuntime = {
  camera: THREE.PerspectiveCamera
  controls: {
    dragging: boolean
    lastX: number
    yaw: number
  }
  mixer: THREE.AnimationMixer | null
  renderer: THREE.WebGLRenderer
  root: THREE.Group
  scene: THREE.Scene
}

const CLASS_META: Record<PlayerClass, { label: string, role: string, detail: string, glyph: string }> = {
  warrior: { label: 'Warrior', role: 'Tank', detail: 'Locked', glyph: 'W' },
  paladin: { label: 'Paladin', role: 'Healer', detail: 'Mail holy support', glyph: 'P' },
  hunter: { label: 'Hunter', role: 'Damage', detail: 'Locked', glyph: 'H' },
  rogue: { label: 'Rogue', role: 'Damage', detail: 'Locked', glyph: 'R' },
  priest: { label: 'Priest', role: 'Healer', detail: 'Cloth direct heals', glyph: '+' },
  shaman: { label: 'Shaman', role: 'Healer', detail: 'Mail ritual caster', glyph: 'S' },
  mage: { label: 'Mage', role: 'Damage', detail: 'Locked', glyph: 'M' },
  warlock: { label: 'Warlock', role: 'Damage', detail: 'Locked', glyph: 'W' },
  druid: { label: 'Druid', role: 'Healer', detail: 'Leather nature magic', glyph: 'D' },
}

const previewLoader = new GLTFLoader().setMeshoptDecoder(MeshoptDecoder)
const previewTextureLoader = new THREE.TextureLoader()
const SKIN_FILES = ['base.png', 'alt_a.png', 'alt_b.png', 'alt_c.png'] as const

export function ActionCharacterCreator({
  existingNames = [],
  onCancel,
  onCreate,
}: {
  existingNames?: string[]
  onCancel?: () => void
  onCreate: (input: ActionCharacterCreateInput) => void
}) {
  const [name, setName] = useState('')
  const [classId, setClassId] = useState<PlayerClass>('priest')
  const [skin, setSkin] = useState(0)
  const [skinCatalog] = useState<ActionSkinCatalog>('class')
  const skinOptions = useMemo(
    () => Array.from({ length: ACTION_CLASS_SKIN_COUNTS[classId] }, (_, index) => index),
    [classId],
  )
  const trimmedName = name.trim()
  const nameTaken = existingNames.some((existing) => existing.toLowerCase() === trimmedName.toLowerCase())
  const canCreate = trimmedName.length >= 2 && !nameTaken

  return (
    <section className="character-creator-panel">
      <div className="character-creator-preview">
        <ActionCharacterModelPreview classId={classId} skin={skin} />
        <div>
          <p className="eyebrow">Appearance</p>
          <h2>{trimmedName || 'New Character'}</h2>
          <span>{CLASS_META[classId].label} · Skin {skin + 1}</span>
        </div>
      </div>

      <form
        className="character-creator-form"
        onSubmit={(event) => {
          event.preventDefault()
          if (!canCreate) return
          onCreate({
            appearance: { skin, skinCatalog },
            classId,
            name: trimmedName,
          })
        }}
      >
        <label>
          <span>Name</span>
          <input
            autoFocus
            maxLength={20}
            onChange={(event) => setName(event.target.value)}
            placeholder="Character name"
            value={name}
          />
        </label>
        {nameTaken && <p className="character-creator-error">Name already exists.</p>}

        <div className="character-creator-group">
          <div>
            <p className="eyebrow">Class</p>
            <h3>Healer</h3>
          </div>
          <div className="character-class-grid">
            {HEALER_ACTION_CLASSES.map((healerClass) => (
              <button
                aria-pressed={classId === healerClass}
                key={healerClass}
                onClick={() => {
                  setClassId(healerClass)
                  setSkin(0)
                }}
                type="button"
              >
                <span>{CLASS_META[healerClass].glyph}</span>
                <strong>{CLASS_META[healerClass].label}</strong>
                <small>{CLASS_META[healerClass].detail}</small>
              </button>
            ))}
          </div>
        </div>

        <div className="character-creator-group">
          <div>
            <p className="eyebrow">Look</p>
            <h3>Skin</h3>
          </div>
          <div className="character-skin-grid" aria-label="Appearance skins">
            {skinOptions.map((skinIndex) => (
              <button
                aria-pressed={skin === skinIndex}
                key={skinIndex}
                onClick={() => setSkin(skinIndex)}
                type="button"
              >
                <span>Skin {skinIndex + 1}</span>
              </button>
            ))}
          </div>
        </div>

        <div className="character-creator-actions">
          {onCancel && <button className="back-button" onClick={onCancel} type="button">Cancel</button>}
          <button className="primary-button" disabled={!canCreate} type="submit">Create</button>
        </div>
      </form>
    </section>
  )
}

function ActionCharacterModelPreview({ classId, skin }: { classId: PlayerClass, skin: number }) {
  const mountRef = useRef<HTMLDivElement | null>(null)
  const runtimeRef = useRef<CharacterPreviewRuntime | null>(null)

  useEffect(() => {
    const mount = mountRef.current
    if (!mount) return

    const scene = new THREE.Scene()
    scene.background = new THREE.Color(0x101319)

    const camera = new THREE.PerspectiveCamera(35, 1, 0.05, 80)
    camera.position.set(0, 1.55, 4.2)

    const renderer = new THREE.WebGLRenderer({ antialias: ACTION_RENDER_QUALITY.antialias, alpha: true, powerPreference: 'high-performance' })
    renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, ACTION_RENDER_QUALITY.maxPixelRatio))
    renderer.shadowMap.enabled = ACTION_RENDER_QUALITY.shadows
    mount.appendChild(renderer.domElement)

    scene.add(new THREE.HemisphereLight(0xf6f0de, 0x202937, 1.65))
    const key = new THREE.DirectionalLight(0xffedbd, 2.8)
    key.position.set(-4, 6, 5)
    scene.add(key)
    const rim = new THREE.DirectionalLight(0x8ec7ff, 1)
    rim.position.set(4, 3, -5)
    scene.add(rim)

    const ground = new THREE.Mesh(
      new THREE.CircleGeometry(1.35, 32),
      new THREE.MeshStandardMaterial({ color: 0x232832, roughness: 0.86 }),
    )
    ground.rotation.x = -Math.PI / 2
    ground.receiveShadow = true
    scene.add(ground)

    const root = new THREE.Group()
    scene.add(root)

    const runtime: CharacterPreviewRuntime = {
      camera,
      controls: { dragging: false, lastX: 0, yaw: 0 },
      mixer: null,
      renderer,
      root,
      scene,
    }
    runtimeRef.current = runtime

    const resize = () => {
      const rect = mount.getBoundingClientRect()
      const width = Math.max(1, rect.width)
      const height = Math.max(1, rect.height)
      renderer.setSize(width, height, false)
      camera.aspect = width / height
      camera.updateProjectionMatrix()
    }
    const observer = new ResizeObserver(resize)
    observer.observe(mount)
    resize()

    const handlePointerDown = (event: PointerEvent) => {
      renderer.domElement.setPointerCapture(event.pointerId)
      runtime.controls.dragging = true
      runtime.controls.lastX = event.clientX
    }
    const handlePointerMove = (event: PointerEvent) => {
      if (!runtime.controls.dragging) return
      runtime.controls.yaw += (event.clientX - runtime.controls.lastX) * 0.012
      runtime.controls.lastX = event.clientX
    }
    const handlePointerUp = (event: PointerEvent) => {
      renderer.domElement.releasePointerCapture(event.pointerId)
      runtime.controls.dragging = false
    }
    renderer.domElement.addEventListener('pointerdown', handlePointerDown)
    renderer.domElement.addEventListener('pointermove', handlePointerMove)
    renderer.domElement.addEventListener('pointerup', handlePointerUp)

    const clock = new THREE.Clock()
    let frameId = 0
    const animate = () => {
      const delta = Math.min(clock.getDelta(), 0.05)
      if (!runtime.controls.dragging) runtime.controls.yaw += delta * 0.32
      root.rotation.y = runtime.controls.yaw
      runtime.mixer?.update(delta)
      renderer.render(scene, camera)
      frameId = requestAnimationFrame(animate)
    }
    animate()

    return () => {
      cancelAnimationFrame(frameId)
      observer.disconnect()
      renderer.domElement.removeEventListener('pointerdown', handlePointerDown)
      renderer.domElement.removeEventListener('pointermove', handlePointerMove)
      renderer.domElement.removeEventListener('pointerup', handlePointerUp)
      disposeObject(root)
      renderer.dispose()
      renderer.domElement.remove()
      runtimeRef.current = null
    }
  }, [])

  useEffect(() => {
    const runtime = runtimeRef.current
    if (!runtime) return
    const activeRuntime = runtime
    let cancelled = false
    clearGroup(activeRuntime.root)
    activeRuntime.mixer = null

    async function loadPreview() {
      const kit = getArenaClassKit(classId as ArenaClassId)
      const gltf = await previewLoader.loadAsync(kit.model)
      if (cancelled) return

      const model = gltf.scene
      applyPreviewSkin(model, classId, skin)
      model.traverse((child) => {
        if (!(child instanceof THREE.Mesh)) return
        child.castShadow = true
        child.receiveShadow = true
        child.frustumCulled = false
      })

      for (const weapon of kit.weapons) {
        await attachPreviewWeapon(model, weapon.hand, weapon.model, () => cancelled)
      }
      if (cancelled) {
        disposeObject(model)
        return
      }

      activeRuntime.root.add(model)
      fitPreviewModel(model, activeRuntime.camera)
      const idle = gltf.animations.find((clip) => clip.name === 'Idle')
        ?? gltf.animations.find((clip) => clip.name.toLowerCase().includes('idle'))
        ?? gltf.animations[0]
      if (idle) {
        activeRuntime.mixer = new THREE.AnimationMixer(model)
        activeRuntime.mixer.clipAction(idle).play()
      }
    }

    loadPreview().catch(() => null)
    return () => {
      cancelled = true
    }
  }, [classId, skin])

  return (
    <div className="character-model-preview" ref={mountRef} aria-label={`${CLASS_META[classId].label} model preview`} />
  )
}

async function attachPreviewWeapon(model: THREE.Object3D, hand: 'handslot.l' | 'handslot.r', url: string, cancelled: () => boolean) {
  const handSlot = model.getObjectByName(hand)
  if (!handSlot) return
  const gltf = await previewLoader.loadAsync(url)
  if (cancelled()) return
  const weapon = gltf.scene
  weapon.name = 'creatorPreviewWeapon'
  weapon.traverse((child) => {
    if (!(child instanceof THREE.Mesh)) return
    child.castShadow = true
    child.receiveShadow = true
    child.frustumCulled = false
  })
  handSlot.add(weapon)
}

function applyPreviewSkin(model: THREE.Object3D, classId: PlayerClass, skin: number) {
  const folder = getSkinFolder(classId)
  const file = SKIN_FILES[Math.max(0, Math.min(SKIN_FILES.length - 1, skin))] ?? SKIN_FILES[0]
  const texture = previewTextureLoader.load(`/textures/skins/${folder}/${file}`)
  texture.colorSpace = THREE.SRGBColorSpace
  texture.flipY = false

  model.traverse((child) => {
    if (!(child instanceof THREE.Mesh)) return
    const materials = Array.isArray(child.material) ? child.material : [child.material]
    for (const material of materials) {
      if (!(material instanceof THREE.MeshStandardMaterial) && !(material instanceof THREE.MeshBasicMaterial)) continue
      material.map = texture
      material.needsUpdate = true
    }
  })
}

function getSkinFolder(classId: PlayerClass) {
  if (classId === 'paladin') return 'paladin'
  if (classId === 'druid') return 'druid'
  if (classId === 'shaman') return 'barbarian'
  return 'mage'
}

function fitPreviewModel(object: THREE.Object3D, camera: THREE.PerspectiveCamera) {
  const box = new THREE.Box3().setFromObject(object)
  const size = new THREE.Vector3()
  const center = new THREE.Vector3()
  box.getSize(size)
  box.getCenter(center)
  object.position.x -= center.x
  object.position.z -= center.z
  object.position.y -= box.min.y
  const height = Math.max(size.y, 1)
  const distance = Math.max(2.7, height * 1.55)
  camera.position.set(0, height * 0.55, distance)
  camera.lookAt(0, height * 0.48, 0)
  camera.updateProjectionMatrix()
}

function clearGroup(group: THREE.Group) {
  for (const child of [...group.children]) {
    group.remove(child)
    disposeObject(child)
  }
}

function disposeObject(object: THREE.Object3D) {
  object.traverse((child) => {
    if (!(child instanceof THREE.Mesh)) return
    child.geometry.dispose()
    if (Array.isArray(child.material)) child.material.forEach(disposeMaterial)
    else disposeMaterial(child.material)
  })
}

function disposeMaterial(material: THREE.Material) {
  const candidate = material as THREE.Material & { map?: THREE.Texture | null }
  candidate.map?.dispose()
  material.dispose()
}

export function ActionCharacterRosterPanel({
  activeCharacterId,
  characters,
  onCreateNew,
  onSwitch,
}: {
  activeCharacterId: string | null
  characters: ActionCharacter[]
  onCreateNew: () => void
  onSwitch: (characterId: string) => void
}) {
  return (
    <section className="character-roster-panel">
      <header>
        <div>
          <p className="eyebrow">Characters</p>
          <h2>Roster</h2>
        </div>
        <button className="primary-button" onClick={onCreateNew} type="button">New Character</button>
      </header>
      <div className="character-roster-list">
        {characters.map((character) => (
          <button
            aria-pressed={character.id === activeCharacterId}
            key={character.id}
            onClick={() => onSwitch(character.id)}
            type="button"
          >
            <div className={`character-roster-avatar class-${character.classId} skin-${character.appearance.skin}`}>
              <span>{CLASS_META[character.classId].glyph}</span>
            </div>
            <div>
              <strong>{character.name}</strong>
              <small>{CLASS_META[character.classId].label} · Level {character.level}</small>
              <small>{character.inventory.length} bag slots · {character.copper} copper</small>
            </div>
          </button>
        ))}
      </div>
    </section>
  )
}
