import { SYSTEM } from "./config/system.mjs"

/**
 * Perform an Oath Hammer skill check and post results to chat.
 *
 * Dice rules (p.38-40):
 *   - Pool = Attribute rank + Skill rank + per-skill modifier + bonus + (luckSpend × 2) + supporters
 *   - White dice succeed on 4+  |  Red: 3+  |  Black: 2+
 *   - All dice explode on 6 (roll extra die, continues while rolling 6s)
 *   - Pool can never drop below 1 die (penalties rule)
 *   - Luck Points: 1 LP spent = +2 dice; LP are deducted from actor.system.luck.value
 *   - Supporters: each ally with ranks in the skill adds +1 die
 *
 * @param {Actor}  actor        The actor performing the check
 * @param {string} skillKey     Skill key (e.g. "fortune")
 * @param {number} dv           Difficulty Value (successes required)
 * @param {object} [options]
 * @param {number}  [options.bonus]        Extra dice from dialog modifier (can be negative)
 * @param {number}  [options.luckSpend]    Luck Points to spend (each adds +2 dice)
 * @param {number}  [options.supporters]   Allies supporting the check (each adds +1 die)
 * @param {string}  [options.attrOverride] Override governing attribute (for dual-attr skills)
 * @param {string}  [options.visibility]   Roll mode (public/gmroll/blindroll/selfroll)
 * @param {string}  [options.flavor]       Optional flavor text for the chat card
 * @returns {Promise<{successes: number, dv: number, isSuccess: boolean}>}
 */
