// Sérialisation `InlineNode[]` ↔ DOM.
//
// Le contentEditable de chaque bloc texte contient un sous-arbre HTML que
// l'utilisateur édite. À chaque frappe, on lit l'arbre via `domToInline`
// pour reconstruire les nœuds. À chaque changement externe (undo, transform,
// toolbar), on réécrit l'arbre via `inlineToDom`.
//
// Ordre canonique des wrappers (extérieur → intérieur) :
//   <a> > <span data-highlight> > <span data-color> > <strong> > <em> > <u>
//        > <s> > <code> > #text
//
// Cet ordre est important pour que la sérialisation aller-retour soit
// stable : `inlineToDom(domToInline(x))` produit le même HTML que `x`.

import { INLINE_COLORS, isHexColor, normalize } from './types.js';

const TAG_TO_MARK = {
  STRONG: 'bold',
  B: 'bold',
  EM: 'italic',
  I: 'italic',
  U: 'underline',
  S: 'strike',
  STRIKE: 'strike',
  DEL: 'strike',
  CODE: 'code',
};

function findMark(marks, type) {
  return marks?.find(m => m.type === type);
}

// Construit un fragment DOM. Reçoit un Document optionnel (utile en SSR /
// tests) ; sinon utilise `document` global.
export function inlineToDom(nodes, doc) {
  const d = doc || (typeof document !== 'undefined' ? document : null);
  if (!d) throw new Error('inlineToDom: document requis');
  const fragment = d.createDocumentFragment();
  if (!Array.isArray(nodes) || nodes.length === 0) return fragment;
  for (const node of nodes) {
    if (node.text?.includes('\n')) {
      const parts = node.text.split('\n');
      for (let i = 0; i < parts.length; i++) {
        if (parts[i]) fragment.appendChild(buildNode(d, { ...node, text: parts[i] }));
        if (i < parts.length - 1) fragment.appendChild(d.createElement('br'));
      }
    } else {
      fragment.appendChild(buildNode(d, node));
    }
  }
  return fragment;
}

function buildNode(d, node) {
  let el = d.createTextNode(node.text ?? '');
  const marks = node.marks || [];

  // Ordre intérieur → extérieur (on enveloppe progressivement).
  // 1. Marks simples (code, strike, underline, italic, bold) — plus on est
  //    interne, plus on est dans la cascade visuelle « précise ».
  if (findMark(marks, 'code')) el = wrap(d, el, 'code', { className: 'rounded px-1 py-0.5 font-mono text-[0.9em] bg-neutral-100 dark:bg-neutral-800/80' });
  if (findMark(marks, 'strike')) el = wrap(d, el, 's');
  if (findMark(marks, 'underline')) el = wrap(d, el, 'u');
  if (findMark(marks, 'italic')) el = wrap(d, el, 'em');
  if (findMark(marks, 'bold')) el = wrap(d, el, 'strong');

  // 2. Couleur du texte. Hex → style inline ; clé palette → classe Tailwind.
  const color = findMark(marks, 'color');
  if (color) {
    const hex = isHexColor(color.color);
    const tw = hex ? '' : (INLINE_COLORS[color.color]?.text || '');
    el = wrap(d, el, 'span', {
      className: tw,
      style: hex ? { color: color.color } : null,
      attrs: { 'data-color': color.color },
    });
  }

  // 3. Surlignage. Hex → style inline ; clé palette → classe Tailwind.
  const highlight = findMark(marks, 'highlight');
  if (highlight) {
    const hex = isHexColor(highlight.color);
    const tw = hex ? '' : (INLINE_COLORS[highlight.color]?.highlight || '');
    el = wrap(d, el, 'span', {
      className: `rounded px-0.5${tw ? ` ${tw}` : ''}`,
      style: hex ? { backgroundColor: highlight.color } : null,
      attrs: { 'data-highlight': highlight.color },
    });
  }

  // 4. Lien — toujours à l'extérieur.
  const link = findMark(marks, 'link');
  if (link) {
    const attrs = { href: link.href };
    if (link.newTab) {
      attrs.target = '_blank';
      attrs.rel = 'noopener noreferrer';
    }
    el = wrap(d, el, 'a', {
      className: 'text-blue-600 dark:text-blue-400 underline underline-offset-2',
      attrs,
    });
  }

  return el;
}

