const ActorSheetV1 = foundry.appv1.sheets.ActorSheet;
const ItemSheetV1 = foundry.appv1.sheets.ItemSheet;

const GRADE_ROMAN = { 1: "I", 2: "II", 3: "III", 4: "IV", 5: "V" };

const CHARACTERISTIC_KEYS = ["puissance", "coordination", "ingeniosite", "perspicacite"];
const SKILL_TYPE_KEYS = ["aptitude", "unique", "racial", "path"];
const XP_AWARD_AMOUNTS = [1, 5, 10];
const CHARACTERISTIC_BASELINE = 50;
const CHARACTERISTIC_XP_BREAKPOINTS = [60, 80];

function triangularNumber(value) {
  return (value * (value + 1)) / 2;
}

function normalizeMaxGrade(rawValue, fallback = 5) {
  const parsed = Number(rawValue ?? fallback);
  return Math.clamp(Math.trunc(Number.isFinite(parsed) ? parsed : fallback), 1, 5);
}

function normalizeItemGrade(rawGrade, { usesGrades = true, maxGrade = 5, fallback = 1 } = {}) {
  if (!usesGrades) return fallback;
  const parsed = Number(rawGrade ?? fallback);
  return Math.clamp(Math.trunc(Number.isFinite(parsed) ? parsed : fallback), 1, maxGrade);
}

function getGradeOptions(maxGrade = 5, currentGrade = 1) {
  const safeMax = normalizeMaxGrade(maxGrade);
  return Array.from({ length: safeMax }, (_, index) => {
    const value = index + 1;
    return {
      value,
      roman: GRADE_ROMAN[value],
      selected: currentGrade === value
    };
  });
}

function getSkillGradeLabel(system) {
  if (system.usesGrades === false) return game.i18n.localize("MYRIADES.Skill.gradeless");
  return GRADE_ROMAN[system.grade] ?? String(system.grade ?? 1);
}

function getSkillXpCost(system) {
  const pointCost = Math.max(1, Math.trunc(Number(system.skillPointCost ?? 1)));
  if (system.usesGrades === false) return pointCost * 5;
  const grade = normalizeItemGrade(system.grade, {
    usesGrades: system.usesGrades !== false,
    maxGrade: system.maxGrade ?? 5
  });
  return pointCost * triangularNumber(grade) * 5;
}

function getPathXpCost(system) {
  const baseCost = Math.max(0, Math.trunc(Number(system.xpCost ?? 0)));
  const grade = normalizeItemGrade(system.grade, { maxGrade: 5 });
  return baseCost * triangularNumber(grade);
}

function getCharacteristicXpCost(value) {
  const score = Math.max(CHARACTERISTIC_BASELINE, Math.trunc(Number(value ?? CHARACTERISTIC_BASELINE)));
  let total = 0;

  for (let current = CHARACTERISTIC_BASELINE + 1; current <= score; current += 1) {
    if (current <= CHARACTERISTIC_XP_BREAKPOINTS[0]) total += 2;
    else if (current <= CHARACTERISTIC_XP_BREAKPOINTS[1]) total += 3;
    else total += 4;
  }

  return total;
}

function calculateActorProgression(actor) {
  const progression = actor.system.progression ?? {};
  const options = actor.system.options ?? {};
  const characteristics = actor.system.characteristics ?? {};
  const creationBudget = Math.trunc(Number(progression.creationBudget ?? 55));
  const earnedXp = Math.trunc(Number(progression.earnedXp ?? 0));
  const manualSpentAdjustment = Math.trunc(Number(progression.manualSpentAdjustment ?? 0));
  const domainlessBonusXp = Math.max(0, Math.trunc(Number(progression.domainlessBonusXp ?? 15)));
  const domainless = options.domainless === true;

  const characteristicSpent = CHARACTERISTIC_KEYS.reduce(
    (total, key) => total + getCharacteristicXpCost(characteristics[key]),
    0
  );

  const skillSpent = actor.items
    .filter(item => item.type === "skill" && !item.system.sourcePathId)
    .reduce((total, item) => total + getSkillXpCost(item.system), 0);

  const pathSpent = actor.items
    .filter(item => item.type === "path")
    .reduce((total, item) => total + getPathXpCost(item.system), 0);

  const domainlessAdjustment = domainless ? domainlessBonusXp : 0;
  const totalBudget = creationBudget + earnedXp + domainlessAdjustment;
  const spentXp = characteristicSpent + skillSpent + pathSpent + manualSpentAdjustment;
  const currentBudget = totalBudget - spentXp;

  return {
    creationBudget,
    earnedXp,
    manualSpentAdjustment,
    domainlessBonusXp,
    domainless,
    totalBudget,
    spentXp,
    currentBudget,
    isOverspent: currentBudget < 0,
    breakdown: {
      characteristics: characteristicSpent,
      skills: skillSpent,
      paths: pathSpent,
      manual: manualSpentAdjustment
    }
  };
}

function buildDefaultBundledSkill() {
  return {
    key: foundry.utils.randomID(),
    name: game.i18n.localize("MYRIADES.Path.newSkill"),
    description: "",
    grade: 1,
    usesGrades: true,
    maxGrade: 5,
    skillType: "aptitude",
    linkedCharacteristic: "puissance",
    isRollable: true,
    skillPointCost: 1,
    rollModifier: 0
  };
}