export async function rollSkillCheck(actor, skillKey, dv, options = {}) {
  const { bonus = 0, luckSpend = 0, luckIsHuman = false, supporters = 0, attrOverride, visibility, flavor, explodeOn5 = false } = options

  const sys = actor.system
  const skillDef = SYSTEM.SKILLS[skillKey]
  if (!skillDef) throw new Error(`Unknown skill: ${skillKey}`)

  // Attribute — use override if provided (dual-attribute skills: Defense, Fighting, Magic)
  const attrKey  = attrOverride && sys.attributes[attrOverride] ? attrOverride : skillDef.attribute
  const attrRank = sys.attributes[attrKey].rank

  const skill      = sys.skills[skillKey]
  const skillRank  = skill.rank
  const skillMod   = skill.modifier ?? 0
  const colorType  = skill.colorDiceType ?? "white"
  const threshold  = colorType === "black" ? 2 : colorType === "red" ? 3 : 4

  const luckDicePerPoint = luckIsHuman ? 3 : 2

  // Total dice pool (never below 1)
  const totalDice = Math.max(attrRank + skillRank + skillMod + bonus + (luckSpend * luckDicePerPoint) + supporters, 1)

  // Deduct spent Luck Points from actor
  if (luckSpend > 0) {
    const currentLuck = sys.luck?.value ?? 0
    await actor.update({ "system.luck.value": Math.max(0, currentLuck - luckSpend) })
  }

  // Roll the dice pool
  const roll = await new Roll(`${totalDice}d6`).evaluate()
  const allRolls = [roll]

  // Count successes — exploding dice produce additional dice
  const explodeThreshold = explodeOn5 ? 5 : 6  // default: always explode on 6
  let successes = 0
  const diceResults = []
  let extraDice = 0

  for (const r of roll.dice[0].results) {
    const val = r.result
    if (val >= threshold) successes++
    if (val >= explodeThreshold) extraDice++
    diceResults.push({ val, exploded: false })
  }

  while (extraDice > 0) {
    const xRoll = await new Roll(`${extraDice}d6`).evaluate()
    allRolls.push(xRoll)
    extraDice = 0
    for (const r of xRoll.dice[0].results) {
      const val = r.result
      if (val >= threshold) successes++
      if (val >= explodeThreshold) extraDice++
      diceResults.push({ val, exploded: true })
    }
  }

  const isOpposed   = dv === 0
  const isSuccess   = isOpposed ? null : successes >= dv
  const colorEmoji  = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜"
  const skillLabel  = game.i18n.localize(skillDef.label)
  const attrLabel   = game.i18n.localize(`OATHHAMMER.Attribute.${attrKey.charAt(0).toUpperCase()}${attrKey.slice(1)}`)

  // Build dice display HTML
  const diceHtml = diceResults.map(({ val, exploded }) => {
    const success  = val >= threshold
    const cssClass = success ? "die-success" : "die-fail"
    const explodedClass = exploded ? " die-exploded" : ""
    return `<span class="oh-die ${cssClass}${explodedClass}" title="${exploded ? "💥 Exploded" : ""}">${val}</span>`
  }).join(" ")

  // Build modifier summary
  const explodedCount = diceResults.filter(d => d.exploded).length
  const modParts = []
  if (bonus !== 0)       modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
  if (luckSpend > 0)    modParts.push(`+${luckSpend * luckDicePerPoint} ${game.i18n.localize("OATHHAMMER.Dialog.LuckSpend")} (${luckSpend}LP${luckIsHuman ? " 👤" : ""})`)
  if (supporters > 0)   modParts.push(`+${supporters} ${game.i18n.localize("OATHHAMMER.Dialog.Supporters")}`)
  if (explodeOn5)        modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`)
  if (explodedCount > 0) modParts.push(`💥 ${explodedCount} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`)
  const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""

  const resultClass = isOpposed ? "roll-opposed" : isSuccess ? "roll-success" : "roll-failure"
  const resultLabel = isOpposed
    ? game.i18n.localize("OATHHAMMER.Roll.Opposed")
    : isSuccess
      ? game.i18n.localize("OATHHAMMER.Roll.Success")
      : game.i18n.localize("OATHHAMMER.Roll.Failure")

  const cardFlavor = flavor ?? (isOpposed
    ? `${skillLabel} ${game.i18n.localize("OATHHAMMER.Roll.Check")} (${game.i18n.localize("OATHHAMMER.Roll.Opposed")})`
    : `${skillLabel} ${game.i18n.localize("OATHHAMMER.Roll.Check")} (DV ${dv})`)

  const successDisplay = isOpposed ? String(successes) : `${successes} / ${dv}`

  const content = `
    <div class="oh-roll-card">
      <div class="oh-roll-header">${cardFlavor}</div>
      <div class="oh-roll-info">
        <span>${attrLabel} ${attrRank} + ${skillLabel} ${skillRank}</span>
        <span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
      </div>
      ${modLine}
      <div class="oh-roll-dice">${diceHtml}</div>
      <div class="oh-roll-result ${resultClass}">
        <span class="oh-roll-successes">${successDisplay}</span>
        <span class="oh-roll-verdict">${resultLabel}</span>
      </div>
    </div>
  `

  const rollMode = visibility ?? game.settings.get("core", "rollMode")
  const msgData  = {
    speaker: ChatMessage.getSpeaker({ actor }),
    content,
    rolls:   allRolls,
    sound:   CONFIG.sounds.dice,
    flags:   { "fvtt-oath-hammer": { luckRoll: _luckFlagData(actor, threshold, colorType, dv, isOpposed, explodeOn5) } },
  }
  ChatMessage.applyRollMode(msgData, rollMode)
  await ChatMessage.create(msgData)

  return { successes, dv, isSuccess }
}

/**
 * Roll a Fortune check to find an item of a given rarity.
 * Used by the rollable rarity button on item sheets.
 * @param {Actor}  actor       The actor making the check
 * @param {string} rarityKey   Rarity key (e.g. "rare", "very-rare")
 * @param {string} [itemName]  Optional item name for flavor text
 */
export async function rollRarityCheck(actor, rarityKey, itemName) {
  const dv = SYSTEM.RARITY_DV[rarityKey] ?? 1
  if (rarityKey === "always") {
    const rarityLabel = game.i18n.localize(SYSTEM.RARITY_CHOICES[rarityKey])
    const content = `
      <div class="oh-roll-card">
        <div class="oh-roll-header">${itemName ?? game.i18n.localize("OATHHAMMER.Label.Rarity")}</div>
        <div class="oh-roll-result roll-success">
          <span class="oh-roll-verdict">${rarityLabel} — ${game.i18n.localize("OATHHAMMER.Roll.AutoSuccess")}</span>
        </div>
      </div>
    `
    await ChatMessage.create({ speaker: ChatMessage.getSpeaker({ actor }), content })
    return { successes: 0, dv: 0, isSuccess: true }
  }

  const rarityLabel = game.i18n.localize(SYSTEM.RARITY_CHOICES[rarityKey])
  const flavor = `${game.i18n.localize("OATHHAMMER.Skill.Fortune")} — ${itemName ?? rarityLabel} (DV ${dv})`
  return rollSkillCheck(actor, "fortune", dv, { flavor })
}

// ============================================================
// SHARED DICE HELPER
// ============================================================

/**
 * Roll a pool of dice, counting successes (including exploding 6s).
 * @param {number} pool       Number of dice to roll
 * @param {number} threshold  Minimum value to count as a success
 * @returns {Promise<{roll: Roll, successes: number, diceResults: Array}>}
 */
export async function _rollPool(pool, threshold, explodeOn5 = false) {
  const explodeThreshold = explodeOn5 ? 5 : 6  // default: always explode on 6
  const roll = await new Roll(`${Math.max(pool, 1)}d6`).evaluate()
  const rolls = [roll]
  let successes = 0
  const diceResults = []
  let extraDice = 0

  for (const r of roll.dice[0].results) {
    const val = r.result
    if (val >= threshold) successes++
    if (val >= explodeThreshold) extraDice++
    diceResults.push({ val, exploded: false })
  }

  while (extraDice > 0) {
    const xRoll = await new Roll(`${extraDice}d6`).evaluate()
    rolls.push(xRoll)
    extraDice = 0
    for (const r of xRoll.dice[0].results) {
      const val = r.result
      if (val >= threshold) successes++
      if (val >= explodeThreshold) extraDice++
      diceResults.push({ val, exploded: true })
    }
  }

  return { roll, rolls, successes, diceResults }
}

/**
 * Render dice results as HTML spans.
 */
export function _diceHtml(diceResults, threshold) {
  return diceResults.map(({ val, exploded }) => {
    const cssClass = val >= threshold ? "die-success" : "die-fail"
    return `<span class="oh-die ${cssClass}${exploded ? " die-exploded" : ""}" title="${exploded ? "💥" : ""}">${val}</span>`
  }).join(" ")
}

/**
 * Build the luck flag data to store on a chat message, enabling post-roll luck spending.
 * @param {Actor}  actor
 * @param {number} threshold   Dice success threshold (2/3/4)
 * @param {string} colorType   "white"|"red"|"black"
 * @param {number} dv          Difficulty value (0 = opposed)
 * @param {boolean} isOpposed
 * @param {boolean} explodeOn5
 */
export function _luckFlagData(actor, threshold, colorType, dv, isOpposed, explodeOn5) {
  return { actorUuid: actor.uuid, threshold, colorType, dv, isOpposed, explodeOn5 }
}

/**
 * Perform a post-roll luck spend: roll extra dice and update the chat message.
 * @param {ChatMessage} message
 * @param {number}      luckSpend
 * @param {boolean}     luckIsHuman
 */
export async function rollPostRollLuck(message, luckSpend, luckIsHuman) {
  const flag = message.getFlag("fvtt-oath-hammer", "luckRoll")
  if (!flag || flag.used || luckSpend <= 0) return

  const actor = await fromUuid(flag.actorUuid)
  if (!actor) return

  const currentLuck = actor.system.luck?.value ?? 0
  const safeSpend   = Math.min(luckSpend, currentLuck)
  if (safeSpend <= 0) {
    ui.notifications.warn(game.i18n.localize("OATHHAMMER.Roll.NoLuckLeft"))
    return
  }

  const luckDicePerPoint = luckIsHuman ? 3 : 2
  const extraDice        = safeSpend * luckDicePerPoint

  const { successes, diceResults } = await _rollPool(extraDice, flag.threshold, flag.explodeOn5)

  await actor.update({ "system.luck.value": Math.max(0, currentLuck - safeSpend) })

  const luckDiceHtml = _diceHtml(diceResults, flag.threshold)

  await message.setFlag("fvtt-oath-hammer", "luckRoll", {
    ...flag,
    used:             true,
    luckSpend:        safeSpend,
    luckIsHuman,
    bonusSuccesses:   successes,
    extraDiceResults: diceResults,
    luckDiceHtml,
  })
}

// ============================================================
// WEAPON ATTACK ROLL
// ============================================================

/**
 * Roll a weapon attack and post the result to chat.
 * The chat card includes a "Roll Damage" button that triggers rollWeaponDamage.
 *
 * @param {Actor}  actor   The attacking actor
 * @param {Item}   weapon  The weapon item
 * @param {object} options From OathHammerWeaponDialog.promptAttack()
 */
export async function rollWeaponAttack(actor, weapon, options = {}) {
  const { attackBonus = 0, rangeCondition = 0, attrOverride, colorOverride, visibility, autoAttackBonus = 0, explodeOn5 = false, luckSpend = 0, luckIsHuman = false } = options

  const sys      = weapon.system
  const actorSys = actor.system

  const isRanged   = sys.proficiencyGroup === "bows" || sys.proficiencyGroup === "throwing"
  const skillKey   = (sys.skillOverride && SYSTEM.SKILLS[sys.skillOverride]) ? sys.skillOverride : (isRanged ? "shooting" : "fighting")
  const skillDef   = SYSTEM.SKILLS[skillKey]
  const defaultAttr = skillDef.attribute

  const attrKey  = attrOverride && actorSys.attributes[attrOverride] ? attrOverride : defaultAttr
  const attrRank = actorSys.attributes[attrKey].rank
  const skillRank = actorSys.skills[skillKey].rank
  const baseColorType = actorSys.skills[skillKey].colorDiceType ?? "white"
  const colorType  = colorOverride || baseColorType
  const threshold  = colorType === "black" ? 2 : colorType === "red" ? 3 : 4
  const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜"

  const luckDicePerPoint = luckIsHuman ? 3 : 2
  const totalDice = Math.max(attrRank + skillRank + attackBonus + rangeCondition + autoAttackBonus + (luckSpend * luckDicePerPoint), 1)

  if (luckSpend > 0) {
    const currentLuck = actorSys.luck?.value ?? 0
    await actor.update({ "system.luck.value": Math.max(0, currentLuck - luckSpend) })
  }

  const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, explodeOn5)

  const skillLabel = game.i18n.localize(skillDef.label)
  const attrLabel  = game.i18n.localize(`OATHHAMMER.Attribute.${attrKey.charAt(0).toUpperCase() + attrKey.slice(1)}`)
  const diceHtml   = _diceHtml(diceResults, threshold)

  const modParts = []
  if (attackBonus !== 0)    modParts.push(`${attackBonus > 0 ? "+" : ""}${attackBonus} ${game.i18n.localize("OATHHAMMER.Dialog.AttackModifier")}`)
  if (rangeCondition !== 0) modParts.push(`${rangeCondition} ${game.i18n.localize("OATHHAMMER.Dialog.RangeCondition")}`)
  if (autoAttackBonus > 0)  modParts.push(`+${autoAttackBonus} auto`)
  if (luckSpend > 0)        modParts.push(`+${luckSpend * luckDicePerPoint} ${game.i18n.localize("OATHHAMMER.Dialog.LuckSpend")} (${luckSpend}LP${luckIsHuman ? " 👤" : ""})`)
  const explodedCount = diceResults.filter(d => d.exploded).length
  if (explodeOn5)           modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`)
  if (explodedCount > 0)    modParts.push(`💥 ${explodedCount} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`)
  const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""

  const content = `
    <div class="oh-roll-card oh-weapon-card">
      <div class="oh-roll-header">
        <img src="${weapon.img}" class="oh-card-weapon-img" alt="${weapon.name}" />
        <span>${weapon.name} — ${game.i18n.localize("OATHHAMMER.Dialog.Attack")}</span>
      </div>
      <div class="oh-roll-info">
        <span>${skillLabel} (${attrLabel} ${attrRank}) + ${skillLabel} ${skillRank}</span>
        <span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
      </div>
      ${modLine}
      <div class="oh-roll-dice">${diceHtml}</div>
      <div class="oh-roll-result">
        <span class="oh-roll-successes">${successes} ${game.i18n.localize("OATHHAMMER.Roll.Successes")}</span>
      </div>
      <div class="oh-weapon-damage-btn-row">
        <button type="button" class="oh-roll-damage-btn" data-action="rollWeaponDamage">
          ⚔ ${game.i18n.localize("OATHHAMMER.Roll.RollDamage")} (SV ${successes})
        </button>
      </div>
    </div>
  `

  const rollMode = visibility ?? game.settings.get("core", "rollMode")
  const flagData = { actorUuid: actor.uuid, weaponUuid: weapon.uuid, attackSuccesses: successes }
  const msgData = {
    speaker: ChatMessage.getSpeaker({ actor }),
    content,
    rolls:   rolls,
    sound:   CONFIG.sounds.dice,
    flags:   { "fvtt-oath-hammer": { weaponAttack: flagData, luckRoll: _luckFlagData(actor, threshold, colorType, 0, true, explodeOn5) } },
  }
  ChatMessage.applyRollMode(msgData, rollMode)
  await ChatMessage.create(msgData)

  return { successes }
}

