// Rôle : normalise, compacte et sérialise les données toolbox persistées.
const ID_PREFIXES = {
  tbx: "t",
  mod: "m",
  item: "i",
  section: "g",
  image: "s",
  link: "l",
  counter: "c",
  combo: "o",
  calc: "r",
  marker: "k",
  timer: "z",
  task: "a",
  relation: "e",
  stroke: "d",
  equipmentType: "y",
  equipment: "q",
  trait: "v",
  material: "n",
  socketItem: "j",
  socketLink: "u",
  socketBonus: "b"
};
const ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
const NOTEPAD_ALLOWED_COLORS = new Set(["#f5f7ff", "#b4bdd3", "#f6c453", "#22d3ee", "#8b5cf6", "#d946ef", "#101426", "#202745"]);
const NOTEPAD_ALLOWED_TAGS = new Set(["p", "div", "br", "strong", "b", "em", "i", "u", "s", "strike", "ul", "ol", "li", "h3", "h4", "span", "mark"]);
const NOTEPAD_RGB_COLOR_MAP = {
  "rgb(245, 247, 255)": "#f5f7ff",
  "rgb(180, 189, 211)": "#b4bdd3",
  "rgb(246, 196, 83)": "#f6c453",
  "rgb(34, 211, 238)": "#22d3ee",
  "rgb(139, 92, 246)": "#8b5cf6",
  "rgb(217, 70, 239)": "#d946ef",
  "rgb(16, 20, 38)": "#101426",
  "rgb(32, 39, 69)": "#202745"
};
const TIMER_TABS = new Set(["stopwatch", "countdown"]);
const COUNTDOWN_TYPES = new Set(["duration", "daily_time", "time_pattern", "interval"]);
const TIMER_ALERT_MODES = new Set(["off", "visible", "site"]);
const TIMER_INTERVAL_START_MODES = new Set(["now", "time"]);
const TIMER_MIN_AUTO_REFRESH_MS = 5 * 60 * 1000;
const TASK_TYPES = new Set(["unique", "daily", "weekly"]);
const COMBO_DEVICES = new Set(["playstation", "xbox", "switch", "n64", "keyboardMouse"]);
const COMBO_INPUT_KINDS = new Set(["button", "direction", "key", "mouse"]);
const EQUIPMENT_SOCKET_SHAPES = new Set(["ball", "jewel"]);
const EQUIPMENT_SOCKET_COLORS = new Set(["red", "orange", "amber", "yellow", "lime", "green", "teal", "cyan", "blue", "indigo", "violet", "purple", "pink", "rose", "white", "gray", "black"]);
const EQUIPMENT_TYPE_ICONS = new Set(["boot", "chest-armor", "helmet", "glove", "shield", "sword", "gun", "earring", "necklace", "ring"]);
const EQUIPMENT_TRAIT_ICONS = new Set(["sword", "shield"]);
const DEFAULT_TASK_PLANNER_WEEKLY_RESET_DAY = 1;
const DEFAULT_TASK_PLANNER_RESET_TIME = "00:00";
const TOOLBOX_ICON_BASE = "/static/img/toolbox-icons/";
const TOOLBOX_ICON_FILES = [
  "toolbox.png",
  "ball.png",
  "capture.png",
  "card.png",
  "city.png",
  "compass.png",
  "crash.png",
  "detective.png",
  "fire.png",
  "horror.png",
  "jump.png",
  "mining.png",
  "parachute.png",
  "puzzle.png",
  "shield.png",
  "sword.png",
  "target.png",
  "tower.png",
  "wheel.png"
];
const DEFAULT_MODULE_TITLES = {
  notepad: "Bloc notes",
  checklist: "Checklist",
  images: "Images",
  links: "Liens",
  counters: "Compteurs",
  combos: "Combos",
  calculator: "Calculateur",
  table: "Tableau",
  timer: "Timer",
  taskPlanner: "Planificateur de tâches",
  equipmentPlanner: "Planificateur d'équipements",
  imageAnnotation: "Annotation d'images"
};

export const TOOLBOX_ICONS = TOOLBOX_ICON_FILES.map((file) => `${TOOLBOX_ICON_BASE}${file}`);
export const DEFAULT_TOOLBOX_ICON = `${TOOLBOX_ICON_BASE}toolbox.png`;

function randomToken(length = 6) {
  const bytes = new Uint8Array(length);
  if (globalThis.crypto?.getRandomValues) {
    globalThis.crypto.getRandomValues(bytes);
    return [...bytes].map((byte) => ID_ALPHABET[byte % ID_ALPHABET.length]).join("");
  }
  return Math.random().toString(36).slice(2, 2 + length).padEnd(length, "0");
}

export function uid(prefix) {
  return `${ID_PREFIXES[prefix] || "x"}${randomToken()}`;
}

function getDefaultModuleTitle(type) {
  return DEFAULT_MODULE_TITLES[type] || "Outil";
}

export function normalizeToolboxIcon(icon) {
  const value = String(icon || "").trim();
  const file = value.replace(TOOLBOX_ICON_BASE, "").split("/").pop();
  return TOOLBOX_ICON_FILES.includes(file) ? `${TOOLBOX_ICON_BASE}${file}` : DEFAULT_TOOLBOX_ICON;
}

function normalizeToolboxModule(module) {
  if (!module || typeof module !== "object" || !module.type) return null;
  const title = String(module.title || "").trim();
  const normalized = { id: module.id || uid("mod"), type: module.type };
  if (title && title !== getDefaultModuleTitle(module.type)) normalized.title = title;
  if (module.scrollable === true) normalized.scrollable = true;
  return normalized;
}

function uniqueKnownIds(ids, knownIds) {
  const known = new Set(knownIds);
  const seen = new Set();
  return (Array.isArray(ids) ? ids : [])
    .map((id) => String(id || ""))
    .filter((id) => {
      if (!known.has(id) || seen.has(id)) return false;
      seen.add(id);
      return true;
    });
}

function completeModuleOrder(ids, knownIds) {
  const ordered = uniqueKnownIds(ids, knownIds);
  const used = new Set(ordered);
  return [...ordered, ...knownIds.filter((id) => !used.has(id))];
}

function splitModuleOrder(ids) {
  const splitIndex = Math.ceil(ids.length / 2);
  return [ids.slice(0, splitIndex), ids.slice(splitIndex)];
}

export function normalizeModuleOrder(moduleOrder, modules) {
  const moduleIds = modules.map((module) => module.id);
  const one = completeModuleOrder(moduleOrder?.one, moduleIds);
  const hasTwoColumnOrder = Array.isArray(moduleOrder?.two) && Array.isArray(moduleOrder.two[0]) && Array.isArray(moduleOrder.two[1]);
  if (!hasTwoColumnOrder) return { one, two: splitModuleOrder(one) };

  const firstColumn = uniqueKnownIds(moduleOrder.two[0], moduleIds);
  const firstColumnIds = new Set(firstColumn);
  const secondColumn = uniqueKnownIds(moduleOrder.two[1], moduleIds).filter((id) => !firstColumnIds.has(id));
  const used = new Set([...firstColumn, ...secondColumn]);
  const missing = moduleIds.filter((id) => !used.has(id));
  const two = [firstColumn, secondColumn];
  missing.forEach((id) => {
    const targetIndex = two[0].length <= two[1].length ? 0 : 1;
    two[targetIndex].push(id);
  });
  return { one, two };
}

export function remapModuleOrderIds(moduleOrder, moduleIdMap) {
  if (!moduleOrder) return null;
  const remapIds = (ids) => (Array.isArray(ids) ? ids.map((id) => moduleIdMap.get(id)).filter(Boolean) : []);
  return {
    one: remapIds(moduleOrder.one),
    two: [
      remapIds(moduleOrder.two?.[0]),
      remapIds(moduleOrder.two?.[1])
    ]
  };
}

