// Rôle : parse et sérialise les formats texte/TSV d'échange des outils toolbox.
import { cellAddress, columnIndexToName } from "./tableFormulaEngine.js";
import { getCountdownTargetMs, getDailyTargetMs, getIntervalAnchorMs } from "./timerUtils.js";

export function parseColonImportLines(text) {
  return String(text || "")
    .split(/\r?\n/)
    .map((line) => line.trim())
    .filter(Boolean)
    .map((line) => {
      const separatorIndex = line.indexOf(":");
      if (separatorIndex === -1) return { label: line, value: "" };
      return {
        label: line.slice(0, separatorIndex).trim(),
        value: line.slice(separatorIndex + 1).trim()
      };
    })
    .filter((entry) => entry.label);
}

const SUPPORTED_TEXT_EXCHANGE_TYPES = new Set([
  "checklist",
  "links",
  "counters",
  "combos",
  "calculator",
  "table",
  "timer",
  "taskPlanner",
  "equipmentPlanner",
  "images",
  "imageAnnotation"
]);
const COMBO_DEVICES = new Set(["playstation", "xbox", "switch", "n64", "keyboardMouse"]);
const COMBO_INPUT_KINDS = new Set(["button", "direction", "key", "mouse"]);
const TIMER_TYPES = new Set(["duration", "daily_time", "time_pattern", "interval"]);
const ALERT_MODES = new Set(["off", "visible", "site"]);
const IMAGE_DATA_URL_PATTERN = /^data:image\/[a-z0-9.+-]+;base64,[a-z0-9+/=\s]+$/i;

export function supportsTextExchange(type) {
  return SUPPORTED_TEXT_EXCHANGE_TYPES.has(type);
}

export function hasTextExchangeContent(type, data) {
  if (type === "checklist") return Boolean(data?.sections?.some((section) => section.items?.length));
  if (type === "links") return Boolean(data?.links?.length);
  if (type === "counters") return Boolean(data?.counters?.length);
  if (type === "combos") return Boolean(data?.combos?.length);
  if (type === "calculator") return Boolean(data?.entries?.length || data?.scrollResults);
  if (type === "table") return Boolean(Object.keys(data?.cells || {}).length || Object.keys(data?.rowLabels || {}).length || Object.keys(data?.columnLabels || {}).length);
  if (type === "timer") return Boolean(data?.stopwatch?.laps?.length || data?.countdowns?.length || data?.stopwatch?.elapsedMs);
  if (type === "taskPlanner") return Boolean(data?.tasks?.length);
  if (type === "equipmentPlanner") return Boolean(data?.equipments?.length);
  if (type === "images") return Boolean(data?.images?.length);
  if (type === "imageAnnotation") return Boolean(data?.image);
  return false;
}

export function exportModuleText(type, data) {
  if (type === "checklist") return exportChecklistText(data);
  if (type === "links") return exportLinksText(data);
  if (type === "counters") return exportCountersText(data);
  if (type === "combos") return exportCombosText(data);
  if (type === "calculator") return exportCalculatorText(data);
  if (type === "table") return exportTableTsv(data);
  if (type === "timer") return exportTimerText(data);
  if (type === "taskPlanner") return exportTaskPlannerText(data);
  if (type === "equipmentPlanner") return exportEquipmentPlannerText(data);
  if (type === "images") return exportImagesText(data);
  if (type === "imageAnnotation") return exportImageAnnotationText(data);
  return "";
}

export function importModuleText(type, text, context) {
  try {
    const value = String(text || "");
    if (!value.trim()) return fail("Le texte à importer est vide.");
    let data = null;
    if (type === "checklist") data = importChecklistText(value, context);
    else if (type === "links") data = importLinksText(value, context);
    else if (type === "counters") data = importCountersText(value, context);
    else if (type === "combos") data = importCombosText(value, context);
    else if (type === "calculator") data = importCalculatorText(value, context);
    else if (type === "table") data = importTableTsv(value);
    else if (type === "timer") data = importTimerText(value, context);
    else if (type === "taskPlanner") data = importTaskPlannerText(value, context);
    else if (type === "equipmentPlanner") data = importEquipmentPlannerText(value, context);
    else if (type === "images") data = importImagesText(value, context);
    else if (type === "imageAnnotation") data = importImageAnnotationText(value, context);
    else return fail("Cet outil ne supporte pas l'import texte.");
    if (!hasTextExchangeContent(type, data)) return fail("Aucun contenu valide détecté.");
    return { ok: true, data };
  } catch (error) {
    return fail(error?.message || "Format invalide.");
  }
}

