import { VermineTotemDice } from "./totem-dice.mjs";

export class VermineUtils {
  /**
   * Rolls dice with Vermine2047-specific rules.
   * @param {Object} options - Roll options
   * @param {Actor} options.actor - The actor rolling
   * @param {number} options.NoD - Base dice pool
   * @param {number} [options.Reroll=0] - Reroll count
   * @param {number} [options.difficulty=7] - Difficulty threshold
   * @param {number} [options.self_control=0] - Self control used
   * @param {string} [options.rollLabel="jet custom"] - Roll label
   * @param {Object} [options.totems={}] - Totems used {human: boolean, adapted: boolean}
   * @param {number} [options.max_effort=0] - Max effort
   * @param {string} [options.skillCategory=null] - Skill category for domain bonuses
   * @param {string} [options.keepTotem=null] - Totem to keep ('human' or 'adapted')
   * @param {number} [options.skillLevel=null] - Skill level for auto-successes
   * @param {boolean} [options.hasSpecialty=false] - Whether a specialty is used
   * @param {number} [options.effectiveRange=null] - Portée effective (Trait Portée)
   * @param {number} [options.mobilityHandicap=0] - Handicap de Mobilité (note)
   * @param {number} [options.ponctuelUsed=0] - Handicaps annulés (Trait Ponctuel)
   * @param {Object|null} [options.intimidant=null] - Trait Intimidant utilisé
   * @returns {Promise<Roll>} The roll result
   */
  static async roll({ 
    actor, 
    NoD, 
    Reroll = 0, 
    difficulty = 7, 
    self_control = 0, 
    rollLabel = "jet custom", 
    totems = { human: false, adapted: false }, 
    max_effort = 0, 
    skillCategory = null, 
    keepTotem = null, 
    skillLevel = null, 
    hasSpecialty = false,
    handicap = 0,
    bonusSuccesses = 0,
    bonusOnSuccess = false,
    poolBreakdown = null,
    specialtyName = null,
    weapon = null,
    targets = [],
    attack = null,
    messageFlags = null,
    effectiveRange = null,
    mobilityHandicap = 0,
    ponctuelUsed = 0,
    intimidant = null
  }) {
    // Validate inputs
    if (!actor) {
      throw new Error("Actor is required for rolling");
    }

    // Sanitize user name for use in dice flavor
    const safeUserName = (game.user?.name ?? "user").replace(/[^a-zA-Z0-9_]/g, '_');

    // Declare variables
    let formula = "";
    let modFormula = null;

    // Influence des Dés de Totems : Bonus/Malus de 1D selon le Totem dominant
    // et le Domaine de la Compétence (règles p. 121). S'applique même sans
    // utiliser un Dé de Totem.
    const influenceMod = this._calculateTotemInfluence(skillCategory, actor);
    NoD += influenceMod;

    // Apply automatic successes from skill mastery
    let adjustedDifficulty = difficulty;

    // Handle human totem
    if (totems.human) {
      NoD--;
      const humanDifficulty = adjustedDifficulty;
      const humanFormula = `(1D10cs>=${humanDifficulty}[human_${safeUserName}]*2)`;

      modFormula = humanFormula;
    }

    // Handle adapted totem
    if (totems.adapted) {
      NoD--;
      const adaptedDifficulty = adjustedDifficulty;
      const adaptedFormula = `(1D10cs>=${adaptedDifficulty}[adapted_${safeUserName}]*2)`;

      // Build combined formula
      if (modFormula !== null) {
        modFormula = `${modFormula}+${adaptedFormula}`;
      } else {
        modFormula = adaptedFormula;
      }
    }

    // Handle keepTotem selection (if both totems are active)
    if (totems.human && totems.adapted && keepTotem) {
      if (keepTotem === 'human' && totems.adapted) {
        modFormula = `(1D10cs>=${adjustedDifficulty}[human_${safeUserName}]*2)`;
        NoD++; // Cancel the decrement for adapted
      } else if (keepTotem === 'adapted' && totems.human) {
        modFormula = `(1D10cs>=${adjustedDifficulty}[adapted_${safeUserName}]*2)`;
        NoD++;       // Cancel the decrement for human
      }
    }

    // Ensure the dice pool cannot go below zero
    NoD = Math.max(0, NoD)

    // Build base formula
    const baseFormula = `${NoD}d10cs>=${adjustedDifficulty}[regular_${safeUserName}]`;

    // Build final formula
    formula = modFormula !== null ? `${baseFormula}+${modFormula}` : baseFormula;

    // Reconcile the breakdown displayed in chat with the dice actually rolled.
    // L'influence s'applique au pool ; les Dés de Totem remplacent (n'ajoutent
    // pas) des dés du pool de base.
    if (poolBreakdown) {
      // Count totem dice present in the final modifier formula.
      let totemDice = 0;
      if (modFormula) {
        if (modFormula.includes("human_")) totemDice += 1;
        if (modFormula.includes("adapted_")) totemDice += 1;
      }
      poolBreakdown.totemDice = totemDice;
      poolBreakdown.domainBonus = influenceMod;
      // Total = nombre réel de dés lancés (NoD réguliers + dés de totem).
      poolBreakdown.total = NoD + totemDice;
    }

    // Create the roll
    const roll = new Roll(formula, actor.getRollData());
    
    // Store metadata for display
    roll.vermineData = {
      totemsUsed: { ...totems },
      keepTotem: keepTotem,
      difficulty: adjustedDifficulty,
      originalDifficulty: difficulty,
      skillCategory: skillCategory,
      skillLevel: skillLevel,
      hasSpecialty: hasSpecialty,
      totemInfluence: influenceMod,
      baseNoD: NoD,
      rerolls: Reroll,
      selfControl: self_control,
      bonusSuccesses: bonusSuccesses
    };
    // Evaluate the roll
    await roll.evaluate();
    
    // Show 3D dice if available
    await VermineUtils.showDiceSoNice(roll);
    
    // Display result in chat
    VermineUtils.diplayChatRoll(roll, { 
      actor, 
      NoD, 
      Reroll, 
      difficulty, 
      self_control, 
      rollLabel, 
      totems, 
      max_effort, 
      skillCategory, 
      keepTotem, 
      skillLevel, 
      hasSpecialty, 
      handicap,
      bonusSuccesses,
      bonusOnSuccess,
      poolBreakdown,
      specialtyName,
      weapon,
      targets,
      attack,
      messageFlags,
      effectiveRange,
      mobilityHandicap,
      ponctuelUsed,
      intimidant
    });
    
    return roll;
  }
  
