const { HandlebarsApplicationMixin } = foundry.applications.api;
const { ActorSheetV2, ItemSheetV2 }  = foundry.applications.sheets;

/* -------------------------------------------- */
/*  Character Sheet                             */
/* -------------------------------------------- */

export class UA2eCharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
  static DEFAULT_OPTIONS = {
    classes: ["ua2e", "sheet", "actor", "character", "vertical-tabs"],
    position: { width: 820, height: 800 },
    actions: {
      editPortrait:       UA2eCharacterSheet.#onEditPortrait,
      rollStat:           UA2eCharacterSheet.#onRollStat,
      rollSkill:          UA2eCharacterSheet.#onRollSkill,
      rollWeaponDamage:   UA2eCharacterSheet.#onRollWeaponDamage,
      rollInitiative:     UA2eCharacterSheet.#onRollInitiative,
      rollStressCheck:    UA2eCharacterSheet.#onRollStressCheck,
      createSkill:        UA2eCharacterSheet.#onCreateSkill,
      createWeapon:       UA2eCharacterSheet.#onCreateWeapon,
      createStrangeness:  UA2eCharacterSheet.#onCreateStrangeness,
      deleteWeapon:       UA2eCharacterSheet.#onDeleteWeapon,
      editWeapon:         UA2eCharacterSheet.#onEditWeapon,
      deleteSkill:        UA2eCharacterSheet.#onDeleteSkill,
      editSkill:          UA2eCharacterSheet.#onEditSkill,
      createSpell:        UA2eCharacterSheet.#onCreateSpell,
      deleteSpell:        UA2eCharacterSheet.#onDeleteSpell,
      editSpell:          UA2eCharacterSheet.#onEditSpell,
      createChannel:      UA2eCharacterSheet.#onCreateChannel,
      deleteChannel:      UA2eCharacterSheet.#onDeleteChannel,
      editChannel:        UA2eCharacterSheet.#onEditChannel,
      createStressAffliction: UA2eCharacterSheet.#onCreateStressAffliction,
      deleteStressAffliction: UA2eCharacterSheet.#onDeleteStressAffliction,
      editStressAffliction: UA2eCharacterSheet.#onEditStressAffliction,
      toggleObsession:    UA2eCharacterSheet.#onToggleObsession,
      addHardenedNotch:   UA2eCharacterSheet.#onAddHardenedNotch,
      addFailedNotch:     UA2eCharacterSheet.#onAddFailedNotch,
      removeHardenedNotch: UA2eCharacterSheet.#onRemoveHardenedNotch,
      removeFailedNotch:  UA2eCharacterSheet.#onRemoveFailedNotch
    },
    form: {
      submitOnChange: true,
      closeOnSubmit:  false
    },
    window: {
      resizable: true
    }
  };

  static PARTS = {
    header: { template: "systems/unknown-armies-v2/templates/actor/character-header.hbs" },
    tabs:   { template: "systems/unknown-armies-v2/templates/actor/character-sidebar-tabs.hbs" },
    stats:  { template: "systems/unknown-armies-v2/templates/actor/character-stats.hbs",  scrollable: [""] },
    weapons:{ template: "systems/unknown-armies-v2/templates/actor/character-weapons.hbs", scrollable: [""] },
    adept:  { template: "systems/unknown-armies-v2/templates/actor/character-adept.hbs",  scrollable: [""] },
    avatar: { template: "systems/unknown-armies-v2/templates/actor/character-avatar.hbs", scrollable: [""] },
    stress:{ template: "systems/unknown-armies-v2/templates/actor/character-madness.hbs",scrollable: [""] },
    bio:    { template: "systems/unknown-armies-v2/templates/actor/character-bio.hbs",    scrollable: [""] }
  };

  tabGroups = {
    sheet: "stats"
  };

  get title() {
    return this.actor?.name ?? super.title;
  }

  /** @override */
  async _prepareContext(options) {
    const context = await super._prepareContext(options);
    const actor  = this.actor;
    const system = actor.system;

    // Build stat labels with rolling context
    const stats = {};
    for (const [key, data] of Object.entries(system.stats)) {
      stats[key] = {
        ...data,
        label: game.i18n.localize(`UA2E.Stats.${key.capitalize()}`),
        key
      };
    }

    // Build skill list grouped by stat, with obsession flag resolved
    const obsessionId  = system.obsessionSkillId;
    const skillsByType = { body: [], speed: [], mind: [], soul: [] };

    for (const item of actor.items) {
      if (item.type === "skill") {
        if (item.name.trim().toLowerCase() === "initiative") continue;
        const s = item.system;
        const isObsession = item.id === obsessionId;
        skillsByType[s.stat]?.push({
          id:          item.id,
          name:        item.name,
          value:       s.value,
          displayValue: `${s.value}%`,
          stat:        s.stat,
          isFree:      s.isFree,
          isObsession,
          notes:       s.notes,
          rollAction:  "rollSkill"
        });
      } else if (item.type === "strangeness") {
        // Strangeness items live in the soul column, tagged so they can be styled differently
        skillsByType.soul.push({
          id:              item.id,
          name:            item.name,
          value:           item.system.value,
          displayValue:    `${item.system.value}%`,
          stat:            "soul",
          isFree:          false,
          isObsession:     item.id === obsessionId,
          notes:           item.system.school,
          isStrangeness:   true,
          strangenessType: item.system.strangenessType,
          rollAction:      "rollSkill"
        });
      }
    }

    const initiative = Math.floor(system.stats.speed.value / 2);
    skillsByType.speed.unshift({
      id: "initiative",
      name: game.i18n.localize("UA2E.Initiative.SkillName"),
      value: initiative,
      displayValue: String(initiative),
      stat: "speed",
      isFree: true,
      isObsession: false,
      isDerived: true,
      rollAction: "rollInitiative"
    });

    const skillColumns = ["body", "speed", "mind", "soul"].map(key => ({
      key,
      label: game.i18n.localize(`UA2E.Stats.${key.capitalize()}`),
      statValue: system.stats[key]?.value ?? 0,
      skills: skillsByType[key] ?? []
    }));

    const objectItems = actor.items
      .filter((item) => ["gear", "weapon"].includes(item.type))
      .map((item) => {
        const category = item.system.category || (item.type === "weapon" ? "weapon" : "general");
        const mode = item.system.weaponMode || "firearm";
        const parsedMax = Number.parseInt(item.system.maxDamage, 10);
        const parsedBonus = Number.parseInt(item.system.weaponBonus, 10);
        const legacyDamage = Number.parseInt(item.system.damage, 10);
        const maxDamage = Number.isFinite(parsedMax)
          ? parsedMax
          : (Number.isFinite(legacyDamage) ? legacyDamage : 50);
        const weaponBonus = Number.isFinite(parsedBonus)
          ? parsedBonus
          : 3;

        return {
          id: item.id,
          name: item.name,
          category,
          weaponMode: mode,
          maxDamage,
          weaponBonus,
          armorPiercing: !!item.system.armorPiercing,
          description: item.system.description,
          notes: item.system.notes,
          damage: item.system.damage
        };
      });

    const weaponItems = objectItems.filter((item) => item.category === "weapon");
    const nonWeaponItems = objectItems.filter((item) => item.category !== "weapon");

    // ---- Adept context ----
    const obsessionItem = actor.items.get(obsessionId);
    const isSoulObsession = obsessionItem
      && (
        obsessionItem.type === "strangeness"
        || (obsessionItem.type === "skill" && obsessionItem.system.stat === "soul")
      );

    const obsessionValue = obsessionItem
      ? (obsessionItem.type === "strangeness" ? obsessionItem.system.value : obsessionItem.system.value)
      : null;

    const adeptSkill = (system.isAdept && isSoulObsession)
      ? { id: obsessionItem.id, name: obsessionItem.name, value: obsessionValue }
      : null;

    const spells = actor.items
      .filter(i => i.type === "spell")
      .map(i => ({
        id:           i.id,
        name:         i.name,
        chargeType:   i.system.chargeType,
        chargeAmount: i.system.chargeAmount,
        costLabel:    i.system.costLabel || `${i.system.chargeAmount} ${i.system.chargeType}`
      }))
      .sort((a, b) => {
        const order = { minor: 0, significant: 1, major: 2 };
        return (order[a.chargeType] ?? 3) - (order[b.chargeType] ?? 3);
      });

    // ---- Avatar context ----
    const soulSkillChoices = Object.values(skillsByType.soul).map(skill => ({
      id: skill.id,
      name: skill.name
    }));
    const avatarPathSkill = actor.items.get(system.avatarPathSkillId);
    const avatarSkill = (system.isAvatar && avatarPathSkill && ["skill", "strangeness"].includes(avatarPathSkill.type))
      ? {
          id: avatarPathSkill.id,
          name: system.avatarPath || avatarPathSkill.name,
          value: avatarPathSkill.system.value
        }
      : null;
    const pathSkillValue = avatarSkill?.value ?? 0;

    const avatarChannels = actor.items
      .filter(i => i.type === "avatarChannel")
      .map(i => ({
        id:          i.id,
        name:        i.name,
        minSkill:    i.system.minSkill,
        unlocked:    pathSkillValue >= i.system.minSkill
      }))
      .sort((a, b) => a.minSkill - b.minSkill);

    const wp = system.woundPoints;
    const woundExactPercent = wp.max > 0 ? Math.round((wp.value / wp.max) * 100) : 0;
    const woundRatio = wp.max > 0 ? (wp.value / wp.max) : 0;

    let woundApproxPercent = 0;
    let woundStateClass = "dead";
    let woundStateLabel = game.i18n.localize("UA2E.Wounds.Dead");
    if (woundRatio > 0.7) {
      woundApproxPercent = 85;
      woundStateClass = "healthy";
      woundStateLabel = game.i18n.localize("UA2E.Wounds.Healthy");
    } else if (woundRatio > 0.4) {
      woundApproxPercent = 60;
      woundStateClass = "hurt";
      woundStateLabel = game.i18n.localize("UA2E.Wounds.Hurt");
    } else if (woundRatio > 0.2) {
      woundApproxPercent = 35;
      woundStateClass = "bad";
      woundStateLabel = game.i18n.localize("UA2E.Wounds.Bad");
    } else if (woundRatio > 0) {
      woundApproxPercent = 15;
      woundStateClass = "critical";
      woundStateLabel = game.i18n.localize("UA2E.Wounds.Critical");
    }

    const stressAfflictions = actor.items
      .filter(item => item.type === "stressAffliction")
      .map(item => ({
        id: item.id,
        name: item.name,
        meter: item.system.meter,
        meterLabel: game.i18n.localize(`UA2E.Stress.${item.system.meter.capitalize()}`),
        severity: item.system.severity,
        ongoing: item.system.ongoing
      }));

    // Stress meters
    const stressMeters = Object.entries(system.stressMeters).map(([key, data]) => ({
      key,
      label:    game.i18n.localize(`UA2E.Stress.${key.capitalize()}`),
      hardened: data.hardened,
      failed:   data.failed,
      hardenedArr: Array.from({ length: 10 }, (_, i) => i < data.hardened),
      failedArr:   Array.from({ length: 5 }, (_, i) => i < data.failed)
    }));

    return {
      ...context,
      actor,
      system,
      stats,
      skillColumns,
      skillsByType,
      weaponItems,
      nonWeaponItems,
      stressMeters,
      stressAfflictions,
      // Adept
      adeptSkill,
      spells,
      adeptTaboo:              system.adeptTaboo,
      adeptChargesMinor:       system.adeptChargesMinor,
      adeptChargesSignificant: system.adeptChargesSignificant,
      adeptChargesMajor:       system.adeptChargesMajor,
      adeptChargesHowTo:       system.adeptChargesHowTo,
      // Avatar
      avatarSkill,
      avatarChannels,
      pathSkillValue,
      soulSkillChoices,
      avatarPath: system.avatarPath,
      avatarPathSkillId: system.avatarPathSkillId,
      avatarDescription: system.avatarDescription,
      avatarTaboo:       system.avatarTaboo,
      // Wounds
      woundPoints:   system.woundPoints,
      woundExactPercent,
      woundApproxPercent,
      woundStateClass,
      woundStateLabel,
      woundBarPercent: game.user.isGM ? woundExactPercent : woundApproxPercent,
      passions:      system.passions,
      obsession:     system.obsession,
      isAdept:   !!system.isAdept,
      isAvatar:  !!system.isAvatar,
      maxSkillRating: system.maxSkillRating,
      isGM: game.user.isGM,
      editable:  this.isEditable,
      owner: this.actor?.isOwner ?? false,
      activeSheetTab: this.tabGroups.sheet,
      tabs: this.#getTabs()
    };
  }

  #getTabs() {
    const cur = this.tabGroups.sheet;
    const sys = this.actor.system;
    const iconRoot = "systems/unknown-armies-v2/assets/tab-icons";

    const all = [
      {
        id: "stats",
        group: "sheet",
        label: game.i18n.localize("UA2E.Tabs.Stats"),
        active: cur === "stats",
        cssClass: cur === "stats" ? "active" : "",
        icon: `${iconRoot}/stats.png`,
        variant: "postit-sand"
      },
      {
        id: "weapons",
        group: "sheet",
        label: game.i18n.localize("UA2E.Tabs.Weapons"),
        active: cur === "weapons",
        cssClass: cur === "weapons" ? "active" : "",
        icon: `${iconRoot}/bag.png`,
        variant: "postit-olive"
      },
      {
        id: "adept",
        group: "sheet",
        label: game.i18n.localize("UA2E.Tabs.Adept"),
        active: cur === "adept",
        cssClass: cur === "adept" ? "active" : "",
        icon: `${iconRoot}/adept.png`,
        variant: "postit-amber",
        hidden: !sys.isAdept
      },
      {
        id: "avatar",
        group: "sheet",
        label: game.i18n.localize("UA2E.Tabs.Avatar"),
        active: cur === "avatar",
        cssClass: cur === "avatar" ? "active" : "",
        icon: `${iconRoot}/avatar.png`,
        variant: "postit-rose",
        hidden: !sys.isAvatar
      },
      {
        id: "stress",
        group: "sheet",
        label: game.i18n.localize("UA2E.Tabs.Stress"),
        active: cur === "stress",
        cssClass: cur === "stress" ? "active" : "",
        icon: `${iconRoot}/stress.png`,
        variant: "postit-blue"
      },
      {
        id: "bio",
        group: "sheet",
        label: game.i18n.localize("UA2E.Tabs.Bio"),
        active: cur === "bio",
        cssClass: cur === "bio" ? "active" : "",
        icon: `${iconRoot}/bio.png`,
        variant: "postit-sage"
      }
    ];

    // If current tab is now hidden (type unchecked), fall back to stats
    const visible = all.filter(t => !t.hidden);
    if (!visible.some(t => t.id === cur)) {
      this.tabGroups.sheet = "stats";
      return visible.map(t => ({ ...t, active: t.id === "stats", cssClass: t.id === "stats" ? "active" : "" }));
    }

    return visible;
  }

  /* ---- Static Action Handlers ---- */

  static async #onEditPortrait() {
    const actor = this.actor;
    new FilePicker({
      type: "image",
      current: actor.img,
      callback: async path => actor.update({ img: path })
    }).browse();
  }

  static async #onRollStat(event, target) {
    const statKey = target.dataset.stat;
    await this.actor.rollStat(statKey);
  }

  static async #onRollSkill(event, target) {
    const skillId = target.closest("[data-item-id]")?.dataset.itemId;
    if (skillId) await this.actor.rollSkill(skillId);
  }

  static async #onRollWeaponDamage(event, target) {
    const itemId = target.closest("[data-item-id]")?.dataset.itemId;
    if (!itemId) return;

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

    const category = item.system.category || (item.type === "weapon" ? "weapon" : "general");
    if (category !== "weapon") return;

    const weaponMode = item.system.weaponMode || "firearm";
    const legacyDamage = Number.parseInt(item.system.damage, 10);
    const maxDamage = Number.isFinite(Number.parseInt(item.system.maxDamage, 10))
      ? Number.parseInt(item.system.maxDamage, 10)
      : (Number.isFinite(legacyDamage) ? legacyDamage : 50);
    const weaponBonus = Number.isFinite(Number.parseInt(item.system.weaponBonus, 10))
      ? Number.parseInt(item.system.weaponBonus, 10)
      : 3;

    const formContent = `
      <form class="ua2e-weapon-damage-dialog">
        <div class="form-group">
          <label>${game.i18n.localize("UA2E.Weapons.AttackRoll")}</label>
          <input type="number" name="attackRoll" value="01" min="1" max="100" />
        </div>
        <p class="hint">${game.i18n.localize("UA2E.Weapons.AttackRollHint")}</p>
        ${weaponMode === "firearm" ? `<div class="form-group checkbox-group">
          <label>${game.i18n.localize("UA2E.Weapon.ArmorPiercing")}</label>
          <input type="checkbox" name="armorPiercing" ${item.system.armorPiercing ? "checked" : ""} />
        </div>` : ""}
      </form>
    `;

    const input = await new Promise(resolve => {
      new Dialog({
        title: game.i18n.format("UA2E.Weapons.RollDamage", { name: item.name }),
        content: formContent,
        buttons: {
          roll: {
            icon: '<i class="fas fa-dice-d10"></i>',
            label: game.i18n.localize("UA2E.Roll.Roll"),
            callback: html => {
              const form = html[0].querySelector("form");
              const raw = Number.parseInt(form.attackRoll.value, 10);
              const attackRoll = Math.clamp(Number.isFinite(raw) ? raw : 1, 1, 100);
              const armorPiercing = !!form.querySelector('input[name="armorPiercing"]')?.checked;
              resolve({ attackRoll, armorPiercing });
            }
          },
          cancel: {
            icon: '<i class="fas fa-times"></i>',
            label: game.i18n.localize("Cancel"),
            callback: () => resolve(null)
          }
        },
        default: "roll"
      }).render(true);
    });

    if (!input) return;

    const attackRoll = input.attackRoll;
    const displayRoll = attackRoll === 100 ? "00" : String(attackRoll).padStart(2, "0");
    const isCrit = attackRoll === 1;
    const isFumble = attackRoll === 100;
    const normalised = attackRoll === 100 ? 0 : attackRoll;
    const tens = Math.floor(normalised / 10);
    const ones = normalised % 10;
    const isMatch = (tens === ones) && !isCrit && !isFumble;

    let damage = 0;
    let ruleText = "";
    const notes = [];

    if (weaponMode === "firearm") {
      if (isFumble) {
        damage = 0;
        notes.push(game.i18n.localize("UA2E.Weapons.FirearmFumble"));
      } else if (isCrit) {
        damage = maxDamage;
        notes.push(game.i18n.localize("UA2E.Weapons.FirearmCrit"));
      } else {
        damage = Math.min(attackRoll, maxDamage);
      }

      if (input.armorPiercing) {
        damage = Math.floor(damage / 2);
        notes.push(game.i18n.localize("UA2E.Weapons.ArmorPiercingRule"));
      }

      if (isMatch) {
        notes.push(game.i18n.localize("UA2E.Weapons.FirearmMatch"));
      }

      ruleText = game.i18n.format("UA2E.Weapons.FirearmRule", {
        roll: displayRoll,
        max: maxDamage,
        damage
      });
    } else {
      const baseDamage = tens + ones;
      damage = baseDamage + weaponBonus;
      ruleText = game.i18n.format("UA2E.Weapons.MeleeRule", {
        roll: displayRoll,
        base: baseDamage,
        bonus: weaponBonus,
        damage
      });
      if (isMatch) {
        notes.push(game.i18n.localize("UA2E.Weapons.MeleeMatch"));
      }
      if (isFumble) {
        notes.push(game.i18n.localize("UA2E.Weapons.MeleeFumble"));
      }
    }

    const content = `
      <div class="ua2e-roll-card success">
        <div class="roll-header">
          <span class="roll-label">${foundry.utils.escapeHTML(item.name)}</span>
          <span class="check-type-badge">${game.i18n.localize("UA2E.Weapons.DamageResult")}</span>
        </div>
        <div class="roll-body">
          <div class="roll-outcome outcome-success">${game.i18n.format("UA2E.Weapons.DamageTotal", { damage })}</div>
          <p>${ruleText}</p>
          ${notes.length ? `<ul>${notes.map(note => `<li>${note}</li>`).join("")}</ul>` : ""}
        </div>
      </div>
    `;

    await ChatMessage.implementation.create({
      speaker: ChatMessage.getSpeaker({ actor: this.actor }),
      content
    });
  }

  static async #onRollInitiative() {
    await this.actor.rollInitiative();
  }

  static async #onRollStressCheck(event, target) {
    const meter = target.dataset.meter;
    if (meter) await this.actor.rollStressCheck(meter);
  }

  static async #onCreateSkill(event, target) {
    const stat = target?.dataset?.stat ?? "body";
    await this.actor.createEmbeddedDocuments("Item", [{
      name: game.i18n.localize("UA2E.Skill.NewSkill"),
      type: "skill",
      system: { stat, value: 15 }
    }]);
  }

  static async #onCreateWeapon(event, target) {
    const category = target?.dataset?.category === "weapon" ? "weapon" : "general";
    await this.actor.createEmbeddedDocuments("Item", [{
      name: category === "weapon"
        ? game.i18n.localize("UA2E.Weapons.NewWeapon")
        : game.i18n.localize("UA2E.Weapons.New"),
      type: "gear",
      system: {
        category,
        weaponMode: "firearm",
        maxDamage: 50,
        weaponBonus: 3,
        damage: "",
        armorPiercing: false,
        description: "",
        notes: ""
      }
    }]);
  }

  static async #onCreateStrangeness() {
    const type = this.actor.system.isAvatar ? "avatar" : "magick";
    await this.actor.createEmbeddedDocuments("Item", [{
      name: game.i18n.localize("UA2E.Strangeness.New"),
      type: "strangeness",
      system: { strangenessType: type, school: "", value: 15 }
    }]);
  }

  static async #onDeleteSkill(event, target) {
    const itemId = target.closest("[data-item-id]")?.dataset.itemId;
    if (!itemId) return;
    await Dialog.confirm({
      title:   game.i18n.localize("UA2E.Skill.DeleteTitle"),
      content: game.i18n.localize("UA2E.Skill.DeletePrompt"),
      yes:     async () => this.actor.deleteEmbeddedDocuments("Item", [itemId])
    });
  }

  static async #onEditSkill(event, target) {
    const itemId = target.closest("[data-item-id]")?.dataset.itemId;
    const item   = this.actor.items.get(itemId);
    if (item) item.sheet.render(true);
  }

  static async #onDeleteWeapon(event, target) {
    const itemId = target.closest("[data-item-id]")?.dataset.itemId;
    if (!itemId) return;
    await Dialog.confirm({
      title: game.i18n.localize("UA2E.Weapons.DeleteTitle"),
      content: game.i18n.localize("UA2E.Weapons.DeletePrompt"),
      yes: async () => this.actor.deleteEmbeddedDocuments("Item", [itemId])
    });
  }

  static async #onEditWeapon(event, target) {
    const itemId = target.closest("[data-item-id]")?.dataset.itemId;
    const item = this.actor.items.get(itemId);
    if (item) item.sheet.render(true);
  }

  static async #onCreateStressAffliction() {
    await this.actor.createEmbeddedDocuments("Item", [{
      name: game.i18n.localize("UA2E.StressAffliction.New"),
      type: "stressAffliction",
      system: { meter: "self", severity: 1, ongoing: true }
    }]);
  }

  static async #onDeleteStressAffliction(event, target) {
    const itemId = target.closest("[data-item-id]")?.dataset.itemId;
    if (!itemId) return;
    await Dialog.confirm({
      title: game.i18n.localize("UA2E.StressAffliction.DeleteTitle"),
      content: game.i18n.localize("UA2E.StressAffliction.DeletePrompt"),
      yes: async () => this.actor.deleteEmbeddedDocuments("Item", [itemId])
    });
  }

  static async #onEditStressAffliction(event, target) {
    const itemId = target.closest("[data-item-id]")?.dataset.itemId;
    const item = this.actor.items.get(itemId);
    if (item) item.sheet.render(true);
  }

  static async #onToggleObsession(event, target) {
    const itemId = target.closest("[data-item-id]")?.dataset.itemId;
    if (!itemId) return;
    const item = this.actor.items.get(itemId);
    if (!item || !["skill", "strangeness"].includes(item.type)) return;
    const current = this.actor.system.obsessionSkillId;
    await this.actor.update({
      "system.obsessionSkillId": current === itemId ? "" : itemId
    });
  }

  static async #onAddHardenedNotch(event, target) {
    const meter = target.dataset.meter;
    await this.actor.addHardenedNotch(meter);
  }

  static async #onAddFailedNotch(event, target) {
    const meter = target.dataset.meter;
    await this.actor.addFailedNotch(meter);
  }

  static async #onRemoveHardenedNotch(event, target) {
    const meter   = target.dataset.meter;
    const current = this.actor.system.stressMeters[meter]?.hardened ?? 0;
    await this.actor.update({
      [`system.stressMeters.${meter}.hardened`]: Math.max(0, current - 1)
    });
  }

  static async #onRemoveFailedNotch(event, target) {
    const meter   = target.dataset.meter;
    const current = this.actor.system.stressMeters[meter]?.failed ?? 0;
    await this.actor.update({
      [`system.stressMeters.${meter}.failed`]: Math.max(0, current - 1)
    });
  }

  /* ---- Spell actions ---- */

  static async #onCreateSpell() {
    await this.actor.createEmbeddedDocuments("Item", [{
      name: game.i18n.localize("UA2E.Spell.New"),
      type: "spell",
      system: { chargeType: "minor", chargeAmount: 1 }
    }]);
  }

  static async #onDeleteSpell(event, target) {
    const itemId = target.closest("[data-item-id]")?.dataset.itemId;
    if (!itemId) return;
    await Dialog.confirm({
      title:   game.i18n.localize("UA2E.Spell.DeleteTitle"),
      content: game.i18n.localize("UA2E.Spell.DeletePrompt"),
      yes:     async () => this.actor.deleteEmbeddedDocuments("Item", [itemId])
    });
  }

  static async #onEditSpell(event, target) {
    const itemId = target.closest("[data-item-id]")?.dataset.itemId;
    const item   = this.actor.items.get(itemId);
    if (item) item.sheet.render(true);
  }

  /* ---- Avatar Channel actions ---- */

  static async #onCreateChannel() {
    await this.actor.createEmbeddedDocuments("Item", [{
      name: game.i18n.localize("UA2E.Channel.New"),
      type: "avatarChannel",
      system: { minSkill: 1 }
    }]);
  }

  static async #onDeleteChannel(event, target) {
    const itemId = target.closest("[data-item-id]")?.dataset.itemId;
    if (!itemId) return;
    await Dialog.confirm({
      title:   game.i18n.localize("UA2E.Channel.DeleteTitle"),
      content: game.i18n.localize("UA2E.Channel.DeletePrompt"),
      yes:     async () => this.actor.deleteEmbeddedDocuments("Item", [itemId])
    });
  }

  static async #onEditChannel(event, target) {
    const itemId = target.closest("[data-item-id]")?.dataset.itemId;
    const item   = this.actor.items.get(itemId);
    if (item) item.sheet.render(true);
  }
}

