import { SYSTEM } from "../config/system.mjs"
import D30Roll from "./d30-roll.mjs"
import LethalFantasyUtils from "../utils.mjs"

/* ***********************************************************/
export async function promptInitiative(options = {}) {
  const rollModes = foundry.utils.duplicate(CONFIG.ChatMessage.modes); // v12 :  Object.fromEntries(Object.entries(CONFIG.Dice.rollModes).map(([key, value]) => [key, game.i18n.localize(value)]))
  const fieldRollMode = new foundry.data.fields.StringField({
    choices: rollModes,
    blank: false,
    default: "public",
  })

  if (SYSTEM.INITIATIVE_DICE_CHOICES_PER_CLASS[options.actorClass]) {
    options.initiativeDiceChoice = SYSTEM.INITIATIVE_DICE_CHOICES_PER_CLASS[options.actorClass]
  } else {
    options.initiativeDiceChoice = SYSTEM.INITIATIVE_DICE_CHOICES_PER_CLASS["untrained"]
  }

  let dialogContext = {
    actorClass: options.actorClass,
    initiativeDiceChoice: options.initiativeDiceChoice,
    initiativeDice: "1D20",
    maxInit: options.maxInit,
    fieldRollMode,
    rollModes
  }

  const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/roll-initiative-dialog.hbs", dialogContext)

  const label = game.i18n.localize("LETHALFANTASY.Label.initiative")
  const rollContext = await foundry.applications.api.DialogV2.wait({
    window: { title: "Initiative Roll" },
    classes: ["lethalfantasy"],
    content,
    buttons: [
      {
        action: "initiative",
        type: "button",
        label: label,
        callback: (event, button) => {
          const output = Array.from(button.form.elements).reduce((obj, input) => {
            if (input.name) obj[input.name] = input.value
            return obj
          }, {})
          return output
        },
      },
    ],
    rejectClose: false // Click on Close button will not launch an error
  })

  if (!rollContext) return

  // When the value is a plain number (e.g. "1" for Declared Ready on Alert), wrapping it in
  // min(1, maxInit) produces a dice-less formula that FoundryVTT cannot evaluate to a valid
  // total. Use the constant directly; min() is only needed for actual dice expressions.
  const isDiceFormula = /[dD]/.test(rollContext.initiativeDice)
  const formula = isDiceFormula ? `min(${rollContext.initiativeDice}, ${options.maxInit})` : rollContext.initiativeDice

  let initRoll = new Roll(formula, options.data)
  await initRoll.evaluate()
  let msg = await initRoll.toMessage({ flavor: `Initiative for ${options.actorName}` }, { messageMode: rollContext.visibility })
  if (game?.dice3d && initRoll.dice?.length) {
    await game.dice3d.waitFor3DAnimationByMessageID(msg.id)
  }

  if (options.combatId && options.combatantId) {
    let combat = game.combats.get(options.combatId)
    await combat.updateEmbeddedDocuments("Combatant", [{ _id: options.combatantId, initiative: initRoll.total, 'system.progressionCount': 0, [`flags.${SYSTEM.id}.firstActionTaken`]: false }])
  }
}