function normalizeBundledSkill(rawSkill, index) {
  const source = rawSkill ?? {};
  const parsedGrade = Number(source.grade ?? 1);
  const parsedPointCost = Number(source.skillPointCost ?? 1);
  const parsedRollModifier = Number(source.rollModifier ?? 0);
  const usesGrades = source.usesGrades !== false;
  const maxGrade = normalizeMaxGrade(source.maxGrade, 5);

  return {
    key: String(source.key ?? "").trim() || `bundled-${index + 1}`,
    name: String(source.name ?? "").trim() || game.i18n.localize("MYRIADES.Path.newSkill"),
    description: String(source.description ?? ""),
    grade: normalizeItemGrade(parsedGrade, { usesGrades, maxGrade }),
    usesGrades,
    maxGrade,
    skillType: SKILL_TYPE_KEYS.includes(source.skillType) ? source.skillType : "aptitude",
    linkedCharacteristic: CHARACTERISTIC_KEYS.includes(source.linkedCharacteristic)
      ? source.linkedCharacteristic
      : "puissance",
    isRollable: source.isRollable !== false,
    skillPointCost: Math.max(1, Math.trunc(Number.isFinite(parsedPointCost) ? parsedPointCost : 1)),
    rollModifier: Math.trunc(Number.isFinite(parsedRollModifier) ? parsedRollModifier : 0)
  };
}

function bundledSkillFromItem(skillItem, key = foundry.utils.randomID()) {
  return normalizeBundledSkill({
    key,
    name: skillItem.name,
    description: skillItem.system.description,
    grade: skillItem.system.grade,
    usesGrades: skillItem.system.usesGrades,
    maxGrade: skillItem.system.maxGrade,
    skillType: skillItem.system.skillType,
    linkedCharacteristic: skillItem.system.linkedCharacteristic,
    isRollable: skillItem.system.isRollable,
    skillPointCost: skillItem.system.skillPointCost,
    rollModifier: skillItem.system.rollModifier
  }, 0);
}

async function renderSkillDescription(description) {
  const trimmed = String(description ?? "").trim();
  if (!trimmed) return null;
  return TextEditor.enrichHTML(trimmed, { async: true });
}

async function addSkillItemToPath(pathItem, skillItem, { rebindLooseSkill = false } = {}) {
  if (!pathItem || pathItem.type !== "path") return { added: false, key: null };
  if (!skillItem || skillItem.type !== "skill") return { added: false, key: null };

  const bundledSkills = Array.isArray(pathItem.system.bundledSkills)
    ? pathItem.system.bundledSkills.map((skill, idx) => normalizeBundledSkill(skill, idx))
    : [];

  let key = String(skillItem.system.sourcePathKey ?? "").trim();
  if (skillItem.system.sourcePathId !== pathItem.id) key = "";
  if (!key) key = foundry.utils.randomID();

  const alreadyIncluded = bundledSkills.some(skill => skill.key === key)
    || bundledSkills.some(skill => skill.name.trim().toLowerCase() === skillItem.name.trim().toLowerCase());
  if (alreadyIncluded) return { added: false, key };

  bundledSkills.push(bundledSkillFromItem(skillItem, key));
  await pathItem.update({ "system.bundledSkills": bundledSkills });

  if (rebindLooseSkill && skillItem.parent === pathItem.parent) {
    await skillItem.update({
      "system.skillType": "path",
      "system.sourcePathId": pathItem.id,
      "system.sourcePathKey": key
    });
  }

  return { added: true, key };
}

async function resolveDroppedItem(data, actor) {
  if (!data || data.type !== "Item") return null;
  if (data.uuid) return fromUuid(data.uuid);
  if (data.itemId && actor) return actor.items.get(data.itemId) ?? null;
  return null;
}

function getPathRowFromDropEvent(event) {
  const nativeEvent = event?.originalEvent ?? event;
  const path = typeof nativeEvent?.composedPath === "function" ? nativeEvent.composedPath() : [];

  for (const node of path) {
    if (node instanceof Element && node.classList.contains("path-row")) return node;
  }

  const baseTarget = nativeEvent?.target instanceof Element
    ? nativeEvent.target
    : nativeEvent?.target?.parentElement;
  return baseTarget?.closest?.(".path-row") ?? null;
}

function isDoubleRoll(rolled) {
  return rolled >= 11 && rolled <= 99 && rolled % 11 === 0;
}

function evaluateRollOutcome(rolled, targetValue) {
  const success = rolled <= targetValue;
  const margin = targetValue - rolled;
  const isCriticalSuccess = rolled <= 5 || (isDoubleRoll(rolled) && success);
  const isCriticalFailure = rolled >= 96 || (isDoubleRoll(rolled) && !success);
  const isCritical = isCriticalSuccess || isCriticalFailure;
  const criticalType = isCriticalSuccess ? "success" : isCriticalFailure ? "failure" : null;

  return { success, margin, isCritical, criticalType };
}

function getMarginDisplayData(success, margin) {
  return {
    hasMargin: Number.isInteger(margin),
    marginValue: Math.abs(margin),
    marginLabel: game.i18n.localize(
      success ? "MYRIADES.Roll.successMarginLabel" : "MYRIADES.Roll.failureMarginLabel"
    )
  };
}

function getEquipmentInventoryUsage(item) {
  const quantity = Math.max(1, Math.trunc(Number(item.system.quantity ?? 1)));
  const slots = Math.max(0, Math.trunc(Number(item.system.slots ?? 0)));
  if (item.system.equipped) return 0;
  return quantity * slots;
}

function getSpellDescriptionHtml(item) {
  const parts = [];
  const description = String(item.system.description ?? "").trim();
  const domain = String(item.system.domain ?? "").trim();
  const art = String(item.system.art ?? "").trim();

  if (description) parts.push(`<p>${foundry.utils.escapeHTML(description)}</p>`);
  if (domain) {
    parts.push(
      `<p><strong>${game.i18n.localize("MYRIADES.Spell.domain")}</strong> ${foundry.utils.escapeHTML(domain)}</p>`
    );
  }
  if (art) {
    parts.push(
      `<p><strong>${game.i18n.localize("MYRIADES.Spell.art")}</strong> ${foundry.utils.escapeHTML(art)}</p>`
    );
  }

  return parts.join("");
}