// ============================================================
// WEAPON DAMAGE ROLL
// ============================================================

/**
 * Roll weapon damage and post to chat.
 *
 * @param {Actor}  actor   The attacking actor
 * @param {Item}   weapon  The weapon item
 * @param {object} options From OathHammerWeaponDialog.promptDamage()
 */
export async function rollWeaponDamage(actor, weapon, options = {}) {
  const { sv = 0, damageBonus = 0, visibility, autoDamageBonus = 0 } = options

  const sys      = weapon.system
  const actorSys = actor.system

  const hasBrutal   = sys.traits.has("brutal")
  const hasDeadly   = sys.traits.has("deadly")
  const colorType   = hasDeadly ? "black" : hasBrutal ? "red" : "white"
  const threshold   = hasDeadly ? 2 : hasBrutal ? 3 : 4
  const colorEmoji  = hasDeadly ? "⬛" : hasBrutal ? "🔴" : "⬜"
  const colorLabel  = hasDeadly ? "Black" : hasBrutal ? "Red" : "White"

  const isRangedDmg    = sys.proficiencyGroup === "bows" || sys.proficiencyGroup === "throwing"
  const dmgSkillKey    = (sys.skillOverride && SYSTEM.SKILLS[sys.skillOverride]) ? sys.skillOverride : (isRangedDmg ? "shooting" : "fighting")
  const dmgAttrKey     = SYSTEM.SKILLS[dmgSkillKey].attribute
  const dmgAttrRank    = actorSys.attributes[dmgAttrKey].rank
  const baseDamageDice = sys.usesMight ? Math.max(dmgAttrRank + sys.damageMod, 1) : Math.max(sys.damageMod, 1)
  const totalDice      = Math.max(baseDamageDice + sv + damageBonus + autoDamageBonus, 1)

  const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold)
  const diceHtml = _diceHtml(diceResults, threshold)

  const modParts = []
  if (sv > 0)             modParts.push(`+${sv} SV`)
  if (damageBonus !== 0)  modParts.push(`${damageBonus > 0 ? "+" : ""}${damageBonus} ${game.i18n.localize("OATHHAMMER.Dialog.DamageModifier")}`)
  if (autoDamageBonus > 0) modParts.push(`+${autoDamageBonus} auto`)
  const explodedCount = diceResults.filter(d => d.exploded).length
  if (explodedCount > 0)  modParts.push(`💥 ${explodedCount} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`)
  const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""

  const apNote = sys.ap > 0 ? `<span class="oh-ap-note">AP ${sys.ap}</span>` : ""

  const content = `
    <div class="oh-roll-card oh-weapon-card">
      <div class="oh-roll-header">
        <img src="${weapon.img}" class="oh-card-weapon-img" alt="${weapon.name}" />
        <span>${weapon.name} — ${game.i18n.localize("OATHHAMMER.Dialog.Damage")}</span>
      </div>
      <div class="oh-roll-info">
        <span>${sys.damageLabel} = ${baseDamageDice}d6 ${sv > 0 ? `+${sv} SV` : ""}</span>
        <span>${colorEmoji} ${totalDice}d6 (${threshold}+) ${colorLabel}</span>
      </div>
      ${modLine}
      <div class="oh-roll-dice">${diceHtml}</div>
      <div class="oh-roll-result">
        <span class="oh-roll-successes">${successes} ${game.i18n.localize("OATHHAMMER.Roll.Damage")}</span>
        ${apNote}
      </div>
    </div>
  `

  const rollMode = visibility ?? game.settings.get("core", "rollMode")
  const msgData = {
    speaker: ChatMessage.getSpeaker({ actor }),
    content,
    rolls:   rolls,
    sound:   CONFIG.sounds.dice,
  }
  ChatMessage.applyRollMode(msgData, rollMode)
  await ChatMessage.create(msgData)

  return { successes }
}


// ============================================================
// SPELL CAST ROLL
// ============================================================

/**
 * Roll a spell casting check (Magic / Intelligence) and post to chat.
 * Counts dice showing 1 and adds Arcane Stress to the actor.
 *
 * @param {Actor}  actor   The caster
 * @param {Item}   spell   The spell item
 * @param {object} options From OathHammerSpellDialog.prompt()
 */
