import { LESOUBLIES_CONFIG } from "./les-oublies-config.js"
import { LesOubliesUtility } from "./les-oublies-utility.js"
import { LesOubliesRolls } from "./les-oublies-rolls.js"

export class LesOubliesActor extends Actor {
  static CREATION_ITEM_TYPES = new Set(["race", "tribu", "metier"])
  static THREAD_RESOURCE_KEYS = new Set(["songesThreads", "cauchemarThreads", "emptyGlobes"])

  prepareDerivedData() {
    super.prepareDerivedData()

    if (this.type === "personnage") {
      const system = this.system
      const sizeValue = Math.clamp(Number(system.size?.value ?? 1), 1, 4)
      const hpMax = Math.max(sizeValue * 4 + Number(system.hp?.bonus ?? 0), 0)
      system.hp.max = hpMax
      system.hp.value = Math.min(Number(system.hp.value ?? hpMax), hpMax)

      const songesValue = Number(system.songes?.value ?? 0)
      const cauchemarValue = Number(system.cauchemar?.value ?? 0)
      const totals = LesOubliesUtility.computeDreamPointTotals(songesValue, cauchemarValue)
      system.songes.max = totals.songesPoints
      system.cauchemar.max = totals.cauchemarPoints
      system.songes.points = Math.clamp(Number(system.songes.points ?? totals.songesPoints), 0, totals.songesPoints)
      system.cauchemar.points = Math.clamp(Number(system.cauchemar.points ?? totals.cauchemarPoints), 0, totals.cauchemarPoints)
      system.reserves.songesThreads = Math.max(Number(system.reserves?.songesThreads ?? 0), 0)
      system.reserves.cauchemarThreads = Math.max(Number(system.reserves?.cauchemarThreads ?? 0), 0)
      system.reserves.emptyGlobes = Math.max(Number(system.reserves?.emptyGlobes ?? 0), 0)
      return
    }

    if (this.type === "compagnie") {
      const system = this.system
      system.power.sharedDreamPoints = Math.max(Number(system.power?.sharedDreamPoints ?? 0), 0)
      system.reserves.songesThreads = Math.max(Number(system.reserves?.songesThreads ?? 0), 0)
      system.reserves.cauchemarThreads = Math.max(Number(system.reserves?.cauchemarThreads ?? 0), 0)
      system.reserves.emptyGlobes = Math.max(Number(system.reserves?.emptyGlobes ?? 0), 0)
      return
    }

    if (this.type !== "creature") return

    const system = this.system
    const sizeValue = Math.clamp(Number(system.size?.value ?? 1), 1, 8)
    const hpMax = Math.max(sizeValue * 4, 0)
    const hpValue = Math.max(Number(system.hp?.value ?? hpMax), 0)
    system.hp.max = hpMax
    system.hp.value = Math.min(hpValue, hpMax)
    const songesPoints = Math.max(Number(system.songes?.points ?? 0), 0)
    const cauchemarPoints = Math.max(Number(system.cauchemar?.points ?? 0), 0)
    system.songes.max = Math.max(Number(system.songes?.max ?? songesPoints), songesPoints)
    system.cauchemar.max = Math.max(Number(system.cauchemar?.max ?? cauchemarPoints), cauchemarPoints)
    system.songes.points = Math.min(songesPoints, system.songes.max)
    system.cauchemar.points = Math.min(cauchemarPoints, system.cauchemar.max)
  }

  getProfileValue(profileKey) {
    return Number(this.system.profils?.[profileKey] ?? 0)
  }

  getCreationItem(type) {
    if (!LesOubliesActor.CREATION_ITEM_TYPES.has(type)) return this.items.find((item) => item.type === type) ?? null
    const referenceId = this.system.references?.[`${type}Id`] ?? ""
    if (referenceId) {
      const referencedItem = this.items.get(referenceId)
      if (referencedItem?.type === type) return referencedItem
    }
    return this.items.find((item) => item.type === type) ?? null
  }

  getEmbeddedItems(type) {
    const items = this.itemTypes?.[type] ?? this.items.filter((item) => item.type === type)
    return LesOubliesUtility.sortByName(items)
  }