function wrap(d, child, tagName, opts = {}) {
  const el = d.createElement(tagName);
  if (opts.className) el.className = opts.className;
  if (opts.style) {
    for (const [k, v] of Object.entries(opts.style)) el.style[k] = v;
  }
  if (opts.attrs) {
    for (const [k, v] of Object.entries(opts.attrs)) el.setAttribute(k, v);
  }
  el.appendChild(child);
  return el;
}

// Walk DOM → InlineNode[]. Accumule les marks dans une pile au fur et à
// mesure qu'on descend. Émet un nœud par run de texte.
export function domToInline(root) {
  if (!root) return [];
  const out = [];
  walk(root, [], out);
  return normalize(out);
}

function walk(node, marks, out) {
  if (node.nodeType === 3 /* TEXT_NODE */) {
    if (node.nodeValue) {
      out.push({
        type: 'text',
        text: node.nodeValue,
        ...(marks.length ? { marks: marks.map(m => ({ ...m })) } : {}),
      });
    }
    return;
  }
  if (node.nodeType !== 1 /* ELEMENT_NODE */) return;

  // <br> : émet un saut de ligne explicite. Notre modèle n'a pas de bloc
  // multi-lignes (Enter crée un nouveau bloc), mais Chrome injecte parfois
  // un <br> trailing dans un contentEditable vide — on l'ignore.
  if (node.tagName === 'BR') {
    if (node.nextSibling || node.previousSibling) {
      out.push({ type: 'text', text: '\n', ...(marks.length ? { marks: marks.map(m => ({ ...m })) } : {}) });
    }
    return;
  }

  const added = [];
  const tag = node.tagName;
  const simple = TAG_TO_MARK[tag];
  if (simple) added.push({ type: simple });

  if (tag === 'A') {
    const href = node.getAttribute('href') || '';
    if (href) {
      const newTab = node.getAttribute('target') === '_blank';
      added.push({ type: 'link', href, ...(newTab ? { newTab: true } : {}) });
    }
  }

  if (tag === 'SPAN') {
    const color = node.getAttribute('data-color');
    const highlight = node.getAttribute('data-highlight');
    if (color) added.push({ type: 'color', color });
    if (highlight) added.push({ type: 'highlight', color: highlight });
  }

  // Styles CSS inline : Google Docs / Word produisent massivement des
  // <span style="font-weight:700"> / <b style="font-weight:normal"> au lieu
  // de <strong>/<em>. On lit ces styles pour préserver le formatage à la
  // collaboration externe.
  const style = node.style;
  if (style) {
    const fw = style.fontWeight;
    const isBoldStyle = fw === 'bold' || fw === 'bolder' || (fw && parseInt(fw, 10) >= 600);
    const isNormalStyle = fw === 'normal' || (fw && parseInt(fw, 10) > 0 && parseInt(fw, 10) < 600);
    // Un <b style="font-weight:normal"> annule le bold du tag (cas Google Docs).
    if (simple === 'bold' && isNormalStyle) {
      added.length = 0;
    } else if (!simple && isBoldStyle && !added.some(m => m.type === 'bold')) {
      added.push({ type: 'bold' });
    }
    if (style.fontStyle === 'italic' && !added.some(m => m.type === 'italic')) {
      added.push({ type: 'italic' });
    }
    const decoLine = style.textDecorationLine || style.textDecoration || '';
    if (decoLine.includes('underline') && !added.some(m => m.type === 'underline')) {
      added.push({ type: 'underline' });
    }
    if (decoLine.includes('line-through') && !added.some(m => m.type === 'strike')) {
      added.push({ type: 'strike' });
    }
  }

  const nextMarks = added.length ? [...marks, ...added] : marks;
  for (const child of node.childNodes) {
    walk(child, nextMarks, out);
  }
}