export async function rollSpellCast(actor, spell, options = {}) {
  const {
    dv            = spell.system.difficultyValue,
    enhancement   = "none",
    stressCost    = 0,
    poolPenalty   = 0,
    redDice       = false,
    noStress      = false,
    elementalBonus = 0,
    bonus          = 0,
    poolSize       = null,
    grimPenalty    = 0,
    visibility,
    explodeOn5    = false,
    luckSpend     = 0,
    luckIsHuman   = false,
  } = options

  const sys      = spell.system
  const actorSys = actor.system

  const intRank   = actorSys.attributes.intelligence.rank
  const magicRank = actorSys.skills.magic.rank
  const luckDicePerPoint = luckIsHuman ? 3 : 2
  const baseDice  = intRank + magicRank + bonus + poolPenalty + elementalBonus + grimPenalty + (luckSpend * luckDicePerPoint)
  // poolSize: voluntary reduction (p.101) — clamped to [1, baseDice]
  const totalDice = poolSize !== null
    ? Math.max(1, Math.min(poolSize + bonus + poolPenalty + elementalBonus + grimPenalty + (luckSpend * luckDicePerPoint), baseDice))
    : Math.max(baseDice, 1)
  const threshold = redDice ? 3 : 4
  const colorEmoji = redDice ? "🔴" : "⬜"

  const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, explodeOn5)
  const diceHtml = _diceHtml(diceResults, threshold)

  // Count 1s for Arcane Stress (unless Safe Spell enhancement)
  const onesCount = noStress ? 0 : diceResults.filter(d => d.val === 1 && !d.exploded).length
  const totalStressGain = stressCost + onesCount
  const isSuccess = successes >= dv

  // Update arcane stress
  if (totalStressGain > 0) {
    const currentStress = actorSys.arcaneStress.value
    await actor.update({ "system.arcaneStress.value": currentStress + totalStressGain })
  }

  if (luckSpend > 0) {
    await actor.update({ "system.luck.value": Math.max(0, (actorSys.luck?.value ?? 0) - luckSpend) })
  }

  const newStress   = (actorSys.arcaneStress.value ?? 0) + totalStressGain
  const stressMax   = actorSys.arcaneStress.threshold
  const isBlocked   = newStress >= stressMax

  const skillLabel  = game.i18n.localize("OATHHAMMER.Skill.Magic")
  const attrLabel   = game.i18n.localize("OATHHAMMER.Attribute.Intelligence")
  const resultClass = isSuccess ? "roll-success" : "roll-failure"
  const resultLabel = isSuccess
    ? game.i18n.localize("OATHHAMMER.Roll.Success")
    : game.i18n.localize("OATHHAMMER.Roll.Failure")

  const modParts = []
  if (poolSize !== null && poolSize < intRank + magicRank) modParts.push(`🎲 ${poolSize}d ${game.i18n.localize("OATHHAMMER.Dialog.PoolSizeReduced")}`)
  if (bonus !== 0)          modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
  if (poolPenalty !== 0)    modParts.push(`${poolPenalty} ${game.i18n.localize("OATHHAMMER.Enhancement." + _cap(enhancement))}`)
  if (elementalBonus > 0)   modParts.push(`+${elementalBonus} ${game.i18n.localize("OATHHAMMER.Dialog.ElementMet")}`)
  if (grimPenalty < 0)      modParts.push(`${grimPenalty} ${game.i18n.localize("OATHHAMMER.Dialog.GrimoireNo")}`)
  if (luckSpend > 0)        modParts.push(`+${luckSpend * luckDicePerPoint} ${game.i18n.localize("OATHHAMMER.Dialog.LuckSpend")} (${luckSpend}LP${luckIsHuman ? " 👤" : ""})`)
  const explodedCountSpell = diceResults.filter(d => d.exploded).length
  if (explodeOn5)             modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`)
  if (explodedCountSpell > 0) modParts.push(`💥 ${explodedCountSpell} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`)
  const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""

  const stressLine = `<div class="oh-stress-line${isBlocked ? " stress-blocked" : ""}">
    🧠 ${game.i18n.localize("OATHHAMMER.Label.ArcaneStress")}: +${totalStressGain}
    (${onesCount} × 1s + ${stressCost} enh.) → <strong>${newStress}/${stressMax}</strong>
    ${isBlocked ? ` ⚠ ${game.i18n.localize("OATHHAMMER.Label.StressBlocked")}` : ""}
  </div>`

  const content = `
    <div class="oh-roll-card oh-spell-card">
      <div class="oh-roll-header">
        <img src="${spell.img}" class="oh-card-weapon-img" alt="${spell.name}" />
        <span>${spell.name} (DV ${dv})</span>
      </div>
      <div class="oh-roll-info">
        <span>${attrLabel} ${intRank} + ${skillLabel} ${magicRank}</span>
        <span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
      </div>
      ${modLine}
      <div class="oh-roll-dice">${diceHtml}</div>
      <div class="oh-roll-result ${resultClass}">
        <span class="oh-roll-successes">${successes} / ${dv}</span>
        <span class="oh-roll-verdict">${resultLabel}</span>
      </div>
      ${stressLine}
    </div>
  `

  const rollMode = visibility ?? game.settings.get("core", "rollMode")
  const spellColorType = redDice ? "red" : "white"
  const msgData  = {
    speaker: ChatMessage.getSpeaker({ actor }),
    content,
    rolls:   rolls,
    sound:   CONFIG.sounds.dice,
    flags:   { "fvtt-oath-hammer": { luckRoll: _luckFlagData(actor, threshold, spellColorType, dv, false, explodeOn5) } },
  }
  ChatMessage.applyRollMode(msgData, rollMode)
  await ChatMessage.create(msgData)

  return { successes, dv, isSuccess, totalStressGain, newStress }
}

// ============================================================
// MIRACLE CAST ROLL
// ============================================================

/**
 * Roll a miracle invocation check (Magic / Willpower) and post to chat.
 * On failure, warns the player they are blocked from miracles for the day.
 *
 * @param {Actor}  actor    The caster
 * @param {Item}   miracle  The miracle item
 * @param {object} options  From OathHammerMiracleDialog.prompt()
 */
export async function rollMiracleCast(actor, miracle, options = {}) {
  const {
    dv         = 1,
    isRitual   = false,
    bonus      = 0,
    visibility,
    explodeOn5 = false,
    luckSpend  = 0,
    luckIsHuman = false,
  } = options

  const sys      = miracle.system
  const actorSys = actor.system

  const wpRank    = actorSys.attributes.willpower.rank
  const magicRank = actorSys.skills.magic.rank
  const luckDicePerPoint = luckIsHuman ? 3 : 2
  const totalDice = Math.max(wpRank + magicRank + bonus + (luckSpend * luckDicePerPoint), 1)
  const threshold = 4
  const colorEmoji = "⬜"

  const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, explodeOn5)
  const diceHtml   = _diceHtml(diceResults, threshold)
  const isSuccess  = successes >= dv

  if (luckSpend > 0) {
    await actor.update({ "system.luck.value": Math.max(0, (actorSys.luck?.value ?? 0) - luckSpend) })
  }

  const skillLabel = game.i18n.localize("OATHHAMMER.Skill.Magic")
  const attrLabel  = game.i18n.localize("OATHHAMMER.Attribute.Willpower")
  const resultClass = isSuccess ? "roll-success" : "roll-failure"
  const resultLabel = isSuccess
    ? game.i18n.localize("OATHHAMMER.Roll.Success")
    : game.i18n.localize("OATHHAMMER.Roll.Failure")

  const modParts = []
  if (bonus !== 0)  modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
  if (luckSpend > 0) modParts.push(`+${luckSpend * luckDicePerPoint} ${game.i18n.localize("OATHHAMMER.Dialog.LuckSpend")} (${luckSpend}LP${luckIsHuman ? " 👤" : ""})`)
  const explodedCountMiracle = diceResults.filter(d => d.exploded).length
  if (explodeOn5)              modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`)
  if (explodedCountMiracle > 0) modParts.push(`💥 ${explodedCountMiracle} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`)
  const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""

  const blockedLine = !isSuccess
    ? `<div class="oh-miracle-blocked">⚠ ${game.i18n.localize("OATHHAMMER.Roll.MiracleBlocked")}</div>`
    : ""

  const dvNote = isRitual
    ? `DV ${dv} (${game.i18n.localize("OATHHAMMER.Label.Ritual")})`
    : `DV ${dv}`

  const content = `
    <div class="oh-roll-card oh-miracle-card">
      <div class="oh-roll-header">
        <img src="${miracle.img}" class="oh-card-weapon-img" alt="${miracle.name}" />
        <span>${miracle.name} (${dvNote})</span>
      </div>
      <div class="oh-roll-info">
        <span>${attrLabel} ${wpRank} + ${skillLabel} ${magicRank}</span>
        <span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
      </div>
      ${modLine}
      <div class="oh-roll-dice">${diceHtml}</div>
      <div class="oh-roll-result ${resultClass}">
        <span class="oh-roll-successes">${successes} / ${dv}</span>
        <span class="oh-roll-verdict">${resultLabel}</span>
      </div>
      ${blockedLine}
    </div>
  `

  const rollMode = visibility ?? game.settings.get("core", "rollMode")
  const msgData  = {
    speaker: ChatMessage.getSpeaker({ actor }),
    content,
    rolls:   rolls,
    sound:   CONFIG.sounds.dice,
    flags:   { "fvtt-oath-hammer": { luckRoll: _luckFlagData(actor, threshold, "white", dv, false, explodeOn5) } },
  }
  ChatMessage.applyRollMode(msgData, rollMode)
  await ChatMessage.create(msgData)

  if (!isSuccess) {
    await actor.update({ "system.miracleBlocked": true })
  }

  return { successes, dv, isSuccess }
}

