import { MyriadesActor, MyriadesItem } from "./module/documents.mjs";
import { MyriadesActorSheet, MyriadesItemSheet } from "./module/sheets.mjs";
import { PATH_COMPENDIUM_ITEMS, SKILL_COMPENDIUM_ITEMS } from "./module/compendium-data.mjs";
import {
  CharacterDataModel,
  NpcDataModel,
  SkillDataModel,
  SpellDataModel,
  PathDataModel,
  EquipmentDataModel,
  EffectDataModel
} from "./module/data-models.mjs";

const ActorsCollection = foundry.documents.collections.Actors;
const ItemsCollection = foundry.documents.collections.Items;
const ActorSheetV1 = foundry.appv1.sheets.ActorSheet;
const ItemSheetV1 = foundry.appv1.sheets.ItemSheet;

const CHARACTERISTIC_KEYS = ["puissance", "coordination", "ingeniosite", "perspicacite"];
const SKILL_TYPE_KEYS = ["aptitude", "unique", "racial", "path"];

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 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 toGrantedSkillData(pathItem, bundledSkill) {
  const pathGrade = Math.clamp(Math.trunc(Number(pathItem.system.grade ?? 1)), 1, 5);
  const maxGrade = normalizeMaxGrade(bundledSkill.maxGrade, 5);
  const usesGrades = bundledSkill.usesGrades !== false;
  const grantedGrade = usesGrades
    ? Math.min(maxGrade, Math.max(pathGrade, normalizeItemGrade(bundledSkill.grade, { usesGrades, maxGrade })))
    : 1;
  return {
    name: bundledSkill.name,
    type: "skill",
    system: {
      grade: grantedGrade,
      usesGrades,
      maxGrade,
      description: bundledSkill.description,
      skillType: bundledSkill.skillType,
      linkedCharacteristic: bundledSkill.linkedCharacteristic,
      isRollable: bundledSkill.isRollable,
      skillPointCost: bundledSkill.skillPointCost,
      rollModifier: bundledSkill.rollModifier,
      sourcePathId: pathItem.id,
      sourcePathKey: bundledSkill.key
    }
  };
}

async function syncBundledPathSkills(pathItem) {
  const actor = pathItem.parent;
  if (!(actor instanceof Actor)) return;
  if (pathItem.type !== "path") return;

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

  const existingGrantedSkills = actor.items.filter(item => (
    item.type === "skill" && item.system.sourcePathId === pathItem.id
  ));

  const existingByKey = new Map(
    existingGrantedSkills.map(skill => [String(skill.system.sourcePathKey ?? ""), skill])
  );

  const expectedKeys = new Set();
  const toCreate = [];
  const toUpdate = [];

  for (const bundledSkill of bundledSkills) {
    expectedKeys.add(bundledSkill.key);
    const grantedData = toGrantedSkillData(pathItem, bundledSkill);
    const existing = existingByKey.get(bundledSkill.key);

    if (!existing) {
      toCreate.push(grantedData);
      continue;
    }

    toUpdate.push({
      _id: existing.id,
      name: grantedData.name,
      "system.grade": grantedData.system.grade,
      "system.usesGrades": grantedData.system.usesGrades,
      "system.maxGrade": grantedData.system.maxGrade,
      "system.description": grantedData.system.description,
      "system.skillType": grantedData.system.skillType,
      "system.linkedCharacteristic": grantedData.system.linkedCharacteristic,
      "system.isRollable": grantedData.system.isRollable,
      "system.skillPointCost": grantedData.system.skillPointCost,
      "system.rollModifier": grantedData.system.rollModifier,
      "system.sourcePathId": grantedData.system.sourcePathId,
      "system.sourcePathKey": grantedData.system.sourcePathKey
    });
  }

  const toDelete = existingGrantedSkills
    .filter(skill => !expectedKeys.has(String(skill.system.sourcePathKey ?? "")))
    .map(skill => skill.id);

  if (toCreate.length) await actor.createEmbeddedDocuments("Item", toCreate);
  if (toUpdate.length) await actor.updateEmbeddedDocuments("Item", toUpdate);
  if (toDelete.length) await actor.deleteEmbeddedDocuments("Item", toDelete);
}

async function deleteBundledPathSkills(pathItem) {
  const actor = pathItem.parent;
  if (!(actor instanceof Actor)) return;

  const grantedSkillIds = actor.items
    .filter(item => item.type === "skill" && item.system.sourcePathId === pathItem.id)
    .map(item => item.id);

  if (grantedSkillIds.length) {
    await actor.deleteEmbeddedDocuments("Item", grantedSkillIds);
  }
}