/* -------------------------------------------- */
/*  Skill Item Sheet                            */
/* -------------------------------------------- */

export class UA2eSkillSheet extends HandlebarsApplicationMixin(ItemSheetV2) {
  static DEFAULT_OPTIONS = {
    classes: ["ua2e", "sheet", "item", "skill"],
    position: { width: 450, height: 400 },
    form: {
      submitOnChange: true,
      closeOnSubmit:  false
    },
    window: { resizable: false }
  };

  static PARTS = {
    form: { template: "systems/unknown-armies-v2/templates/item/skill-sheet.hbs" }
  };

  async _prepareContext(options) {
    const context = await super._prepareContext(options);
    return {
      ...context,
      item:   this.item,
      system: this.item.system,
      statChoices: {
        body:  game.i18n.localize("UA2E.Stats.Body"),
        speed: game.i18n.localize("UA2E.Stats.Speed"),
        mind:  game.i18n.localize("UA2E.Stats.Mind"),
        soul:  game.i18n.localize("UA2E.Stats.Soul")
      }
    };
  }
}

/* -------------------------------------------- */
/*  Gear Item Sheet                             */
/* -------------------------------------------- */

export class UA2eGearSheet extends HandlebarsApplicationMixin(ItemSheetV2) {
  static DEFAULT_OPTIONS = {
    classes: ["ua2e", "sheet", "item", "gear"],
    position: { width: 450, height: 420 },
    form: {
      submitOnChange: true,
      closeOnSubmit:  false
    },
    window: { resizable: false }
  };

