'use client'

import { useEffect, useRef, useState } from 'react'

/**
 * Visor 3D del personaje (ZamModelViewer de wowhead, servido bajo /modelviewer/*
 * como proxy same-origin para evitar CORS).
 *
 * Problema de fondo (3.4.3 sobre CDN de wowhead):
 *  - La rama `wrath` SÍ tiene los IDs de personalización de WotLK que guarda
 *    AzerothCore, pero NO publica la geometría de los modelos (los .mo3 dan 404).
 *  - La rama `live` (retail) SÍ tiene geometría y texturas, pero sus choiceID son
 *    los de retail actual, distintos a los de WotLK → la piel salía negra.
 *
 * Solución: renderizar con geometría de `live` y REMAPEAR cada choiceID de la BD
 * (WotLK) a su equivalente retail por POSICIÓN dentro de la opción, usando el orden
 * de choices de `wrath` como referencia. Así la piel/cara/pelo se ven correctos.
 */

const GEO_BRANCH = 'live' // geometría + texturas (wrath no publica modelos)
const CONTENT_PATH = `/modelviewer/${GEO_BRANCH}/`
const ZAM_VIEWER = `/modelviewer/${GEO_BRANCH}/viewer/viewer.min.js`
const JQUERY = 'https://code.jquery.com/jquery-3.7.1.min.js'
const MODEL_TYPE_CHARACTER = 16

type Choice = { Id: number }
type Option = { Id: number; Name?: string; Choices: Choice[] }
type CustomizationDoc = { Options?: Option[]; data?: { Options?: Option[] } }

type Win = typeof window & {
  jQuery?: (sel: string) => unknown
  ZamModelViewer?: new (opts: unknown) => unknown
  CONTENT_PATH?: string
  WOTLK_TO_RETAIL_DISPLAY_ID_API?: unknown
}

function ensureWH() {
  const w = window as unknown as { WH?: Record<string, unknown> }
  const WH = (w.WH = w.WH || {})
  if (typeof WH.debug !== 'function') WH.debug = () => {}
  if (!WH.defaultAnimation) WH.defaultAnimation = 'Stand'
  if (!WH.WebP) WH.WebP = { getImageExtension: () => '.webp' }
  if (!WH.Wow) {
    WH.Wow = {
      Item: {
        INVENTORY_TYPE_HEAD: 1, INVENTORY_TYPE_NECK: 2, INVENTORY_TYPE_SHOULDERS: 3,
        INVENTORY_TYPE_SHIRT: 4, INVENTORY_TYPE_CHEST: 5, INVENTORY_TYPE_WAIST: 6,
        INVENTORY_TYPE_LEGS: 7, INVENTORY_TYPE_FEET: 8, INVENTORY_TYPE_WRISTS: 9,
        INVENTORY_TYPE_HANDS: 10, INVENTORY_TYPE_FINGER: 11, INVENTORY_TYPE_TRINKET: 12,
        INVENTORY_TYPE_ONE_HAND: 13, INVENTORY_TYPE_SHIELD: 14, INVENTORY_TYPE_RANGED: 15,
        INVENTORY_TYPE_BACK: 16, INVENTORY_TYPE_TWO_HAND: 17, INVENTORY_TYPE_BAG: 18,
        INVENTORY_TYPE_TABARD: 19, INVENTORY_TYPE_ROBE: 20, INVENTORY_TYPE_MAIN_HAND: 21,
        INVENTORY_TYPE_OFF_HAND: 22, INVENTORY_TYPE_HELD_IN_OFF_HAND: 23,
        INVENTORY_TYPE_PROJECTILE: 24, INVENTORY_TYPE_THROWN: 25, INVENTORY_TYPE_RANGED_RIGHT: 26,
        INVENTORY_TYPE_QUIVER: 27, INVENTORY_TYPE_RELIC: 28, INVENTORY_TYPE_PROFESSION_TOOL: 29,
        INVENTORY_TYPE_PROFESSION_ACCESSORY: 30,
      },
    }
  }
}

function loadScript(src: string): Promise<void> {
  return new Promise((resolve, reject) => {
    if (document.querySelector(`script[src="${src}"]`)) return resolve()
    const s = document.createElement('script')
    s.src = src
    s.async = true
    s.onload = () => resolve()
    s.onerror = () => reject(new Error(`no se pudo cargar ${src}`))
    document.head.appendChild(s)
  })
}

function optionsById(doc: CustomizationDoc): Map<number, Option> {
  const list = doc.Options ?? doc.data?.Options ?? []
  return new Map(list.map((o) => [o.Id, o]))
}