function fail(error) {
  return { ok: false, error };
}

function splitLines(text) {
  return String(text || "").split(/\r?\n/);
}

function parseMetaLine(line) {
  const match = String(line || "").trim().match(/^@([a-zA-Z][\w-]*)\s*:\s*(.*)$/);
  return match ? { key: match[1], value: match[2] } : null;
}

function parsePipeParts(line) {
  const [title = "", ...parts] = String(line || "").split("|").map((part) => part.trim());
  const meta = {};
  parts.forEach((part) => {
    const separatorIndex = part.indexOf("=");
    if (separatorIndex < 0) return;
    meta[part.slice(0, separatorIndex).trim()] = part.slice(separatorIndex + 1).trim();
  });
  return { title, meta };
}

function parseBoolean(value) {
  return String(value || "").trim().toLowerCase() === "true";
}

function lineError(index, message) {
  return new Error(`Ligne ${index + 1}: ${message}`);
}

function parseNumber(value, fallback = 0) {
  const parsed = Number(String(value || "").replace(",", "."));
  return Number.isFinite(parsed) ? parsed : fallback;
}

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

function formatMeta(meta) {
  return Object.entries(meta)
    .filter(([, value]) => value !== "" && value !== undefined && value !== false)
    .map(([key, value]) => `${key}=${value}`)
    .join(" | ");
}

function isValidImageDataUrl(value) {
  return IMAGE_DATA_URL_PATTERN.test(String(value || "").trim());
}

function cloneDrawingData(drawings) {
  return {
    strokes: (drawings?.strokes || []).map((stroke) => ({
      ...stroke,
      points: (stroke.points || []).map((point) => ({ ...point }))
    }))
  };
}

function exportChecklistText(data) {
  return (data.sections || []).flatMap((section) => [
    section.title ? `# ${section.title}` : "",
    ...(section.items || []).map((item) => `${item.label}: ${Math.max(1, parseInteger(item.qtyTarget, 1))}`)
  ]).filter((line, index, lines) => line || lines[index + 1]).join("\n").trim();
}