  static PARTS = {
    form: { template: "systems/unknown-armies-v2/templates/item/weapon-sheet.hbs" }
  };

  get title() {
    return this.item?.name ?? super.title;
  }

  async _prepareContext(options) {
    const context = await super._prepareContext(options);
    const category = this.item.system.category || (this.item.type === "weapon" ? "weapon" : "general");
    const weaponMode = this.item.system.weaponMode || "firearm";
    return {
      ...context,
      item:   this.item,
      system: this.item.system,
      category,
      weaponMode,
      categoryChoices: {
        general: game.i18n.localize("UA2E.Item.CategoryGeneral"),
        weapon: game.i18n.localize("UA2E.Item.CategoryWeapon")
      },
      weaponModeChoices: {
        firearm: game.i18n.localize("UA2E.Weapon.ModeFirearm"),
        melee: game.i18n.localize("UA2E.Weapon.ModeMelee")
      }
    };
  }
}

/* -------------------------------------------- */
/*  Strangeness Item Sheet                      */
/* -------------------------------------------- */

export class UA2eStrangenessSheet extends HandlebarsApplicationMixin(ItemSheetV2) {
  static DEFAULT_OPTIONS = {
    classes: ["ua2e", "sheet", "item", "strangeness"],
    position: { width: 480, height: 500 },
    form: {
      submitOnChange: true,
      closeOnSubmit:  false
    },
    window: { resizable: true }
  };