/* ***********************************************************/
export async function promptCombatAction(options = {}) {

  const rollModes = foundry.utils.duplicate(CONFIG.ChatMessage.modes); // v12 : Object.fromEntries(Object.entries(CONFIG.Dice.rollModes).map(([key, value]) => [key, game.i18n.localize(value)]))
  const fieldRollMode = new foundry.data.fields.StringField({
    choices: rollModes,
    blank: false,
    default: "public",
  })

  let combatant = game.combats.get(options.combatId)?.combatants?.get(options.combatantId)
  if (!combatant) {
    console.error("No combatant found for this combat")
    return
  }
  let currentAction = combatant.getFlag(SYSTEM.id, "currentAction")

  let position = game.user.getFlag(SYSTEM.id, "combat-action-dialog-pos") || { top: -1, left: -1 }

  let dialogContext = {
    progressionDiceId: "",
    fieldRollMode,
    rollModes,
    currentAction,
    ...options
  }

  const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/combat-action-dialog.hbs", dialogContext)

  let buttons = []
  if (currentAction) {
    if (currentAction.type === "weapon") {
      let weaponLabel = "Roll progression dice"
      if (currentAction.rangedMode) {
        // Compute loading count from the speed formula (e.g. "3+1d6" → load=3)
        const speedStr = currentAction.system?.speed?.[currentAction.rangedMode] ?? ""
        const rangedLoad = currentAction.rangedLoad ?? (Number(speedStr.split("+")[0]) || 0)
        if (rangedLoad > 0 && !currentAction.weaponLoaded) {
          weaponLabel = "Load weapon"
        }
      }
      buttons.push({
        action: "roll",
        type: "button",
        label: weaponLabel,
        callback: (event, button) => {
          let pos = $('#combat-action-dialog').position()
          game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos)
          return "rollProgressionDice"
        },
      })
    } else if (currentAction.type === "spell" || currentAction.type === "miracle") {
      let label = ""
      if (currentAction.spellStatus === "castingTime") {
        let pos = $('#combat-action-dialog').position()
        game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos)
        label = "Wait casting time"
      }
      if (currentAction.spellStatus === "toBeCasted") {
        let pos = $('#combat-action-dialog').position()
        game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos)
        label = "Cast spell/miracle"
      }
      if (currentAction.spellStatus === "lethargy") {
        let pos = $('#combat-action-dialog').position()
        game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos)
        label = "Roll lethargy dice"
      }
      buttons.push({
        action: "roll",
        type: "button",
        label: label,
        callback: (event, button) => {
          let pos = $('#combat-action-dialog').position()
          game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", foundry.utils.duplicate(pos))
          return "rollLethargyDice"
        },
      })
    }
  } else {
    buttons.push({
      action: "roll",
      type: "button",
      label: "Select action",
      callback: (event, button) => {
        let pos = $('#combat-action-dialog').position()
        game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", foundry.utils.duplicate(pos))
        const output = Array.from(button.form.elements).reduce((obj, input) => {
          if (input.name) obj[input.name] = input.value
          return obj
        }, {})
        return output
      },
    },
    )
  }
  buttons.push({
    action: "cancel",
    type: "button",
    label: "Other action, not listed here",
    callback: (event, button) => {
      let pos = $('#combat-action-dialog').position()
      game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", foundry.utils.duplicate(pos))
      return null;
    }
  })

  let rollContext = await foundry.applications.api.DialogV2.wait({
    window: { title: "Combat Action Dialog" },
    id: "combat-action-dialog",
    classes: ["lethalfantasy"],
    position,
    content,
    buttons,
    rejectClose: false // Click on Close button will not launch an error
  })

  log("RollContext", dialogContext, rollContext)
  // If action is cancelled, exit
  if (rollContext === null || rollContext === "cancel") {
    await combatant.setFlag(SYSTEM.id, "currentAction", "")
    let message = `${combatant.name} : Other action, progression reset`
    await ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
    return
  }

  // Setup the current action
  if (!currentAction || currentAction === "") {
    // Get the item from the returned selectedChoice value
    let selectedChoice = rollContext.selectedChoice
    let rangedMode
    if (selectedChoice.match("simpleAim")) {
      selectedChoice = selectedChoice.replace("simpleAim", "")
      rangedMode = "simpleAim"
    }
    if (selectedChoice.match("carefulAim")) {
      selectedChoice = selectedChoice.replace("carefulAim", "")
      rangedMode = "carefulAim"
    }
    if (selectedChoice.match("focusedAim")) {
      selectedChoice = selectedChoice.replace("focusedAim", "")
      rangedMode = "focusedAim"
    }
    let selectedItem = combatant.actor.items.find(i => i.id === selectedChoice)
    // Setup flag for combat action usage
    let actionItem = foundry.utils.duplicate(selectedItem)
    // First action of this combat: use the class-based starting threshold;
    // all subsequent actions reset to 1 (normal progression).
    const firstActionTaken = combatant.getFlag(SYSTEM.id, "firstActionTaken")
    actionItem.progressionCount = firstActionTaken ? 1 : (combatant.actor.system.combat?.combatProgressionStart ?? 1)
    if (!firstActionTaken) await combatant.setFlag(SYSTEM.id, "firstActionTaken", true)
    actionItem.rangedMode = rangedMode
    // If this is a spell/miracle with multiple damage tiers, prompt tier choice
    if (actionItem.system?.damageDice) {
      const tiers = [
        { id: "standard", label: "Standard", dice: actionItem.system.damageDice },
        { id: "overpowered", label: "Overpowered", dice: actionItem.system.damageDiceOverpowered },
        { id: "overpowered2", label: "Overpowered 2", dice: actionItem.system.damageDiceOverpowered2 },
      ].filter(t => t.dice)
      if (tiers.length > 1) {
        const tierChoice = await foundry.applications.api.DialogV2.wait({
          window: { title: "Choose Damage Tier" },
          classes: ["lethalfantasy"],
          content: await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/dialogs/damage-tier.hbs", {itemName: selectedItem.name}),
          buttons: tiers.map(t => ({
            action: t.id,
            type: "button",
            label: `${t.label} (${t.dice.toUpperCase()})`,
            icon: "fa-solid fa-wand-magic-sparkles",
            callback: () => t.id
          })),
          rejectClose: false
        })
        actionItem.damageTier = tierChoice || "standard"
      }
    }
    actionItem.castingTime = 1
    actionItem.spellStatus = "castingTime"
    // Set the flag on the combatant
    await combatant.setFlag(SYSTEM.id, "currentAction", actionItem)
    let message = `${combatant.name} action : ${selectedItem.name}, start rolling progression dice or casting time`
    await ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
    rollContext = (actionItem.type === "weapon") ? "rollProgressionDice" : "rollLethargyDice" // Set the roll context to rollProgressionDice
    currentAction = actionItem
  }

  if (currentAction) {
    if (rollContext === "rollLethargyDice") {
      if (currentAction.spellStatus === "castingTime") {
        let time = currentAction.type === "spell" ? currentAction.system.castingTime : currentAction.system.prayerTime
        if (currentAction.castingTime < time) {
          let message = `Casting time : ${currentAction.name}, count : ${currentAction.castingTime}/${time}`
          await ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
          currentAction.castingTime += 1
          await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction))
          return
        } else {
          // Last counting second — announce ready and transition immediately (no extra second consumed)
          let message = `Casting time : ${currentAction.name}, count : ${time}/${time} — ready to cast next second !`
          await ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
          currentAction.castingTime = 1
          currentAction.spellStatus = "toBeCasted"
          await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction))
          return
        }
      }
      if (currentAction.spellStatus === "toBeCasted") {
        combatant.actor.prepareRoll((currentAction.type === "spell") ? "spell-attack" : "miracle-attack", currentAction._id)
        if (currentAction.type === "spell") {
          currentAction.spellStatus = "lethargy"
          await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction))
        } else {
          // No lethargy for miracle
          await combatant.setFlag(SYSTEM.id, "currentAction", "")
        }
        return
      }
      if (currentAction.spellStatus === "lethargy") {
        // Roll lethargy dice
        let dice = LethalFantasyUtils.getLethargyDice(currentAction.system.level)
        let roll = new Roll(dice)
        await roll.evaluate()
        if (game?.dice3d) {
          await game.dice3d.showForRoll(roll)
        }
        let max = roll.dice[0].faces - 1
        let toCompare = Math.min(currentAction.progressionCount, max)
        if (roll.total <= toCompare) {
          // Notify that the player can act now with a chat message
          const messageContent = await foundry.applications.handlebars.renderTemplate(
            "systems/fvtt-lethal-fantasy/templates/progression-message.hbs",
            {
              success: true,
              actorName: combatant.actor.name,
              weaponName: currentAction.name,
              rollResult: roll.total,
              isLethargy: true
            }
          )
          await ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
          // Update the combatant progression count
          await combatant.setFlag(SYSTEM.id, "currentAction", "")
          // Display the action selection window again
          combatant.actor.system.rollProgressionDice(options.combatId, options.combatantId)
        } else {
          // Notify that the player cannot act now with a chat message
          currentAction.progressionCount += 1
          await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction))
          const messageContent = await foundry.applications.handlebars.renderTemplate(
            "systems/fvtt-lethal-fantasy/templates/progression-message.hbs",
            {
              success: false,
              actorName: combatant.actor.name,
              weaponName: currentAction.name,
              rollResult: roll.total,
              progressionCount: currentAction.progressionCount,
              isLethargy: true
            }
          )
          await ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
        }
      }
    }

    if (rollContext === "rollProgressionDice") {
      let formula = currentAction.system.combatProgressionDice
      if (currentAction?.rangedMode) {
        let toSplit = currentAction.system.speed[currentAction.rangedMode]
        let split = toSplit.split("+")
        currentAction.rangedLoad = Number(split[0]) || 0
        formula = split[1]
        log("Ranged Mode", currentAction.rangedMode, currentAction.rangedLoad, formula)
      }
      // Range weapon loading
      if (!currentAction.weaponLoaded && currentAction.rangedLoad) {
        if (currentAction.progressionCount < currentAction.rangedLoad) {
          let message = `Ranged weapon ${currentAction.name} is loading, loading count : ${currentAction.progressionCount}/${currentAction.rangedLoad}`
          await ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
          currentAction.progressionCount += 1
          await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction))
        } else {
          let message = `Ranged weapon ${currentAction.name} is loaded !`
          await ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
          currentAction.weaponLoaded = true
          currentAction.progressionCount = 1
          await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction))
        }
        return
      }

      // Melee mode
      let isMonster = combatant.actor.type === "monster"
      // Get the dice and roll it    if
      let roll = new Roll(formula)
      await roll.evaluate()

      let max = roll.dice[0].faces - 1
      max = Math.min(currentAction.progressionCount, max)
      let msg = await roll.toMessage({ flavor: `Progression Roll for ${currentAction.name}, progression count : ${currentAction.progressionCount}/${max}` }, { messageMode: rollContext.visibility })
      if (game?.dice3d) {
        await game.dice3d.waitFor3DAnimationByMessageID(msg.id)
      }

      if (roll.total <= max) {
        // Notify that the player can act now with a chat message
        const messageContent = await foundry.applications.handlebars.renderTemplate(
          "systems/fvtt-lethal-fantasy/templates/progression-message.hbs",
          {
            success: true,
            actorName: combatant.actor.name,
            weaponName: currentAction.name,
            rollResult: roll.total
          }
        )
        await ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
        await combatant.setFlag(SYSTEM.id, "currentAction", "")
        combatant.actor.prepareRoll(currentAction.type === "weapon" ? "weapon-attack" : "spell-attack", currentAction._id)
      } else {
        // Notify that the player cannot act now with a chat message
        currentAction.progressionCount += 1
        await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction))
        const messageContent = await foundry.applications.handlebars.renderTemplate(
          "systems/fvtt-lethal-fantasy/templates/progression-message.hbs",
          {
            success: false,
            actorName: combatant.actor.name,
            weaponName: currentAction.name,
            rollResult: roll.total,
            progressionCount: currentAction.progressionCount
          }
        )
        await ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
      }
    }
  }
}