function importChecklistText(text, context) {
  const sections = [];
  let current = { id: context.uid("section"), title: "", items: [] };
  splitLines(text).forEach((line) => {
    const cleanLine = line.trim();
    if (!cleanLine) return;
    if (cleanLine.startsWith("#")) {
      if (current.items.length) sections.push(current);
      current = { id: context.uid("section"), title: cleanLine.replace(/^#+/, "").trim(), items: [] };
      return;
    }
    parseColonImportLines(cleanLine).forEach((entry) => {
      current.items.push({
        id: context.uid("item"),
        label: entry.label,
        qtyTarget: Math.max(1, parseInteger(String(entry.value).split("/").pop(), 1)),
        qtyCurrent: 0
      });
    });
  });
  if (current.items.length) sections.push(current);
  return context.normalizeChecklistData({ sections });
}

function exportLinksText(data) {
  return (data.links || []).map((link) => link.title ? `${link.title}: ${link.url}` : link.url).join("\n");
}

function importLinksText(text, context) {
  const links = [];
  splitLines(text).forEach((rawLine, index) => {
    const line = rawLine.trim();
    if (!line) return;
    const separatorIndex = line.indexOf(":");
    const maybeUrl = separatorIndex >= 0 ? line.slice(separatorIndex + 1).trim() : line;
    const title = separatorIndex >= 0 && !/^[a-z][a-z0-9+.-]*:\/\//i.test(line) ? line.slice(0, separatorIndex).trim() : "";
    const url = context.normalizeUrl(title ? maybeUrl : line);
    if (!url) throw lineError(index, `URL invalide: ${line}`);
    links.push({ id: context.uid("link"), title, url });
  });
  return context.normalizeLinksData({ links });
}

function exportCountersText(data) {
  return (data.counters || []).map((counter) => `${counter.label}: ${counter.value}`).join("\n");
}

function importCountersText(text, context) {
  const counters = [];
  splitLines(text).forEach((line, index) => {
    const cleanLine = line.trim();
    if (!cleanLine) return;
    const separatorIndex = cleanLine.indexOf(":");
    const entry = separatorIndex === -1
      ? { label: cleanLine, value: "" }
      : { label: cleanLine.slice(0, separatorIndex).trim(), value: cleanLine.slice(separatorIndex + 1).trim() };
    if (!entry.label) return;
    if (!entry.value && entry.value !== "0") throw lineError(index, `valeur manquante pour ${entry.label}.`);
    const value = Number.parseInt(entry.value, 10);
    if (!Number.isFinite(value)) throw lineError(index, `valeur invalide pour ${entry.label}.`);
    counters.push({ id: context.uid("counter"), label: entry.label, value });
  });
  return context.normalizeCountersData({ counters });
}

function exportCalculatorText(data) {
  const lines = [];
  if (data.scrollResults) lines.push("@scrollResults: true");
  function visit(parentId = "", depth = 0) {
    (data.entries || []).filter((entry) => (entry.parentId || "") === parentId).forEach((entry) => {
      lines.push(`${"  ".repeat(depth)}${entry.label || ""}: ${entry.value}`);
      visit(entry.id, depth + 1);
    });
  }
  visit();
  return lines.join("\n");
}

function importCalculatorText(text, context) {
  const entries = [];
  const parentStack = [];
  let scrollResults = false;
  splitLines(text).forEach((line, index) => {
    if (!line.trim()) return;
    const meta = parseMetaLine(line);
    if (meta?.key === "scrollResults") {
      scrollResults = parseBoolean(meta.value);
      return;
    }
    const depth = Math.floor((line.match(/^ */)?.[0].length || 0) / 2);
    const separatorIndex = line.indexOf(":");
    if (separatorIndex < 0) throw lineError(index, `ligne calculateur invalide: ${line.trim()}`);
    const label = line.slice(0, separatorIndex).trim();
    const value = parseNumber(line.slice(separatorIndex + 1), Number.NaN);
    if (!Number.isFinite(value)) throw lineError(index, `valeur calculateur invalide: ${line.trim()}`);
    const entry = { id: context.uid("calc"), parentId: parentStack[depth - 1] || "", label, value };
    entries.push(entry);
    parentStack[depth] = entry.id;
    parentStack.length = depth + 1;
  });
  return context.normalizeCalculatorData({ entries, scrollResults });
}

function exportTableTsv(data) {
  const lines = [];
  if (data.rows !== 10 || data.columns !== 6) lines.push(`@size: ${data.rows}x${data.columns}`);
  if (Object.keys(data.columnLabels || {}).length) lines.push(`@columns: ${Array.from({ length: data.columns }, (_, index) => data.columnLabels?.[index] || columnIndexToName(index)).join("\t")}`);
  if (Object.keys(data.rowLabels || {}).length) lines.push(`@rows: ${Array.from({ length: data.rows }, (_, index) => data.rowLabels?.[index] || index + 1).join("\t")}`);
  if (lines.length) lines.push("@tsv");
  for (let rowIndex = 0; rowIndex < data.rows; rowIndex += 1) {
    lines.push(Array.from({ length: data.columns }, (_, columnIndex) => data.cells?.[cellAddress(rowIndex, columnIndex)] || "").join("\t"));
  }
  return lines.join("\n").replace(/\n+$/, "");
}

function importTableTsv(text) {
  const lines = splitLines(text);
  let rows = 0;
  let columns = 0;
  let rowLabels = {};
  let columnLabels = {};
  let dataLines = lines;
  const metaLines = [];
  const tsvIndex = lines.findIndex((line) => line.trim() === "@tsv");
  if (tsvIndex >= 0) {
    metaLines.push(...lines.slice(0, tsvIndex));
    dataLines = lines.slice(tsvIndex + 1);
  }
  metaLines.forEach((line, index) => {
    const meta = parseMetaLine(line);
    if (!meta) throw lineError(index, `meta tableau invalide: ${line}`);
    if (meta.key === "size") {
      const match = meta.value.match(/^(\d+)x(\d+)$/);
      if (!match) throw lineError(index, "taille de tableau invalide.");
      rows = parseInteger(match[1], 0);
      columns = parseInteger(match[2], 0);
    }
    if (meta.key === "columns") {
      columnLabels = Object.fromEntries(meta.value.split("\t").map((value, index) => [index, value]).filter(([index, value]) => value && value !== columnIndexToName(index)));
    }
    if (meta.key === "rows") {
      rowLabels = Object.fromEntries(meta.value.split("\t").map((value, index) => [index, value]).filter(([index, value]) => value && value !== String(index + 1)));
    }
  });
  const tableRows = dataLines.filter((line) => line.trim() || line.includes("\t")).map((line) => line.split("\t"));
  if (!tableRows.length) throw new Error("TSV vide.");
  rows = rows || tableRows.length;
  columns = columns || Math.max(...tableRows.map((row) => row.length));
  const cells = {};
  for (let rowIndex = 0; rowIndex < Math.min(rows, 50); rowIndex += 1) {
    for (let columnIndex = 0; columnIndex < Math.min(columns, 20); columnIndex += 1) {
      const value = tableRows[rowIndex]?.[columnIndex] || "";
      if (value.trim()) cells[cellAddress(rowIndex, columnIndex)] = value;
    }
  }
  return { rows, columns, cells, rowLabels, columnLabels };
}

function exportComboInput(input) {
  let suffix = "";
  if (input.holdMs) suffix = `[${Math.round(input.holdMs / 1000)}s]`;
  else if (input.hold) suffix = "[hold]";
  return `${input.kind}:${input.value}${suffix}`;
}

function parseComboInput(text) {
  const match = String(text || "").trim().match(/^([a-zA-Z]+):([^\[]+?)(?:\[(hold|\d+s)\])?$/);
  if (!match || !COMBO_INPUT_KINDS.has(match[1])) throw new Error(`Input combo invalide: ${text}`);
  const input = { kind: match[1], value: match[2].trim() };
  if (!input.value) throw new Error(`Input combo invalide: ${text}`);
  if (match[3] === "hold") input.hold = true;
  if (match[3]?.endsWith("s")) {
    input.hold = true;
    input.holdMs = Math.max(1000, Math.min(99000, parseInteger(match[3], 1) * 1000));
  }
  return input;
}

function exportCombosText(data) {
  const lines = [];
  if (data.device && data.device !== "playstation") lines.push(`@device: ${data.device}`);
  if (data.collapsedCategories?.length) lines.push(`@collapsed: ${data.collapsedCategories.join(", ")}`);
  let lastCategory = null;
  (data.combos || []).forEach((combo) => {
    const category = combo.category || "";
    if (category && category !== lastCategory) {
      lines.push(`# ${category}`);
      lastCategory = category;
    }
    const meta = formatMeta({ device: combo.device || "" });
    const sequence = (combo.inputs || []).map((step) => step.map(exportComboInput).join("+")).join(" > ");
    lines.push([combo.name, meta, sequence].filter(Boolean).join(" | "));
  });
  return lines.join("\n");
}

function importCombosText(text, context) {
  const combos = [];
  let device = "playstation";
  let collapsedCategories = [];
  let category = "";
  splitLines(text).forEach((line, index) => {
    const cleanLine = line.trim();
    if (!cleanLine) return;
    const meta = parseMetaLine(cleanLine);
    if (meta?.key === "device") {
      if (!COMBO_DEVICES.has(meta.value)) throw lineError(index, "périphérique combo invalide.");
      device = meta.value;
      return;
    }
    if (meta?.key === "collapsed") {
      collapsedCategories = meta.value.split(",").map((item) => item.trim()).filter(Boolean);
      return;
    }
    if (cleanLine.startsWith("#")) {
      category = cleanLine.replace(/^#+/, "").trim();
      return;
    }
    const { title, meta: lineMeta } = parsePipeParts(cleanLine);
    const sequence = cleanLine.split("|").map((part) => part.trim()).filter((part, index) => index > 0 && !part.includes("=")).join(" | ");
    const comboDevice = lineMeta.device || device;
    if (!COMBO_DEVICES.has(comboDevice)) throw lineError(index, `périphérique combo invalide: ${comboDevice}`);
    let inputs = [];
    try {
      inputs = sequence ? String(sequence).split(">").map((step) => step.split("+").map(parseComboInput)) : [];
    } catch (error) {
      throw lineError(index, error?.message || "input combo invalide.");
    }
    combos.push({ id: context.uid("combo"), name: title || "Combo", category, device: comboDevice, inputs });
  });
  return context.normalizeCombosData({ device, collapsedCategories, combos });
}

function exportTaskPlannerText(data) {
  const lines = [];
  if (data.weeklyResetDay !== 1) lines.push(`@weeklyResetDay: ${data.weeklyResetDay}`);
  if (data.resetTime && data.resetTime !== "00:00") lines.push(`@resetTime: ${data.resetTime}`);
  if (data.hideCompleted) lines.push("@hideCompleted: true");
  if (data.collapsedCategories?.length) lines.push(`@collapsed: ${data.collapsedCategories.join(", ")}`);
  const parentByChild = new Map((data.relations || []).map((relation) => [relation.fromTaskId, relation]));
  const childrenByParent = new Map();
  (data.tasks || []).forEach((task) => {
    const parentId = parentByChild.get(task.id)?.toTaskId || "";
    childrenByParent.set(parentId, [...(childrenByParent.get(parentId) || []), task]);
  });
  function writeTask(task, depth = 0) {
    const prefix = "  ".repeat(depth);
    lines.push(`${prefix}- ${task.type || "unique"} ${task.title}`);
    if (task.description) lines.push(`${prefix}  @description: ${task.description.replace(/\n/g, "\\n")}`);
    if (task.dailyResetTime) lines.push(`${prefix}  @dailyResetTime: ${task.dailyResetTime}`);
    if (task.weeklyResetDay !== undefined) lines.push(`${prefix}  @weeklyResetDay: ${task.weeklyResetDay}`);
    if (parentByChild.get(task.id)?.prerequisite) lines.push(`${prefix}  @prerequisite: true`);
    (childrenByParent.get(task.id) || []).forEach((child) => writeTask(child, depth + 1));
  }
  let lastCategory = null;
  (childrenByParent.get("") || []).forEach((task) => {
    const category = task.category || "";
    if (category && category !== lastCategory) {
      lines.push(`# ${category}`);
      lastCategory = category;
    }
    writeTask(task);
  });
  return lines.join("\n");
}

function importTaskPlannerText(text, context) {
  const tasks = [];
  const relations = [];
  const stack = [];
  let category = "";
  const data = { weeklyResetDay: 1, resetTime: "00:00", hideCompleted: false, collapsedCategories: [], tasks, relations };
  splitLines(text).forEach((line, index) => {
    if (!line.trim()) return;
    const cleanLine = line.trim();
    const rootMeta = parseMetaLine(cleanLine);
    if (rootMeta && !stack.length) {
      if (rootMeta.key === "weeklyResetDay") data.weeklyResetDay = parseInteger(rootMeta.value, 1);
      if (rootMeta.key === "resetTime") data.resetTime = rootMeta.value;
      if (rootMeta.key === "hideCompleted") data.hideCompleted = parseBoolean(rootMeta.value);
      if (rootMeta.key === "collapsed") data.collapsedCategories = rootMeta.value.split(",").map((item) => item.trim()).filter(Boolean);
      return;
    }
    if (cleanLine.startsWith("#")) {
      category = cleanLine.replace(/^#+/, "").trim();
      return;
    }
    const depth = Math.floor((line.match(/^ */)?.[0].length || 0) / 2);
    const meta = parseMetaLine(cleanLine);
    if (meta) {
      const task = stack[Math.max(0, depth - 1)];
      if (!task) throw lineError(index, `meta de tâche sans tâche: ${cleanLine}`);
      if (meta.key === "description") task.description = meta.value.replace(/\\n/g, "\n");
      if (meta.key === "dailyResetTime") task.dailyResetTime = meta.value;
      if (meta.key === "weeklyResetDay") task.weeklyResetDay = parseInteger(meta.value, 1);
      if (meta.key === "prerequisite") {
        const relation = relations.find((item) => item.fromTaskId === task.id);
        if (relation) relation.prerequisite = parseBoolean(meta.value);
      }
      return;
    }
    const match = cleanLine.match(/^-\s+(unique|daily|weekly)\s+(.+)$/);
    if (!match) throw lineError(index, `ligne tâche invalide: ${cleanLine}`);
    const task = { id: context.uid("task"), title: match[2].trim(), description: "", type: match[1], checked: false, checkedAt: 0 };
    const parent = stack[depth - 1];
    if (parent) relations.push({ id: context.uid("relation"), fromTaskId: task.id, toTaskId: parent.id, prerequisite: false });
    else if (category) task.category = category;
    tasks.push(task);
    stack[depth] = task;
    stack.length = depth + 1;
  });
  return context.normalizeTaskPlannerData(data);
}

function exportTimerText(data) {
  const lines = [];
  if (data.activeTab && data.activeTab !== "stopwatch") lines.push(`@activeTab: ${data.activeTab}`);
  if (data.scrollResults) lines.push("@scrollResults: true");
  if (data.sortResults) lines.push("@sortResults: true");
  if (data.stopwatch?.elapsedMs) lines.push(`@stopwatchElapsedMs: ${data.stopwatch.elapsedMs}`);
  (data.stopwatch?.laps || []).forEach((lap) => lines.push(`lap | label=${lap.label || ""} | elapsedMs=${lap.elapsedMs}`));
  (data.countdowns || []).forEach((countdown) => {
    const meta = { label: countdown.label, type: countdown.type, alertMode: countdown.alertMode || "", autoRefresh: countdown.autoRefresh === true };
    if (countdown.type === "duration") meta.durationMs = countdown.durationMs;
    if (countdown.type === "daily_time") meta.time = countdown.time;
    if (countdown.type === "time_pattern") meta.pattern = countdown.pattern;
    if (countdown.type === "interval") {
      meta.intervalMs = countdown.intervalMs;
      meta.startMode = countdown.startMode || "now";
      meta.startTime = countdown.startTime || "";
    }
    lines.push(`countdown | ${formatMeta(meta)}`);
  });
  return lines.join("\n");
}

function importTimerText(text, context) {
  const now = Date.now();
  const data = { activeTab: "stopwatch", scrollResults: false, sortResults: false, stopwatch: { elapsedMs: 0, startedAt: 0, laps: [] }, countdowns: [] };
  splitLines(text).forEach((line, index) => {
    const cleanLine = line.trim();
    if (!cleanLine) return;
    const meta = parseMetaLine(cleanLine);
    if (meta) {
      if (meta.key === "activeTab") data.activeTab = meta.value === "countdown" ? "countdown" : "stopwatch";
      if (meta.key === "scrollResults") data.scrollResults = parseBoolean(meta.value);
      if (meta.key === "sortResults") data.sortResults = parseBoolean(meta.value);
      if (meta.key === "stopwatchElapsedMs") data.stopwatch.elapsedMs = Math.max(0, parseInteger(meta.value, 0));
      return;
    }
    const { title, meta: lineMeta } = parsePipeParts(cleanLine);
    if (title === "lap") {
      data.stopwatch.laps.push({ id: context.uid("timer"), label: lineMeta.label || "", elapsedMs: Math.max(0, parseInteger(lineMeta.elapsedMs, 0)) });
      return;
    }
    if (title !== "countdown") throw lineError(index, `ligne timer invalide: ${cleanLine}`);
    const type = lineMeta.type;
    if (!TIMER_TYPES.has(type)) throw lineError(index, `type timer invalide: ${type}`);
    const countdown = { id: context.uid("timer"), label: lineMeta.label || "Timer", type, alertMode: ALERT_MODES.has(lineMeta.alertMode) ? lineMeta.alertMode : "off" };
    if (type === "duration") {
      countdown.durationMs = Math.max(1, parseInteger(lineMeta.durationMs, 0));
      countdown.targetAt = now + countdown.durationMs;
    }
    if (type === "daily_time") {
      countdown.time = lineMeta.time;
      countdown.targetAt = getDailyTargetMs(countdown.time, now);
      countdown.autoRefresh = parseBoolean(lineMeta.autoRefresh);
    }
    if (type === "time_pattern") {
      countdown.pattern = lineMeta.pattern;
      countdown.autoRefresh = parseBoolean(lineMeta.autoRefresh);
    }
    if (type === "interval") {
      countdown.intervalMs = Math.max(1, parseInteger(lineMeta.intervalMs, 0));
      countdown.startMode = lineMeta.startMode === "time" && lineMeta.startTime ? "time" : "now";
      if (countdown.startMode === "time") countdown.startTime = lineMeta.startTime;
      countdown.anchorAt = getIntervalAnchorMs(countdown.startMode, countdown.startTime, now);
      countdown.targetAt = getCountdownTargetMs({ ...countdown, autoRefresh: true }, now);
      countdown.autoRefresh = parseBoolean(lineMeta.autoRefresh);
    }
    data.countdowns.push(countdown);
  });
  return context.normalizeTimerData(data);
}

function exportImagesText(data) {
  return (data.images || []).flatMap((image) => [
    image.label ? `# ${image.label}` : "#",
    image.dataUrl
  ]).join("\n");
}

function importImagesText(text, context) {
  const images = [];
  let label = "";
  splitLines(text).forEach((line, index) => {
    const cleanLine = line.trim();
    if (!cleanLine) return;
    if (cleanLine.startsWith("#")) {
      label = cleanLine.replace(/^#+/, "").trim();
      return;
    }
    if (!isValidImageDataUrl(cleanLine)) throw lineError(index, "image base64 invalide.");
    images.push({ id: context.uid("image"), label, dataUrl: cleanLine });
    label = "";
  });
  return { images };
}

function exportImageAnnotationText(data) {
  const lines = [data.image];
  (data.markers || []).forEach((marker) => lines.push(`@marker: ${marker.x},${marker.y},${marker.label || ""}`));
  if (data.drawings?.strokes?.length) lines.push(`@drawings: ${JSON.stringify(cloneDrawingData(data.drawings))}`);
  return lines.join("\n");
}

function importImageAnnotationText(text, context) {
  const markers = [];
  let image = "";
  let drawings = { strokes: [] };
  splitLines(text).forEach((line, index) => {
    const cleanLine = line.trim();
    if (!cleanLine) return;
    const meta = parseMetaLine(cleanLine);
    if (meta?.key === "marker") {
      const [x, y, ...labelParts] = meta.value.split(",");
      const marker = { id: context.uid("marker"), x: parseNumber(x, Number.NaN), y: parseNumber(y, Number.NaN), label: labelParts.join(",").trim() };
      if (!Number.isFinite(marker.x) || !Number.isFinite(marker.y) || marker.x < 0 || marker.x > 100 || marker.y < 0 || marker.y > 100) throw lineError(index, "marqueur invalide.");
      markers.push(marker);
      return;
    }
    if (meta?.key === "drawings") {
      try {
        drawings = JSON.parse(meta.value);
      } catch {
        throw lineError(index, "dessins invalides.");
      }
      return;
    }
    if (image) throw lineError(index, "une seule image peut être importée.");
    if (!isValidImageDataUrl(cleanLine)) throw lineError(index, "image base64 invalide.");
    image = cleanLine;
  });
  return context.normalizeImageAnnotationData({ image, markers, drawings });
}

function exportEquipmentPlannerText(data) {
  const lines = [];
  const typesById = new Map((data.types || []).map((type) => [type.id, type]));
  (data.types || [{ id: "", title: "Équipements", icon: "shield", collapsed: false }]).forEach((type) => {
    const equipments = (data.equipments || []).filter((equipment) => type.id ? equipment.typeId === type.id : !typesById.has(equipment.typeId));
    if (!equipments.length) return;
    lines.push(`# ${type.title} | ${formatMeta({ icon: type.icon, collapsed: type.collapsed })}`);
    equipments.forEach((equipment) => {
    lines.push(`## ${equipment.name} | ${formatMeta({ icon: equipment.icon, active: equipment.active !== false })}`);
    if (equipment.characteristics?.length) {
      lines.push("Stats");
      equipment.characteristics.forEach((trait) => lines.push(`${trait.category || ""} | ${trait.icon || "sword"} | ${trait.name} | ${trait.value ?? ""}`));
    }
    if (equipment.socketItems?.length) {
      lines.push("Sockets");
      equipment.socketItems.forEach((socketItem) => {
        lines.push(`${socketItem.name} | shape=${socketItem.shape} | color=${socketItem.color}`);
        (socketItem.bonuses || []).forEach((bonus) => lines.push(`  bonus | ${bonus.category || ""} | ${bonus.icon || "sword"} | ${bonus.name} | ${bonus.value ?? ""}`));
      });
      if (equipment.socketLinks?.length) {
        const socketItemsById = new Map(equipment.socketItems.map((socketItem) => [socketItem.id, socketItem.name]));
        equipment.socketLinks.forEach((link) => {
          const fromName = socketItemsById.get(link.fromSocketItemId);
          const toName = socketItemsById.get(link.toSocketItemId);
          if (fromName && toName) lines.push(`  link | ${fromName} | ${toName}`);
        });
      }
    }
    if (equipment.obtain || equipment.materials?.length) {
      lines.push("Craft");
      if (equipment.obtain) lines.push(`@obtain: ${equipment.obtain.replace(/\n/g, "\\n")}`);
      (equipment.materials || []).forEach((material) => lines.push(`${material.name}: ${material.qty}`));
    }
    });
  });
  return lines.join("\n");
}

function importEquipmentPlannerText(text, context) {
  const types = [];
  const equipments = [];
  let currentType = null;
  let currentEquipment = null;
  let section = "";
  let currentSocket = null;
  const socketsByName = new Map();
  splitLines(text).forEach((line, index) => {
    const cleanLine = line.trim();
    if (!cleanLine) return;
    if (cleanLine.startsWith("##")) {
      if (!currentType) throw lineError(index, "équipement sans type.");
      const { title, meta } = parsePipeParts(cleanLine.replace(/^##+/, "").trim());
      currentEquipment = { id: context.uid("equipment"), typeId: currentType.id, name: title, icon: meta.icon || "shield", active: meta.active !== "false", obtain: "", characteristics: [], socketItems: [], socketLinks: [], materials: [], categoryOrder: [], collapsedCategories: [] };
      equipments.push(currentEquipment);
      section = "";
      currentSocket = null;
      socketsByName.clear();
      return;
    }
    if (cleanLine.startsWith("#")) {
      const { title, meta } = parsePipeParts(cleanLine.replace(/^#+/, "").trim());
      currentType = { id: context.uid("equipmentType"), title, icon: meta.icon || "shield", collapsed: parseBoolean(meta.collapsed) };
      types.push(currentType);
      currentEquipment = null;
      section = "";
      return;
    }
    if (["Stats", "Sockets", "Craft"].includes(cleanLine)) {
      if (!currentEquipment) throw lineError(index, `${cleanLine} sans équipement.`);
      section = cleanLine;
      currentSocket = null;
      return;
    }
    if (!currentEquipment || !section) throw lineError(index, `ligne équipement invalide: ${cleanLine}`);
    if (section === "Stats") {
      const [category = "", icon = "sword", name = "", value = ""] = cleanLine.split("|").map((part) => part.trim());
      currentEquipment.characteristics.push({ id: context.uid("trait"), category, icon, name, value });
    }
    if (section === "Sockets") {
      if (cleanLine.startsWith("bonus")) {
        if (!currentSocket) throw lineError(index, "bonus sans objet serti.");
        const [, category = "", icon = "sword", name = "", value = ""] = cleanLine.split("|").map((part) => part.trim());
        currentSocket.bonuses.push({ id: context.uid("socketBonus"), category, icon, name, value });
      } else if (cleanLine.startsWith("link")) {
        const [, fromName = "", toName = ""] = cleanLine.split("|").map((part) => part.trim());
        const fromSocketItemId = socketsByName.get(fromName);
        const toSocketItemId = socketsByName.get(toName);
        if (!fromSocketItemId || !toSocketItemId) throw lineError(index, "lien de sertissage invalide.");
        currentEquipment.socketLinks.push({ id: context.uid("socketLink"), fromSocketItemId, toSocketItemId });
      } else {
        const { title, meta } = parsePipeParts(cleanLine);
        currentSocket = { id: context.uid("socketItem"), name: title, shape: meta.shape || "jewel", color: meta.color || "yellow", bonuses: [] };
        currentEquipment.socketItems.push(currentSocket);
        socketsByName.set(title, currentSocket.id);
      }
    }
    if (section === "Craft") {
      const meta = parseMetaLine(cleanLine);
      if (meta?.key === "obtain") {
        currentEquipment.obtain = meta.value.replace(/\\n/g, "\n");
      } else {
        const [entry] = parseColonImportLines(cleanLine);
        if (!entry) throw lineError(index, `matériau invalide: ${cleanLine}`);
        currentEquipment.materials.push({ id: context.uid("material"), name: entry.label, qty: Math.max(1, parseInteger(entry.value, 1)) });
      }
    }
  });
  return context.normalizeEquipmentPlannerData({ types, equipments, typeOrder: types.map((type) => type.id) });
}