  async assignCreationItem(sourceItem) {
    if (!sourceItem || !LesOubliesActor.CREATION_ITEM_TYPES.has(sourceItem.type)) return null

    const previousItem = this.getCreationItem(sourceItem.type)
    const itemData = sourceItem.toObject()
    delete itemData._id

    const existingIds = this.getEmbeddedItems(sourceItem.type).map((item) => item.id)
    if (existingIds.length) {
      await this.deleteEmbeddedDocuments("Item", existingIds, { renderSheet: false })
    }

    const [createdItem] = await this.createEmbeddedDocuments("Item", [itemData], { renderSheet: false })
    if (!createdItem) return null

    await this.update({
      [`system.references.${sourceItem.type}Id`]: createdItem.id,
    })

    if (sourceItem.type === "race") {
      await this.syncRaceProfiles({ currentRace: createdItem })
      await this.syncRaceDomains({ currentRace: createdItem, previousRace: previousItem })
    }

    return createdItem
  }

  async clearCreationItem(type) {
    if (!LesOubliesActor.CREATION_ITEM_TYPES.has(type)) return

    const previousItem = this.getCreationItem(type)

    const existingIds = this.getEmbeddedItems(type).map((item) => item.id)
    if (existingIds.length) {
      await this.deleteEmbeddedDocuments("Item", existingIds, { renderSheet: false })
    }

    await this.update({
      [`system.references.${type}Id`]: "",
    })

    if (type === "race") {
      await this.syncRaceProfiles({ currentRace: null })
      await this.syncRaceDomains({ currentRace: null, previousRace: previousItem })
    }
  }

  getCompagnie() {
    const compagnieId = this.system.references?.compagnieId
    return compagnieId ? game.actors.get(compagnieId) ?? null : null
  }

  getThreadReserveOwner(source = "actor") {
    if (source === "company" || source === "compagnie") return this.getCompagnie()
    return this
  }

  getThreadReserves(source = "actor") {
    const owner = this.getThreadReserveOwner(source)
    return {
      owner,
      songesThreads: Math.max(Number(owner?.system?.reserves?.songesThreads ?? 0), 0),
      cauchemarThreads: Math.max(Number(owner?.system?.reserves?.cauchemarThreads ?? 0), 0),
      emptyGlobes: Math.max(Number(owner?.system?.reserves?.emptyGlobes ?? 0), 0),
    }
  }

  async transferThreadReserve(resourceKey, amount, direction = "toCompany") {
    if (!LesOubliesActor.THREAD_RESOURCE_KEYS.has(resourceKey)) return false
    const company = this.getCompagnie()
    if (!company) return false

    const transferAmount = Math.max(Math.trunc(Number(amount ?? 0)), 0)
    if (transferAmount < 1) return false

    const fromActor = direction === "toCompany" ? this : company
    const toActor = direction === "toCompany" ? company : this
    const current = Math.max(Number(fromActor.system?.reserves?.[resourceKey] ?? 0), 0)
    if (current < transferAmount) return false

    const path = `system.reserves.${resourceKey}`
    const targetCurrent = Math.max(Number(toActor.system?.reserves?.[resourceKey] ?? 0), 0)

    await fromActor.update({ [path]: current - transferAmount })
    await toActor.update({ [path]: targetCurrent + transferAmount })
    return true
  }

  getCompetenceByKey(skillKey) {
    return this.getEmbeddedItems("competence").find((item) => item.system.key === skillKey) ?? null
  }

  getRaceLanguageDomains(race = this.getCreationItem("race")) {
    return LesOubliesUtility.uniqueStrings(race?.system?.languageDomains ?? [])
  }

  getRaceProfiles(race = this.getCreationItem("race")) {
    const profiles = LesOubliesUtility.createEmptyProfiles()
    for (const key of Object.keys(profiles)) {
      profiles[key] = Math.trunc(Number(race?.system?.profiles?.[key] ?? 0))
    }
    return profiles
  }

  async syncRaceProfiles({ currentRace = this.getCreationItem("race") } = {}) {
    if (this.type !== "personnage") return false
    const profiles = this.getRaceProfiles(currentRace)
    const updateData = Object.fromEntries(
      Object.entries(profiles).map(([key, value]) => [`system.profils.${key}`, value]),
    )
    await this.update(updateData)
    if (currentRace) {
      ui.notifications.info(`Profils raciaux appliqués depuis ${currentRace.name}.`)
    }
    return true
  }