async function seedCompendiumPack(packId, entries) {
  const pack = game.packs.get(packId);
  if (!pack || pack.documentName !== "Item") return 0;

  const existingDocuments = await pack.getDocuments();
  const existingNames = new Set(existingDocuments.map(document => document.name));
  const toCreate = entries.filter(entry => !existingNames.has(entry.name));
  if (!toCreate.length) return 0;

  const wasLocked = pack.locked;
  if (wasLocked) await pack.configure({ locked: false });

  try {
    await Item.implementation.createDocuments(toCreate, { pack: pack.collection });
  } finally {
    if (wasLocked) await pack.configure({ locked: true });
  }

  return toCreate.length;
}

async function updateCompendiumPack(packId, entries, { isPathPack = false } = {}) {
  const pack = game.packs.get(packId);
  if (!pack || pack.documentName !== "Item") return 0;

  const desiredByName = new Map(entries.map(entry => [entry.name, entry]));
  const documents = await pack.getDocuments();

  const updates = [];
  for (const doc of documents) {
    const desired = desiredByName.get(doc.name);
    if (!desired) continue;

    if (!isPathPack) {
      const desiredRollable = desired.system?.isRollable !== false;
      const currentRollable = doc.system?.isRollable !== false;
      if (desiredRollable !== currentRollable) {
        updates.push({ _id: doc.id, "system.isRollable": desiredRollable });
      }
      continue;
    }

    const currentBundled = Array.isArray(doc.system?.bundledSkills) ? doc.system.bundledSkills : [];
    const desiredBundled = Array.isArray(desired.system?.bundledSkills) ? desired.system.bundledSkills : [];

    const desiredByKey = new Map(desiredBundled.map(skill => [String(skill.key ?? ""), skill]));
    const desiredBySkillName = new Map(desiredBundled.map(skill => [String(skill.name ?? ""), skill]));

    let changed = false;
    const nextBundled = currentBundled.map(skill => {
      const byKey = desiredByKey.get(String(skill.key ?? ""));
      const byName = desiredBySkillName.get(String(skill.name ?? ""));
      const desiredSkill = byKey ?? byName;
      if (!desiredSkill) return skill;

      const currentRollable = skill?.isRollable !== false;
      const desiredRollable = desiredSkill?.isRollable !== false;
      if (currentRollable === desiredRollable) return skill;

      changed = true;
      return { ...skill, isRollable: desiredRollable };
    });

    if (changed) {
      updates.push({ _id: doc.id, "system.bundledSkills": nextBundled });
    }
  }

  if (!updates.length) return 0;

  const wasLocked = pack.locked;
  if (wasLocked) await pack.configure({ locked: false });
  try {
    await Item.implementation.updateDocuments(updates, { pack: pack.collection });
  } finally {
    if (wasLocked) await pack.configure({ locked: true });
  }

  return updates.length;
}

async function seedSystemCompendiums() {
  if (!game.user?.isGM) return;

  const skillCount = await seedCompendiumPack(`${game.system.id}.skills`, SKILL_COMPENDIUM_ITEMS);
  const pathCount = await seedCompendiumPack(`${game.system.id}.paths`, PATH_COMPENDIUM_ITEMS);
  const updatedSkills = await updateCompendiumPack(`${game.system.id}.skills`, SKILL_COMPENDIUM_ITEMS);
  const updatedPaths = await updateCompendiumPack(`${game.system.id}.paths`, PATH_COMPENDIUM_ITEMS, {
    isPathPack: true
  });

  if (skillCount || pathCount || updatedSkills || updatedPaths) {
    console.info(
      "Myriades compendiums synced"
      + ` (${skillCount} skills added, ${pathCount} paths added,`
      + ` ${updatedSkills} skills updated, ${updatedPaths} paths updated).`
    );
  }
}

Hooks.once("setup", () => {
  loadTemplates([
    `systems/${game.system.id}/templates/chat/roll-card.hbs`
  ]);
});