  /**
   * Calculates domain bonuses/penalties for totems.
   * @param {string} skillCategory - The skill category
   * @param {Actor} actor - The actor
   * @returns {number} Modificateur d'influence (-1, 0 ou +1)
   */
  static _calculateTotemInfluence(skillCategory, actor) {
    // Les Dés de Totems n'existent que sur les personnages.
    if (actor?.type !== "character" || !skillCategory) return 0;
    return VermineTotemDice.influenceModifier(actor, skillCategory);
  }
  
  /**
   * Handles reroll events on dice in chat messages.
   * @param {Object} message - The chat message containing the reroll event
   * @param {HTMLElement} target - The clicked die element
   * @returns {Promise<boolean>} Whether the reroll was successful
   */
  static async onReroll(message, target) {
    // Verify user permissions. Foundry v14 renamed the ChatMessage field
    // `user` to `author`, so `message.user` is undefined on this core.
    const msgAuthorId = message.author?.id ?? message.user?.id ?? message.user;
    if (msgAuthorId !== game.user?.id && !game.user?.isGM) {
      ui.notifications.warn(game.i18n.localize('VERMINE.error_cannot_reroll'));
      return false;
    }

    // Get reroll count
    const rollMessage = target.closest('div.vermine-roll-message');
    if (!rollMessage) {
      return false;
    }

    let rerollCount = rollMessage.querySelector('#allowed_reroll')?.innerText;
    
    // Check if rerolls are available
    if (!rerollCount || parseInt(rerollCount, 10) < 1) {
      ui.notifications.warn(game.i18n.localize('VERMINE.error_no_rerolls_left'));
      const rerollables = target.closest('ul')?.querySelectorAll('.rerollable');
      if (rerollables) {
        rerollables.forEach(el => el.classList.remove('rerollable'));
      }
      return false;
    }

    target.classList.add('rerolled');

    // Get difficulty and dice type
    const ulElement = target.closest('ul');
    const difficulty = ulElement?.dataset.difficulty ?? 7;
    let diceType = target.dataset.diceType;

    // Sanitize user name
    const safeUserName = (game.user?.name ?? "user").replace(/[^a-zA-Z0-9_]/g, '_');

    // Build reroll formula
    let formula = `1d10cs>=${difficulty}`;
    
    switch ((diceType ?? '').trim()) {
      case 'human':
        formula = `(1d10cs>=${difficulty}[human_${safeUserName}])*2`;
        break;
      case 'adapted':
        formula = `(1d10cs>=${difficulty}[adapted_${safeUserName}])*2`;
        break;
      default:
        formula += `[regular_${safeUserName}]`;
        break;
    }

    // Create and evaluate reroll
    const reroll = new Roll(formula);
    await reroll.evaluate();

    // Show 3D dice if available
    await VermineUtils.showDiceSoNice(reroll);

    // Update die display
    const result = reroll.dice[0]?.results[0]?.result ?? 0;
    const dieSpan = target.querySelector('span');
    if (dieSpan) {
      dieSpan.innerText = result;
    }
    // Update the face used for the CSS background image.
    target.dataset.result = result;

    // Set/clear the success class from the rerolled die (a previous success
    // must be removed if the new result fails).
    const success = reroll.dice[0]?.results[0]?.success;
    target.classList.toggle('success', !!success);
    target.classList.remove("rerollable");

    // Recompute the total from the live dice in the DOM. This avoids the
    // cumulative-increment drift of the old logic (which never removed a
    // removed success from #total). Totem dice (human/adapted) count double.
    const totalEl = rollMessage.querySelector('#total');
    if (totalEl) {
      let total = 0;
      rollMessage.querySelectorAll('li.die.success').forEach(die => {
        const isTotem = die.classList.contains('human') || die.classList.contains('adapted');
        total += isTotem ? 2 : 1;
      });
      // Réussites automatiques (ex. bonus de Réaction) : ajoutées au total.
      const freeSuccesses = parseInt(rollMessage.dataset?.freeSuccesses ?? '0', 10) || 0;
      total += freeSuccesses;
      totalEl.innerText = total;
    }

    // Update message content
    const messageContent = target.closest('div.message-content');
    if (messageContent) {
      const newRerollCount = parseInt(rerollCount, 10) - 1;
      rollMessage.querySelector('#allowed_reroll').innerText = newRerollCount;

      // Recompute the verdict banner from the recomputed total.
      const verdictEl = rollMessage.querySelector('.roll-verdict');
      const requiredEl = rollMessage.querySelector('#required');
      if (verdictEl && totalEl && requiredEl) {
        const total = parseInt(totalEl.innerText, 10) || 0;
        const required = parseInt(requiredEl.innerText, 10) || 1;
        const won = total >= required;
        verdictEl.classList.toggle('success', won);
        verdictEl.classList.toggle('failure', !won);
        const label = won
          ? game.i18n.localize('VERMINE.roll_success')
          : game.i18n.localize('VERMINE.roll_failure');
        verdictEl.innerHTML = `${won ? '<i class="fas fa-check-circle"></i>' : '<i class="fas fa-times-circle"></i>'}<span>${label}</span>`;
      }

      // Recompute the dice summary from the current die list.
      const summaryEl = rollMessage.querySelector('.roll-dice-summary');
      if (summaryEl) {
        const stats = { regular: { count: 0, success: 0 }, human: { count: 0, success: 0 }, adapted: { count: 0, success: 0 } };
        rollMessage.querySelectorAll('li.die').forEach(die => {
          const type = die.classList.contains('human') ? 'human' : die.classList.contains('adapted') ? 'adapted' : 'regular';
          stats[type].count += 1;
          if (die.classList.contains('success')) stats[type].success += 1;
        });
        const mk = (key, count, s) => count ? `<span class="die-summary ${key}">${count} ${game.i18n.localize(`VERMINE.die_${key}`)} · ${s} ${game.i18n.localize('VERMINE.die_successes')}</span>` : '';
        summaryEl.innerHTML = mk('regular', stats.regular.count, stats.regular.success)
          + mk('human', stats.human.count, stats.human.success)
          + mk('adapted', stats.adapted.count, stats.adapted.success);
      }

      // Persist the reroll. The GM writes the message directly ; players
      // cannot update ChatMessages on Foundry v14 (permission "OWNER"), so
      // their reroll is relayed through the system socket and applied by the
      // GM client (see registerRerollSocket).
      if (game.user?.isGM) {
        await message.setFlag("world", "reroll", true);
        await message.update({
          content: messageContent.outerHTML
        });
      } else {
        game.socket?.emit(`system.${game.system.id}`, {
          type: "reroll",
          messageId: message.id,
          requestId: foundry.utils.randomID(8),
          content: messageContent.outerHTML
        });
      }
    }

    return true;
  }

