/**
 * Donjon & Cie - Systeme FoundryVTT
 *
 * Donjon & Cie est un jeu de role edite par John Doe.
 * Ce systeme FoundryVTT est une implementation independante et n'est pas
 * affilie a John Doe.
 *
 * @author    LeRatierBretonnien
 * @copyright 2025–2026 LeRatierBretonnien
 * @license   CC BY-NC-SA 4.0 – https://creativecommons.org/licenses/by-nc-sa/4.0/
 */

import { DonjonEtCieUtility } from "./donjon-et-cie-utility.mjs";
import { DONJON_ET_CIE } from "./donjon-et-cie-config.mjs";

export class DonjonEtCieRolls {
  static async #createChatCard(actor, template, context, { rolls = [] } = {}) {
    const content = await foundry.applications.handlebars.renderTemplate(template, context);
    const validRolls = rolls.filter((roll) => roll instanceof Roll);
    await ChatMessage.create({
      speaker: ChatMessage.getSpeaker({ actor }),
      user: game.user.id,
      content,
      rolls: validRolls
    });
  }

  static #selectKeptValue(values, mode, favorable = "low") {
    if (!values.length) return null;
    if (mode === "normal") return values[0];

    const selector = favorable === "low"
      ? (mode === "avantage" ? Math.min : Math.max)
      : (mode === "avantage" ? Math.max : Math.min);

    return selector(...values);
  }

  static #getModeLabel(mode) {
    if (mode === "avantage") return game.i18n.localize("DNC.UI.ModeAdvantage");
    if (mode === "desavantage") return game.i18n.localize("DNC.UI.ModeDisadvantage");
    if (mode === "normal") return game.i18n.localize("DNC.UI.ModeNormal");
    return null;
  }

  static #applyFavorMode(mode) {
    if (mode === "desavantage") return "normal";
    return "avantage";
  }

  static async #resolveFormulaRoll(formula, data = {}, { mode = "normal", favorable = "high" } = {}) {
    const rollCount = mode === "normal" ? 1 : 2;
    const rolls = await Promise.all(Array.from({ length: rollCount }, () => (new Roll(formula, data)).evaluate()));
    const values = rolls.map((roll) => roll.total);
    const kept = this.#selectKeptValue(values, mode, favorable);
    const keptIndex = Math.max(0, values.findIndex((value) => value === kept));
    const keptRoll = rolls[keptIndex] ?? rolls[0];

    return { rolls, values, kept, keptIndex, keptRoll, mode, formula: keptRoll.formula };
  }

  static async #resolveCharacteristic(actor, characteristicKey, { mode = "normal" } = {}) {
    const characteristic = actor.system.caracteristiques?.[characteristicKey];
    if (!characteristic) return null;

    const target = Number(characteristic.value ?? 0);
    const rollCount = mode === "normal" ? 1 : 2;
    const roll = await (new Roll(`${rollCount}d20`)).evaluate();
    const values = roll.dice[0]?.results?.map((result) => result.result) ?? [];
    const kept = this.#selectKeptValue(values, mode, "low");
    const success = kept <= target;

    return { characteristic, characteristicKey, target, values, kept, success, mode, roll, isNaturalOne: kept === 1, isNaturalTwenty: kept === 20 };
  }

  static async #resolveFavorBoost(actor, favorKey, mode = "normal") {
    if (!favorKey) return null;

    const label = DonjonEtCieUtility.getFavorLabel(favorKey);
    const path = `system.faveurs.${favorKey}.delta`;
    const before = Number(foundry.utils.getProperty(actor, path) ?? 0);
    if (!before) {
      ui.notifications.warn(game.i18n.format("DNC.Warn.NoFavorAvailable", { label }));
      return null;
    }

    const resolved = await this.#resolveFormulaRoll(`1d${before}`, {}, { favorable: "high" });
    const result = resolved.kept;
    const degraded = result <= 3;
    const after = degraded ? DonjonEtCieUtility.degradeUsageDie(before) : before;
    if (after !== before) {
      await actor.update({ [path]: after });
    }

    return {
      key: favorKey,
      label,
      before,
      after,
      result,
      degraded,
      stable: !degraded,
      effectiveMode: this.#applyFavorMode(mode),
      modeBefore: mode,
      modeAfter: this.#applyFavorMode(mode),
      rolls: resolved.rolls,
      note: degraded
        ? "Le coup de pouce reste anonyme : un collegue du departement a donne l'info utile."
        : "Le coup de pouce tient bon : nommez le collegue, ses trois traits et la relation pour le trombinoscope."
    };
  }

  static async useFavorService(actor, favorKey) {
    if (!favorKey) return null;

    const label = DonjonEtCieUtility.getFavorLabel(favorKey);
    const path = `system.faveurs.${favorKey}.delta`;
    const before = Number(foundry.utils.getProperty(actor, path) ?? 0);
    if (!before) {
      ui.notifications.warn(game.i18n.format("DNC.Warn.NoFavorAvailable", { label }));
      return null;
    }

    const after = DonjonEtCieUtility.degradeUsageDie(before);
    await actor.update({ [path]: after });

    await this.#createChatCard(actor, "systems/fvtt-donjon-et-cie/templates/chat/favor-card.hbs", {
      title: game.i18n.localize("DNC.Roll.Favor"),
      subtitle: label,
      kindLabel: "Service",
      before: DonjonEtCieUtility.formatUsageDie(before),
      after: DonjonEtCieUtility.formatUsageDie(after),
      autoSpent: true,
      note: "La faveur est brulee pour obtenir directement l'aide souhaitee, a la discretion du MJ."
    }, { rolls: [] });

    return { key: favorKey, label, before, after };
  }

  static async #ensureFocus(actor) {
    const focusDelta = Number(actor.system.magie?.focus?.delta ?? 0);
    const focusResult = Number(actor.system.magie?.focus?.resultat ?? 0);
    const focusSceneId = actor.system.magie?.focus?.sceneId ?? "";
    const currentSceneId = DonjonEtCieUtility.getCurrentSceneId();
    const sameScene = focusSceneId === currentSceneId;
    const activeFocus = sameScene ? focusResult : 0;

    if (!focusDelta) {
      return { delta: 0, activeValue: 0, rolled: false, before: 0, after: 0, degraded: false };
    }

    if (sameScene) {
      return { delta: focusDelta, activeValue: activeFocus, rolled: false, before: focusDelta, after: focusDelta, degraded: false };
    }

    const resolved = await this.#resolveFormulaRoll(`1d${focusDelta}`, {}, { favorable: "high" });
    const result = resolved.kept;
    const degraded = result <= 3;
    const after = degraded ? DonjonEtCieUtility.degradeUsageDie(focusDelta) : focusDelta;
    const updateData = {
      "system.magie.focus.resultat": result,
      "system.magie.focus.sceneId": currentSceneId
    };

    if (after !== focusDelta) {
      updateData["system.magie.focus.delta"] = after;
    }

    await actor.update(updateData);

    return {
      delta: after,
      activeValue: result,
      rolled: true,
      before: focusDelta,
      after,
      degraded,
      rolls: resolved.rolls,
      values: resolved.values
    };
  }

  static async rollCharacteristic(actor, characteristicKey, { mode = "normal", label = null, favorKey = "" } = {}) {
    const favor = await this.#resolveFavorBoost(actor, favorKey, mode);
    const effectiveMode = favor?.effectiveMode ?? mode;
    const result = await this.#resolveCharacteristic(actor, characteristicKey, { mode: effectiveMode });
    if (!result) return null;

    await this.#createChatCard(actor, "systems/fvtt-donjon-et-cie/templates/chat/roll-card.hbs", {
      title: label ?? game.i18n.localize("DNC.Roll.Characteristic"),
      subtitle: result.characteristic.label,
      formula: result.values.length > 1 ? "2d20" : "1d20",
      mode: effectiveMode,
      modeLabel: this.#getModeLabel(effectiveMode),
      target: result.target,
      targetPillLabel: game.i18n.localize("DNC.Chat.Target"),
      targetPillValue: result.target,
      values: result.values,
      kept: result.kept,
      keptPillLabel: game.i18n.localize("DNC.Chat.Kept"),
      keptPillValue: result.kept,
      success: result.success,
      favorLabel: favor?.label ?? null,
      favorNote: favor?.note ?? null,
      details: [
        { label: game.i18n.localize("DNC.UI.Characteristic"), value: result.characteristic.label },
        { label: game.i18n.localize("DNC.Chat.TargetValue"), value: result.target },
        ...(favor ? [
          { label: game.i18n.localize("DNC.Chat.Favor"), value: favor.label },
          { label: game.i18n.localize("DNC.Chat.FavorDie"), value: favor.result },
          { label: game.i18n.localize("DNC.Chat.Before"), value: DonjonEtCieUtility.formatUsageDie(favor.before) },
          { label: game.i18n.localize("DNC.Chat.After"), value: DonjonEtCieUtility.formatUsageDie(favor.after) }
        ] : [])
      ]
    }, { rolls: [...(favor?.rolls ?? []), result.roll] });

    return { ...result, favor, mode: effectiveMode };
  }

  static async rollInitiative(actor, { mode = "normal" } = {}) {
    const dex = Number(actor.system.caracteristiques?.dexterite?.value ?? 0);
    const sheetBonus = Number(actor.system.combat?.initiativeBonus ?? 0);
    const result = await this.#resolveFormulaRoll("1d20 + @dex + @sheetBonus", { dex, sheetBonus }, { mode, favorable: "high" });
    const dieValues = result.rolls.map((roll) => roll.dice[0]?.results?.[0]?.result ?? roll.total);
    const die = dieValues[result.keptIndex] ?? dieValues[0] ?? result.kept;

    let syncedCombat = null;
    const activeCombat = game.combats?.contents?.find((combat) => combat.active);
    const combatant = activeCombat?.combatants?.find((entry) => entry.actorId === actor.id);
    if (combatant) {
      await activeCombat.setInitiative(combatant.id, result.kept);
      const ordered = [...activeCombat.combatants].sort((a, b) => (b.initiative ?? -Infinity) - (a.initiative ?? -Infinity));
      syncedCombat = {
        name: activeCombat.name,
        initiative: result.kept,
        rank: ordered.findIndex((entry) => entry.id === combatant.id) + 1,
        total: ordered.length
      };
    }

    await this.#createChatCard(actor, "systems/fvtt-donjon-et-cie/templates/chat/initiative-card.hbs", {
      title: game.i18n.localize("DNC.Roll.Initiative"),
      actorName: actor.name,
      total: result.kept,
      formula: result.rolls.length > 1 ? `2 × ${result.formula}` : result.formula,
      die,
      dieValues,
      dex,
      bonus: sheetBonus,
      mode: result.mode,
      modeLabel: this.#getModeLabel(result.mode),
      syncedCombat
    }, { rolls: result.rolls });

    return { total: result.kept, die, dieValues, dex, bonus: sheetBonus, mode: result.mode, syncedCombat };
  }

  static async rollHitDice(actor) {
    const formula = String(actor.system.sante?.dv ?? "").trim();
    if (!formula) return null;

    let roll;
    try {
      roll = await (new Roll(formula)).evaluate();
    } catch (error) {
      ui.notifications.error(`Formule de DV invalide : ${formula}`);
      throw error;
    }

    const dieValues = roll.dice.flatMap((die) => die.results?.map((result) => result.result) ?? []);

    await this.#createChatCard(actor, "systems/fvtt-donjon-et-cie/templates/chat/hit-dice-card.hbs", {
      title: game.i18n.localize("DNC.Roll.HitDice"),
      actorName: actor.name,
      formula: roll.formula,
      total: roll.total,
      dieValues
    }, { rolls: [roll] });

    return { formula: roll.formula, total: roll.total, dieValues };
  }

  static async rollWeapon(actor, item, { mode = "normal", favorKey = "" } = {}) {
    const characteristicKey = DonjonEtCieUtility.getWeaponCharacteristicKey(item.system.categorie);
    const favor = await this.#resolveFavorBoost(actor, favorKey, mode);
    const effectiveMode = favor?.effectiveMode ?? mode;
    const result = await this.#resolveCharacteristic(actor, characteristicKey, { mode: effectiveMode });

    if (!result) return null;

    const characteristicLabel = DONJON_ET_CIE.characteristics[characteristicKey]?.label ?? characteristicKey;
    const characteristicShort = DONJON_ET_CIE.characteristics[characteristicKey]?.short ?? characteristicKey;

    await this.#createChatCard(actor, "systems/fvtt-donjon-et-cie/templates/chat/roll-card.hbs", {
      title: `${game.i18n.localize("DNC.Roll.Attack")} : ${item.name}`,
      subtitle: DONJON_ET_CIE.weaponCategoryOptions[item.system.categorie] ?? item.system.categorie,
      formula: result.values.length > 1 ? "2d20" : "1d20",
      mode: effectiveMode,
      modeLabel: this.#getModeLabel(effectiveMode),
      target: result.target,
      targetPillLabel: characteristicShort,
      targetPillValue: result.target,
      values: result.values,
      kept: result.kept,
      keptPillLabel: game.i18n.localize("DNC.Chat.RollValue"),
      keptPillValue: result.kept,
      success: result.success,
      favorLabel: favor?.label ?? null,
      favorNote: favor?.note ?? null,
      showDamageButton: result.success && Boolean(item.system.degats),
      showAmmoButton: Number(item.system.munitionsDelta ?? 0) > 0,
      itemUuid: item.uuid,
      details: [
        { label: game.i18n.localize("DNC.UI.Weapon"), value: item.name },
        { label: game.i18n.localize("DNC.UI.Characteristic"), value: characteristicLabel },
        { label: `Valeur de ${characteristicLabel}`, value: result.target },
        { label: game.i18n.localize("DNC.UI.Damage"), value: item.system.degats || "—" },
        { label: game.i18n.localize("DNC.UI.Ammunition"), value: Number(item.system.munitionsDelta ?? 0) > 0 ? DonjonEtCieUtility.formatUsageDie(item.system.munitionsDelta) : "—" },
        { label: game.i18n.localize("DNC.UI.Range"), value: item.system.portee || "—" },
        ...(favor ? [
          { label: game.i18n.localize("DNC.Chat.Favor"), value: favor.label },
          { label: game.i18n.localize("DNC.Chat.FavorDie"), value: favor.result },
          { label: game.i18n.localize("DNC.Chat.Before"), value: DonjonEtCieUtility.formatUsageDie(favor.before) },
          { label: game.i18n.localize("DNC.Chat.After"), value: DonjonEtCieUtility.formatUsageDie(favor.after) }
        ] : [])
      ]
    }, { rolls: [...(favor?.rolls ?? []), result.roll] });

    return { ...result, favor, mode: effectiveMode };
  }

  static async rollWeaponAmmoUsage(item, { mode = "normal" } = {}) {
    const before = Number(item?.system?.munitionsDelta ?? 0);
    if (!before) {
      ui.notifications.warn(game.i18n.localize("DNC.Warn.NoAmmunitionAvailable"));
      return null;
    }

    const resolved = await this.#resolveFormulaRoll(`1d${before}`, {}, { mode, favorable: "high" });
    const result = resolved.kept;
    const degraded = result <= 3;
    const after = degraded ? DonjonEtCieUtility.degradeUsageDie(before) : before;

    if (after !== before) {
      await item.update({ "system.munitionsDelta": after });
    }

    await this.#createChatCard(item.actor, "systems/fvtt-donjon-et-cie/templates/chat/usage-card.hbs", {
      title: `${game.i18n.localize("DNC.Roll.Ammunition")} : ${item.name}`,
      value: result,
      values: resolved.values,
      mode: resolved.mode,
      modeLabel: this.#getModeLabel(resolved.mode),
      before: DonjonEtCieUtility.formatUsageDie(before),
      after: DonjonEtCieUtility.formatUsageDie(after),
      protectionStored: null,
      degraded,
      exhausted: after === 0,
      isAmmunition: true
    }, { rolls: resolved.rolls });

    return { result, values: resolved.values, mode: resolved.mode, before, after, degraded };
  }

  static async rollDamage(actor, item, { mode = "normal" } = {}) {
    const isUsageDie = Boolean(item.system.degatsEstUsageDe);
    const degatsDelta = Number(item.system.degatsDelta ?? 0);

    if (isUsageDie && !degatsDelta) {
      ui.notifications.warn(game.i18n.localize("DNC.Warn.DamageExhausted"));
      return null;
    }
    if (!isUsageDie && !item.system.degats) return null;

    // Arme à 2 mains de corps à corps : avantage automatique
    const isMeleeTwoHanded = item.type === "arme" && item.system?.categorie === "melee" && Number(item.system?.mains ?? 1) > 1;
    if (isMeleeTwoHanded && mode === "normal") {
      mode = "avantage";
    }

    const damageContext = DonjonEtCieUtility.getMartialDamageContext(actor, item);
    const effectiveDamage = damageContext.effectiveFormula || (isUsageDie ? `1d${degatsDelta}` : item.system.degats);
    const formula = effectiveDamage;
    const result = await this.#resolveFormulaRoll(formula, {}, { mode, favorable: "high" });
    const targets = DonjonEtCieUtility.getSceneDamageTargets();
    const rollDieLabels = result.rolls.map((roll) => {
      const dieValues = roll.dice.flatMap((die) => die.results?.map((dieResult) => dieResult.result) ?? []);
      return dieValues.length ? dieValues.join(" + ") : String(roll.total ?? "—");
    });
    const keptDieLabel = rollDieLabels[result.keptIndex] ?? rollDieLabels[0] ?? String(result.kept);
    const baseDamageDisplay = isUsageDie ? DonjonEtCieUtility.formatUsageDie(degatsDelta) : item.system.degats;

    await this.#createChatCard(actor ?? item.actor, "systems/fvtt-donjon-et-cie/templates/chat/damage-card.hbs", {
      title: `${game.i18n.localize("DNC.Roll.Damage")} : ${item.name}`,
      subtitle: item.system.portee || item.type,
      formula: result.rolls.length > 1 ? `2 × ${result.formula}` : result.formula,
      mode: result.mode,
      modeLabel: this.#getModeLabel(result.mode),
      rollDieLabels,
      keptDieLabel,
      values: result.values,
      total: result.kept,
      baseDamage: baseDamageDisplay,
      effectiveDamage,
      damageCapped: damageContext.capped,
      martialDvLabel: damageContext.martialDvSides ? `d${damageContext.martialDvSides}` : damageContext.martialDvFormula,
      sourceLabel: item.name,
      targets,
      hasTargets: targets.length > 0,
      showDamageUsageButton: isUsageDie && degatsDelta > 0,
      itemUuid: item.uuid
    }, { rolls: result.rolls });

    return {
      total: result.kept,
      formula: result.formula,
      baseDamage: baseDamageDisplay,
      effectiveDamage,
      damageCapped: damageContext.capped,
      values: result.values,
      mode: result.mode
    };
  }

  static async rollWeaponDamageUsage(item, { mode = "normal" } = {}) {
    const before = Number(item?.system?.degatsDelta ?? 0);
    if (!before) {
      ui.notifications.warn(game.i18n.localize("DNC.Warn.DamageExhausted"));
      return null;
    }

    // Arme à 2 mains de corps à corps : avantage automatique
    const isMeleeTwoHanded = item.type === "arme" && item.system?.categorie === "melee" && Number(item.system?.mains ?? 1) > 1;
    if (isMeleeTwoHanded && mode === "normal") {
      mode = "avantage";
    }

    const resolved = await this.#resolveFormulaRoll(`1d${before}`, {}, { mode, favorable: "high" });
    const result = resolved.kept;
    const degraded = result <= 3;
    const after = degraded ? DonjonEtCieUtility.degradeUsageDie(before) : before;

    if (after !== before) {
      await item.update({ "system.degatsDelta": after });
    }

    await this.#createChatCard(item.actor, "systems/fvtt-donjon-et-cie/templates/chat/usage-card.hbs", {
      title: `${game.i18n.localize("DNC.Roll.DamageUsage")} : ${item.name}`,
      value: result,
      values: resolved.values,
      mode: resolved.mode,
      modeLabel: this.#getModeLabel(resolved.mode),
      before: DonjonEtCieUtility.formatUsageDie(before),
      after: DonjonEtCieUtility.formatUsageDie(after),
      protectionStored: null,
      degraded,
      exhausted: after === 0,
      isDamageUsage: true
    }, { rolls: resolved.rolls });

    return { result, values: resolved.values, mode: resolved.mode, before, after, degraded };
  }

  static async applyDamage(target, { damage = 0, useArmor = false, sourceLabel = "" } = {}) {
    const actor = target?.actor ?? target;
    if (!actor || actor.documentName !== "Actor") {
      ui.notifications.warn(game.i18n.localize("DNC.Chat.InvalidDamageTarget"));
      return null;
    }

    const targetName = target?.name ?? actor.name;
    const applied = await actor.applyIncomingDamage(damage, { useArmor });

    await this.#createChatCard(actor, "systems/fvtt-donjon-et-cie/templates/chat/damage-application-card.hbs", {
      title: game.i18n.localize("DNC.Chat.DamageApplied"),
      subtitle: targetName,
      sourceLabel,
      total: applied.hpDamage,
      incoming: applied.incoming,
      useArmor: applied.useArmor,
      armorLabel: applied.armorLabel,
      armorAvailable: applied.armorAvailable,
      armorBefore: applied.armorBefore,
      armorAbsorbed: applied.armorAbsorbed,
      armorAfter: applied.armorAfter,
      pvBefore: applied.pvBefore,
      pvAfter: applied.pvAfter,
      pvMax: applied.pvMax
    });

    return { actor, targetName, ...applied };
  }

  static async rollSpell(actor, item, { mode = "normal", favorKey = "" } = {}) {
    const characteristicKey = item.system.caracteristique || "intelligence";
    const focus = await this.#ensureFocus(actor);
    const currentSceneId = DonjonEtCieUtility.getCurrentSceneId();
    const rank = Number(actor.system.anciennete?.rang ?? actor.system.sante?.dv ?? 0);
    const cost = Number(item.system.coutPv ?? 0);
    const autoDisadvantage = cost > rank;
    const baseMode = autoDisadvantage ? "desavantage" : mode;
    const favor = await this.#resolveFavorBoost(actor, favorKey, baseMode);
    const effectiveMode = favor?.effectiveMode ?? baseMode;
    const result = await this.#resolveCharacteristic(actor, characteristicKey, { mode: effectiveMode });

    if (!result) return null;

    const liveActor = game.actors.get(actor.id) ?? actor;
    const currentPv = Number(liveActor.system.sante?.pv?.value ?? 0);
    const currentFocusSceneId = liveActor.system.magie?.focus?.sceneId ?? "";
    const currentFocusValue = currentFocusSceneId === currentSceneId
      ? Number(liveActor.system.magie?.focus?.resultat ?? 0)
      : 0;
    const availableMagicHp = currentPv + currentFocusValue;

    if (cost > availableMagicHp) {
      ui.notifications.warn(game.i18n.localize("DNC.Warn.SpellInsufficientResources"));
      return null;
    }

    const characteristicShort = DONJON_ET_CIE.characteristics[characteristicKey]?.short ?? characteristicKey;
    const success = result.isNaturalTwenty ? false : result.success;
    const focusSpent = result.isNaturalOne ? 0 : Math.min(cost, currentFocusValue);
    const focusRemaining = Math.max(currentFocusValue - focusSpent, 0);
    const spentPv = result.isNaturalOne ? 0 : Math.max(cost - focusSpent, 0);
    const remainingPv = Math.max(currentPv - spentPv, 0);
    const updateData = {};

    if (spentPv !== 0) {
      updateData["system.sante.pv.value"] = remainingPv;
    }

    if (focusSpent !== 0) {
      updateData["system.magie.focus.resultat"] = focusRemaining;
    }

    if (Object.keys(updateData).length) {
      await actor.update(updateData);
    }

    const canInvokeChaos = !success && !result.isNaturalTwenty && Number(actor.system.magie?.chaos?.delta ?? 12) >= 4;
    const specialNote = result.isNaturalTwenty
      ? "20 naturel : la magie tourne a la catastrophe, au choix du MJ."
      : (result.isNaturalOne ? "1 naturel : effet benefique possible ; par defaut, aucun PV n'est depense." : null);

    await this.#createChatCard(actor, "systems/fvtt-donjon-et-cie/templates/chat/spell-card.hbs", {
      title: `${game.i18n.localize("DNC.Roll.Spell")} : ${item.name}`,
      subtitle: item.system.portee || "Sortilege",
      formula: result.values.length > 1 ? "2d20" : "1d20",
      mode: effectiveMode,
      modeLabel: this.#getModeLabel(effectiveMode),
      autoDisadvantage,
      autoDisadvantageCanceled: autoDisadvantage && Boolean(favor),
      favorLabel: favor?.label ?? null,
      favorNote: favor?.note ?? null,
      targetPillLabel: characteristicShort,
      targetPillValue: result.target,
      values: result.values,
      kept: result.kept,
      keptPillLabel: game.i18n.localize("DNC.Chat.RollValue"),
      keptPillValue: result.kept,
      success,
      specialNote,
      showDamageButton: success && Boolean(item.system.degats),
      showChaosButton: canInvokeChaos,
      itemUuid: item.uuid,
      actorUuid: actor.uuid,
      details: [
        { label: game.i18n.localize("DNC.UI.Spell"), value: item.name },
        { label: game.i18n.localize("DNC.UI.Characteristic"), value: result.characteristic.label },
        { label: game.i18n.localize("DNC.Chat.CharacteristicValue"), value: result.target },
        { label: game.i18n.localize("DNC.Chat.HpCost"), value: cost },
        { label: game.i18n.localize("DNC.UI.Focus"), value: focus.activeValue > 0 ? `${focus.activeValue} (${DonjonEtCieUtility.formatUsageDie(focus.before)})` : "—" },
        { label: game.i18n.localize("DNC.Chat.FocusSpent"), value: focusSpent },
        { label: game.i18n.localize("DNC.Chat.FocusRemaining"), value: focusRemaining },
        { label: game.i18n.localize("DNC.Chat.HpSpent"), value: spentPv },
        { label: game.i18n.localize("DNC.Chat.HpRemaining"), value: remainingPv },
        { label: game.i18n.localize("DNC.Chat.CasterRank"), value: rank },
        { label: game.i18n.localize("DNC.Chat.Difficulty"), value: item.system.difficulte ?? 0 },
        { label: game.i18n.localize("DNC.Chat.Effect"), value: item.system.effet || "—" },
        ...(favor ? [
          { label: game.i18n.localize("DNC.Chat.Favor"), value: favor.label },
          { label: game.i18n.localize("DNC.Chat.FavorDie"), value: favor.result },
          { label: game.i18n.localize("DNC.Chat.Before"), value: DonjonEtCieUtility.formatUsageDie(favor.before) },
          { label: game.i18n.localize("DNC.Chat.After"), value: DonjonEtCieUtility.formatUsageDie(favor.after) }
        ] : [])
      ],
      focusRolled: focus.rolled,
      focusValue: focus.activeValue,
      focusSpent,
      focusRemaining,
      focusBeforeLabel: DonjonEtCieUtility.formatUsageDie(focus.before),
      focusAfterLabel: DonjonEtCieUtility.formatUsageDie(focus.after),
      focusDegraded: focus.degraded,
      spentPv,
      remainingPv
    }, { rolls: [...(favor?.rolls ?? []), ...(focus.rolls ?? []), result.roll] });

    return { ...result, success, spentPv, remainingPv, cost, focus, focusSpent, focusRemaining, favor, mode: effectiveMode };
  }

  static async rollSpellChaos(actor, item) {
    const before = Number(actor?.system?.magie?.chaos?.delta ?? 12);
    if (!before || before < 4) {
      ui.notifications.warn(game.i18n.localize("DNC.Warn.ChaosUnavailable"));
      return null;
    }

    const resolved = await this.#resolveFormulaRoll(`1d${before}`, {}, { favorable: "high" });
    const result = resolved.kept;
    const degraded = result <= 3;
    const after = degraded ? DonjonEtCieUtility.degradeUsageDie(before) : before;
    const chaosEntry = DONJON_ET_CIE.chaosTable[result] ?? null;

    if (after !== before) {
      await actor.update({ "system.magie.chaos.delta": after });
    }

    await this.#createChatCard(actor, "systems/fvtt-donjon-et-cie/templates/chat/chaos-card.hbs", {
      title: `Chaos : ${item.name}`,
      value: result,
      before: DonjonEtCieUtility.formatUsageDie(before),
      after: DonjonEtCieUtility.formatUsageDie(after),
      chaosEntry,
      degraded,
      exhausted: after < 4,
      itemName: item.name
    }, { rolls: resolved.rolls });

    return { result, before, after, degraded, chaosEntry };
  }

  static async rollUsage(item, { mode = "normal" } = {}) {
    const before = Number(item.system.delta ?? 0);
    if (!before) return null;

    const resolved = await this.#resolveFormulaRoll(`1d${before}`, {}, { mode, favorable: "high" });
    const result = resolved.kept;
    const degraded = result <= 3;
    const after = degraded ? DonjonEtCieUtility.degradeUsageDie(before) : before;
    const updateData = {};

    if (item.type === "armure") {
      updateData["system.resultatProtection"] = result;
    }

    if (after !== before) {
      updateData["system.delta"] = after;
    }

    if (Object.keys(updateData).length) {
      await item.update(updateData);
    }

    await this.#createChatCard(item.actor, "systems/fvtt-donjon-et-cie/templates/chat/usage-card.hbs", {
      title: `${game.i18n.localize("DNC.Roll.Usage")} : ${item.name}`,
      value: result,
      values: resolved.values,
      mode: resolved.mode,
      modeLabel: this.#getModeLabel(resolved.mode),
      before: DonjonEtCieUtility.formatUsageDie(before),
      after: DonjonEtCieUtility.formatUsageDie(after),
      protectionStored: item.type === "armure" ? result : null,
      degraded,
      exhausted: after === 0
    }, { rolls: resolved.rolls });

    return { result, values: resolved.values, mode: resolved.mode, before, after, degraded };
  }
}