  static PARTS = {
    form: { template: "systems/unknown-armies-v2/templates/item/strangeness-sheet.hbs" }
  };

  async _prepareContext(options) {
    const context = await super._prepareContext(options);
    return {
      ...context,
      item:   this.item,
      system: this.item.system,
      typeChoices: {
        magick: game.i18n.localize("UA2E.Strangeness.Magick"),
        avatar: game.i18n.localize("UA2E.Strangeness.Avatar")
      }
    };
  }
}

export class UA2eStressAfflictionSheet extends HandlebarsApplicationMixin(ItemSheetV2) {
  static DEFAULT_OPTIONS = {
    classes: ["ua2e", "sheet", "item", "stress-affliction"],
    position: { width: 520, height: 420 },
    form: {
      submitOnChange: true,
      closeOnSubmit: false
    },
    window: { resizable: true }
  };

  static PARTS = {
    form: { template: "systems/unknown-armies-v2/templates/item/stress-affliction-sheet.hbs" }
  };

  async _prepareContext(options) {
    const context = await super._prepareContext(options);
    return {
      ...context,
      item: this.item,
      system: this.item.system,
      editable: this.isEditable,
      meterChoices: {
        violence: game.i18n.localize("UA2E.Stress.Violence"),
        unnatural: game.i18n.localize("UA2E.Stress.Unnatural"),
        helplessness: game.i18n.localize("UA2E.Stress.Helplessness"),
        isolation: game.i18n.localize("UA2E.Stress.Isolation"),
        self: game.i18n.localize("UA2E.Stress.Self")
      }
    };
  }
}