function canManageXpAwards() {
  return game.user?.isGM || game.settings.get("myriades-ind100", "showPlayerXpControls");
}

async function handleSkillDropOnPathRow(actor, pathRow, data) {
  if (!pathRow) return false;

  const pathId = pathRow.dataset.itemId;
  const pathItem = actor.items.get(pathId);
  if (!pathItem || pathItem.type !== "path") return false;

  const droppedItem = await resolveDroppedItem(data, actor);
  if (!droppedItem || droppedItem.type !== "skill") return false;

  const result = await addSkillItemToPath(pathItem, droppedItem, { rebindLooseSkill: true });
  if (!result.added) {
    ui.notifications?.info(game.i18n.localize("MYRIADES.Path.dropAlreadyIncluded"));
  }
  return true;
}

export class MyriadesActorSheet extends ActorSheetV1 {
  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      classes: ["myriad-system", "sheet", "actor"],
      width: 660,
      height: 720,
      tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "characteristics" }]
    });
  }

  get template() {
    const systemId = game.system.id;
    const actorType = this.actor.type;
    if (actorType === "npc") return `systems/${systemId}/templates/actors/npc.hbs`;
    return `systems/${systemId}/templates/actors/character.hbs`;
  }

  getData() {
    const context = super.getData();
    context.system = context.system ?? this.actor?.system ?? {};
    context.sheetTheme = game.settings.get("myriades-ind100", "sheetTheme") ?? "dark";
    const typeIcons = CONFIG.Item.typeIcons ?? {};

    // Structured characteristics list with labels for the template
    context.characteristicsList = Object.entries(this.actor.system.characteristics).map(([key, value]) => ({
      key,
      value,
      label: game.i18n.localize(`MYRIADES.Characteristics.${key}`),
      help: game.i18n.localize(`MYRIADES.CharacteristicsHelp.${key}`)
    }));

    // Skills with linked characteristic label pre-resolved
    context.skills = this.actor.items
      .filter(i => i.type === "skill")
      .sort((a, b) => a.name.localeCompare(b.name))
      .map(item => ({
        id: item.id,
        name: item.name,
        img: item.img,
        typeIcon: typeIcons.skill ?? "fas fa-book",
        system: item.system,
        subtitle: String(item.system.description ?? "").trim(),
        isRollable: item.system.isRollable !== false,
        linkedCharLabel: game.i18n.localize(`MYRIADES.Characteristics.${item.system.linkedCharacteristic}`),
        gradeLabel: getSkillGradeLabel(item.system),
        xpCost: getSkillXpCost(item.system)
      }));

    // Equipment list
    context.equipment = this.actor.items
      .filter(i => i.type === "equipment")
      .sort((a, b) => a.name.localeCompare(b.name))
      .map(item => ({
        id: item.id,
        name: item.name,
        img: item.img,
        typeIcon: typeIcons.equipment ?? "fas fa-shield-halved",
        system: item.system,
        inventoryUsage: getEquipmentInventoryUsage(item)
      }));

    context.spells = this.actor.items
      .filter(i => i.type === "spell")
      .sort((a, b) => a.name.localeCompare(b.name))
      .map(item => ({
        id: item.id,
        name: item.name,
        img: item.img,
        typeIcon: typeIcons.spell ?? "fas fa-wand-magic-sparkles",
        system: item.system
      }));

    context.paths = this.actor.items
      .filter(i => i.type === "path")
      .sort((a, b) => a.name.localeCompare(b.name))
      .map(item => {
        const bundledSkills = Array.isArray(item.system.bundledSkills) ? item.system.bundledSkills : [];
        const normalizedSkills = bundledSkills.map((skill, index) => normalizeBundledSkill(skill, index));

        return {
          id: item.id,
          name: item.name,
          img: item.img,
          typeIcon: typeIcons.path ?? "fas fa-star",
          system: item.system,
          linkedSkillsCount: normalizedSkills.length,
          linkedSkillsLabel: normalizedSkills.map(skill => skill.name).join(", "),
          xpCost: getPathXpCost(item.system)
        };
      });

    const progression = calculateActorProgression(this.actor);
    context.progression = progression;
    context.showXpControls = canManageXpAwards();
    context.xpAwardButtons = XP_AWARD_AMOUNTS.map(amount => ({
      amount,
      label: amount > 0 ? `+${amount}` : `${amount}`
    }));

    const puissance = this.actor.system.characteristics.puissance ?? 0;
    const secondaryModifiers = this.actor.system.secondaryModifiers ?? {};
    const healthModifier = secondaryModifiers.health ?? 0;
    const magicModifier = secondaryModifiers.magic ?? 0;
    const inventoryModifier = secondaryModifiers.inventory ?? 0;

    context.secondaryModifiers = {
      health: healthModifier,
      magic: magicModifier,
      inventory: inventoryModifier
    };

    context.inventorySlotsMax = 36 + Math.floor(puissance / 5) + inventoryModifier;
  context.inventorySlotsUsed = context.equipment.reduce((total, item) => total + item.inventoryUsage, 0);

    const magicCharacteristic = this.actor.system.magicCharacteristic ?? "perspicacite";
    const magicCharacteristicLabel = game.i18n.localize(`MYRIADES.Characteristics.${magicCharacteristic}`);
    const magicCharacteristicValue = this.actor.system.characteristics[magicCharacteristic] ?? 0;

    context.healthFormulaHelper = game.i18n.format("MYRIADES.Secondary.formula.health", {
      power: puissance,
      mod: healthModifier,
      result: this.actor.system.resources.health.max
    });

    context.magicFormulaHelper = game.i18n.format("MYRIADES.Secondary.formula.magic", {
      sourceLabel: magicCharacteristicLabel,
      sourceValue: magicCharacteristicValue,
      mod: magicModifier,
      result: this.actor.system.resources.magic.max
    });

    context.inventoryFormulaHelper = game.i18n.format("MYRIADES.Secondary.formula.inventory", {
      base: 36,
      power: puissance,
      mod: inventoryModifier,
      result: context.inventorySlotsMax
    });

    const healthHelp = game.i18n.localize("MYRIADES.ResourcesHelp.health");
    const magicHelp = game.i18n.localize("MYRIADES.ResourcesHelp.magic");
    const inventoryHelp = game.i18n.localize("MYRIADES.Inventory.help");
    context.healthResourceTooltip = `${healthHelp}\n${context.healthFormulaHelper}`;
    context.magicResourceTooltip = `${magicHelp}\n${context.magicFormulaHelper}`;
    context.inventoryTooltip = `${inventoryHelp}\n${context.inventoryFormulaHelper}`;
    context.settingsTitle = game.i18n.localize("MYRIADES.Settings.character.title");

    // Effects list with modifier counts
    context.effects = this.actor.items
      .filter(i => i.type === "effect")
      .sort((a, b) => a.name.localeCompare(b.name))
      .map(item => ({
        id: item.id,
        name: item.name,
        img: item.img,
        typeIcon: typeIcons.effect ?? "fas fa-sparkles",
        system: item.system,
        modifierCount: (Array.isArray(item.system.modifiers) ? item.system.modifiers.length : 0)
      }));

    return context;
  }

  activateListeners(html) {
    super.activateListeners(html);
    if (!this.isEditable) return;

    html.find(".open-modifiers").click(() => {
      html.find(".modifiers-modal").removeClass("hidden");
    });

    html.find(".modifiers-cancel").click(() => {
      html.find(".modifiers-modal").addClass("hidden");
    });

    html.find(".modifiers-save").click(async ev => {
      const modal = ev.currentTarget.closest(".modifiers-modal");
      if (!modal) return;

      const health = Number(modal.querySelector('[name="mod-health"]')?.value ?? 0);
      const magic = Number(modal.querySelector('[name="mod-magic"]')?.value ?? 0);
      const inventory = Number(modal.querySelector('[name="mod-inventory"]')?.value ?? 0);
      const creationBudget = Number(modal.querySelector('[name="progression-creation-budget"]')?.value ?? 55);
      const manualSpentAdjustment = Number(modal.querySelector('[name="progression-manual-spent"]')?.value ?? 0);
      const domainlessBonusXp = Number(modal.querySelector('[name="progression-domainless-bonus"]')?.value ?? 15);
      const domainless = modal.querySelector('[name="option-domainless"]')?.checked === true;

      await this.actor.update({
        "system.secondaryModifiers.health": Math.trunc(health),
        "system.secondaryModifiers.magic": Math.trunc(magic),
        "system.secondaryModifiers.inventory": Math.trunc(inventory),
        "system.progression.creationBudget": Math.trunc(creationBudget),
        "system.progression.manualSpentAdjustment": Math.trunc(manualSpentAdjustment),
        "system.progression.domainlessBonusXp": Math.max(0, Math.trunc(domainlessBonusXp)),
        "system.options.domainless": domainless
      });

      html.find(".modifiers-modal").addClass("hidden");
    });

    html.find(".award-xp").click(async ev => {
      const amount = Number(ev.currentTarget.dataset.amount ?? 0);
      if (!Number.isFinite(amount) || amount === 0) return;

      const currentEarned = Math.trunc(Number(this.actor.system.progression?.earnedXp ?? 0));
      await this.actor.update({
        "system.progression.earnedXp": currentEarned + Math.trunc(amount)
      });
    });

    // Roll a characteristic
    html.find(".roll-characteristic").click(ev => {
      const characteristic = ev.currentTarget.dataset.characteristic;
      this._onRollCharacteristic(characteristic);
    });

    // Roll a skill
    html.find(".roll-skill").click(ev => {
      const itemId = ev.currentTarget.closest(".item-row").dataset.itemId;
      this._onRollSkill(itemId);
    });

    html.find(".share-skill").click(ev => {
      const itemId = ev.currentTarget.closest(".item-row").dataset.itemId;
      this._onShareSkillToChat(itemId);
    });

    html.find(".cast-spell").click(ev => {
      const itemId = ev.currentTarget.closest(".item-row").dataset.itemId;
      this._onCastSpell(itemId);
    });

    html.find(".free-cast-spell").click(ev => {
      const itemId = ev.currentTarget.closest(".item-row").dataset.itemId;
      this._onFreeCastSpell(itemId);
    });

    // Open item sheet on name click or edit button click
    html.find(".item-open, .item-edit").click(ev => {
      const itemId = ev.currentTarget.closest(".item-row").dataset.itemId;
      const item = this.actor.items.get(itemId);
      item?.sheet.render(true);
    });

    html.find(".item-open").keydown(ev => {
      if ((ev.key !== "Enter") && (ev.key !== " ")) return;
      ev.preventDefault();
      const itemId = ev.currentTarget.closest(".item-row").dataset.itemId;
      const item = this.actor.items.get(itemId);
      item?.sheet.render(true);
    });


    // Create a new item
    html.find(".item-create").click(ev => {
      const type = ev.currentTarget.dataset.type;
      this._onItemCreate(type);
    });

    // Delete an item
    html.find(".item-delete").click(ev => {
      const itemId = ev.currentTarget.closest(".item-row").dataset.itemId;
      const item = this.actor.items.get(itemId);
      item?.delete();
    });

    // Toggle effect enabled state
    html.find(".item-toggle-enabled").click(async ev => {
      ev.preventDefault();
      ev.stopPropagation();
      const itemId = ev.currentTarget.closest(".item-row").dataset.itemId;
      const item = this.actor.items.get(itemId);
      if (!item || item.type !== "effect") return;
      await item.update({ "system.enabled": !item.system.enabled });
    });

    // Drag a skill onto a path
    html.find(".skill-row").attr("draggable", true).on("dragstart", ev => {
      const itemId = ev.currentTarget?.dataset?.itemId;
      const skillItem = this.actor.items.get(itemId);
      if (!skillItem || skillItem.type !== "skill") return;
      const nativeEvent = ev.originalEvent ?? ev;
      nativeEvent.dataTransfer?.setData("text/plain", JSON.stringify({
        type: "Item",
        uuid: skillItem.uuid,
        actorId: this.actor.id,
        itemId: skillItem.id
      }));
    });

    html.find(".path-row").attr("draggable", true).on("dragstart", ev => {
      const itemId = ev.currentTarget?.dataset?.itemId;
      const pathItem = this.actor.items.get(itemId);
      if (!pathItem || pathItem.type !== "path") return;
      const nativeEvent = ev.originalEvent ?? ev;
      nativeEvent.dataTransfer?.setData("text/plain", JSON.stringify({
        type: "Item",
        uuid: pathItem.uuid,
        actorId: this.actor.id,
        itemId: pathItem.id
      }));
    });

    html.find(".path-row")
      .on("dragover", ev => {
        ev.preventDefault();
        ev.currentTarget.classList.add("drag-over");
      })
      .on("dragleave", ev => {
        ev.currentTarget.classList.remove("drag-over");
      })
      .on("drop", async ev => {
        ev.preventDefault();
        ev.stopPropagation();
        ev.currentTarget.classList.remove("drag-over");

        const nativeEvent = ev.originalEvent ?? ev;
        const data = TextEditor.getDragEventData(nativeEvent);
        await handleSkillDropOnPathRow(this.actor, ev.currentTarget, data);
      });
  }

  async _onDrop(event) {
    const nativeEvent = event.originalEvent ?? event;
    const data = TextEditor.getDragEventData(event);
    if (data.type !== "Item") return super._onDrop(event);

    const pathRow = getPathRowFromDropEvent(nativeEvent);
    if (pathRow) {
      nativeEvent.preventDefault?.();
      nativeEvent.stopPropagation?.();
      const handled = await handleSkillDropOnPathRow(this.actor, pathRow, data);
      if (handled) return false;
      return super._onDrop(event);
    }

    return super._onDrop(event);
  }

  async _onRollCharacteristic(characteristic) {
    const value = this.actor.system.characteristics[characteristic];
    const label = game.i18n.localize(`MYRIADES.Characteristics.${characteristic}`);
    const roll = await new Roll("1d100").roll();
    const { success, margin, isCritical, criticalType } = evaluateRollOutcome(roll.total, value);
    const marginData = getMarginDisplayData(success, margin);
    const diceTens = String((Math.floor(roll.total / 10) % 10) * 10).padStart(2, "0");
    const diceUnits = String(roll.total % 10);
    const content = await renderTemplate(
      `systems/${game.system.id}/templates/chat/roll-card.hbs`,
      {
        actorName: this.actor.name,
        rollTypeLabel: game.i18n.localize("MYRIADES.Roll.rollCharacteristic"),
        subjectName: label,
        subjectIcon: "fas fa-person-running",
        showRollData: true,
        formulaLine: null,
        rolled: roll.total,
        resultLabel: game.i18n.localize("MYRIADES.Roll.resultLabel"),
        diceTens,
        diceUnits,
        targetValue: value,
        targetLabel: label,
        success,
        outcomeClass: [
          success ? "outcome-success" : "outcome-failure",
          isCritical ? `outcome-critical-${criticalType}` : null
        ].filter(Boolean).join(" "),
        outcomeText: success
          ? game.i18n.localize("MYRIADES.Roll.success")
          : game.i18n.localize("MYRIADES.Roll.failure"),
        criticalLabel: criticalType
          ? game.i18n.localize(`MYRIADES.Roll.critical${criticalType === "success" ? "Success" : "Failure"}`)
          : null,
        hasMargin: marginData.hasMargin,
        margin: marginData.marginValue,
        marginLabel: marginData.marginLabel
      }
    );
    ChatMessage.create({
      speaker: ChatMessage.getSpeaker({ actor: this.actor }),
      rolls: [roll],
      content,
      sound: CONFIG.sounds.dice,
      style: CONST.CHAT_MESSAGE_STYLES.ROLL
    });
  }

  async _onRollSkill(itemId) {
    const item = this.actor.items.get(itemId);
    if (!item) return;
    if (item.system.isRollable === false) return this._onShareSkillToChat(itemId);

    const characteristic = item.system.linkedCharacteristic;
    const charValue = this.actor.system.characteristics[characteristic];
    const rollModifier = Number(item.system.rollModifier ?? 0);
    const targetValue = charValue + rollModifier;
    const charLabel = game.i18n.localize(`MYRIADES.Characteristics.${characteristic}`);
    const roll = await new Roll("1d100").roll();
    const { success, margin, isCritical, criticalType } = evaluateRollOutcome(roll.total, targetValue);
    const marginData = getMarginDisplayData(success, margin);
    const modifierDisplay = rollModifier >= 0 ? `+${rollModifier}` : `${rollModifier}`;
    const diceTens = String((Math.floor(roll.total / 10) % 10) * 10).padStart(2, "0");
    const diceUnits = String(roll.total % 10);
    const formulaLine = `${charLabel} (${charValue}) ${modifierDisplay} = ${targetValue}`;
    const subjectDescription = await renderSkillDescription(item.system.description);
    const content = await renderTemplate(
      `systems/${game.system.id}/templates/chat/roll-card.hbs`,
      {
        actorName: this.actor.name,
        rollTypeLabel: game.i18n.localize("MYRIADES.Roll.rollSkill"),
        subjectName: item.name,
        subjectIcon: "fas fa-book",
        subjectDescription,
        showRollData: true,
        formulaLine,
        rolled: roll.total,
        resultLabel: game.i18n.localize("MYRIADES.Roll.resultLabel"),
        diceTens,
        diceUnits,
        targetValue,
        targetLabel: game.i18n.localize("MYRIADES.Roll.targetLabel"),
        success,
        outcomeClass: [
          success ? "outcome-success" : "outcome-failure",
          isCritical ? `outcome-critical-${criticalType}` : null
        ].filter(Boolean).join(" "),
        outcomeText: success
          ? game.i18n.localize("MYRIADES.Roll.success")
          : game.i18n.localize("MYRIADES.Roll.failure"),
        criticalLabel: criticalType
          ? game.i18n.localize(`MYRIADES.Roll.critical${criticalType === "success" ? "Success" : "Failure"}`)
          : null,
        hasMargin: marginData.hasMargin,
        margin: marginData.marginValue,
        marginLabel: marginData.marginLabel
      }
    );
    ChatMessage.create({
      speaker: ChatMessage.getSpeaker({ actor: this.actor }),
      rolls: [roll],
      content,
      sound: CONFIG.sounds.dice,
      style: CONST.CHAT_MESSAGE_STYLES.ROLL
    });
  }

  async _onShareSkillToChat(itemId) {
    const item = this.actor.items.get(itemId);
    if (!item) return;

    const subjectDescription = await renderSkillDescription(item.system.description);
    const content = await renderTemplate(
      `systems/${game.system.id}/templates/chat/roll-card.hbs`,
      {
        actorName: this.actor.name,
        rollTypeLabel: game.i18n.localize("MYRIADES.Action.sendToChat"),
        subjectName: item.name,
        subjectIcon: "fas fa-scroll",
        subjectDescription,
        showRollData: false
      }
    );

    ChatMessage.create({
      speaker: ChatMessage.getSpeaker({ actor: this.actor }),
      content,
      style: CONST.CHAT_MESSAGE_STYLES.OTHER
    });
  }

  async _onCastSpell(itemId) {
    const item = this.actor.items.get(itemId);
    if (!item || item.type !== "spell") return;

    const magicCost = Math.max(1, Math.trunc(Number(item.system.magicCost ?? item.system.grade ?? 1)));
    const currentMagic = Math.max(0, Math.trunc(Number(this.actor.system.resources.magic.value ?? 0)));

    if (currentMagic < magicCost) {
      ui.notifications?.warn(game.i18n.format("MYRIADES.Spell.notEnoughMagic", {
        actorName: this.actor.name,
        cost: magicCost,
        current: currentMagic
      }));
      return;
    }

    const remainingMagic = currentMagic - magicCost;
    await this.actor.update({ "system.resources.magic.value": remainingMagic });

    const content = await renderTemplate(
      `systems/${game.system.id}/templates/chat/roll-card.hbs`,
      {
        actorName: this.actor.name,
        rollTypeLabel: game.i18n.localize("MYRIADES.Roll.rollSpell"),
        subjectName: item.name,
        subjectIcon: "fas fa-wand-magic-sparkles",
        subjectDescription: getSpellDescriptionHtml(item),
        formulaLine: game.i18n.format("MYRIADES.Spell.castPaidFormula", {
          before: currentMagic,
          cost: magicCost,
          after: remainingMagic
        }),
        showRollData: false
      }
    );

    ChatMessage.create({
      speaker: ChatMessage.getSpeaker({ actor: this.actor }),
      content,
      style: CONST.CHAT_MESSAGE_STYLES.OTHER
    });
  }

  async _onFreeCastSpell(itemId) {
    const item = this.actor.items.get(itemId);
    if (!item || item.type !== "spell") return;

    const grade = Math.max(1, Math.trunc(Number(item.system.grade ?? 1)));
    const magicCharacteristic = this.actor.system.magicCharacteristic ?? "perspicacite";
    const characteristicLabel = game.i18n.localize(`MYRIADES.Characteristics.${magicCharacteristic}`);
    const characteristicValue = Math.max(0, Math.trunc(Number(this.actor.system.characteristics[magicCharacteristic] ?? 0)));
    const rolls = await Promise.all(
      Array.from({ length: grade }, () => new Roll("1d100").roll())
    );

    let characteristicDelta = 0;
    const rollSummary = rolls.map((roll, index) => {
      const outcome = evaluateRollOutcome(roll.total, characteristicValue);
      let adjustment = 0;

      if (outcome.criticalType === "failure") adjustment = -10;
      else if (!outcome.success) adjustment = -5;
      else if (outcome.criticalType === "success") adjustment = 5;

      characteristicDelta += adjustment;

      const criticalLabel = outcome.criticalType
        ? ` (${game.i18n.localize(`MYRIADES.Roll.critical${outcome.criticalType === "success" ? "Success" : "Failure"}`)})`
        : "";
      const adjustmentLabel = adjustment === 0
        ? ""
        : ` - ${game.i18n.format("MYRIADES.Spell.characteristicAdjustment", {
            label: characteristicLabel,
            amount: adjustment > 0 ? `+${adjustment}` : `${adjustment}`
          })}`;

      return `<li>${game.i18n.format("MYRIADES.Spell.freeCastRollLine", {
        index: index + 1,
        rolled: roll.total,
        target: characteristicValue,
        outcome: game.i18n.localize(`MYRIADES.Roll.${outcome.success ? "success" : "failure"}`)
      })}${criticalLabel}${adjustmentLabel}</li>`;
    });

    const nextCharacteristic = Math.clamp(characteristicValue + characteristicDelta, 0, 200);
    if (nextCharacteristic !== characteristicValue) {
      await this.actor.update({ [`system.characteristics.${magicCharacteristic}`]: nextCharacteristic });
    }

    const summaryHtml = [
      getSpellDescriptionHtml(item),
      `<p>${game.i18n.format("MYRIADES.Spell.freeCastTarget", {
        label: characteristicLabel,
        value: characteristicValue,
        diceCount: grade
      })}</p>`,
      `<ul>${rollSummary.join("")}</ul>`,
      `<p><strong>${game.i18n.format("MYRIADES.Spell.characteristicChangeSummary", {
        label: characteristicLabel,
        before: characteristicValue,
        after: nextCharacteristic,
        delta: characteristicDelta > 0 ? `+${characteristicDelta}` : `${characteristicDelta}`
      })}</strong></p>`
    ].filter(Boolean).join("");

    const content = await renderTemplate(
      `systems/${game.system.id}/templates/chat/roll-card.hbs`,
      {
        actorName: this.actor.name,
        rollTypeLabel: game.i18n.localize("MYRIADES.Action.freeCastSpell"),
        subjectName: item.name,
        subjectIcon: "fas fa-burst",
        subjectDescription: summaryHtml,
        formulaLine: game.i18n.format("MYRIADES.Spell.freeCastFormula", {
          label: characteristicLabel,
          value: characteristicValue,
          diceCount: grade
        }),
        showRollData: false
      }
    );

    ChatMessage.create({
      speaker: ChatMessage.getSpeaker({ actor: this.actor }),
      rolls,
      content,
      sound: CONFIG.sounds.dice,
      style: CONST.CHAT_MESSAGE_STYLES.ROLL
    });
  }

  async _onItemCreate(type) {
    const typeLabel = game.i18n.localize(`MYRIADES.ItemType.${type}`);
    const itemData = {
      name: `${typeLabel}`,
      type: type
    };

    if (type === "skill") {
      itemData.system = {
        isRollable: true,
        usesGrades: true,
        maxGrade: 5
      };
    }

    if (type === "path") {
      itemData.system = {
        xpCost: 5,
        bundledSkills: [buildDefaultBundledSkill()]
      };
    }

    const [item] = await this.actor.createEmbeddedDocuments("Item", [itemData]);
    item?.sheet.render(true);
  }
}

