// Sérialisation `Block[]` ↔ HTML pour le presse-papier.
//
// `blocksToHtml` produit un HTML structuré qu'un autre éditeur (Word, Google
// Docs, un autre BlockEditor, …) peut interpréter. `htmlToBlocks` fait
// l'inverse : du HTML quelconque (provenance externe ou interne) vers la
// liste de blocs typés.
//
// Module **neutre** — pas d'import React. Utilise le `document` global
// (paste/copy s'exécutent toujours côté client) et `DOMParser` pour le
// parsing.

import { newBlockId } from '../utils/ids.js';
import { inlineToDom, domToInline } from './serialize.js';
import { inlineFromText, inlineToPlainText, normalize } from './types.js';

const HEADING_RE = /^heading_([1-6])$/;
const BLOCK_TAG_RE = /^(P|H[1-6]|UL|OL|BLOCKQUOTE|PRE|HR|FIGURE|DIV|TABLE)$/;
const MEDIA_FILE_URL_RE = /^\/zen\/api\/media\/file\/([^/?#]+)$/;

// Quand une image collée pointe sur la médiathèque interne, on dérive le
// slug pour reconstituer le lien `mediaSlug` (un copier-coller ne traverse
// pas les champs cachés). Pour les URL externes, retourne null.
function imageSlugFromSrc(src) {
  if (typeof src !== 'string') return null;
  const m = MEDIA_FILE_URL_RE.exec(src);
  return m ? m[1] : null;
}

// Block[] → HTML string. Regroupe les listes consécutives sous un seul
// <ul>/<ol>. Les blocs inconnus deviennent un <p> au texte aplati.
export function blocksToHtml(blocks) {
  if (!Array.isArray(blocks) || blocks.length === 0) return '';
  if (typeof document === 'undefined') return '';

  const fragment = document.createDocumentFragment();
  let i = 0;
  while (i < blocks.length) {
    const block = blocks[i];
    if (
      block.type === 'bullet_item' ||
      block.type === 'numbered_item' ||
      block.type === 'checklist'
    ) {
      const tag = block.type === 'numbered_item' ? 'ol' : 'ul';
      const list = document.createElement(tag);
      if (block.type === 'checklist') list.setAttribute('data-checklist', '');
      while (i < blocks.length && blocks[i].type === block.type) {
        const li = document.createElement('li');
        if (blocks[i].type === 'checklist') {
          const cb = document.createElement('input');
          cb.type = 'checkbox';
          if (blocks[i].checked) cb.setAttribute('checked', '');
          li.appendChild(cb);
          li.appendChild(document.createTextNode(' '));
        }
        li.appendChild(inlineToDom(blocks[i].content ?? []));
        list.appendChild(li);
        i++;
      }
      fragment.appendChild(list);
      continue;
    }

    fragment.appendChild(blockToElement(block));
    i++;
  }

  const wrapper = document.createElement('div');
  wrapper.appendChild(fragment);
  return wrapper.innerHTML;
}

function blockToElement(block) {
  const heading = HEADING_RE.exec(block.type);
  if (heading) {
    const h = document.createElement(`h${heading[1]}`);
    h.appendChild(inlineToDom(block.content ?? []));
    return h;
  }
  if (block.type === 'paragraph') {
    const p = document.createElement('p');
    p.appendChild(inlineToDom(block.content ?? []));
    return p;
  }
  if (block.type === 'quote') {
    const bq = document.createElement('blockquote');
    bq.appendChild(inlineToDom(block.content ?? []));
    return bq;
  }
  if (block.type === 'code') {
    const pre = document.createElement('pre');
    const code = document.createElement('code');
    code.appendChild(inlineToDom(block.content ?? []));
    pre.appendChild(code);
    return pre;
  }
  if (block.type === 'divider') {
    return document.createElement('hr');
  }
  if (block.type === 'image') {
    const fig = document.createElement('figure');
    const align = block.align || 'center';
    fig.setAttribute('data-align', align);
    if (align === 'left' || align === 'right' || align === 'center') {
      // CSS inline minimal pour les destinations qui ignorent data-align.
      fig.setAttribute('style',
        align === 'center' ? 'text-align:center'
        : align === 'left' ? 'text-align:left'
        : 'text-align:right');
    }
    // Quand le bloc est lié à un média (`mediaSlug`), alt/caption sont
    // résolus côté serveur (`_resolvedAlt` / `_resolvedCaption`) ou
    // captés à l'insertion (`_mediaAlt` / `_mediaCaption`). Pour les URL
    // externes, on retombe sur les champs locaux du bloc.
    const altText = block._resolvedAlt ?? block._mediaAlt ?? block.alt ?? '';
    const captionText = block._resolvedCaption ?? block._mediaCaption ?? block.caption ?? '';
    const img = document.createElement('img');
    img.setAttribute('src', block.src || '');
    if (altText) img.setAttribute('alt', altText);
    if (align === 'full') img.setAttribute('width', '100%');
    let imgHost = img;
    if (block.href) {
      const a = document.createElement('a');
      a.setAttribute('href', block.href);
      if (block.newTab) {
        a.setAttribute('target', '_blank');
        a.setAttribute('rel', 'noopener noreferrer');
      }
      a.appendChild(img);
      imgHost = a;
    }
    fig.appendChild(imgHost);
    if (captionText) {
      const cap = document.createElement('figcaption');
      cap.textContent = captionText;
      fig.appendChild(cap);
    }
    return fig;
  }
  // Type inconnu : on aplatit en paragraphe.
  const p = document.createElement('p');
  p.textContent = inlineToPlainText(block.content ?? []);
  return p;
}

// Block[] → texte brut pour le MIME `text/plain` complémentaire.
export function blocksToPlainText(blocks) {
  if (!Array.isArray(blocks) || blocks.length === 0) return '';
  return blocks
    .map(b => {
      if (b.type === 'divider') return '---';
      if (b.type === 'image') {
        const alt = b._resolvedAlt ?? b._mediaAlt ?? b.alt ?? '';
        const cap = b._resolvedCaption ?? b._mediaCaption ?? b.caption ?? '';
        return alt || cap || '';
      }
      return inlineToPlainText(b.content ?? []);
    })
    .join('\n');
}

// JSON Notion (MIME `text/_notion-blocks-v3-production`) → Block[].
// Notion expose ses blocs natifs en parallèle du HTML ; quand cette MIME
// est disponible, on l'utilise en priorité car elle conserve les types
// (`to_do`, `sub_sub_header`, …) que le HTML aplatit en `<ul><li>`.
export function notionJsonToBlocks(json) {
  if (!json) return [];
  let parsed;
  try { parsed = typeof json === 'string' ? JSON.parse(json) : json; }
  catch { return []; }
  const items = parsed?.blocks;
  if (!Array.isArray(items)) return [];
  const out = [];
  for (const item of items) {
    const id = item?.blockId;
    const value = item?.blockSubtree?.block?.[id]?.value;
    if (!value) continue;
    const block = notionValueToBlock(value);
    if (block) out.push(block);
  }
  return out;
}

function notionValueToBlock(value) {
  const type = value.type;
  const title = value.properties?.title;
  const content = notionTitleToInline(title);
  const checkedProp = value.properties?.checked?.[0]?.[0];
  const checked = checkedProp === 'Yes' || checkedProp === true;

  switch (type) {
    case 'header':       return { id: newBlockId(), type: 'heading_1', content };
    case 'sub_header':   return { id: newBlockId(), type: 'heading_2', content };
    case 'sub_sub_header': return { id: newBlockId(), type: 'heading_3', content };
    case 'to_do':        return { id: newBlockId(), type: 'checklist', content, checked };
    case 'bulleted_list': return { id: newBlockId(), type: 'bullet_item', content };
    case 'numbered_list': return { id: newBlockId(), type: 'numbered_item', content };
    case 'quote':        return { id: newBlockId(), type: 'quote', content };
    case 'code':         return { id: newBlockId(), type: 'code', content };
    case 'divider':      return { id: newBlockId(), type: 'divider' };
    case 'text':
    default:             return { id: newBlockId(), type: 'paragraph', content };
  }
}

function notionTitleToInline(title) {
  if (!Array.isArray(title)) return [];
  const nodes = [];
  for (const run of title) {
    if (!Array.isArray(run)) continue;
    const text = run[0];
    if (typeof text !== 'string' || text.length === 0) continue;
    const fmts = run[1];
    const marks = [];
    if (Array.isArray(fmts)) {
      for (const fmt of fmts) {
        const tag = fmt?.[0];
        if (tag === 'b') marks.push({ type: 'bold' });
        else if (tag === 'i') marks.push({ type: 'italic' });
        else if (tag === '_') marks.push({ type: 'underline' });
        else if (tag === 's') marks.push({ type: 'strike' });
        else if (tag === 'c') marks.push({ type: 'code' });
        else if (tag === 'a' && typeof fmt[1] === 'string') marks.push({ type: 'link', href: fmt[1] });
        else if (tag === 'h' && typeof fmt[1] === 'string') marks.push({ type: 'color', color: fmt[1] });
      }
    }
    nodes.push(marks.length ? { type: 'text', text, marks } : { type: 'text', text });
  }
  return normalize(nodes);
}

// HTML string → Block[]. `DOMParser` n'exécute pas les scripts ; les tags
// inconnus contribuent uniquement leur contenu inline (via `domToInline`).
export function htmlToBlocks(html) {
  if (!html || typeof DOMParser === 'undefined') return [];
  const doc = new DOMParser().parseFromString(html, 'text/html');
  const out = [];
  parseChildren(doc.body, out);
  return out;
}

function parseChildren(node, out) {
  const buffer = { nodes: [] };
  function flush() {
    if (buffer.nodes.length === 0) return;
    const content = normalize(buffer.nodes);
    buffer.nodes = [];
    if (content.length === 0) return;
    out.push({ id: newBlockId(), type: 'paragraph', content });
  }

  for (const child of node.childNodes) {
    if (child.nodeType === 3 /* TEXT_NODE */) {
      const t = child.nodeValue;
      if (t && t.trim()) buffer.nodes.push({ type: 'text', text: t });
      continue;
    }
    if (child.nodeType !== 1 /* ELEMENT_NODE */) continue;
    const tag = child.tagName;

    const heading = /^H([1-6])$/.exec(tag);
    if (heading) {
      flush();
      const content = domToInline(child);
      if (content.length > 0) {
        out.push({ id: newBlockId(), type: `heading_${heading[1]}`, content });
      }
      continue;
    }

    if (tag === 'P') {
      flush();
      const content = domToInline(child);
      if (content.length > 0) {
        out.push({ id: newBlockId(), type: 'paragraph', content });
      }
      continue;
    }

    if (tag === 'UL' || tag === 'OL') {
      flush();
      parseList(child, tag === 'OL', out);
      continue;
    }

    if (tag === 'BLOCKQUOTE') {
      flush();
      const content = domToInline(child);
      if (content.length > 0) {
        out.push({ id: newBlockId(), type: 'quote', content });
      }
      continue;
    }

    if (tag === 'PRE') {
      flush();
      const codeEl = child.querySelector('code') || child;
      const text = codeEl.textContent || '';
      out.push({ id: newBlockId(), type: 'code', content: inlineFromText(text) });
      continue;
    }

    if (tag === 'HR') {
      flush();
      out.push({ id: newBlockId(), type: 'divider' });
      continue;
    }

    if (tag === 'FIGURE') {
      flush();
      const img = child.querySelector('img');
      if (img) {
        const cap = child.querySelector('figcaption');
        const linkEl = img.parentElement?.tagName === 'A' ? img.parentElement : null;
        const dataAlign = child.getAttribute('data-align');
        const align = ['left', 'center', 'right', 'full'].includes(dataAlign) ? dataAlign : 'center';
        const href = linkEl?.getAttribute('href') || '';
        const newTab = !!href && linkEl?.getAttribute('target') === '_blank';
        const src = img.getAttribute('src') || '';
        const slug = imageSlugFromSrc(src);
        const altText = img.getAttribute('alt') || '';
        const captionText = cap?.textContent?.trim() || '';
        out.push({
          id: newBlockId(),
          type: 'image',
          src,
          mediaSlug: slug || '',
          alt: slug ? '' : altText,
          caption: slug ? '' : captionText,
          align,
          href,
          newTab,
          ...(slug ? { _mediaAlt: altText, _mediaCaption: captionText } : null),
        });
      }
      continue;
    }

    if (tag === 'IMG') {
      flush();
      const src = child.getAttribute('src') || '';
      const slug = imageSlugFromSrc(src);
      const altText = child.getAttribute('alt') || '';
      out.push({
        id: newBlockId(),
        type: 'image',
        src,
        mediaSlug: slug || '',
        alt: slug ? '' : altText,
        caption: '',
        align: 'center',
        href: '',
        newTab: false,
        ...(slug ? { _mediaAlt: altText } : null),
      });
      continue;
    }

    if (tag === 'BR') {
      // Saut de ligne au top-level → coupure de paragraphe.
      flush();
      continue;
    }

    // Wrappers Google Docs / Word ou <div> de mise en page : si l'élément
    // contient au moins un descendant block-level, on flush le buffer puis
    // on recurse dedans pour récupérer la structure. Sinon on traite
    // l'élément comme du contenu inline.
    if (hasBlockDescendant(child)) {
      flush();
      parseChildren(child, out);
      continue;
    }

    // Inline : ajouter au buffer (paragraphe en cours d'accumulation).
    const inline = domToInline(child);
    buffer.nodes.push(...inline);
  }
  flush();
}

function hasBlockDescendant(el) {
  for (const c of el.children) {
    if (BLOCK_TAG_RE.test(c.tagName)) return true;
    if (hasBlockDescendant(c)) return true;
  }
  return false;
}

function parseList(listEl, ordered, out) {
  // Détection « liste de cases à cocher » : data-checklist (export interne),
  // ou class="to-do-list" (Notion), ou class contenant "task-list" (GitHub-flavored markdown).
  const listClass = listEl.getAttribute('class') || '';
  const isChecklist =
    listEl.hasAttribute('data-checklist') ||
    /\b(to-do-list|task-list|checklist)\b/i.test(listClass);
  for (const li of listEl.children) {
    if (li.tagName !== 'LI') continue;
    // Détection checkbox au niveau item :
    //  - <input type="checkbox"> direct child (GitHub task list, export interne)
    //  - <div class="checkbox"> direct child (Notion : checkbox-on / checkbox-off)
    const inputCb = li.querySelector(':scope > input[type="checkbox"]');
    const divCb = li.querySelector(':scope > div.checkbox, :scope > div[class*="checkbox"]');
    const checkbox = inputCb || divCb;
    const isChecklistItem = isChecklist || !!checkbox;
    let checked = false;
    if (inputCb) {
      checked = inputCb.checked || inputCb.hasAttribute('checked');
      inputCb.remove();
    } else if (divCb) {
      checked = /checkbox-on/.test(divCb.getAttribute('class') || '');
      divCb.remove();
    }

    // Si le <li> contient lui-même des sous-listes, on émet d'abord un
    // item pour son contenu inline, puis on traite les sous-listes comme
    // des items frères (pas de nesting dans notre modèle).
    const subLists = Array.from(li.children).filter(
      c => c.tagName === 'UL' || c.tagName === 'OL',
    );
    for (const sub of subLists) sub.remove();

    const content = domToInline(li);
    if (content.length > 0 || isChecklistItem) {
      if (isChecklistItem) {
        out.push({ id: newBlockId(), type: 'checklist', content, checked: !!checked });
      } else if (ordered) {
        out.push({ id: newBlockId(), type: 'numbered_item', content });
      } else {
        out.push({ id: newBlockId(), type: 'bullet_item', content });
      }
    }

    for (const sub of subLists) parseList(sub, sub.tagName === 'OL', out);
  }
}