Hooks.once("init", () => {
  const GRADE_ROMAN = { 1: "I", 2: "II", 3: "III", 4: "IV", 5: "V" };
  Handlebars.registerHelper("gradeRoman", value => GRADE_ROMAN[Number(value)] ?? String(value));

  game.settings.register("myriades-ind100", "sheetTheme", {
    name: "MYRIADES.Settings.sheetTheme.name",
    hint: "MYRIADES.Settings.sheetTheme.hint",
    scope: "world",
    config: true,
    type: String,
    choices: {
      light: "MYRIADES.Settings.sheetTheme.light",
      dark: "MYRIADES.Settings.sheetTheme.dark"
    },
    default: "dark",
    onChange: () => {
      ui.actors?.render(true);
      ui.items?.render(true);
    }
  });

  game.settings.register("myriades-ind100", "showPlayerXpControls", {
    name: "MYRIADES.Settings.showPlayerXpControls.name",
    hint: "MYRIADES.Settings.showPlayerXpControls.hint",
    scope: "world",
    config: true,
    type: Boolean,
    default: false,
    onChange: () => {
      ui.actors?.render(true);
    }
  });

  CONFIG.Actor.documentClass = MyriadesActor;
  CONFIG.Item.documentClass = MyriadesItem;

  CONFIG.Item.typeIcons = {
    skill:     "fas fa-book",
    equipment: "fas fa-shield-halved",
    path:      "fas fa-star",
    spell:     "fas fa-wand-magic-sparkles",
    effect:    "fas fa-sparkles"
  };

  CONFIG.Actor.dataModels = {
    character: CharacterDataModel,
    npc: NpcDataModel
  };

  CONFIG.Item.dataModels = {
    skill: SkillDataModel,
    spell: SpellDataModel,
    path: PathDataModel,
    equipment: EquipmentDataModel,
    effect: EffectDataModel
  };

  CONFIG.Actor.trackableAttributes = {
    character: {
      bar: ["resources.health", "resources.magic"],
      value: [
        "characteristics.puissance",
        "characteristics.coordination",
        "characteristics.ingeniosite",
        "characteristics.perspicacite"
      ]
    },
    npc: {
      bar: ["resources.health", "resources.magic"],
      value: []
    }
  };

  ActorsCollection.unregisterSheet("core", ActorSheetV1);
  ActorsCollection.registerSheet("myriades-ind100", MyriadesActorSheet, {
    types: ["character", "npc"],
    makeDefault: true
  });

  ItemsCollection.unregisterSheet("core", ItemSheetV1);
  ItemsCollection.registerSheet("myriades-ind100", MyriadesItemSheet, {
    types: ["skill", "spell", "path", "equipment", "effect"],
    makeDefault: true
  });
});

Hooks.once("ready", () => {
  void seedSystemCompendiums();
});

// Replace thumbnail images with FA type icons in the items sidebar
Hooks.on("renderItemDirectory", (_app, html) => {
  const typeIcons = CONFIG.Item.typeIcons ?? {};
  const root = html instanceof HTMLElement ? html : html[0];
  root.querySelectorAll(".directory-item").forEach(el => {
    const itemId = el.dataset.documentId ?? el.dataset.entryId;
    if (!itemId) return;
    const item = game.items.get(itemId);
    if (!item || !typeIcons[item.type]) return;
    const img = el.querySelector(".thumbnail");
    if (!img) return;
    const icon = document.createElement("i");
    icon.className = `${typeIcons[item.type]} myr-type-icon`;
    img.replaceWith(icon);
  });
});

Hooks.on("getItemContextOptions", (_html, entryOptions) => {
  const getEntryId = li => {
    if (!li) return null;
    if (typeof li.data === "function") return li.data("documentId") ?? li.data("entryId");
    if (li.dataset) return li.dataset.documentId ?? li.dataset.entryId;
    const element = li[0];
    return element?.dataset?.documentId ?? element?.dataset?.entryId ?? null;
  };

  entryOptions.push({
    name: game.i18n.localize("MYRIADES.Action.edit"),
    icon: '<i class="fas fa-pen-to-square"></i>',
    condition: li => !!getEntryId(li),
    callback: async li => {
      const itemId = getEntryId(li);
      if (!itemId) return;
      const item = game.items.get(itemId) ?? await game.items.getDocument?.(itemId);
      item?.sheet.render(true);
    }
  });
});

Hooks.on("createItem", async (item, _options, userId) => {
  if (game.userId !== userId) return;
  if (item.type !== "path") return;
  if (!(item.parent instanceof Actor)) return;
  await syncBundledPathSkills(item);
});

Hooks.on("updateItem", async (item, _changes, _options, userId) => {
  if (game.userId !== userId) return;
  if (item.type !== "path") return;
  if (!(item.parent instanceof Actor)) return;
  await syncBundledPathSkills(item);
});

Hooks.on("deleteItem", async (item, _options, userId) => {
  if (game.userId !== userId) return;
  if (item.type !== "path") return;
  if (!(item.parent instanceof Actor)) return;
  await deleteBundledPathSkills(item);
});