function _cap(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : "" }

// ============================================================
// DEFENSE ROLL
// ============================================================

/**
 * Roll a defense check (Agility/Might + Defense skill) and post to chat.
 *
 * @param {Actor}  actor
 * @param {object} options  From OathHammerDefenseDialog.prompt()
 */
export async function rollDefense(actor, options = {}) {
  const {
    attackType   = "melee",
    attrRank     = 0,
    attrChoice   = "agility",
    redDice      = false,
    traitBonus   = 0,
    armorPenalty = 0,
    bonus        = 0,
    visibility,
  } = options

  const defRank   = actor.system.skills.defense.rank
  const totalDice = Math.max(attrRank + defRank + traitBonus + armorPenalty + bonus, 1)
  const threshold = redDice ? 3 : 4
  const colorEmoji = redDice ? "🔴" : "⬜"

  const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold)
  const diceHtml = _diceHtml(diceResults, threshold)

  const attrLabel  = game.i18n.localize(`OATHHAMMER.Attribute.${attrChoice.charAt(0).toUpperCase() + attrChoice.slice(1)}`)
  const skillLabel = game.i18n.localize("OATHHAMMER.Skill.Defense")
  const typeLabel  = game.i18n.localize(attackType === "ranged" ? "OATHHAMMER.Dialog.DefenseRanged" : "OATHHAMMER.Dialog.DefenseMelee")

  const modParts = []
  if (traitBonus > 0)   modParts.push(`+${traitBonus} ${game.i18n.localize(attackType === "melee" ? "OATHHAMMER.WeaponTrait.Parry" : "OATHHAMMER.WeaponTrait.Block")}`)
  if (armorPenalty < 0) modParts.push(`${armorPenalty} ${game.i18n.localize("OATHHAMMER.Dialog.ArmorPenalty")}`)
  if (bonus !== 0)      modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
  const explodedCountDef = diceResults.filter(d => d.exploded).length
  if (explodedCountDef > 0) modParts.push(`💥 ${explodedCountDef} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`)
  const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""

  const content = `
    <div class="oh-roll-card oh-defense-card">
      <div class="oh-roll-header">
        <i class="fa-solid fa-shield-halved oh-defense-icon"></i>
        <span>${game.i18n.localize("OATHHAMMER.Roll.Defense")} — ${typeLabel}</span>
      </div>
      <div class="oh-roll-info">
        <span>${attrLabel} ${attrRank} + ${skillLabel} ${defRank}</span>
        <span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
      </div>
      ${modLine}
      <div class="oh-roll-dice">${diceHtml}</div>
      <div class="oh-roll-result">
        <span class="oh-roll-successes">${successes} ${game.i18n.localize("OATHHAMMER.Roll.Successes")}</span>
        <span class="oh-roll-verdict">${game.i18n.localize("OATHHAMMER.Roll.DefenseResult")}</span>
      </div>
    </div>
  `

  const rollMode = visibility ?? game.settings.get("core", "rollMode")
  const defColorType = redDice ? "red" : "white"
  const msgData  = {
    speaker: ChatMessage.getSpeaker({ actor }),
    content,
    rolls:   rolls,
    sound:   CONFIG.sounds.dice,
    flags:   { "fvtt-oath-hammer": { luckRoll: _luckFlagData(actor, threshold, defColorType, 0, true, false) } },
  }
  ChatMessage.applyRollMode(msgData, rollMode)
  await ChatMessage.create(msgData)

  return { successes }
}

// ============================================================
// WEAPON DEFENSE ROLL
// ============================================================

/**
 * Roll a defense check triggered from a specific weapon, applying the
 * weapon's Parry / Block traits, and post to chat.
 *
 * Pool = (Agility or Might) + Defense skill
 *        + traitBonus (Parry: +1 if two Parry weapons; Block: +1 vs ranged)
 *        + armorPenalty (≤ 0)
 *        + diminishPenalty (0 / −2 / −4 for 1st / 2nd / 3rd+ defense)
 *        + bonus
 *
 * Parry trait → red dice (3+) when defending vs melee attacks
 * Block trait → red dice (3+) + +1 bonus when defending vs ranged attacks
 *
 * @param {Actor}  actor
 * @param {Item}   weapon  The weapon used to defend
 * @param {object} options From OathHammerWeaponDialog.promptDefense()
 */