/* -------------------------------------------- */
/*  Spell Item Sheet (Adept)                    */
/* -------------------------------------------- */

export class UA2eSpellSheet extends HandlebarsApplicationMixin(ItemSheetV2) {
  static DEFAULT_OPTIONS = {
    classes: ["ua2e", "sheet", "item", "spell"],
    position: { width: 540, height: 480 },
    form: { submitOnChange: true, closeOnSubmit: false },
    window: { resizable: true }
  };

  static PARTS = {
    form: { template: "systems/unknown-armies-v2/templates/item/spell-sheet.hbs" }
  };

  async _prepareContext(options) {
    const context = await super._prepareContext(options);
    return {
      ...context,
      item:    this.item,
      system:  this.item.system,
      editable: this.isEditable,
      chargeTypeChoices: {
        minor:       game.i18n.localize("UA2E.Charge.Minor"),
        significant: game.i18n.localize("UA2E.Charge.Significant"),
        major:       game.i18n.localize("UA2E.Charge.Major")
      }
    };
  }
}

/* -------------------------------------------- */
/*  Avatar Channel Item Sheet                   */
/* -------------------------------------------- */

export class UA2eAvatarChannelSheet extends HandlebarsApplicationMixin(ItemSheetV2) {
  static DEFAULT_OPTIONS = {
    classes: ["ua2e", "sheet", "item", "avatar-channel"],
    position: { width: 540, height: 440 },
    form: { submitOnChange: true, closeOnSubmit: false },
    window: { resizable: true }
  };

  static PARTS = {
    form: { template: "systems/unknown-armies-v2/templates/item/avatar-channel-sheet.hbs" }
  };

  async _prepareContext(options) {
    const context = await super._prepareContext(options);
    return {
      ...context,
      item:    this.item,
      system:  this.item.system,
      editable: this.isEditable
    };
  }
}
