export function toggleInlineTag(
  root: HTMLElement,
  tag: 'strong' | 'em' | 'u' | 'span',
  attrs?: Record<string, string>
) {
  const sel = window.getSelection();
  if (!sel || sel.rangeCount === 0) return;
  const range = sel.getRangeAt(0);
  const aliases: Record<string, string[]> = {
    strong: ['strong', 'b'],
    em: ['em', 'i'],
    u: ['u'],
    span: ['span'],
  };
  const candidates = aliases[tag] ?? [tag];
  const isMatch = (el: HTMLElement) =>
    candidates.includes(el.tagName.toLowerCase());

  function unwrap(el: HTMLElement) {
    const p = el.parentNode;
    if (!p) return;
    while (el.firstChild) p.insertBefore(el.firstChild, el);
    p.removeChild(el);
  }

  function removeEmptyAndNested(root: HTMLElement) {
    root.querySelectorAll(candidates.join(',')).forEach(el => {
      if (!el.textContent?.trim()) {
        el.remove();
        return;
      }
      const nested = Array.from(el.querySelectorAll(candidates.join(',')));
      nested.forEach(n => {
        if (n === el) return;
        while (n.firstChild) el.insertBefore(n.firstChild, n);
        n.remove();
      });
    });
  }

  function mergeAdjacent(root: HTMLElement) {
    const list = Array.from(root.querySelectorAll(candidates.join(',')));
    list.forEach(el => {
      const next = el.nextSibling;
      if (next instanceof HTMLElement && isMatch(next)) {
        while (next.firstChild) el.appendChild(next.firstChild);
        next.remove();
      }
    });
  }
  if (range.collapsed) {
    let inside = false;
    let n: Node | null = range.startContainer;
    while (n && n !== root) {
      if (n instanceof HTMLElement && isMatch(n)) {
        inside = true;
        break;
      }
      n = n.parentNode;
    }

    if (inside && n instanceof HTMLElement) {
      unwrap(n);
    } else {
      const elm = document.createElement(tag);
      if (attrs)
        Object.entries(attrs).forEach(([k, v]) => elm.setAttribute(k, v));
      elm.appendChild(document.createTextNode('\u200b'));
      range.insertNode(elm);
      range.setStart(elm.firstChild!, 1);
      range.collapse(true);
      sel.removeAllRanges();
      sel.addRange(range);
    }

    removeEmptyAndNested(root);
    mergeAdjacent(root);
    return;
  }
  if (range.startContainer.nodeType === Node.TEXT_NODE)
    (range.startContainer as Text).splitText(range.startOffset);
  if (range.endContainer.nodeType === Node.TEXT_NODE)
    (range.endContainer as Text).splitText(range.endOffset);
  const walker = document.createTreeWalker(
    range.commonAncestorContainer,
    NodeFilter.SHOW_TEXT,
    {
      acceptNode: n => {
        if (!n.nodeValue?.trim()) return NodeFilter.FILTER_REJECT;
        const r = document.createRange();
        r.selectNodeContents(n);
        const inter =
          range.compareBoundaryPoints(Range.END_TO_START, r) < 0 &&
          range.compareBoundaryPoints(Range.START_TO_END, r) > 0;
        return inter ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
      },
    }
  );

  const textNodes: Text[] = [];
  let tn: Node | null;
  while ((tn = walker.nextNode())) textNodes.push(tn as Text);

  const allInside =
    textNodes.length > 0 &&
    textNodes.every(txt => {
      let n: Node | null = txt;
      while (n && n !== root) {
        if (n instanceof HTMLElement && isMatch(n)) return true;
        n = n.parentNode;
      }
      return false;
    });

  if (allInside) {
    const first = textNodes[0];
    const last = textNodes[textNodes.length - 1];
    const common = first.parentElement?.closest(tag) as HTMLElement | null;
    if (common && common.contains(last)) {
      const parent = common.parentNode;
      if (parent) {
        const beforeRange = document.createRange();
        beforeRange.setStartBefore(common);
        beforeRange.setEndBefore(first);
        const before = beforeRange.cloneContents();

        const afterRange = document.createRange();
        afterRange.setStartAfter(last);
        afterRange.setEndAfter(common);
        const after = afterRange.cloneContents();

        const middleRange = document.createRange();
        middleRange.setStartBefore(first);
        middleRange.setEndAfter(last);
        const middle = middleRange.extractContents();

        const frag = document.createDocumentFragment();
        if (before.childNodes.length) {
          const b = document.createElement(tag);
          while (before.firstChild) b.appendChild(before.firstChild);
          frag.appendChild(b);
        }
        while (middle.firstChild) frag.appendChild(middle.firstChild);
        if (after.childNodes.length) {
          const a = document.createElement(tag);
          while (after.firstChild) a.appendChild(after.firstChild);
          frag.appendChild(a);
        }
        parent.insertBefore(frag, common);
        parent.removeChild(common);
      }
    } else {
      textNodes.forEach(txt => {
        let n: Node | null = txt;
        while (n && n !== root) {
          if (n instanceof HTMLElement && isMatch(n)) {
            unwrap(n);
            break;
          }
          n = n.parentNode;
        }
      });
    }
  } else {
    textNodes.forEach(txt => {
      const inProtected = txt.parentElement?.closest('.mention, nile-mention');
      if (inProtected) return;
      const inAnchor = txt.parentElement?.closest('a');
      if (inAnchor && tag === 'span') return;
      const wrapper = document.createElement(tag);
      if (attrs)
        Object.entries(attrs).forEach(([k, v]) => wrapper.setAttribute(k, v));
      if (inAnchor) {
        inAnchor.insertBefore(wrapper, txt);
        wrapper.appendChild(txt);
      } else {
        txt.parentNode?.insertBefore(wrapper, txt);
        wrapper.appendChild(txt);
      }
    });
  }

  removeEmptyAndNested(root);
  mergeAdjacent(root);

  sel.removeAllRanges();
  sel.addRange(range);
}