export async function rollWeaponDefense(actor, weapon, options = {}) {
  const {
    attackType      = "melee",
    attrRank        = 0,
    attrChoice      = "agility",
    colorOverride   = "white",
    traitBonus      = 0,
    armorPenalty    = 0,
    diminishPenalty = 0,
    bonus           = 0,
    visibility,
    explodeOn5      = false,
    luckSpend       = 0,
    luckIsHuman     = false,
  } = options

  const defRank          = actor.system.skills.defense.rank
  const luckDicePerPoint = luckIsHuman ? 3 : 2
  const totalDice  = Math.max(attrRank + defRank + traitBonus + armorPenalty + diminishPenalty + bonus + (luckSpend * luckDicePerPoint), 1)
  const threshold  = colorOverride === "black" ? 2 : colorOverride === "red" ? 3 : 4
  const colorEmoji = colorOverride === "black" ? "⬛" : colorOverride === "red" ? "🔴" : "⬜"

  if (luckSpend > 0) {
    const currentLuck = actor.system.luck?.value ?? 0
    await actor.update({ "system.luck.value": Math.max(0, currentLuck - luckSpend) })
  }

  const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, explodeOn5)
  const diceHtml = _diceHtml(diceResults, threshold)

  const attrLabel  = game.i18n.localize(`OATHHAMMER.Attribute.${_cap(attrChoice)}`)
  const skillLabel = game.i18n.localize("OATHHAMMER.Skill.Defense")
  const typeLabel  = game.i18n.localize(attackType === "ranged" ? "OATHHAMMER.Dialog.DefenseRanged" : "OATHHAMMER.Dialog.DefenseMelee")

  const modParts = []
  if (traitBonus > 0)       modParts.push(`+${traitBonus} ${game.i18n.localize(attackType === "melee" ? "OATHHAMMER.WeaponTrait.Parry" : "OATHHAMMER.WeaponTrait.Block")}`)
  if (armorPenalty < 0)     modParts.push(`${armorPenalty} ${game.i18n.localize("OATHHAMMER.Dialog.ArmorPenalty")}`)
  if (diminishPenalty < 0)  modParts.push(`${diminishPenalty} ${game.i18n.localize("OATHHAMMER.Dialog.DiminishingDefense")}`)
  if (bonus !== 0)           modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
  if (luckSpend > 0)        modParts.push(`+${luckSpend * luckDicePerPoint} ${game.i18n.localize("OATHHAMMER.Dialog.LuckSpend")} (${luckSpend}LP${luckIsHuman ? " 👤" : ""})`)
  const explodedCountWDef = diceResults.filter(d => d.exploded).length
  if (explodeOn5)            modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`)
  if (explodedCountWDef > 0) modParts.push(`💥 ${explodedCountWDef} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`)
  const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""

  const content = `
    <div class="oh-roll-card oh-defense-card">
      <div class="oh-roll-header">
        <img src="${weapon.img}" class="oh-card-weapon-img" alt="${weapon.name}" />
        <span>${weapon.name} — ${game.i18n.localize("OATHHAMMER.Roll.Defense")} (${typeLabel})</span>
      </div>
      <div class="oh-roll-info">
        <span>${attrLabel} ${attrRank} + ${skillLabel} ${defRank}</span>
        <span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
      </div>
      ${modLine}
      <div class="oh-roll-dice">${diceHtml}</div>
      <div class="oh-roll-result">
        <span class="oh-roll-successes">${successes} ${game.i18n.localize("OATHHAMMER.Roll.Successes")}</span>
        <span class="oh-roll-verdict">${game.i18n.localize("OATHHAMMER.Roll.DefenseResult")}</span>
      </div>
    </div>
  `

  const rollMode = visibility ?? game.settings.get("core", "rollMode")
  const msgData  = {
    speaker: ChatMessage.getSpeaker({ actor }),
    content,
    rolls:   rolls,
    sound:   CONFIG.sounds.dice,
    flags:   { "fvtt-oath-hammer": { luckRoll: _luckFlagData(actor, threshold, colorOverride, 0, true, explodeOn5) } },
  }
  ChatMessage.applyRollMode(msgData, rollMode)
  await ChatMessage.create(msgData)

  return { successes }
}

// ============================================================
// ARMOR ROLL
// ============================================================

/**
 * Roll an armor saving roll (AV dice − AP) and post to chat.
 * Unlike other rolls, AP can reduce the pool to 0 (armor bypassed).
 * Each success reduces incoming damage by 1.
 *
 * @param {Actor}  actor
 * @param {Item}   armor
 * @param {object} options  From OathHammerArmorDialog.prompt()
 */
export async function rollArmorSave(actor, armor, options = {}) {
  const {
    av            = armor.system.armorValue ?? 0,
    isReinforced  = false,
    colorOverride = null,
    apPenalty     = 0,
    bonus         = 0,
    visibility,
    explodeOn5    = false,
    luckSpend     = 0,
    luckIsHuman   = false,
  } = options

  const luckDicePerPoint = luckIsHuman ? 3 : 2
  // Armor CAN be reduced to 0 dice (fully bypassed by AP) — luck can still rescue
  const totalDice  = Math.max(av + apPenalty + bonus + (luckSpend * luckDicePerPoint), 0)
  const colorType  = colorOverride || (isReinforced ? "red" : "white")
  const threshold  = colorType === "black" ? 2 : colorType === "red" ? 3 : 4
  const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜"

  if (luckSpend > 0) {
    const currentLuck = actor.system.luck?.value ?? 0
    await actor.update({ "system.luck.value": Math.max(0, currentLuck - luckSpend) })
  }

  let successes = 0
  let diceHtml  = `<em>${game.i18n.localize("OATHHAMMER.Roll.ArmorBypassed")}</em>`
  let roll
  let rolls = []
  let armorDiceResults = []

  if (totalDice > 0) {
    const result = await _rollPool(totalDice, threshold, explodeOn5)
    roll = result.roll
    rolls = result.rolls
    successes = result.successes
    armorDiceResults = result.diceResults
    diceHtml = _diceHtml(result.diceResults, threshold)
  } else {
    // Zero dice — create a dummy roll with no results so Foundry can still attach it
    roll = new Roll("0d6")
    await roll.evaluate()
    rolls = [roll]
  }

  const modParts = []
  if (apPenalty < 0)  modParts.push(`AP ${apPenalty}`)
  if (bonus !== 0)    modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
  if (luckSpend > 0)  modParts.push(`+${luckSpend * luckDicePerPoint} ${game.i18n.localize("OATHHAMMER.Dialog.LuckSpend")} (${luckSpend}LP${luckIsHuman ? " 👤" : ""})`)
  const explodedCountArmor = armorDiceResults.filter(d => d.exploded).length
  if (explodeOn5)          modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`)
  if (explodedCountArmor > 0) modParts.push(`💥 ${explodedCountArmor} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`)
  const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""

  const content = `
    <div class="oh-roll-card oh-armor-card">
      <div class="oh-roll-header">
        <img src="${armor.img}" class="oh-card-weapon-img" alt="${armor.name}" />
        <span>${armor.name} (AV ${av})</span>
      </div>
      <div class="oh-roll-info">
        <span>${game.i18n.localize("OATHHAMMER.Roll.ArmorRoll")}</span>
        <span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
      </div>
      ${modLine}
      <div class="oh-roll-dice">${diceHtml}</div>
      <div class="oh-roll-result ${successes > 0 ? "roll-success" : ""}">
        <span class="oh-roll-successes">${successes} ${game.i18n.localize("OATHHAMMER.Roll.Successes")}</span>
        <span class="oh-roll-verdict">−${successes} ${game.i18n.localize("OATHHAMMER.Roll.Damage")}</span>
      </div>
    </div>
  `

  const rollMode = visibility ?? game.settings.get("core", "rollMode")
  const msgData  = {
    speaker: ChatMessage.getSpeaker({ actor }),
    content,
    rolls:   rolls,
    sound:   CONFIG.sounds.dice,
  }
  ChatMessage.applyRollMode(msgData, rollMode)
  await ChatMessage.create(msgData)

  return { successes, totalDice }
}

// ============================================================
// INITIATIVE ROLL
// ============================================================

/**
 * Roll an initiative check for an actor and post the result to chat.
 *
 * Characters: opposed Leadership check (Fate + Leadership skill, DV=0).
 * NPCs: pool of Fate rank + initiativeBonus dice (no skill), threshold 4+.
 *
 * @param {Actor}  actor
 * @param {object} [options]
 * @param {number}  [options.bonus]      Extra dice modifier
 * @param {string}  [options.visibility] Roll mode
 * @param {boolean} [options.explodeOn5] Explode on 5+
 * @returns {Promise<{successes: number, dv: number, isSuccess: null}>}
 */