export function normalizeToolbox(toolbox) {
  if (!toolbox || typeof toolbox !== "object") return null;
  const modules = (Array.isArray(toolbox.modules) ? toolbox.modules : []).map(normalizeToolboxModule).filter(Boolean);
  return {
    id: toolbox.id || uid("tbx"),
    name: String(toolbox.name || "Nouvelle toolbox").trim() || "Nouvelle toolbox",
    icon: normalizeToolboxIcon(toolbox.icon),
    moduleColumns: Number(toolbox.moduleColumns) === 1 ? 1 : 2,
    modules,
    moduleOrder: normalizeModuleOrder(toolbox.moduleOrder, modules),
    updatedAt: toolbox.updatedAt || new Date().toISOString()
  };
}

export function compactToolboxForStorage(toolbox) {
  const normalized = normalizeToolbox(toolbox);
  if (!normalized) return null;
  const compact = {
    id: normalized.id,
    name: normalized.name,
    modules: normalized.modules,
    moduleOrder: normalized.moduleOrder,
    updatedAt: normalized.updatedAt
  };
  if (normalized.moduleColumns === 1) compact.moduleColumns = 1;
  if (normalized.icon !== DEFAULT_TOOLBOX_ICON) compact.icon = normalized.icon;
  return compact;
}

export function compactToolboxesForStorage(toolboxes) {
  return (Array.isArray(toolboxes) ? toolboxes : []).map(compactToolboxForStorage).filter(Boolean);
}

export function moduleStorageKey(toolboxId, moduleId) {
  return `${toolboxId}:${moduleId}`;
}

export function globalModuleKey(toolboxId, moduleId) {
  return `${toolboxId}:${moduleId}`;
}

function parsePositiveInt(value, fallback = 1) {
  const parsed = Number.parseInt(value, 10);
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}

function escapeHtml(value) {
  return String(value || "")
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;");
}