export function surroundInline(
  range: Range,
  tag: string,
  attrs?: Record<string, string>
) {
  const wrap = document.createElement(tag);
  if (attrs) Object.entries(attrs).forEach(([k, v]) => wrap.setAttribute(k, v));
  try {
    range.surroundContents(wrap);
  } catch {
    const frag = range.extractContents();
    wrap.appendChild(frag);
    range.insertNode(wrap);
  }
}

export function unwrap(node: HTMLElement) {
  const p = node.parentNode;
  if (!p) return;
  while (node.firstChild) p.insertBefore(node.firstChild, node);
  p.removeChild(node);
}

function unwrapAllMatching(
  node: Node,
  root: HTMLElement,
  matcher: (el: HTMLElement) => boolean
) {
  let n: Node | null = node.parentNode;
  while (n && n !== root) {
    if (n instanceof HTMLElement && matcher(n)) {
      unwrap(n);

      n = node.parentNode;
      continue;
    }
    n = n.parentNode;
  }
}

export function rgbToHex(rgb: string): string {
  const m = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
  if (!m) return '#000000';
  const r = Number(m[1]).toString(16).padStart(2, '0');
  const g = Number(m[2]).toString(16).padStart(2, '0');
  const b = Number(m[3]).toString(16).padStart(2, '0');
  return `#${r}${g}${b}`;
}

export function setFontFamily(root: HTMLElement, family: string) {
  const sel = document.getSelection();
  if (!sel || sel.rangeCount === 0) return;
  const range = sel.getRangeAt(0);
  if (range.collapsed) {
    const span = document.createElement('span');
    span.style.fontFamily = family;
    span.appendChild(document.createTextNode('\u200b'));
    range.insertNode(span);
    const r = document.createRange();
    r.setStart(span.firstChild!, 1);
    r.collapse(true);
    sel.removeAllRanges();
    sel.addRange(r);
    return;
  }
  surroundInline(range, 'span', { style: `font-family:${family}` });
}