export class MyriadesItemSheet extends ItemSheetV1 {
  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      classes: ["myriad-system", "sheet", "item"],
      width: 520,
      height: 560,
      submitOnChange: true,
      submitOnClose: true,
      closeOnSubmit: false,
      resizable: true
    });
  }

  get template() {
    return `systems/${game.system.id}/templates/items/item.hbs`;
  }

  getData() {
    const context = super.getData();
    context.system = context.system ?? this.item?.system ?? {};
    context.sheetTheme = game.settings.get("myriades-ind100", "sheetTheme") ?? "dark";

    context.itemTypeLabel = game.i18n.localize(`MYRIADES.ItemType.${this.item.type}`);

    // All item types use a static type icon — no editable portrait for any item
    const TYPE_ICONS = {
      skill:     "fas fa-book",
      equipment: "fas fa-shield-halved",
      path:      "fas fa-star",
      spell:     "fas fa-wand-magic-sparkles",
      effect:    "fas fa-sparkles"
    };
    context.hasEditableImage = false;
    context.itemTypeIcon = TYPE_ICONS[this.item.type] ?? "fas fa-question";

    context.characteristicOptions = CHARACTERISTIC_KEYS.map(key => ({
      key,
      label: game.i18n.localize(`MYRIADES.Characteristics.${key}`),
      selected: context.system.linkedCharacteristic === key
    }));

    context.skillTypeOptions = SKILL_TYPE_KEYS.map(key => ({
      key,
      label: game.i18n.localize(`MYRIADES.SkillType.${key}`),
      selected: context.system.skillType === key
    }));
    context.skillIsRollable = context.system.isRollable !== false;
    context.skillUsesGrades = context.system.usesGrades !== false;
    context.skillMaxGrade = normalizeMaxGrade(context.system.maxGrade, 5);

    context.skillXpCost = getSkillXpCost(context.system);
    context.equipmentTotalSlots = (context.system.quantity ?? 1) * (context.system.slots ?? 1);
    context.pathXpCost = getPathXpCost(context.system);

    const bundledSkills = Array.isArray(context.system.bundledSkills) ? context.system.bundledSkills : [];
    const currentGrade = normalizeItemGrade(context.system.grade, {
      usesGrades: context.item.type === "skill" ? context.skillUsesGrades : true,
      maxGrade: context.item.type === "skill" ? context.skillMaxGrade : 5
    });
    const currentItemMaxGrade = context.item.type === "skill" ? context.skillMaxGrade : 5;
    context.gradeOptions = getGradeOptions(currentItemMaxGrade, currentGrade);

    context.pathBundledSkills = bundledSkills.map((skill, idx) => {
      const normalized = normalizeBundledSkill(skill, idx);
      return {
        ...normalized,
        idx,
        displayIndex: idx + 1,
        gradeLabel: getSkillGradeLabel(normalized),
        xpCost: getSkillXpCost(normalized),
        gradeOptions: getGradeOptions(normalized.maxGrade, normalized.grade),
        characteristicOptions: CHARACTERISTIC_KEYS.map(key => ({
          key,
          label: game.i18n.localize(`MYRIADES.Characteristics.${key}`),
          selected: normalized.linkedCharacteristic === key
        })),
        skillTypeOptions: SKILL_TYPE_KEYS.map(key => ({
          key,
          label: game.i18n.localize(`MYRIADES.SkillType.${key}`),
          selected: normalized.skillType === key
        }))
      };
    });
    context.pathBundledSkillsCount = context.pathBundledSkills.length;

    // Effect modifiers for effect item type
    const effectModifiers = Array.isArray(context.system.modifiers) ? context.system.modifiers : [];
    context.effectModifiers = effectModifiers.map((mod, idx) => ({
      ...mod,
      idx,
      target: mod.target ?? "puissance",
      mode: mod.mode ?? "add",
      value: Number(mod.value ?? 0),
      perGrade: mod.perGrade === true,
      grade: Number(mod.grade ?? 1)
    }));

    return context;
  }

  activateListeners(html) {
    super.activateListeners(html);
    if (!this.isEditable) return;

    // Path-specific listeners
    if (this.item.type === "path") {
      html.find(".path-bundled-list")
        .on("dragover", ev => {
          ev.preventDefault();
          ev.currentTarget.classList.add("drag-over");
        })
        .on("dragleave", ev => {
          ev.currentTarget.classList.remove("drag-over");
        })
        .on("drop", async ev => {
          ev.preventDefault();
          ev.stopPropagation();
          ev.currentTarget.classList.remove("drag-over");

          const nativeEvent = ev.originalEvent ?? ev;
          const data = TextEditor.getDragEventData(nativeEvent);
          if (data.type !== "Item") return;

          const droppedItem = await resolveDroppedItem(data, this.item.parent);
          if (!droppedItem || droppedItem.type !== "skill") return;

          const result = await addSkillItemToPath(this.item, droppedItem, { rebindLooseSkill: true });
          if (!result.added) {
            ui.notifications?.info(game.i18n.localize("MYRIADES.Path.dropAlreadyIncluded"));
          }
        });

      html.find(".path-bundle-add").click(async ev => {
        ev.preventDefault();
        const bundledSkills = Array.isArray(this.item.system.bundledSkills)
          ? this.item.system.bundledSkills.map((skill, idx) => normalizeBundledSkill(skill, idx))
          : [];

        bundledSkills.push(buildDefaultBundledSkill());
        await this.item.update({ "system.bundledSkills": bundledSkills });
      });

      html.find(".path-bundle-delete").click(async ev => {
        ev.preventDefault();
        const index = Number(ev.currentTarget.dataset.index ?? -1);
        if (index < 0) return;

        const bundledSkills = Array.isArray(this.item.system.bundledSkills)
          ? this.item.system.bundledSkills.map((skill, idx) => normalizeBundledSkill(skill, idx))
          : [];

        bundledSkills.splice(index, 1);
        await this.item.update({ "system.bundledSkills": bundledSkills });
      });
    }

    // Effect-specific listeners
    if (this.item.type === "effect") {
      html.find(".add-modifier").click(async ev => {
        ev.preventDefault();
        const modifiers = Array.isArray(this.item.system.modifiers) ? [...this.item.system.modifiers] : [];
        modifiers.push({
          target: "puissance",
          mode: "add",
          value: 0,
          perGrade: false,
          grade: 1
        });
        await this.item.update({ "system.modifiers": modifiers });
      });

      html.find(".remove-modifier").click(async ev => {
        ev.preventDefault();
        const idx = Number(ev.currentTarget.dataset.idx ?? -1);
        if (idx < 0) return;

        const modifiers = Array.isArray(this.item.system.modifiers) ? [...this.item.system.modifiers] : [];
        modifiers.splice(idx, 1);
        await this.item.update({ "system.modifiers": modifiers });
      });

      html.find(".modifier-per-grade").change(async ev => {
        const idx = Number(ev.currentTarget.closest(".modifier-row").dataset.modifierIdx ?? -1);
        if (idx < 0) return;

        const modifiers = Array.isArray(this.item.system.modifiers) ? [...this.item.system.modifiers] : [];
        if (modifiers[idx]) {
          modifiers[idx] = { ...modifiers[idx], perGrade: ev.currentTarget.checked };
          await this.item.update({ "system.modifiers": modifiers });
        }
      });
    }
  }

  async _onDrop(event) {
    if (this.item.type !== "path") return super._onDrop(event);

    const nativeEvent = event.originalEvent ?? event;
    const data = TextEditor.getDragEventData(nativeEvent);
    if (data.type !== "Item") return super._onDrop(event);

    const droppedItem = await resolveDroppedItem(data, this.item.parent);
    if (!droppedItem || droppedItem.type !== "skill") return super._onDrop(event);

    const result = await addSkillItemToPath(this.item, droppedItem, { rebindLooseSkill: true });
    if (!result.added) {
      ui.notifications?.info(game.i18n.localize("MYRIADES.Path.dropAlreadyIncluded"));
    }
    return false;
  }

  async _updateObject(event, formData) {
    if (this.item.type !== "path") return super._updateObject(event, formData);

    const expanded = foundry.utils.expandObject(formData);
    const rawBundledSkills = expanded.system?.bundledSkills;

    const bundledAsArray = rawBundledSkills === undefined
      ? []
      : Array.isArray(rawBundledSkills)
        ? rawBundledSkills
        : Object.entries(rawBundledSkills)
          .sort((a, b) => Number(a[0]) - Number(b[0]))
          .map(([, value]) => value);

    expanded.system = expanded.system ?? {};
    expanded.system.bundledSkills = bundledAsArray
      .map((skill, index) => normalizeBundledSkill(skill, index))
      .filter(skill => !!skill.name);

    expanded.system.grade = normalizeItemGrade(expanded.system.grade, { maxGrade: 5 });

    return super._updateObject(event, foundry.utils.flattenObject(expanded));
  }
}