function decodeBasicEntities(value) {
  return String(value || "")
    .replace(/&nbsp;/gi, " ")
    .replace(/&lt;/gi, "<")
    .replace(/&gt;/gi, ">")
    .replace(/&quot;/gi, "\"")
    .replace(/&#39;/g, "'")
    .replace(/&amp;/gi, "&");
}

function normalizeStyleAttribute(value) {
  const declarations = String(value || "")
    .split(";")
    .map((item) => item.trim())
    .filter(Boolean);
  const allowed = [];
  declarations.forEach((declaration) => {
    const [property, rawValue] = declaration.split(":").map((item) => item?.trim().toLowerCase());
    if (!["color", "background-color"].includes(property)) return;
    const color = NOTEPAD_RGB_COLOR_MAP[rawValue] || rawValue;
    if (!NOTEPAD_ALLOWED_COLORS.has(color)) return;
    allowed.push(`${property}: ${color}`);
  });
  return allowed.join("; ");
}

export function sanitizeNotepadHtml(value) {
  const source = String(value || "")
    .replace(/<script[\s\S]*?<\/script>/gi, "")
    .replace(/<style[\s\S]*?<\/style>/gi, "");
  let html = "";
  let cursor = 0;
  source.replace(/<\/?([a-z0-9]+)([^>]*)>/gi, (match, rawTag, rawAttributes, offset) => {
    html += escapeHtml(source.slice(cursor, offset));
    cursor = offset + match.length;
    const tag = rawTag.toLowerCase();
    if (!NOTEPAD_ALLOWED_TAGS.has(tag)) return "";
    if (match.startsWith("</")) {
      if (tag !== "br") html += `</${tag}>`;
      return "";
    }
    if (tag === "br") {
      html += "<br>";
      return "";
    }
    const styleMatch = String(rawAttributes || "").match(/\sstyle=(?:"([^"]*)"|'([^']*)')/i);
    const style = normalizeStyleAttribute(styleMatch?.[1] || styleMatch?.[2] || "");
    html += style ? `<${tag} style="${style}">` : `<${tag}>`;
    return "";
  });
  html += escapeHtml(source.slice(cursor));
  return html.trim();
}

function textToNotepadHtml(value) {
  return String(value || "")
    .split(/\r?\n/)
    .map((line) => line ? `<p>${escapeHtml(line)}</p>` : "<p><br></p>")
    .join("");
}

function notepadHtmlToText(value) {
  const withBreaks = String(value || "")
    .replace(/<br\s*\/?>/gi, "\n")
    .replace(/<\/(p|div|h3|h4|li)>/gi, "\n")
    .replace(/<li[^>]*>/gi, "- ");
  return decodeBasicEntities(withBreaks.replace(/<[^>]*>/g, ""))
    .split("\n")
    .map((line) => line.trimEnd())
    .join("\n")
    .replace(/\n{3,}/g, "\n\n")
    .trim();
}

function normalizeIsoDate(value) {
  const text = String(value || "").trim();
  if (!text) return "";
  const timestamp = Date.parse(text);
  return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : "";
}

function normalizeDrawingPoint(point, options = {}) {
  const x = Number(point?.x);
  const y = Number(point?.y);
  if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
  const max = Number(options.max);
  return {
    x: Math.min(Number.isFinite(max) ? max : Number.POSITIVE_INFINITY, Math.max(0, Math.round(x * 100) / 100)),
    y: Math.min(Number.isFinite(max) ? max : Number.POSITIVE_INFINITY, Math.max(0, Math.round(y * 100) / 100))
  };
}

function normalizeDrawingStroke(stroke, options = {}) {
  const points = (Array.isArray(stroke?.points) ? stroke.points : []).map((point) => normalizeDrawingPoint(point, options)).filter(Boolean);
  if (points.length < 2) return null;
  const color = NOTEPAD_ALLOWED_COLORS.has(String(stroke?.color || "").toLowerCase()) ? String(stroke.color).toLowerCase() : "#f6c453";
  const width = Math.min(24, Math.max(1, Number(stroke?.width) || 4));
  return {
    id: stroke?.id || uid("stroke"),
    color,
    width: Math.round(width * 10) / 10,
    points
  };
}

export function normalizeDrawingData(data, options = {}) {
  return {
    strokes: (Array.isArray(data?.strokes) ? data.strokes : []).map((stroke) => normalizeDrawingStroke(stroke, options)).filter(Boolean)
  };
}

export function normalizeNotepadData(data) {
  const source = data && typeof data === "object" ? data : {};
  const legacyText = String(source.text || "");
  const html = sanitizeNotepadHtml(source.html ? source.html : textToNotepadHtml(legacyText));
  const text = notepadHtmlToText(html) || legacyText.trim();
  const updatedAt = normalizeIsoDate(source.updatedAt);
  const drawingMode = source.drawingMode === "permanent" ? "permanent" : "temporary";
  const drawings = normalizeDrawingData(source.drawings);
  return { html, text, updatedAt, drawingMode, drawings };
}

export function clampQty(value, target) {
  const parsed = Number.parseInt(value, 10);
  const safeValue = Number.isFinite(parsed) ? parsed : 0;
  return Math.min(safeValue, Math.max(1, target));
}

function normalizeChecklistItem(item) {
  const qtyTarget = Math.max(1, parsePositiveInt(item?.qtyTarget, 1));
  const qtyCurrent = clampQty(item?.qtyCurrent, qtyTarget);
  return {
    id: item?.id || uid("item"),
    label: String(item?.label || "").trim(),
    qtyTarget,
    qtyCurrent
  };
}

function normalizeChecklistSection(section, fallbackTitle = "") {
  const normalized = {
    id: section?.id || uid("section"),
    title: String(section?.title ?? fallbackTitle).trim(),
    items: (section?.items || []).map(normalizeChecklistItem).filter((item) => item.label)
  };
  if (typeof section?.hideWhenComplete === "boolean") normalized.hideWhenComplete = section.hideWhenComplete;
  if (section?.collapsed === true) normalized.collapsed = true;
  return normalized;
}

export function normalizeChecklistData(data) {
  const hideCompletedSections = Boolean(data?.hideCompletedSections);
  const hideCompletedSectionsFully = Boolean(data?.hideCompletedSectionsFully);
  const sections = Array.isArray(data?.sections)
    ? data.sections.map((section) => normalizeChecklistSection(section)).filter((section) => section.items.length)
    : [];
  const simpleItems = (data?.items || []).map(normalizeChecklistItem).filter((item) => item.label);
  if (!sections.length && simpleItems.length) return { hideCompletedSections, hideCompletedSectionsFully, sections: [{ id: uid("section"), title: "", items: simpleItems }], items: simpleItems };
  const items = sections.flatMap((section) => section.items);
  return { hideCompletedSections, hideCompletedSectionsFully, sections, items };
}

export function normalizeUrl(value) {
  const cleanValue = String(value || "").trim();
  if (!cleanValue) return "";

  try {
    const url = new URL(cleanValue.includes("://") ? cleanValue : `https://${cleanValue}`);
    if (url.protocol !== "http:" && url.protocol !== "https:") return "";
    return url.href;
  } catch {
    return "";
  }
}

export function hostnameFromUrl(value) {
  try {
    return new URL(value).hostname.replace(/^www\./, "");
  } catch {
    return value;
  }
}

export function normalizeLinksData(data) {
  return {
    links: (data?.links || [])
      .map((link) => {
        const url = normalizeUrl(link?.url);
        if (!url) return null;
        return {
          id: link?.id || uid("link"),
          title: String(link?.title || "").trim(),
          url
        };
      })
      .filter(Boolean)
  };
}

function normalizeCounterValue(value) {
  const parsed = Number.parseInt(value, 10);
  return Number.isFinite(parsed) ? parsed : 0;
}

export function normalizeCountersData(data) {
  return {
    counters: (data?.counters || [])
      .map((counter) => ({
        id: counter?.id || uid("counter"),
        label: String(counter?.label || "").trim(),
        value: normalizeCounterValue(counter?.value)
      }))
      .filter((counter) => counter.label)
  };
}

function normalizeComboInput(input) {
  const kind = COMBO_INPUT_KINDS.has(input?.kind) ? input.kind : "";
  const value = String(input?.value || "").trim().slice(0, 24);
  if (!kind || !value) return null;
  const holdMs = Number(input?.holdMs);
  const normalizedHoldMs = Number.isFinite(holdMs) && holdMs > 0
    ? Math.min(99000, Math.round(holdMs / 1000) * 1000)
    : 0;
  const normalized = { kind, value };
  if (input?.hold === true || normalizedHoldMs > 0) normalized.hold = true;
  if (normalizedHoldMs > 0) normalized.holdMs = normalizedHoldMs;
  return normalized;
}

function normalizeComboSteps(inputs) {
  return (Array.isArray(inputs) ? inputs : [])
    .slice(0, 64)
    .map((step) => (Array.isArray(step) ? step : [])
      .map(normalizeComboInput)
      .filter(Boolean)
      .slice(0, 8))
    .filter((step) => step.length);
}

function normalizeCombo(combo) {
  const name = String(combo?.name || "").trim().slice(0, 80);
  const category = String(combo?.category || "").trim().slice(0, 80);
  const device = COMBO_DEVICES.has(combo?.device) ? combo.device : "";
  const inputs = normalizeComboSteps(combo?.inputs);
  if (!name && !inputs.length) return null;
  const normalized = {
    id: combo?.id || uid("combo"),
    name: name || "Combo",
    inputs
  };
  if (category) normalized.category = category;
  if (device) normalized.device = device;
  return normalized;
}

function normalizeComboCategoryOrder(value, combos) {
  const categories = combos.map((combo) => combo.category).filter(Boolean);
  const orderedCategories = (Array.isArray(value) ? value : [])
    .map((category) => String(category || "").trim().slice(0, 80))
    .filter((category, index, order) => category && categories.includes(category) && order.indexOf(category) === index);
  return [
    ...orderedCategories,
    ...categories.filter((category) => !orderedCategories.includes(category))
  ];
}

export function normalizeCombosData(data) {
  const combos = (Array.isArray(data?.combos) ? data.combos : []).map(normalizeCombo).filter(Boolean);
  const categoryOrder = normalizeComboCategoryOrder(data?.categoryOrder, combos);
  return {
    device: COMBO_DEVICES.has(data?.device) ? data.device : "playstation",
    combos,
    collapsedCategories: (Array.isArray(data?.collapsedCategories) ? data.collapsedCategories : [])
      .map((category) => String(category || "").trim().slice(0, 80))
      .filter((category, index, categories) => category && categoryOrder.includes(category) && categories.indexOf(category) === index),
    categoryOrder
  };
}

function normalizeCalculatorValue(value) {
  const parsed = Number(value);
  return Number.isFinite(parsed) ? parsed : 0;
}

function clampTableSize(value, fallback, max) {
  const parsed = Number.parseInt(value, 10);
  return Number.isInteger(parsed) ? Math.min(max, Math.max(1, parsed)) : fallback;
}

function columnIndexToName(index) {
  let value = Math.max(0, Number(index) || 0) + 1;
  let name = "";
  while (value > 0) {
    const remainder = (value - 1) % 26;
    name = String.fromCharCode(65 + remainder) + name;
    value = Math.floor((value - 1) / 26);
  }
  return name;
}

function tableCellAddress(rowIndex, columnIndex) {
  return `${columnIndexToName(columnIndex)}${rowIndex + 1}`;
}

function normalizeTableLabels(labels, length, getFallback) {
  const sourceLabels = labels && typeof labels === "object" ? labels : {};
  const normalized = {};
  for (let index = 0; index < length; index += 1) {
    const value = String(sourceLabels[index] || "").slice(0, 80);
    if (value.trim() && value !== String(getFallback(index))) normalized[index] = value;
  }
  return normalized;
}

function isTimeString(value, allowWildcard = false) {
  const pattern = allowWildcard ? /^(?:\d{2}|X):(?:\d{2}|X):(?:\d{2}|X)$/ : /^\d{2}:\d{2}:\d{2}$/;
  if (!pattern.test(String(value || ""))) return false;
  return String(value).split(":").every((part, index) => {
    if (part === "X") return allowWildcard;
    const number = Number(part);
    return Number.isInteger(number) && number >= 0 && number <= (index === 0 ? 23 : 59);
  });
}

function isClockTimeString(value) {
  if (!/^\d{2}:\d{2}$/.test(String(value || ""))) return false;
  const [hours, minutes] = String(value).split(":").map(Number);
  return Number.isInteger(hours) && Number.isInteger(minutes) && hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59;
}

function normalizeWeekDay(value, fallback = DEFAULT_TASK_PLANNER_WEEKLY_RESET_DAY) {
  const parsed = Number.parseInt(value, 10);
  return Number.isInteger(parsed) && parsed >= 0 && parsed <= 6 ? parsed : fallback;
}

function normalizeTimestamp(value) {
  const parsed = Number(value);
  return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
}

export function labelFromFileName(name) {
  if (!name || name === "image.png") return "";
  return name.replace(/\.[^.]+$/, "").trim();
}

export function normalizeCalculatorData(data) {
  const entries = (data?.entries || [])
    .map((entry) => ({
      id: entry?.id || uid("calc"),
      parentId: String(entry?.parentId || ""),
      label: String(entry?.label || "").trim(),
      value: normalizeCalculatorValue(entry?.value)
    }));
  const entryIds = new Set(entries.map((entry) => entry.id));
  return {
    scrollResults: data?.scrollResults === true,
    entries: entries.map((entry) => entryIds.has(entry.parentId) ? entry : { ...entry, parentId: "" })
  };
}

export function normalizeTableData(data) {
  const rows = clampTableSize(data?.rows, 10, 50);
  const columns = clampTableSize(data?.columns, 6, 20);
  const sourceCells = data?.cells && typeof data.cells === "object" ? data.cells : {};
  const cells = {};

  for (let rowIndex = 0; rowIndex < rows; rowIndex += 1) {
    for (let columnIndex = 0; columnIndex < columns; columnIndex += 1) {
      const address = tableCellAddress(rowIndex, columnIndex);
      const value = String(sourceCells[address] || "");
      if (value.trim()) cells[address] = value.slice(0, 500);
    }
  }

  return {
    rows,
    columns,
    cells,
    rowLabels: normalizeTableLabels(data?.rowLabels, rows, (rowIndex) => rowIndex + 1),
    columnLabels: normalizeTableLabels(data?.columnLabels, columns, columnIndexToName)
  };
}

export function normalizeTimerData(data) {
  const stopwatch = data?.stopwatch || {};
  const laps = (Array.isArray(stopwatch.laps) ? stopwatch.laps : [])
    .map((lap) => ({
      id: lap?.id || uid("timer"),
      label: String(lap?.label || "").trim(),
      elapsedMs: Math.max(0, Number(lap?.elapsedMs) || 0)
    }))
    .filter((lap) => lap.elapsedMs > 0);
  const countdowns = (Array.isArray(data?.countdowns) ? data.countdowns : [])
    .map((countdown) => {
      const type = COUNTDOWN_TYPES.has(countdown?.type) ? countdown.type : "duration";
      const normalized = {
        id: countdown?.id || uid("timer"),
        label: String(countdown?.label || "Timer").trim() || "Timer",
        type,
        alertMode: TIMER_ALERT_MODES.has(countdown?.alertMode) ? countdown.alertMode : "off"
      };
      if (type === "duration") {
        const durationMs = Math.max(0, Number(countdown?.durationMs) || 0);
        const targetAt = Math.max(0, Number(countdown?.targetAt) || 0);
        return durationMs > 0 && targetAt > 0 ? { ...normalized, durationMs, targetAt } : null;
      }
      if (type === "daily_time") {
        const time = String(countdown?.time || "").trim();
        const targetAt = Math.max(0, Number(countdown?.targetAt) || 0);
        return isTimeString(time) && targetAt > 0 ? { ...normalized, time, targetAt, autoRefresh: countdown?.autoRefresh === true } : null;
      }
      if (type === "time_pattern") {
        const pattern = String(countdown?.pattern || "").trim().toUpperCase();
        return isTimeString(pattern, true) ? { ...normalized, pattern } : null;
      }
      if (type === "interval") {
        const intervalMs = Math.max(0, Number(countdown?.intervalMs) || 0);
        const anchorAt = Math.max(0, Number(countdown?.anchorAt) || 0);
        const targetAt = Math.max(0, Number(countdown?.targetAt) || 0);
        const startMode = TIMER_INTERVAL_START_MODES.has(countdown?.startMode) ? countdown.startMode : "now";
        const startTime = String(countdown?.startTime || "").trim();
        const autoRefresh = countdown?.autoRefresh === true || (countdown?.autoRefresh !== false && !targetAt && intervalMs >= TIMER_MIN_AUTO_REFRESH_MS);
        return intervalMs > 0
          ? {
            ...normalized,
            intervalMs,
            anchorAt,
            targetAt: targetAt || anchorAt + intervalMs,
            startMode: startMode === "time" && isTimeString(startTime) ? "time" : "now",
            ...(startMode === "time" && isTimeString(startTime) ? { startTime } : {}),
            autoRefresh: autoRefresh && intervalMs >= TIMER_MIN_AUTO_REFRESH_MS
          }
          : null;
      }
      return null;
    })
    .filter(Boolean);

  return {
    activeTab: TIMER_TABS.has(data?.activeTab) ? data.activeTab : "stopwatch",
    scrollResults: data?.scrollResults === true,
    sortResults: data?.sortResults === true,
    stopwatch: {
      elapsedMs: Math.max(0, Number(stopwatch.elapsedMs) || 0),
      startedAt: Math.max(0, Number(stopwatch.startedAt) || 0),
      laps
    },
    countdowns
  };
}

function normalizeTaskPlannerTask(task) {
  const title = String(task?.title || "").trim() || "Tâche";
  const type = TASK_TYPES.has(task?.type) ? task.type : "unique";
  const category = String(task?.category || "").trim();
  const normalized = {
    id: task?.id || uid("task"),
    title,
    description: String(task?.description || "").trim(),
    type,
    checked: task?.checked === true,
    checkedAt: normalizeTimestamp(task?.checkedAt)
  };
  if (category) normalized.category = category;
  if (type === "daily" && isClockTimeString(task?.dailyResetTime)) normalized.dailyResetTime = task.dailyResetTime;
  if (type === "weekly" && task?.weeklyResetDay !== undefined) normalized.weeklyResetDay = normalizeWeekDay(task.weeklyResetDay);
  if (!normalized.checked) normalized.checkedAt = 0;
  return normalized;
}

function normalizeTaskPlannerCategoryOrder(value, tasks) {
  const categories = new Set(tasks.map((task) => task.category).filter(Boolean));
  const ordered = [];
  (Array.isArray(value) ? value : []).forEach((category) => {
    const normalized = String(category || "").trim();
    if (normalized && categories.has(normalized) && !ordered.includes(normalized)) ordered.push(normalized);
  });
  tasks.forEach((task) => {
    if (task.category && !ordered.includes(task.category)) ordered.push(task.category);
  });
  return ordered;
}

function normalizeTaskPlannerRelations(data, taskIds) {
  const seen = new Set();
  const seenChildren = new Set();
  return (Array.isArray(data?.relations) ? data.relations : [])
    .map((relation) => {
      const fromTaskId = String(relation?.fromTaskId || "");
      const toTaskId = String(relation?.toTaskId || "");
      if (!taskIds.has(fromTaskId) || !taskIds.has(toTaskId) || fromTaskId === toTaskId) return null;
      const key = `${fromTaskId}:${toTaskId}`;
      if (seen.has(key) || seenChildren.has(fromTaskId)) return null;
      seen.add(key);
      seenChildren.add(fromTaskId);
      return {
        id: relation?.id || uid("relation"),
        fromTaskId,
        toTaskId,
        prerequisite: relation?.prerequisite === true
      };
    })
    .filter(Boolean);
}

export function normalizeTaskPlannerData(data) {
  const rawTasks = (Array.isArray(data?.tasks) ? data.tasks : []).map(normalizeTaskPlannerTask).filter(Boolean);
  const rawTaskIds = new Set(rawTasks.map((task) => task.id));
  const relations = normalizeTaskPlannerRelations(data, rawTaskIds);
  const childTaskIds = new Set(relations.map((relation) => relation.fromTaskId));
  const tasks = rawTasks.map((task) => {
    if (!childTaskIds.has(task.id) || !task.category) return task;
    const normalized = { ...task };
    delete normalized.category;
    return normalized;
  });
  const taskIds = new Set(tasks.map((task) => task.id));
  const categoryOrder = normalizeTaskPlannerCategoryOrder(data?.categoryOrder, tasks);
  return {
    weeklyResetDay: normalizeWeekDay(data?.weeklyResetDay),
    resetTime: isClockTimeString(data?.resetTime) ? data.resetTime : DEFAULT_TASK_PLANNER_RESET_TIME,
    lastResetAt: normalizeTimestamp(data?.lastResetAt),
    hideCompleted: data?.hideCompleted === true,
    collapsedCategories: (Array.isArray(data?.collapsedCategories) ? data.collapsedCategories : [])
      .map((category) => String(category || "").trim())
      .filter((category, index, categories) => category && categoryOrder.includes(category) && categories.indexOf(category) === index),
    categoryOrder,
    tasks,
    relations: relations.filter((relation) => taskIds.has(relation.fromTaskId) && taskIds.has(relation.toTaskId))
  };
}

function normalizeEquipmentPlannerType(type) {
  const title = String(type?.title || "").trim().slice(0, 80);
  if (!title) return null;
  const icon = EQUIPMENT_TYPE_ICONS.has(type?.icon) ? type.icon : "shield";
  return {
    id: type?.id || uid("equipmentType"),
    title,
    icon,
    collapsed: type?.collapsed === true
  };
}

function normalizeEquipmentPlannerValue(value) {
  if (typeof value === "number") return Number.isFinite(value) ? Math.round(value * 1000) / 1000 : "";
  const text = String(value ?? "").trim().slice(0, 80);
  if (!text) return "";
  const numeric = Number(text.replace(",", "."));
  return Number.isFinite(numeric) && /^[-+]?\d+(?:[,.]\d+)?$/.test(text) ? Math.round(numeric * 1000) / 1000 : text;
}

function normalizeEquipmentPlannerTrait(trait, prefix = "trait") {
  const name = String(trait?.name || "").trim().slice(0, 80);
  if (!name) return null;
  return {
    id: trait?.id || uid(prefix),
    category: String(trait?.category || "").trim().slice(0, 80),
    icon: EQUIPMENT_TRAIT_ICONS.has(trait?.icon) ? trait.icon : "sword",
    name,
    value: normalizeEquipmentPlannerValue(trait?.value)
  };
}

function normalizeEquipmentPlannerMaterial(material) {
  const name = String(material?.name || "").trim().slice(0, 80);
  if (!name) return null;
  return {
    id: material?.id || uid("material"),
    name,
    qty: Math.max(1, parsePositiveInt(material?.qty, 1))
  };
}

function normalizeEquipmentPlannerSocketItem(socketItem) {
  const name = String(socketItem?.name || "").trim().slice(0, 80);
  if (!name) return null;
  const shape = EQUIPMENT_SOCKET_SHAPES.has(socketItem?.shape) ? socketItem.shape : "jewel";
  const color = EQUIPMENT_SOCKET_COLORS.has(socketItem?.color) ? socketItem.color : "yellow";
  return {
    id: socketItem?.id || uid("socketItem"),
    name,
    shape,
    color,
    bonuses: (Array.isArray(socketItem?.bonuses) ? socketItem.bonuses : [])
      .map((bonus) => normalizeEquipmentPlannerTrait(bonus, "socketBonus"))
      .filter(Boolean)
  };
}

function normalizeEquipmentPlannerCategoryOrder(value, categories) {
  const available = new Set(categories.filter(Boolean));
  const ordered = [];
  (Array.isArray(value) ? value : []).forEach((category) => {
    const normalized = String(category || "").trim().slice(0, 80);
    if (normalized && available.has(normalized) && !ordered.includes(normalized)) ordered.push(normalized);
  });
  categories.forEach((category) => {
    if (category && !ordered.includes(category)) ordered.push(category);
  });
  return ordered;
}

function normalizeEquipmentPlannerEquipment(equipment, typeIds) {
  const name = String(equipment?.name || "").trim().slice(0, 100);
  const typeId = String(equipment?.typeId || "");
  if (!name || !typeIds.has(typeId)) return null;
  const characteristics = (Array.isArray(equipment?.characteristics) ? equipment.characteristics : [])
    .map((trait) => normalizeEquipmentPlannerTrait(trait, "trait"))
    .filter(Boolean);
  const socketItems = (Array.isArray(equipment?.socketItems) ? equipment.socketItems : [])
    .map(normalizeEquipmentPlannerSocketItem)
    .filter(Boolean);
  const socketItemIds = new Set(socketItems.map((socketItem) => socketItem.id));
  const socketLinks = (Array.isArray(equipment?.socketLinks) ? equipment.socketLinks : [])
    .map((link) => {
      const fromSocketItemId = String(link?.fromSocketItemId || "");
      const toSocketItemId = String(link?.toSocketItemId || "");
      if (!socketItemIds.has(fromSocketItemId) || !socketItemIds.has(toSocketItemId) || fromSocketItemId === toSocketItemId) return null;
      const pairKey = [fromSocketItemId, toSocketItemId].sort().join(":");
      return {
        id: link?.id || uid("socketLink"),
        fromSocketItemId,
        toSocketItemId,
        pairKey
      };
    })
    .filter(Boolean)
    .filter((link, index, links) => links.findIndex((item) => item.pairKey === link.pairKey) === index)
    .map(({ pairKey, ...link }) => link);
  const categories = [
    ...characteristics.map((trait) => trait.category),
    ...socketItems.flatMap((socketItem) => socketItem.bonuses.map((bonus) => bonus.category))
  ].filter(Boolean);
  const categoryOrder = normalizeEquipmentPlannerCategoryOrder(equipment?.categoryOrder, categories);
  return {
    id: equipment?.id || uid("equipment"),
    typeId,
    name,
    icon: EQUIPMENT_TYPE_ICONS.has(equipment?.icon) ? equipment.icon : "shield",
    active: equipment?.active !== false,
    obtain: String(equipment?.obtain || "").trim().slice(0, 500),
    characteristics,
    socketItems,
    socketLinks,
    materials: (Array.isArray(equipment?.materials) ? equipment.materials : [])
      .map(normalizeEquipmentPlannerMaterial)
      .filter(Boolean),
    categoryOrder,
    collapsedCategories: (Array.isArray(equipment?.collapsedCategories) ? equipment.collapsedCategories : [])
      .map((category) => String(category || "").trim().slice(0, 80))
      .filter((category, index, collapsedCategories) => category && categoryOrder.includes(category) && collapsedCategories.indexOf(category) === index)
  };
}

export function normalizeEquipmentPlannerData(data) {
  const rawTypes = (Array.isArray(data?.types) ? data.types : []).map(normalizeEquipmentPlannerType).filter(Boolean);
  const typeIds = new Set(rawTypes.map((type) => type.id));
  const equipments = (Array.isArray(data?.equipments) ? data.equipments : [])
    .map((equipment) => normalizeEquipmentPlannerEquipment(equipment, typeIds))
    .filter(Boolean);
  const usedTypeIds = new Set(equipments.map((equipment) => equipment.typeId));
  const types = rawTypes.filter((type) => usedTypeIds.has(type.id));
  const orderedTypeIds = (Array.isArray(data?.typeOrder) ? data.typeOrder : [])
    .map((typeId) => String(typeId || ""))
    .filter((typeId, index, order) => usedTypeIds.has(typeId) && order.indexOf(typeId) === index);
  return {
    types,
    equipments,
    typeOrder: [
      ...orderedTypeIds,
      ...types.map((type) => type.id).filter((typeId) => !orderedTypeIds.includes(typeId))
    ]
  };
}

export function summarizeEquipmentPlannerData(data) {
  const normalized = normalizeEquipmentPlannerData(data);
  const totals = new Map();
  const textBonuses = [];
  const activeEquipments = normalized.equipments.filter((equipment) => equipment.active);

  function incrementCount(counts, key) {
    counts.set(key, (counts.get(key) || 0) + 1);
  }

  function pickMostUsedKey(counts) {
    return [...counts.entries()].reduce((picked, [key, count]) => {
      if (!picked) return { key, count };
      if (count !== picked.count) return count > picked.count ? { key, count } : picked;
      return key.localeCompare(picked.key) < 0 ? { key, count } : picked;
    }, null);
  }

  function parseIconKey(key) {
    const [icon, color = ""] = String(key || "").split(":");
    return { icon: icon || "sword", color };
  }

  function pickSummaryIcon(iconSources) {
    return parseIconKey(pickMostUsedKey(iconSources.direct)?.key || pickMostUsedKey(iconSources.socket)?.key || "sword:");
  }

  function getSummarySourceKind(sourceKinds) {
    if (sourceKinds.size !== 1) return "mixed";
    return sourceKinds.has("direct") ? "direct" : "socket";
  }

  function collectEntry(entry, contribution, icon, sourceKind, color = "") {
    if (typeof entry.value === "number") {
      const existing = totals.get(entry.name);
      const iconKey = icon || "sword";
      const colorKey = sourceKind === "socket" ? color || "gray" : "";
      const iconSources = existing?.iconSources || { direct: new Map(), socket: new Map() };
      const sourceKinds = existing?.sourceKinds || new Set();
      const contributions = existing?.contributions || [];
      sourceKinds.add(sourceKind);
      incrementCount(iconSources[sourceKind], `${iconKey}:${colorKey}`);
      const pickedIcon = pickSummaryIcon(iconSources);
      totals.set(entry.name, {
        name: entry.name,
        icon: pickedIcon.icon,
        color: pickedIcon.color,
        sourceKind: getSummarySourceKind(sourceKinds),
        iconSources,
        sourceKinds,
        contributions: [
          ...contributions,
          {
            id: entry.id,
            sourceKind,
            equipmentIcon: contribution.equipmentIcon,
            equipmentName: contribution.equipmentName,
            sourceName: contribution.sourceName,
            value: entry.value
          }
        ],
        value: (existing?.value || 0) + entry.value
      });
      return;
    }
    textBonuses.push({
      id: entry.id,
      category: entry.category,
      name: entry.name,
      value: entry.value,
      source: contribution.sourceName
    });
  }

  activeEquipments.forEach((equipment) => {
    equipment.characteristics.forEach((trait) => collectEntry(
      trait,
      { equipmentIcon: equipment.icon || "shield", equipmentName: equipment.name, sourceName: equipment.name },
      trait.icon || "sword",
      "direct"
    ));
    equipment.socketItems.forEach((socketItem) => {
      socketItem.bonuses.forEach((bonus) => collectEntry(
        bonus,
        { equipmentIcon: equipment.icon || "shield", equipmentName: equipment.name, sourceName: `${equipment.name} · ${socketItem.name}` },
        socketItem.shape || "jewel",
        "socket",
        socketItem.color
      ));
    });
  });

  const numericTotals = [...totals.values()]
    .map(({ iconSources, sourceKinds, color, sourceKind, ...total }) => ({
      ...total,
      ...(color ? { color } : {}),
      sourceKind
    }))
    .sort((a, b) => a.name.localeCompare(b.name));

  return {
    activeEquipmentCount: activeEquipments.length,
    totals: numericTotals,
    equipmentTotals: numericTotals.filter((total) => total.sourceKind === "direct"),
    socketTotals: numericTotals.filter((total) => total.sourceKind === "socket"),
    mixedTotals: numericTotals.filter((total) => total.sourceKind === "mixed"),
    textBonuses,
    socketItems: activeEquipments.flatMap((equipment) => equipment.socketItems.map((socketItem) => ({
      ...socketItem,
      equipmentId: equipment.id,
      equipmentName: equipment.name
    }))),
    socketLinks: activeEquipments.flatMap((equipment) => equipment.socketLinks.map((link) => ({
      ...link,
      equipmentId: equipment.id,
      equipmentName: equipment.name
    })))
  };
}

function clampPercent(value) {
  const parsed = Number(value);
  if (!Number.isFinite(parsed)) return 0;
  return Math.min(100, Math.max(0, parsed));
}

export function normalizeImageAnnotationData(data) {
  const image = String(data?.image || "");
  const markers = (data?.markers || [])
    .map((marker) => ({
      id: marker?.id || uid("marker"),
      x: clampPercent(marker?.x),
      y: clampPercent(marker?.y),
      label: String(marker?.label || "").trim()
    }));
  return { image, markers, drawings: normalizeDrawingData(data?.drawings, { max: 100 }) };
}

function compactChecklistItemForStorage(item) {
  const normalized = normalizeChecklistItem(item);
  const compact = { id: normalized.id, label: normalized.label };
  if (normalized.qtyTarget !== 1) compact.qtyTarget = normalized.qtyTarget;
  if (normalized.qtyCurrent !== 0) compact.qtyCurrent = normalized.qtyCurrent;
  return compact;
}

function compactChecklistSectionForStorage(section) {
  const normalized = normalizeChecklistSection(section);
  const compact = {
    id: normalized.id,
    items: normalized.items.map(compactChecklistItemForStorage)
  };
  if (normalized.title) compact.title = normalized.title;
  if (typeof normalized.hideWhenComplete === "boolean") compact.hideWhenComplete = normalized.hideWhenComplete;
  if (normalized.collapsed === true) compact.collapsed = true;
  return compact;
}

function compactEquipmentPlannerTraitForStorage(trait, prefix = "trait") {
  const normalized = normalizeEquipmentPlannerTrait(trait, prefix);
  if (!normalized) return null;
  const compact = { id: normalized.id, name: normalized.name };
  if (normalized.category) compact.category = normalized.category;
  if (normalized.icon !== "sword") compact.icon = normalized.icon;
  if (normalized.value !== "") compact.value = normalized.value;
  return compact;
}

function compactEquipmentPlannerMaterialForStorage(material) {
  const normalized = normalizeEquipmentPlannerMaterial(material);
  return normalized ? normalized : null;
}

function compactEquipmentPlannerSocketItemForStorage(socketItem) {
  const normalized = normalizeEquipmentPlannerSocketItem(socketItem);
  if (!normalized) return null;
  const compact = {
    id: normalized.id,
    name: normalized.name,
    shape: normalized.shape,
    color: normalized.color
  };
  if (normalized.bonuses.length) {
    compact.bonuses = normalized.bonuses
      .map((bonus) => compactEquipmentPlannerTraitForStorage(bonus, "socketBonus"))
      .filter(Boolean);
  }
  return compact;
}

function compactEquipmentPlannerEquipmentForStorage(equipment) {
  const compact = {
    id: equipment.id,
    typeId: equipment.typeId,
    name: equipment.name
  };
  if (equipment.icon !== "shield") compact.icon = equipment.icon;
  if (equipment.active === false) compact.active = false;
  if (equipment.obtain) compact.obtain = equipment.obtain;
  if (equipment.characteristics.length) {
    compact.characteristics = equipment.characteristics
      .map((trait) => compactEquipmentPlannerTraitForStorage(trait, "trait"))
      .filter(Boolean);
  }
  if (equipment.socketItems.length) {
    compact.socketItems = equipment.socketItems
      .map(compactEquipmentPlannerSocketItemForStorage)
      .filter(Boolean);
  }
  if (equipment.socketLinks.length) compact.socketLinks = equipment.socketLinks;
  if (equipment.materials.length) compact.materials = equipment.materials.map(compactEquipmentPlannerMaterialForStorage).filter(Boolean);
  if (equipment.categoryOrder.length) compact.categoryOrder = equipment.categoryOrder;
  if (equipment.collapsedCategories.length) compact.collapsedCategories = equipment.collapsedCategories;
  return compact;
}

export function compactModuleDataForStorage(type, value) {
  if (type === "notepad") {
    const normalized = normalizeNotepadData(value);
    const compact = {};
    if (normalized.html && normalized.text) compact.html = normalized.html;
    if (normalized.text) compact.text = normalized.text;
    if (normalized.updatedAt) compact.updatedAt = normalized.updatedAt;
    if (normalized.drawingMode === "permanent") compact.drawingMode = "permanent";
    if (normalized.drawingMode === "permanent" && normalized.drawings.strokes.length) compact.drawings = normalized.drawings;
    return Object.keys(compact).length ? compact : null;
  }
  if (type === "checklist") {
    const normalized = normalizeChecklistData(value);
    const sections = normalized.sections.map(compactChecklistSectionForStorage).filter((section) => section.items.length);
    if (!sections.length) return null;
    const settings = {
      ...(normalized.hideCompletedSections ? { hideCompletedSections: true } : {}),
      ...(normalized.hideCompletedSectionsFully ? { hideCompletedSectionsFully: true } : {})
    };
    if (sections.length === 1 && !sections[0].title && typeof sections[0].hideWhenComplete !== "boolean") return { ...settings, items: sections[0].items };
    return { ...settings, sections };
  }
  if (type === "images") {
    const images = (Array.isArray(value?.images) ? value.images : [])
      .filter((image) => image?.dataUrl)
      .map((image) => {
        const compact = { id: image.id || uid("image"), dataUrl: image.dataUrl };
        if (image.label) compact.label = image.label;
        return compact;
      });
    return images.length ? { images } : null;
  }
  if (type === "imageAnnotation") {
    const normalized = normalizeImageAnnotationData(value);
    if (!normalized.image) return null;
    const compact = {
      image: normalized.image,
      markers: normalized.markers.map((marker) => {
        const compact = { id: marker.id, x: marker.x, y: marker.y };
        if (marker.label) compact.label = marker.label;
        return compact;
      })
    };
    if (normalized.drawings.strokes.length) compact.drawings = normalized.drawings;
    return compact;
  }
  if (type === "links") {
    const links = normalizeLinksData(value).links.map((link) => {
      const compact = {
        id: link.id,
        url: link.url
      };
      if (link.title) compact.title = link.title;
      return compact;
    });
    return links.length ? { links } : null;
  }
  if (type === "counters") {
    const counters = normalizeCountersData(value).counters.map((counter) => ({
      id: counter.id,
      label: counter.label,
      value: counter.value
    }));
    return counters.length ? { counters } : null;
  }
  if (type === "combos") {
    const normalized = normalizeCombosData(value);
    const compact = {
      combos: normalized.combos.map((combo) => {
        const compactCombo = {
          id: combo.id,
          name: combo.name,
          inputs: combo.inputs
        };
        if (combo.category) compactCombo.category = combo.category;
        if (combo.device) compactCombo.device = combo.device;
        return compactCombo;
      })
    };
    if (normalized.device !== "playstation") compact.device = normalized.device;
    if (normalized.collapsedCategories.length) compact.collapsedCategories = normalized.collapsedCategories;
    if (normalized.categoryOrder.length) compact.categoryOrder = normalized.categoryOrder;
    if (!compact.combos.length) delete compact.combos;
    return compact.combos ? compact : null;
  }
  if (type === "calculator") {
    const normalized = normalizeCalculatorData(value);
    const entries = normalized.entries.map((entry) => {
      const compact = {
        id: entry.id,
        label: entry.label,
        value: entry.value
      };
      if (entry.parentId) compact.parentId = entry.parentId;
      return compact;
    });
    if (!entries.length && !normalized.scrollResults) return null;
    return normalized.scrollResults ? { entries, scrollResults: true } : { entries };
  }
  if (type === "table") {
    const normalized = normalizeTableData(value);
    const compact = { cells: normalized.cells };
    if (normalized.rows !== 10) compact.rows = normalized.rows;
    if (normalized.columns !== 6) compact.columns = normalized.columns;
    if (Object.keys(normalized.rowLabels).length) compact.rowLabels = normalized.rowLabels;
    if (Object.keys(normalized.columnLabels).length) compact.columnLabels = normalized.columnLabels;
    return Object.keys(compact.cells).length || compact.rows || compact.columns || compact.rowLabels || compact.columnLabels ? compact : null;
  }
  if (type === "timer") {
    const normalized = normalizeTimerData(value);
    const compact = {};
    if (normalized.activeTab !== "stopwatch") compact.activeTab = normalized.activeTab;
    if (normalized.scrollResults) compact.scrollResults = true;
    if (normalized.sortResults) compact.sortResults = true;
    if (normalized.stopwatch.elapsedMs || normalized.stopwatch.startedAt || normalized.stopwatch.laps.length) {
      compact.stopwatch = {
        ...(normalized.stopwatch.elapsedMs ? { elapsedMs: normalized.stopwatch.elapsedMs } : {}),
        ...(normalized.stopwatch.startedAt ? { startedAt: normalized.stopwatch.startedAt } : {}),
        ...(normalized.stopwatch.laps.length ? { laps: normalized.stopwatch.laps } : {})
      };
    }
    if (normalized.countdowns.length) {
      compact.countdowns = normalized.countdowns.map((countdown) => {
        const { alertMode, autoRefresh, startMode, startTime, ...compactCountdown } = countdown;
        if (alertMode !== "off") compactCountdown.alertMode = alertMode;
        if (autoRefresh) compactCountdown.autoRefresh = true;
        if (startMode === "time" && startTime) {
          compactCountdown.startMode = startMode;
          compactCountdown.startTime = startTime;
        }
        return compactCountdown;
      });
    }
    return Object.keys(compact).length ? compact : null;
  }
  if (type === "taskPlanner") {
    const normalized = normalizeTaskPlannerData(value);
    const compact = {};
    if (normalized.weeklyResetDay !== DEFAULT_TASK_PLANNER_WEEKLY_RESET_DAY) compact.weeklyResetDay = normalized.weeklyResetDay;
    if (normalized.resetTime !== DEFAULT_TASK_PLANNER_RESET_TIME) compact.resetTime = normalized.resetTime;
    if (normalized.lastResetAt) compact.lastResetAt = normalized.lastResetAt;
    if (normalized.hideCompleted) compact.hideCompleted = true;
    if (normalized.collapsedCategories.length) compact.collapsedCategories = normalized.collapsedCategories;
    if (normalized.tasks.length) {
      compact.tasks = normalized.tasks.map((task) => {
        const compactTask = {
          id: task.id,
          title: task.title,
          type: task.type
        };
        if (task.description) compactTask.description = task.description;
        if (task.category) compactTask.category = task.category;
        if (task.checked) compactTask.checked = true;
        if (task.checked && task.checkedAt) compactTask.checkedAt = task.checkedAt;
        if (task.type === "daily" && task.dailyResetTime && task.dailyResetTime !== normalized.resetTime) compactTask.dailyResetTime = task.dailyResetTime;
        if (task.type === "weekly" && task.weeklyResetDay !== undefined) compactTask.weeklyResetDay = task.weeklyResetDay;
        return compactTask;
      });
    }
    if (normalized.categoryOrder.length) compact.categoryOrder = normalized.categoryOrder;
    if (normalized.relations.length) {
      compact.relations = normalized.relations.map((relation) => {
        const compactRelation = {
          id: relation.id,
          fromTaskId: relation.fromTaskId,
          toTaskId: relation.toTaskId
        };
        if (relation.prerequisite) compactRelation.prerequisite = true;
        return compactRelation;
      });
    }
    return Object.keys(compact).length ? compact : null;
  }
  if (type === "equipmentPlanner") {
    const normalized = normalizeEquipmentPlannerData(value);
    if (!normalized.equipments.length) return null;
    const compact = {
      types: normalized.types.map((type) => {
        const compactType = { id: type.id, title: type.title };
        if (type.icon !== "shield") compactType.icon = type.icon;
        if (type.collapsed) compactType.collapsed = true;
        return compactType;
      }),
      equipments: normalized.equipments.map(compactEquipmentPlannerEquipmentForStorage)
    };
    if (normalized.typeOrder.length) compact.typeOrder = normalized.typeOrder;
    return compact;
  }
  return value;
}

export function prepareModuleDataForStorage(toolboxes, toolboxId, moduleId, value, moduleType = "") {
  const module = toolboxes.find((toolbox) => toolbox.id === toolboxId)?.modules.find((item) => item.id === moduleId);
  return compactModuleDataForStorage(moduleType || module?.type, value);
}

function createExportIdFactory() {
  const counts = {};
  return (prefix) => {
    counts[prefix] = (counts[prefix] || 0) + 1;
    return `${ID_PREFIXES[prefix] || "x"}${counts[prefix].toString(36)}`;
  };
}

function remapModuleDataForExport(type, data, nextId) {
  const compact = compactModuleDataForStorage(type, data);
  if (!compact) return null;
  if (type === "checklist") {
    if (compact.sections) {
      return {
        sections: compact.sections.map((section) => ({
          ...section,
          id: nextId("section"),
          items: section.items.map((item) => ({ ...item, id: nextId("item") }))
        }))
      };
    }
    return { items: compact.items.map((item) => ({ ...item, id: nextId("item") })) };
  }
  if (type === "images") {
    return { images: compact.images.map((image) => ({ ...image, id: nextId("image") })) };
  }

  if (type === "imageAnnotation") {
    const remapped = {
      ...compact,
      markers: compact.markers.map((marker) => ({ ...marker, id: nextId("marker") }))
    };
    if (compact.drawings?.strokes) {
      remapped.drawings = {
        strokes: compact.drawings.strokes.map((stroke) => ({ ...stroke, id: nextId("stroke") }))
      };
    }
    return remapped;
  }

  if (type === "links") {
    return { links: compact.links.map((link) => ({ ...link, id: nextId("link") })) };
  }

  if (type === "counters") {
    return { counters: compact.counters.map((counter) => ({ ...counter, id: nextId("counter") })) };
  }

  if (type === "combos") {
    const remapped = {};
    if (compact.device) remapped.device = compact.device;
    if (compact.combos) remapped.combos = compact.combos.map((combo) => ({ ...combo, id: nextId("combo") }));
    if (compact.collapsedCategories) remapped.collapsedCategories = compact.collapsedCategories;
    if (compact.categoryOrder) remapped.categoryOrder = compact.categoryOrder;
    return remapped;
  }

  if (type === "calculator") {
    const entryIdMap = new Map();
    compact.entries.forEach((entry) => entryIdMap.set(entry.id, nextId("calc")));
    const remapped = {
      entries: compact.entries.map((entry) => {
        const remapped = { ...entry, id: entryIdMap.get(entry.id) };
        if (entry.parentId) remapped.parentId = entryIdMap.get(entry.parentId) || "";
        return remapped;
      })
    };
    if (compact.scrollResults) remapped.scrollResults = true;
    return remapped;
  }

  if (type === "timer") {
    const remapped = { ...compact };
    if (compact.stopwatch?.laps) {
      remapped.stopwatch = {
        ...compact.stopwatch,
        laps: compact.stopwatch.laps.map((lap) => ({ ...lap, id: nextId("timer") }))
      };
    }
    if (compact.countdowns) {
      remapped.countdowns = compact.countdowns.map((countdown) => ({ ...countdown, id: nextId("timer") }));
    }
    return remapped;
  }

  if (type === "taskPlanner") {
    const taskIdMap = new Map();
    compact.tasks?.forEach((task) => taskIdMap.set(task.id, nextId("task")));
    const remapped = { ...compact };
    if (compact.tasks) remapped.tasks = compact.tasks.map((task) => ({ ...task, id: taskIdMap.get(task.id) }));
    if (compact.relations) {
      remapped.relations = compact.relations
        .map((relation) => ({
          ...relation,
          id: nextId("relation"),
          fromTaskId: taskIdMap.get(relation.fromTaskId),
          toTaskId: taskIdMap.get(relation.toTaskId)
        }))
        .filter((relation) => relation.fromTaskId && relation.toTaskId);
    }
    return remapped;
  }

  if (type === "equipmentPlanner") {
    const typeIdMap = new Map();
    const equipmentIdMap = new Map();
    compact.types?.forEach((typeEntry) => typeIdMap.set(typeEntry.id, nextId("equipmentType")));
    compact.equipments?.forEach((equipment) => equipmentIdMap.set(equipment.id, nextId("equipment")));
    const remapped = {
      ...compact,
      types: (compact.types || []).map((typeEntry) => ({ ...typeEntry, id: typeIdMap.get(typeEntry.id) })),
      equipments: (compact.equipments || []).map((equipment) => {
        const socketItemIdMap = new Map();
        equipment.socketItems?.forEach((socketItem) => socketItemIdMap.set(socketItem.id, nextId("socketItem")));
        return {
          ...equipment,
          id: equipmentIdMap.get(equipment.id),
          typeId: typeIdMap.get(equipment.typeId) || "",
          characteristics: equipment.characteristics?.map((trait) => ({ ...trait, id: nextId("trait") })),
          materials: equipment.materials?.map((material) => ({ ...material, id: nextId("material") })),
          socketItems: equipment.socketItems?.map((socketItem) => ({
            ...socketItem,
            id: socketItemIdMap.get(socketItem.id),
            bonuses: socketItem.bonuses?.map((bonus) => ({ ...bonus, id: nextId("socketBonus") }))
          })),
          socketLinks: equipment.socketLinks?.map((link) => ({
            ...link,
            id: nextId("socketLink"),
            fromSocketItemId: socketItemIdMap.get(link.fromSocketItemId),
            toSocketItemId: socketItemIdMap.get(link.toSocketItemId)
          })).filter((link) => link.fromSocketItemId && link.toSocketItemId)
        };
      }).filter((equipment) => equipment.typeId)
    };
    if (compact.typeOrder) remapped.typeOrder = compact.typeOrder.map((typeId) => typeIdMap.get(typeId)).filter(Boolean);
    return remapped;
  }

  return compact;
}

export function createToolboxExportPayload(toolbox, moduleData) {
  const source = normalizeToolbox(toolbox);
  const nextId = createExportIdFactory();
  const moduleIdMap = new Map();
  const exportedToolbox = compactToolboxForStorage({
    ...source,
    id: nextId("tbx"),
    modules: source.modules.map((module) => {
      const id = nextId("mod");
      moduleIdMap.set(module.id, id);
      return { ...module, id };
    }),
    moduleOrder: remapModuleOrderIds(source.moduleOrder, moduleIdMap)
  });
  const modules = Object.fromEntries(source.modules
    .map((module) => [
      moduleIdMap.get(module.id),
      remapModuleDataForExport(module.type, moduleData[moduleStorageKey(source.id, module.id)] || null, nextId)
    ])
    .filter(([, data]) => data != null));
  return { toolbox: exportedToolbox, modules };
}

export function createGlobalExportPayload(toolboxes, links, moduleData) {
  const modules = {};
  toolboxes.forEach((toolbox) => {
    toolbox.modules.forEach((module) => {
      const data = compactModuleDataForStorage(module.type, moduleData[moduleStorageKey(toolbox.id, module.id)] || null);
      if (data) modules[globalModuleKey(toolbox.id, module.id)] = data;
    });
  });
  return {
    version: 1,
    exportedAt: new Date().toISOString(),
    toolboxes: compactToolboxesForStorage(toolboxes),
    modules,
    links
  };
}