/**
 * Remapea los choiceID de WotLK (de la BD) a choiceID de retail por índice.
 * Usa el orden de choices de `wrath` (WotLK) para hallar la posición, y el de `live`
 * (retail) para obtener el ID equivalente. Si algo falla, devuelve lo que pueda.
 */
async function remapCustomizations(
  raceGender: number,
  db: { optionId: number; choiceId: number }[],
): Promise<{ optionId: number; choiceId: number }[]> {
  try {
    const [wrathDoc, liveDoc] = await Promise.all([
      fetch(`/modelviewer/wrath/meta/charactercustomization/${raceGender}.json`).then((r) => r.json()),
      fetch(`/modelviewer/live/meta/charactercustomization/${raceGender}.json`).then((r) => r.json()),
    ])
    const W = optionsById(wrathDoc as CustomizationDoc)
    const L = optionsById(liveDoc as CustomizationDoc)
    const out: { optionId: number; choiceId: number }[] = []
    for (const { optionId, choiceId } of db) {
      const wo = W.get(optionId)
      const lo = L.get(optionId)
      if (!wo || !lo) continue
      const idx = wo.Choices.findIndex((c) => c.Id === choiceId)
      if (idx < 0) continue
      const lc = lo.Choices[idx] ?? lo.Choices[lo.Choices.length - 1]
      if (lc) out.push({ optionId, choiceId: lc.Id })
    }
    return out
  } catch (err) {
    console.warn('[armory-3d] no se pudo remapear la apariencia:', err)
    return []
  }
}

export function ArmoryModel3D({
  race,
  gender,
  equip,
  customizations,
  labels,
}: {
  race: number
  gender: number
  /** Equipo ya resuelto a slots del visor: [slotVisor, displayId]. */
  equip: { slot: number; display: number }[]
  /** Apariencia desde character_customizations (IDs de WotLK): {optionId, choiceId}. */
  customizations: { optionId: number; choiceId: number }[]
  labels: { loading: string; unavailable: string }
}) {
  const ref = useRef<HTMLDivElement>(null)
  const [state, setState] = useState<'loading' | 'ok' | 'fail'>('loading')

  useEffect(() => {
    let cancelled = false
    const failTimer = setTimeout(() => !cancelled && setState((s) => (s === 'loading' ? 'fail' : s)), 15000)

    ;(async () => {
      try {
        const w = window as Win
        w.CONTENT_PATH = CONTENT_PATH
        w.WOTLK_TO_RETAIL_DISPLAY_ID_API = false

        const raceGender = race * 2 - 1 + gender
        // El remapeo (peticiones fetch) puede ir en paralelo, pero jQuery debe cargar
        // ANTES que viewer.min.js (que lo usa como global), así que ese orden es estricto.
        const remapPromise = remapCustomizations(raceGender, customizations)
        await loadScript(JQUERY)
        await loadScript(ZAM_VIEWER)
        const options = await remapPromise
        ensureWH()
        if (cancelled || !ref.current) return

        const jQuery = w.jQuery
        const ZamModelViewer = w.ZamModelViewer
        if (!jQuery || !ZamModelViewer) throw new Error('ZamModelViewer/jQuery no disponibles')

        ref.current.id = ref.current.id || `armory-model-${race}-${gender}`
        const items = equip.filter((e) => e.display > 0).map((e) => [e.slot, e.display])

        const models = {
          type: 2,
          contentPath: CONTENT_PATH,
          container: jQuery(`#${ref.current.id}`),
          aspect: 1,
          hd: true,
          items,
          models: { id: raceGender, type: MODEL_TYPE_CHARACTER },
          charCustomization: { options },
        }

        // eslint-disable-next-line new-cap
        new ZamModelViewer(models)

        if (!cancelled) {
          clearTimeout(failTimer)
          setState('ok')
        }
      } catch (err) {
        console.error('[armory-3d] no se pudo cargar el visor 3D:', err)
        if (!cancelled) {
          clearTimeout(failTimer)
          setState('fail')
        }
      }
    })()

    return () => {
      cancelled = true
      clearTimeout(failTimer)
    }
    // solo al montar / cambiar de personaje
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [race, gender])

  return (
    <div className="armory-model">
      <div ref={ref} className="armory-model-canvas" />
      {state === 'loading' && <p className="armory-hint">{labels.loading}</p>}
      {state === 'fail' && <p className="armory-hint">{labels.unavailable}</p>}
    </div>
  )
}