  /**
   * Registers the system socket listener which applies rerolls requested by
   * players. Foundry v14 only allows GMs to update ChatMessages, so player
   * rerolls are relayed to the GM client through `system.<id>` (see onReroll).
   * Call once after the world is ready; re-registration replaces the handler.
   */
  static registerRerollSocket() {
    const eventName = `system.${game.system.id}`;
    const handler = async (payload, userId) => {
      if (!game.user?.isGM) return;
      if (payload?.type !== "reroll") return;
      const message = game.messages.get(payload.messageId);
      if (!message) return;
      // Security: only the message author may request the reroll update.
      if (message.author?.id !== userId) return;
      await message.setFlag("world", "reroll", true);
      await message.update({ content: payload.content });
    };
    if (VermineUtils.#rerollSocketHandler) {
      game.socket?.off(eventName, VermineUtils.#rerollSocketHandler);
    }
    VermineUtils.#rerollSocketHandler = handler;
    game.socket?.on(eventName, handler);
  }

  static #rerollSocketHandler;

  /**
   * Sets up event listeners for chat messages.
   * @param {HTMLElement} html - The HTML element containing chat events.
   */
  static async chatListenners(html) {
    // Get reroll count
    const rerollCountElement = html.querySelector('#allowed_reroll');
    const rerollCount = rerollCountElement?.innerText;

    // Enable/disable rerolls based on count
    const dieClass = !rerollCount || parseInt(rerollCount, 10) < 1 ? 'remove' : 'add';
    html.querySelectorAll('.die').forEach(el => el.classList[dieClass]("rerollable"));

    // Add click event for rerollable dice
    html.querySelectorAll('.rerollable').forEach(el => {
      el.addEventListener('click', async (ev) => {
        ev.preventDefault();
        const dieEl = ev.currentTarget;
        const msgId = dieEl.closest("li.message")?.dataset?.messageId;
        if (msgId) {
          const message = await game.messages.get(msgId);
          await VermineUtils.onReroll(message, dieEl);
        }
      });
    });

    // Update granted reroll label
    const effortReroll = html.querySelector("#effort-reroll");
    if (effortReroll) {
      effortReroll.addEventListener('change', ev => {
        const label = html.querySelector("#granted-reroll");
        if (label) {
          label.innerText = ev.currentTarget.value;
        }
      });
    }

    // Add click event for spending Effort points as rerolls (règles p.35) :
    // 1 point d'Effort = 1 relance, ajouté aux relances de Compétence restantes.
    html.querySelectorAll("button.grant-reroll").forEach(el => {
      el.addEventListener('click', async (ev) => {
        const btn = ev.currentTarget;
        const rollMessage = btn.closest(".vermine-roll-message");
        const grantedRerollElement = html.querySelector('#granted-reroll');
        const allowedRerollElement = html.querySelector("#allowed_reroll");
        const requested = parseInt(grantedRerollElement?.innerText, 10) || 0;

        // Déduit les points d'Effort de l'acteur (max : ce qu'il lui reste).
        const actor = await VermineUtils.#resolveMessageActor(rollMessage);
        const effort = actor?.system?.attributes?.effort;
        let spent = 0;
        if (effort && requested > 0) {
          spent = Math.min(requested, parseInt(effort.value, 10) || 0);
          if (spent > 0) {
            const remaining = (parseInt(effort.value, 10) || 0) - spent;
            await actor.update({ 'system.attributes.effort.value': remaining });
            const poolEl = rollMessage?.querySelector('.effort-pool');
            if (poolEl) poolEl.innerText = `${remaining}/${effort.max || 0}`;
          }
        }

        // Les relances d'Effort s'ajoutent aux relances de Compétence restantes.
        if (allowedRerollElement) {
          const current = parseInt(allowedRerollElement.innerText, 10) || 0;
          allowedRerollElement.innerText = current + spent;
        }
        const effortSection = btn.closest('.reroll-from-effort');
        if (effortSection) effortSection.style.display = "none";

        const mesEl = btn.closest('[data-message-id]');
        const messageId = mesEl?.dataset?.messageId;

        if (messageId) {
          const content = rollMessage.outerHTML;
          // GM applies the update directly ; players relay through the
          // system socket (Foundry v14 ChatMessage updates are GM-only).
          if (game.user?.isGM) {
            const message = await game.messages.get(messageId);
            await message.update({ content: content });
          } else {
            game.socket?.emit(`system.${game.system.id}`, {
              type: "reroll",
              messageId: messageId,
              requestId: foundry.utils.randomID(8),
              content: content
            });
          }
        }
      });
    });
  }

  /**
   * Résout l'acteur à l'origine d'un message de jet depuis les attributs
   * data-actor-id / data-actor-uuid posés sur le message. Le uuid est
   * privilégié : il distingue les tokens non liés (PNJ mineurs) issus de la
   * même fiche, qui partagent le même id d'acteur monde.
   * @param {HTMLElement} rollMessage
   * @returns {Promise<Actor|null>}
   */
  static async #resolveMessageActor(rollMessage) {
    if (!rollMessage) return null;
    const id = rollMessage.dataset.actorId ?? null;
    const uuid = rollMessage.dataset.actorUuid ?? null;
    if (!id && !uuid) return null;
    const { VermineExchange } = await import('./exchange.mjs');
    return VermineExchange.resolveDefender({ id, uuid });
  }