/* ***********************************************************/
export async function promptRangedDefense(options = {}) {

  const rollModes = foundry.utils.duplicate(CONFIG.ChatMessage.modes);
  const fieldRollMode = new foundry.data.fields.StringField({
    choices: rollModes,
    blank: false,
    default: "public",
  })

  let dialogContext = {
    movementChoices: SYSTEM.MOVEMENT_CHOICES,
    moveDirectionChoices: SYSTEM.MOVE_DIRECTION_CHOICES,
    sizeChoices: SYSTEM.SIZE_CHOICES,
    rangeChoices: SYSTEM.RANGE_CHOICES,
    attackerAimChoices: SYSTEM.ATTACKER_AIM_CHOICES,
    movement: "none",
    moveDirection: "none",
    size: "+5",
    range: "short",
    attackerAim: "simple",
    fieldRollMode,
    rollModes
  }

  const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/range-defense-dialog.hbs", dialogContext)

  const label = game.i18n.localize("LETHALFANTASY.Label.rangeDefenseRoll")
  const rollContext = await foundry.applications.api.DialogV2.wait({
    window: { title: "Range Defense" },
    classes: ["lethalfantasy"],
    content,
    buttons: [
      {
        action: "rangeDefense",
        type: "button",
        label: label,
        callback: (event, button) => {
          const output = Array.from(button.form.elements).reduce((obj, input) => {
            if (input.name) obj[input.name] = input.value
            return obj
          }, {})
          return output
        },
      },
    ],
    rejectClose: false // Click on Close button will not launch an error
  })

  // If the user cancels the dialog, exit
  if (rollContext === null) return

  log("RollContext", rollContext)
  // Add disfavor/favor option if point blank range
  if (rollContext.range === "pointblank") {
    rollContext.movement = rollContext.movement.replace("kh", "")
    rollContext.movement = rollContext.movement.replace("kl", "")
    rollContext.movement += "kl" // Add the kl to the movement (disfavor for point blank range)
    rollContext.range = "0"
  }
  if (rollContext.range === "beyondskill") {
    rollContext.movement = rollContext.movement.replace("kh", "")
    rollContext.movement = rollContext.movement.replace("kl", "")
    rollContext.movement += "kh" // Add the kl to the movement (favor for point blank range)
    rollContext.range = "+11"
  }

  // Build the final modifier
  let fullModifier = Number(rollContext.moveDirection) +
    Number(rollContext.size) +
    Number(rollContext.range) +
    Number(rollContext?.attackerAim || 0)

  let modifierFormula
  if (fullModifier === 0) {
    modifierFormula = "0"
  } else {
    let modAbs = Math.abs(fullModifier)
    modifierFormula = `D${modAbs + 1} -1`
  }

  let rollData = { ...rollContext }
  // Merge rollContext object into options object
  options = { ...options, ...rollContext }
  options.rollName = "Ranged Defense"
  options.rollType = "weapon-defense"
  options.type = options.rollType         // Required: this.type reads options.type
  options.rollMode = rollContext.visibility  // Required: callers pass roll.options.rollMode to toMessage

  const rollBase = new this(rollContext.movement, options.data, rollData)
  const rollModifier = new Roll(modifierFormula, options.data, rollData)
  await rollModifier.evaluate()
  await rollBase.evaluate()
  let rollD30 = await new Roll("1D30").evaluate()
  options.D30result = rollD30.total
  options.D30message = D30Roll.getResult(rollD30.total, options.rollType, options.rollTarget?.weapon, { isRanged: true })

  let badResult = 0
  if (rollContext.movement.includes("kh")) {
    rollData.favor = "favor"
    badResult = Math.min(rollBase.terms[0].results[0].result, rollBase.terms[0].results[1]?.result || 20)
  }
  if (rollContext.movement.includes("kl")) {
    rollData.favor = "disfavor"
    badResult = Math.max(rollBase.terms[0].results[0].result, rollBase.terms[0].results[1]?.result || 1)
  }
  let dice = rollContext.movement
  let maxValue = 20 // As per latest changes (was : Number(dice.match(/\d+$/)[0])
  let rollTotal = -1
  let diceResults = []
  let resultType

  let diceResult = rollBase.dice[0].results[0].result
  diceResults.push({ dice: `${dice.toUpperCase()}`, value: diceResult })
  let diceSum = diceResult
  while (diceResult === maxValue) {
    let r = await new Roll(dice).evaluate()
    diceResult = r.dice[0].results[0].result
    diceResults.push({ dice: `${dice.toUpperCase()}-1`, value: diceResult - 1 })
    diceSum += (diceResult - 1)
    rollBase.dice[0].results.push({ result: diceResult, active: true })
  }
  if (fullModifier !== 0) {
    diceResults.push({ dice: `${rollModifier.formula.toUpperCase()}`, value: rollModifier.total })
    if (fullModifier < 0) {
      rollTotal = Math.max(diceSum - rollModifier.total, 0)
    } else {
      rollTotal = diceSum + rollModifier.total
    }
  } else {
    rollTotal = diceSum
  }
  rollBase.options = { ...rollBase.options, ...options }
  rollBase.options.resultType = resultType
  rollBase.options.rollTotal = rollTotal
  rollBase.options.diceResults = diceResults
  rollBase.options.rollTarget = options.rollTarget
  rollBase.options.titleFormula = `1D20E + ${modifierFormula}`
  rollBase.options.D30result = options.D30result
  rollBase.options.D30message = options.D30message
  rollBase.options.rollName = "Ranged Defense"
  rollBase.options.badResult = badResult
  rollBase.options.rollData = foundry.utils.duplicate(rollData)

  return rollBase
}