export async function rollInitiativeCheck(actor, options = {}) {
  const { bonus = 0, visibility, explodeOn5 = false } = options

  if (actor.type === "character") {
    return rollSkillCheck(actor, "leadership", 0, {
      bonus,
      visibility,
      explodeOn5,
      flavor: game.i18n.localize("OATHHAMMER.Roll.Initiative"),
    })
  }

  // NPC: find Leadership skillnpc item, fall back to Fate rank + initiativeBonus
  const leadershipSkill = actor.items.find(
    i => i.type === "skillnpc" && i.name.toLowerCase() === "leadership"
  )
  if (leadershipSkill) {
    return rollNPCSkill(actor, leadershipSkill, { bonus, visibility })
  }

  // Fallback: Fate rank + initiativeBonus
  const sys       = actor.system
  const fateRank  = sys.attributes?.fate?.rank ?? 1
  const initBonus = sys.initiativeBonus ?? 0
  const pool      = Math.max(fateRank + initBonus + bonus, 1)
  const threshold = 4

  const { roll, rolls, successes, diceResults } = await _rollPool(pool, threshold, explodeOn5)
  const diceHtml = _diceHtml(diceResults, threshold)

  const modParts = []
  if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
  const explodedCountInit = diceResults.filter(d => d.exploded).length
  if (explodeOn5)          modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`)
  if (explodedCountInit > 0) modParts.push(`💥 ${explodedCountInit} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`)
  const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""

  const content = `
    <div class="oh-roll-card">
      <div class="oh-roll-header">⚔ ${game.i18n.localize("OATHHAMMER.Roll.Initiative")} — ${actor.name}</div>
      <div class="oh-roll-info">
        <span>${game.i18n.localize("OATHHAMMER.Attribute.Fate")} ${fateRank}${initBonus ? ` + ${initBonus}` : ""}</span>
        <span>⬜ ${pool}d6 (4+)</span>
      </div>
      ${modLine}
      <div class="oh-roll-dice">${diceHtml}</div>
      <div class="oh-roll-result roll-opposed">
        <span class="oh-roll-successes">${successes}</span>
        <span class="oh-roll-verdict">${game.i18n.localize("OATHHAMMER.Roll.Opposed")}</span>
      </div>
    </div>
  `

  const rollMode = visibility ?? game.settings.get("core", "rollMode")
  const msgData  = { speaker: ChatMessage.getSpeaker({ actor }), content, rolls: rolls, sound: CONFIG.sounds.dice }
  ChatMessage.applyRollMode(msgData, rollMode)
  await ChatMessage.create(msgData)

  return { successes, dv: 0, isSuccess: null }
}

// ============================================================
// NPC SKILL ROLL
// ============================================================

/**
 * Roll an NPC skill check (skillnpc item) and post to chat.
 *
 * @param {Actor}  actor      The NPC/creature actor
 * @param {Item}   skillItem  The skillnpc item
 * @param {object} options
 */
export async function rollNPCSkill(actor, skillItem, options = {}) {
  const { bonus = 0, colorOverride, visibility, explodeOn5 = false } = options
  const sys = skillItem.system

  const colorType  = colorOverride || sys.colorDiceType
  const threshold  = colorType === "black" ? 2 : colorType === "red" ? 3 : 4
  const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜"

  const totalDice = Math.max(sys.dicePool + bonus, 1)

  const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, explodeOn5)
  const diceHtml = _diceHtml(diceResults, threshold)

  const explodedCount = diceResults.filter(d => d.exploded).length
  const modParts = []
  if (bonus !== 0)       modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
  if (explodeOn5)        modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`)
  if (explodedCount > 0) modParts.push(`💥 ${explodedCount} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`)
  const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""

  const content = `
    <div class="oh-roll-card">
      <div class="oh-roll-header">
        <img src="${skillItem.img}" class="oh-card-weapon-img" alt="${skillItem.name}" />
        <span>${skillItem.name} — ${actor.name}</span>
      </div>
      <div class="oh-roll-info">
        <span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
      </div>
      ${modLine}
      <div class="oh-roll-dice">${diceHtml}</div>
      <div class="oh-roll-result">
        <span class="oh-roll-successes">${successes} ${game.i18n.localize("OATHHAMMER.Roll.Successes")}</span>
      </div>
    </div>
  `

  const rollMode = visibility ?? game.settings.get("core", "rollMode")
  const msgData  = { speaker: ChatMessage.getSpeaker({ actor }), content, rolls: rolls, sound: CONFIG.sounds.dice }
  ChatMessage.applyRollMode(msgData, rollMode)
  await ChatMessage.create(msgData)

  return { successes }
}

/**
 * NPC weapon attack roll — uses NPC's flat attackBonus as dice pool.
 * Rolls white dice (4+) with optional bonus modifier.
 */
export async function rollNPCWeaponAttack(actor, weapon, options = {}) {
  const { bonus = 0, visibility } = options
  const sys = actor.system
  const basePool = sys.attackBonus ?? 0
  const totalDice = Math.max(basePool + bonus, 1)
  const threshold = 4
  const colorEmoji = "⬜"

  const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, false)
  const diceHtml = _diceHtml(diceResults, threshold)

  const modParts = []
  if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
  if (diceResults.filter(d => d.exploded).length > 0) modParts.push(`💥 ${diceResults.filter(d => d.exploded).length} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`)
  const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""

  const content = `
    <div class="oh-roll-card oh-weapon-card">
      <div class="oh-roll-header">
        <img src="${weapon.img}" class="oh-card-weapon-img" alt="${weapon.name}" />
        <span>${weapon.name} — ${game.i18n.localize("OATHHAMMER.Dialog.Attack")} (${actor.name})</span>
      </div>
      <div class="oh-roll-info">
        <span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
      </div>
      ${modLine}
      <div class="oh-roll-dice">${diceHtml}</div>
      <div class="oh-roll-result">
        <span class="oh-roll-successes">${successes} ${game.i18n.localize("OATHHAMMER.Roll.Successes")}</span>
      </div>
    </div>
  `

  const rollMode = visibility ?? game.settings.get("core", "rollMode")
  const msgData  = { speaker: ChatMessage.getSpeaker({ actor }), content, rolls: rolls, sound: CONFIG.sounds.dice }
  ChatMessage.applyRollMode(msgData, rollMode)
  await ChatMessage.create(msgData)
  return { successes }
}

/**
 * NPC weapon damage roll — uses NPC damageBonus + weapon damageMod as dice pool.
 */
export async function rollNPCWeaponDamage(actor, weapon, options = {}) {
  const { bonus = 0, visibility } = options
  const sys = actor.system
  const basePool = (sys.damageBonus ?? 0) + (weapon.system.damageMod ?? 0)
  const totalDice = Math.max(basePool + bonus, 1)
  const threshold = 4
  const colorEmoji = "⬜"

  const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, false)
  const diceHtml = _diceHtml(diceResults, threshold)

  const modParts = []
  if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
  if (diceResults.filter(d => d.exploded).length > 0) modParts.push(`💥 ${diceResults.filter(d => d.exploded).length} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`)
  const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""

  const content = `
    <div class="oh-roll-card oh-weapon-card">
      <div class="oh-roll-header">
        <img src="${weapon.img}" class="oh-card-weapon-img" alt="${weapon.name}" />
        <span>${weapon.name} — ${game.i18n.localize("OATHHAMMER.Dialog.Damage")} (${actor.name})</span>
      </div>
      <div class="oh-roll-info">
        <span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
        <span>${game.i18n.localize("OATHHAMMER.Label.Damage")}: ${weapon.system.damageLabel}</span>
      </div>
      ${modLine}
      <div class="oh-roll-dice">${diceHtml}</div>
      <div class="oh-roll-result">
        <span class="oh-roll-successes">${successes} ${game.i18n.localize("OATHHAMMER.Roll.Successes")}</span>
      </div>
    </div>
  `

  const rollMode = visibility ?? game.settings.get("core", "rollMode")
  const msgData  = { speaker: ChatMessage.getSpeaker({ actor }), content, rolls: rolls, sound: CONFIG.sounds.dice }
  ChatMessage.applyRollMode(msgData, rollMode)
  await ChatMessage.create(msgData)
  return { successes }
}

/**
 * NPC armor dice roll — rolls actor's armorDice.value dice with armorDice.colorDiceType color.
 */