  /**
   * Displays dice rolls in 3D if available.
   * @param {Roll} roll - The roll to display
   * @param {string} [rollMode] - The roll mode (uses game settings if not provided)
   * @returns {Promise<boolean>} Whether 3D dice were shown
   */
  static async showDiceSoNice(roll, rollMode) {
    if (!game.dice3d) {
      return false;
    }

    rollMode = rollMode ?? game.settings.get("core", "rollMode");
    let whisper = null;
    let blind = false;

    switch (rollMode) {
      case "blindroll": // GM only
        blind = true;
        // Falls through
      case "gmroll": // GM + rolling player
        whisper = game.users?.filter(user => user.isGM) || [];
        break;
      case "roll": // Everybody
        whisper = game.users?.filter(user => user.active) || [];
        break;
      case "selfroll":
        whisper = [game.user.id];
        break;
    }

    await game.dice3d.showForRoll(roll, game.user, true, whisper, blind);
    return true;
  }

  /**
   * Displays a dice roll in the chat.
   * @param {Roll} roll - The roll to display
   * @param {Object} param - Roll parameters
   * @returns {Promise<ChatMessage>} The created chat message
   */
  static async diplayChatRoll(roll, param) {
    // Aggregate dice stats per type (regular / human / adapted)
    const diceStats = { regular: { count: 0, success: 0 }, human: { count: 0, success: 0 }, adapted: { count: 0, success: 0 } };
    for (const die of roll.dice ?? []) {
      // Flavors look like "<type>_<user>"; classify from the leading token.
      const flavor = die.options?.flavor ?? "";
      const prefix = flavor.split("_")[0];
      const type = prefix === "human" ? "human" : prefix === "adapted" ? "adapted" : "regular";
      for (const r of die.results ?? []) {
        diceStats[type].count += 1;
        if (r.success) diceStats[type].success += 1;
      }
    }
    // Verdict
    const required = 1 + (param.handicap ?? 0);
    const rawTotal = roll._total ?? 0;
    // Réussites automatiques : appliquées inconditionnellement, ou uniquement
    // si le jet est réussi (Trait Rafale — bonusOnSuccess).
    const requested = param.bonusSuccesses ?? 0;
    const bonusSuccesses = requested > 0 && (!param.bonusOnSuccess || rawTotal >= required) ? requested : 0;
    const total = rawTotal + bonusSuccesses;
    const verdict = {
      success: total >= required,
      total,
      required,
      bonusSuccesses
    };

    const content = await foundry.applications.handlebars.renderTemplate("systems/vermine2047/templates/roll-message.hbs", { roll, param, diceStats, verdict });
    const chatData = {
      user: game.user?._id,
      speaker: ChatMessage.getSpeaker(),
      content: content
    };
    if (param.messageFlags) {
      chatData.flags = { world: param.messageFlags };
    }
    const msg = await ChatMessage.create(chatData);
    return msg;
  }


}