/**
 * Prompts the GM for ranged attack context (movement, range, target size, aim) when a monster
 * attacks with a ranged weapon, then evaluates an exploding D20 attack roll with the resulting modifiers.
 *
 * @param {Object} options              Options for the roll.
 * @param {string} options.actorId      The attacker actor ID.
 * @param {string} options.actorName    The attacker actor name.
 * @param {Object} options.rollTarget   The rollTarget containing attackModifier and related data.
 * @returns {Promise<LethalFantasyRoll|null>}  The resulting roll, or null if cancelled.
 */
export async function promptRangedAttack(options = {}) {
  const rollModes = foundry.utils.duplicate(CONFIG.ChatMessage.modes)
  const fieldRollMode = new foundry.data.fields.StringField({
    choices: rollModes,
    blank: false,
    default: "public",
  })

  let dialogContext = {
    attackerMovementChoices: SYSTEM.ATTACKER_MOVEMENT_CHOICES,
    rangeChoices: SYSTEM.RANGE_CHOICES,
    sizeChoices: SYSTEM.SIZE_CHOICES,
    attackerAimChoices: SYSTEM.ATTACKER_AIM_CHOICES,
    movement: "none",
    range: "short",
    size: "+5",
    attackerAim: "simple",
    fieldRollMode,
    rollModes
  }

  const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/range-attack-dialog.hbs", dialogContext)

  const label = game.i18n.localize("LETHALFANTASY.Label.rangeAttackRoll")
  const rollContext = await foundry.applications.api.DialogV2.wait({
    window: { title: "Ranged Attack" },
    classes: ["lethalfantasy"],
    content,
    buttons: [
      {
        action: "rangedAttack",
        type: "button",
        label,
        callback: (event, button) => {
          const output = Array.from(button.form.elements).reduce((obj, input) => {
            if (input.name) obj[input.name] = input.value
            return obj
          }, {})
          return output
        },
      },
    ],
    rejectClose: false
  })

  if (rollContext === null) return null

  // Handle pointblank: attacker at point blank gets favor (standing still easier to aim)
  if (rollContext.range === "pointblank") {
    rollContext.movement = rollContext.movement.replace("kh", "")
    rollContext.movement = rollContext.movement.replace("kl", "")
    rollContext.movement += "kh" // Favor for attacker at point blank
    rollContext.range = "0"
  }
  // Handle beyondskill: extreme range gives disfavor to attacker
  if (rollContext.range === "beyondskill") {
    rollContext.movement = rollContext.movement.replace("kh", "")
    rollContext.movement = rollContext.movement.replace("kl", "")
    rollContext.movement += "kl" // Disfavor for attacker beyond skill range
    rollContext.range = "+11"
  }

  // Compute contextual penalty: range + target_size, reduced by aim bonus and attack modifier
  const attackModifier = options.rollTarget?.attackModifier ?? 0
  const contextualPenalty = Number(rollContext.range) + Number(rollContext.size)
  const aimBonus = Number(rollContext.attackerAim || 0)
  const fullModifier = contextualPenalty - aimBonus - attackModifier

  let modifierFormula
  if (fullModifier === 0) {
    modifierFormula = "0"
  } else {
    const modAbs = Math.abs(fullModifier)
    modifierFormula = `D${modAbs + 1} -1`
  }

  const rollData = { ...rollContext }
  options = { ...options, ...rollContext }
  options.rollName = "Ranged Attack"
  options.rollType = options.rollType || "monster-attack"
  options.type = options.rollType  // Required: this.type reads options.type, used to build weaponDamageOptions in toHTML
  options.rollMode = rollContext.visibility   // Required: callers pass roll.options.rollMode to toMessage
  options.isRangedAttack = true

  const rollBase = new this(rollContext.movement, options.data, rollData)
  const rollModifier = new Roll(modifierFormula, options.data, rollData)
  await rollModifier.evaluate()
  await rollBase.evaluate()
  const rollD30 = await new Roll("1D30").evaluate()
  options.D30result = rollD30.total
  options.D30message = D30Roll.getResult(rollD30.total, options.rollType, undefined, { isRanged: true })

  // Determine favor from dice formula
  let badResult = 0
  if (rollContext.movement.includes("kh")) {
    rollData.favor = "favor"
    badResult = Math.min(rollBase.terms[0].results[0].result, rollBase.terms[0].results[1]?.result || 20)
  }
  if (rollContext.movement.includes("kl")) {
    rollData.favor = "disfavor"
    badResult = Math.max(rollBase.terms[0].results[0].result, rollBase.terms[0].results[1]?.result || 1)
  }

  const dice = rollContext.movement
  const maxValue = 20
  let rollTotal = -1
  let diceResults = []

  let diceResult = rollBase.dice[0].results[0].result
  diceResults.push({ dice: `${dice.toUpperCase()}`, value: diceResult })
  let diceSum = diceResult
  // Exploding dice
  while (diceResult === maxValue) {
    const r = await new Roll(dice).evaluate()
    diceResult = r.dice[0].results[0].result
    diceResults.push({ dice: `${dice.toUpperCase()}-1`, value: diceResult - 1 })
    diceSum += (diceResult - 1)
    rollBase.dice[0].results.push({ result: diceResult, active: true })
  }

  if (fullModifier !== 0) {
    diceResults.push({ dice: `${rollModifier.formula.toUpperCase()}`, value: rollModifier.total })
    if (fullModifier > 0) {
      // Net penalty: subtract from roll
      rollTotal = Math.max(diceSum - rollModifier.total, 0)
    } else {
      // Net bonus: add to roll
      rollTotal = diceSum + rollModifier.total
    }
  } else {
    rollTotal = diceSum
  }

  rollBase.options = { ...rollBase.options, ...options }
  rollBase.options.resultType = undefined
  rollBase.options.rollTotal = rollTotal
  rollBase.options.diceResults = diceResults
  rollBase.options.rollTarget = options.rollTarget
  rollBase.options.titleFormula = `1D20E + ${modifierFormula}`
  rollBase.options.D30result = options.D30result
  rollBase.options.D30message = options.D30message
  rollBase.options.rollName = "Ranged Attack"
  rollBase.options.badResult = badResult
  rollBase.options.rollData = foundry.utils.duplicate(rollData)

  return rollBase
}