  async syncRaceDomains({ currentRace = this.getCreationItem("race"), previousRace = null } = {}) {
    if (this.type !== "personnage") return false

    const competence = this.getCompetenceByKey("langues")
    if (!competence) return false

    const currentAutoDomains = LesOubliesUtility.uniqueStrings(competence.system.fixedDomains ?? [])
    const previousRaceDomains = previousRace
      ? this.getRaceLanguageDomains(previousRace)
      : currentAutoDomains
    const autoDomainsToReplace = currentAutoDomains.length ? currentAutoDomains : previousRaceDomains
    const nextAutoDomains = this.getRaceLanguageDomains(currentRace)
    const manualDomains = LesOubliesUtility.uniqueStrings(
      (competence.system.domains ?? []).filter((domain) => !autoDomainsToReplace.includes(domain)),
    )

    await competence.update({
      "system.fixedDomains": nextAutoDomains,
      "system.domains": LesOubliesUtility.uniqueStrings([...manualDomains, ...nextAutoDomains]),
    })
    return true
  }

  getSkillScoreByKey(skillKey) {
    const competence = this.getCompetenceByKey(skillKey)
    return competence ? this.computeSkillValue(competence) : 0
  }

  computeSkillValue(item) {
    const base = Number(item.system.base ?? 0)
    const profileValue = this.getProfileValue(item.system.profileKey)
    if (item.system.closed && base === 0) return 0
    return base + profileValue
  }

  getCompetences() {
    return this.getEmbeddedItems("competence").map((item) => ({
      item,
      finalValue: this.computeSkillValue(item),
      profileLabel: LESOUBLIES_CONFIG.profileLabels[item.system.profileKey] ?? item.system.profileKey,
      domains: LesOubliesUtility.uniqueStrings(item.system.domains ?? []),
      fixedDomains: LesOubliesUtility.uniqueStrings(item.system.fixedDomains ?? []),
    }))
  }

  getGroupedCompetences() {
    return LESOUBLIES_CONFIG.profiles.map((profile) => ({
      ...profile,
      profileValue: this.getProfileValue(profile.id),
      items: this.getCompetences().filter((entry) => entry.item.system.profileKey === profile.id),
    }))
  }

  getDerivedOverview() {
    const hpValue = Number(this.system.hp?.value ?? 0)
    const hpMax = Number(this.system.hp?.max ?? 0)
    const hpDisplay = this.type === "creature"
      ? (this.system.hp?.display || (hpValue === hpMax ? String(hpValue) : `${hpValue}/${hpMax}`))
      : `${hpValue}/${hpMax}`

    return {
      sizeLabel: LESOUBLIES_CONFIG.sizes[this.system.size?.value] ?? this.system.size?.value,
      hpMax,
      hpValue,
      hpDisplay,
      songesMax: this.system.songes?.max ?? this.system.songes?.points ?? 0,
      cauchemarMax: this.system.cauchemar?.max ?? this.system.cauchemar?.points ?? 0,
      songesPoints: this.system.songes?.points ?? 0,
      cauchemarPoints: this.system.cauchemar?.points ?? 0,
      reserves: this.getThreadReserves(),
      companyReserves: this.getThreadReserves("company"),
      race: this.getCreationItem("race"),
      tribu: this.getCreationItem("tribu"),
      metier: this.getCreationItem("metier"),
      compagnie: this.getCompagnie(),
    }
  }

  async openTestRollDialog(preset = {}) {
    return LesOubliesRolls.openTestDialog(this, preset)
  }

  async openConfrontationRollDialog() {
    return LesOubliesRolls.openConfrontationDialog(this)
  }

  async openInitiativeRollDialog() {
    return LesOubliesRolls.openInitiativeDialog(this)
  }

  async openAttackRollDialog({ itemId = null, mode = null } = {}) {
    return LesOubliesRolls.openAttackDialog(this, { itemId, mode })
  }

  async openDamageDialog({ itemId = null } = {}) {
    return LesOubliesRolls.openDamageDialog(this, { itemId })
  }

  async openSpellActivationDialog(itemId) {
    return LesOubliesRolls.openSpellDialog(this, itemId)
  }

  async openCombatPresetDialog(actionKey) {
    return LesOubliesRolls.openCombatPresetDialog(this, actionKey)
  }

  async openThreadHarvestDialog() {
    return LesOubliesRolls.openThreadHarvestDialog(this)
  }

  async rollProfile(profileKey) {
    return this.openTestRollDialog({
      label: LESOUBLIES_CONFIG.profileLabels[profileKey] ?? profileKey,
      score: this.getProfileValue(profileKey),
      difficulty: 0,
      rollMode: LesOubliesRolls.getDefaultRollMode(this),
    })
  }

  async rollCompetence(itemId) {
    const item = this.items.get(itemId)
    if (!item) return null
    return this.openTestRollDialog({
      label: item.name,
      score: this.computeSkillValue(item),
      difficulty: 0,
      rollMode: LesOubliesRolls.getDefaultRollMode(this),
    })
  }
}
