"use client";

import { RefObject, useCallback, useEffect, useMemo, useRef } from "react";

type Props = {
  name: string;
  value: string;
  inputRef: RefObject<HTMLTextAreaElement | null>;
  validVariables?: string[];
};

type HighlightRegistry = {
  set: (name: string, value: unknown) => void;
  delete: (name: string) => boolean;
};

type HighlightConstructor = new (...ranges: Range[]) => unknown;

const sizes = ["10", "12", "14", "18", "24", "32"];
const legacySizes: Record<string, string> = {
  "1": "10px",
  "2": "12px",
  "3": "14px",
  "4": "18px",
  "5": "24px",
  "6": "32px",
  "7": "48px",
};
const validHighlightName = "documenta-template-variable-valid";
const invalidHighlightName = "documenta-template-variable-invalid";

function editorHtml(value: string) {
  if (/<\/?[a-z][^>]*>/i.test(value)) return value;
  const escape = (text: string) =>
    text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
  return value
    .split("\n")
    .map((line) => `<p>${escape(line) || "<br>"}</p>`)
    .join("");
}

function highlightApi() {
  if (typeof window === "undefined" || typeof CSS === "undefined") return null;
  const registry = (CSS as typeof CSS & { highlights?: HighlightRegistry }).highlights;
  const constructor = (window as typeof window & { Highlight?: HighlightConstructor }).Highlight;
  return registry && constructor ? { registry, constructor } : null;
}

function normalizeLegacyFontSizes(root: HTMLElement) {
  root.querySelectorAll<HTMLElement>("font[size]").forEach((element) => {
    const size = element.getAttribute("size") || "";
    if (legacySizes[size]) element.style.fontSize = legacySizes[size];
    element.removeAttribute("size");
  });
}