export async function rollNPCArmor(actor, options = {}) {
  const { bonus = 0, colorOverride, visibility, explodeOn5 = false } = options
  const sys = actor.system

  const basePool   = sys.armorDice?.value ?? 0
  const colorType  = colorOverride || sys.armorDice?.colorDiceType || "white"
  const threshold  = colorType === "black" ? 2 : colorType === "red" ? 3 : 4
  const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜"
  const totalDice  = Math.max(basePool + bonus, 1)

  const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, explodeOn5)
  const diceHtml = _diceHtml(diceResults, threshold)

  const explodedCount = diceResults.filter(d => d.exploded).length
  const modParts = []
  if (bonus !== 0)       modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
  if (explodeOn5)        modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`)
  if (explodedCount > 0) modParts.push(`💥 ${explodedCount} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`)
  const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""

  const label = game.i18n.localize("OATHHAMMER.Label.ArmorDice")
  const content = `
    <div class="oh-roll-card oh-armor-card">
      <div class="oh-roll-header">
        <img src="${actor.img}" class="oh-card-weapon-img" alt="${actor.name}" />
        <span>${label} — ${actor.name}</span>
      </div>
      <div class="oh-roll-info">
        <span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
      </div>
      ${modLine}
      <div class="oh-roll-dice">${diceHtml}</div>
      <div class="oh-roll-result ${successes > 0 ? "roll-success" : ""}">
        <span class="oh-roll-successes">${successes} ${game.i18n.localize("OATHHAMMER.Roll.Successes")}</span>
        <span class="oh-roll-verdict">−${successes} ${game.i18n.localize("OATHHAMMER.Roll.Damage")}</span>
      </div>
    </div>
  `

  const rollMode = visibility ?? game.settings.get("core", "rollMode")
  const msgData  = { speaker: ChatMessage.getSpeaker({ actor }), content, rolls: rolls, sound: CONFIG.sounds.dice }
  ChatMessage.applyRollMode(msgData, rollMode)
  await ChatMessage.create(msgData)
  return { successes }
}

/**
 * NPC spell cast — flat dice pool, no arcane stress, posts DV success/failure to chat.
 */
export async function rollNPCSpell(actor, spell, options = {}) {
  const { dicePool = 3, bonus = 0, colorOverride, visibility, explodeOn5 = false } = options
  const dv         = spell.system.difficultyValue ?? 1
  const colorType  = colorOverride || "white"
  const threshold  = colorType === "black" ? 2 : colorType === "red" ? 3 : 4
  const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜"
  const totalDice  = Math.max(dicePool + bonus, 1)

  const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, explodeOn5)
  const diceHtml = _diceHtml(diceResults, threshold)
  const isSuccess = successes >= dv

  const explodedCount = diceResults.filter(d => d.exploded).length
  const modParts = []
  if (bonus !== 0)       modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
  if (explodeOn5)        modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`)
  if (explodedCount > 0) modParts.push(`💥 ${explodedCount} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`)
  const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""

  const resultClass = isSuccess ? "roll-success" : "roll-failure"
  const resultLabel = isSuccess
    ? game.i18n.localize("OATHHAMMER.Roll.Success")
    : game.i18n.localize("OATHHAMMER.Roll.Failure")

  const content = `
    <div class="oh-roll-card oh-spell-card">
      <div class="oh-roll-header">
        <img src="${spell.img}" class="oh-card-weapon-img" alt="${spell.name}" />
        <span>${spell.name} (DV ${dv}) — ${actor.name}</span>
      </div>
      <div class="oh-roll-info">
        <span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
      </div>
      ${modLine}
      <div class="oh-roll-dice">${diceHtml}</div>
      <div class="oh-roll-result ${resultClass}">
        <span class="oh-roll-successes">${successes} / ${dv}</span>
        <span class="oh-roll-verdict">${resultLabel}</span>
      </div>
    </div>
  `

  const rollMode = visibility ?? game.settings.get("core", "rollMode")
  const msgData  = { speaker: ChatMessage.getSpeaker({ actor }), content, rolls, sound: CONFIG.sounds.dice }
  ChatMessage.applyRollMode(msgData, rollMode)
  await ChatMessage.create(msgData)
  return { successes, dv, isSuccess }
}

/**
 * NPC miracle invocation — flat dice pool, no blocked tracking, posts DV success/failure to chat.
 */
export async function rollNPCMiracle(actor, miracle, options = {}) {
  const { dicePool = 3, bonus = 0, visibility, explodeOn5 = false } = options
  const dv         = 1
  const threshold  = 4
  const colorEmoji = "⬜"
  const totalDice  = Math.max(dicePool + bonus, 1)

  const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, explodeOn5)
  const diceHtml = _diceHtml(diceResults, threshold)
  const isSuccess = successes >= dv

  const explodedCount = diceResults.filter(d => d.exploded).length
  const modParts = []
  if (bonus !== 0)       modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
  if (explodeOn5)        modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`)
  if (explodedCount > 0) modParts.push(`💥 ${explodedCount} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`)
  const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""

  const resultClass = isSuccess ? "roll-success" : "roll-failure"
  const resultLabel = isSuccess
    ? game.i18n.localize("OATHHAMMER.Roll.Success")
    : game.i18n.localize("OATHHAMMER.Roll.Failure")

  const content = `
    <div class="oh-roll-card oh-miracle-card">
      <div class="oh-roll-header">
        <img src="${miracle.img}" class="oh-card-weapon-img" alt="${miracle.name}" />
        <span>${miracle.name} — ${actor.name}</span>
      </div>
      <div class="oh-roll-info">
        <span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
      </div>
      ${modLine}
      <div class="oh-roll-dice">${diceHtml}</div>
      <div class="oh-roll-result ${resultClass}">
        <span class="oh-roll-successes">${successes} / ${dv}</span>
        <span class="oh-roll-verdict">${resultLabel}</span>
      </div>
    </div>
  `

  const rollMode = visibility ?? game.settings.get("core", "rollMode")
  const msgData  = { speaker: ChatMessage.getSpeaker({ actor }), content, rolls, sound: CONFIG.sounds.dice }
  ChatMessage.applyRollMode(msgData, rollMode)
  await ChatMessage.create(msgData)
  return { successes, dv, isSuccess }
}

/**
 * NPC attack damage roll — flat dice pool from the npcattack item, no Might.
 */
export async function rollNPCAttackDamage(actor, attack, options = {}) {
  const { bonus = 0, visibility, explodeOn5 = false } = options
  const sys       = attack.system
  const colorType  = sys.colorDiceType || "white"
  const threshold  = colorType === "black" ? 2 : colorType === "red" ? 3 : 4
  const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜"
  const totalDice  = Math.max((sys.damageDice ?? 1) + bonus, 1)
  const ap         = sys.ap ?? 0

  const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, explodeOn5)
  const diceHtml = _diceHtml(diceResults, threshold)

  const explodedCount = diceResults.filter(d => d.exploded).length
  const modParts = []
  if (bonus !== 0)       modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
  if (ap > 0)            modParts.push(`AP ${ap}`)
  if (explodeOn5)        modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`)
  if (explodedCount > 0) modParts.push(`💥 ${explodedCount} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`)
  const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""

  const content = `
    <div class="oh-roll-card oh-weapon-card">
      <div class="oh-roll-header">
        <img src="${attack.img}" class="oh-card-weapon-img" alt="${attack.name}" />
        <span>${attack.name} — ${game.i18n.localize("OATHHAMMER.Dialog.Damage")} (${actor.name})</span>
      </div>
      <div class="oh-roll-info">
        <span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
        ${ap > 0 ? `<span>AP ${ap}</span>` : ""}
      </div>
      ${modLine}
      <div class="oh-roll-dice">${diceHtml}</div>
      <div class="oh-roll-result">
        <span class="oh-roll-successes">${successes} ${game.i18n.localize("OATHHAMMER.Roll.Successes")}</span>
      </div>
    </div>
  `

  const rollMode = visibility ?? game.settings.get("core", "rollMode")
  const msgData  = { speaker: ChatMessage.getSpeaker({ actor }), content, rolls, sound: CONFIG.sounds.dice }
  ChatMessage.applyRollMode(msgData, rollMode)
  await ChatMessage.create(msgData)
  return { successes }
}