export default function RichTextEditor({
  name,
  value,
  inputRef,
  validVariables = [],
}: Props) {
  const editor = useRef<HTMLDivElement>(null);
  const savedSelection = useRef<Range | null>(null);
  const highlightFrame = useRef<number | null>(null);
  const refreshHighlightsRef = useRef<() => void>(() => undefined);
  const validVariableSet = useMemo(() => new Set(validVariables), [validVariables]);

  const refreshHighlights = useCallback(() => {
    const root = editor.current;
    const api = highlightApi();
    if (!root || !api) return;

    const nodes: { node: Text; start: number; end: number }[] = [];
    const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
    let content = "";
    let current = walker.nextNode();
    while (current) {
      const node = current as Text;
      const start = content.length;
      content += node.data;
      if (node.data.length) nodes.push({ node, start, end: content.length });
      current = walker.nextNode();
    }

    const validRanges: Range[] = [];
    const invalidRanges: Range[] = [];
    const pattern = /\{\{\s*([a-zA-Z][a-zA-Z0-9_.-]*)\s*\}\}|\[\s*([a-zA-Z][a-zA-Z0-9_.-]*)\s*\]/g;

    function startPoint(offset: number) {
      return nodes.find((item) => offset >= item.start && offset < item.end) || null;
    }
    function endPoint(offset: number) {
      return nodes.find((item) => offset > item.start && offset <= item.end) || null;
    }

    for (const match of content.matchAll(pattern)) {
      const start = match.index;
      const end = start + match[0].length;
      const first = startPoint(start);
      const last = endPoint(end);
      if (!first || !last) continue;
      const range = document.createRange();
      range.setStart(first.node, start - first.start);
      range.setEnd(last.node, end - last.start);
      const variable = match[1] || match[2];
      (validVariableSet.has(variable) ? validRanges : invalidRanges).push(range);
    }

    api.registry.delete(validHighlightName);
    api.registry.delete(invalidHighlightName);
    if (validRanges.length) {
      api.registry.set(validHighlightName, new api.constructor(...validRanges));
    }
    if (invalidRanges.length) {
      api.registry.set(invalidHighlightName, new api.constructor(...invalidRanges));
    }
  }, [validVariableSet]);

  useEffect(() => {
    refreshHighlightsRef.current = refreshHighlights;
  }, [refreshHighlights]);

  const scheduleHighlights = useCallback(() => {
    if (highlightFrame.current !== null) window.cancelAnimationFrame(highlightFrame.current);
    highlightFrame.current = window.requestAnimationFrame(() => {
      highlightFrame.current = null;
      refreshHighlightsRef.current();
    });
  }, []);

  useEffect(() => {
    const normalized = editorHtml(value || "") || "<p><br></p>";
    if (editor.current) {
      editor.current.innerHTML = normalized;
      normalizeLegacyFontSizes(editor.current);
      savedSelection.current = null;
    }
    if (inputRef.current) inputRef.current.value = editor.current?.innerHTML || normalized;
    scheduleHighlights();
  }, [value, inputRef, scheduleHighlights]);

  useEffect(() => {
    scheduleHighlights();
  }, [validVariableSet, scheduleHighlights]);

  useEffect(() => () => {
    if (highlightFrame.current !== null) window.cancelAnimationFrame(highlightFrame.current);
    const api = highlightApi();
    api?.registry.delete(validHighlightName);
    api?.registry.delete(invalidHighlightName);
  }, []);

  function rememberSelection() {
    const root = editor.current;
    const selection = window.getSelection();
    if (!root || !selection?.rangeCount) return;
    const range = selection.getRangeAt(0);
    if (root.contains(range.commonAncestorContainer)) savedSelection.current = range.cloneRange();
  }

  function restoreSelection() {
    const root = editor.current;
    if (!root) return;
    root.focus({ preventScroll: true });
    const range = savedSelection.current;
    if (!range || !root.contains(range.commonAncestorContainer)) return;
    const selection = window.getSelection();
    selection?.removeAllRanges();
    selection?.addRange(range);
  }

  function sync() {
    if (inputRef.current) inputRef.current.value = editor.current?.innerHTML || "";
    rememberSelection();
    scheduleHighlights();
  }

  function command(commandName: string, argument?: string) {
    restoreSelection();
    document.execCommand(commandName, false, argument);
    sync();
  }

  function applyFontSize(size: string) {
    restoreSelection();
    document.execCommand("fontSize", false, "7");
    editor.current?.querySelectorAll<HTMLElement>('font[size="7"]').forEach((element) => {
      element.removeAttribute("size");
      element.style.fontSize = `${size}px`;
    });
    sync();
  }

  function button(label: string, commandName: string, title: string) {
    return (
      <button
        type="button"
        title={title}
        aria-label={title}
        onMouseDown={(event) => {
          rememberSelection();
          event.preventDefault();
          command(commandName);
        }}
      >
        {label}
      </button>
    );
  }

  return (
    <div className="rich-editor">
      <div className="rich-editor-toolbar" role="toolbar" aria-label="Formato de la plantilla">
        <select
          aria-label="Fuente"
          defaultValue="Arial"
          onPointerDown={rememberSelection}
          onChange={(event) => command("fontName", event.target.value)}
        >
          {["Arial", "Calibri", "Georgia", "Times New Roman", "Verdana"].map((font) => (
            <option key={font}>{font}</option>
          ))}
        </select>
        <select
          aria-label="Tamaño"
          defaultValue="12"
          onPointerDown={rememberSelection}
          onChange={(event) => applyFontSize(event.target.value)}
        >
          {sizes.map((size) => (
            <option key={size} value={size}>{size} px</option>
          ))}
        </select>
        <select
          aria-label="Estilo de párrafo"
          defaultValue="p"
          onPointerDown={rememberSelection}
          onChange={(event) => command("formatBlock", event.target.value)}
        >
          <option value="p">Párrafo</option>
          <option value="h1">Título 1</option>
          <option value="h2">Título 2</option>
          <option value="h3">Título 3</option>
          <option value="blockquote">Cita</option>
        </select>
        <span className="rich-editor-separator" />
        {button("B", "bold", "Negrita")}
        {button("I", "italic", "Cursiva")}
        {button("U", "underline", "Subrayado")}
        {button("S", "strikeThrough", "Tachado")}
        <label className="rich-editor-color" title="Color del texto">
          <span>A</span>
          <input
            type="color"
            defaultValue="#111317"
            onPointerDown={rememberSelection}
            onChange={(event) => command("foreColor", event.target.value)}
          />
        </label>
        <span className="rich-editor-separator" />
        {button("≡←", "justifyLeft", "Alinear a la izquierda")}
        {button("≡", "justifyCenter", "Centrar")}
        {button("→≡", "justifyRight", "Alinear a la derecha")}
        {button("☰", "justifyFull", "Justificar")}
        {button("• Lista", "insertUnorderedList", "Lista con viñetas")}
        {button("1. Lista", "insertOrderedList", "Lista numerada")}
        <span className="rich-editor-separator" />
        {button("↶", "undo", "Deshacer")}
        {button("↷", "redo", "Rehacer")}
        {button("Tx", "removeFormat", "Limpiar formato")}
      </div>
      <div
        ref={editor}
        className="rich-editor-canvas"
        contentEditable
        suppressContentEditableWarning
        role="textbox"
        aria-label="Contenido de la plantilla"
        aria-multiline="true"
        spellCheck
        onFocus={rememberSelection}
        onMouseUp={rememberSelection}
        onKeyUp={rememberSelection}
        onInput={sync}
        onBlur={sync}
      />
      <textarea
        ref={inputRef}
        name={name}
        className="rich-editor-value"
        tabIndex={-1}
        aria-hidden="true"
      />
      <div className="rich-editor-status">
        <span>Documento enriquecido</span>
        <span>Variables disponibles en azul; variables no disponibles en rojo.</span>
      </div>
    </div>
  );
}
