import {
  mergeAttributes,
  Node,
  nodeInputRule,
  nodePasteRule,
  type Editor
} from "@tiptap/core";
import Image from "@tiptap/extension-image";
import { EditorContent, useEditor, useEditorState } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import {
  Bold,
  Code2,
  ImagePlus,
  Italic,
  Link2,
  Link2Off,
  List,
  ListOrdered,
  Quote,
  Redo2,
  RemoveFormatting,
  Strikethrough,
  Underline,
  Undo2
} from "lucide-react";
import {
  forwardRef,
  useEffect,
  useImperativeHandle,
  useMemo,
  useRef,
  useState,
  type CSSProperties,
  type ChangeEvent,
  type MouseEvent
} from "react";
import { usePlatformLanguage } from "../i18n/LanguageContext";
import Button from "./Button";
import Dialog from "./Dialog";
import FormField from "./FormField";
import IconButton from "./IconButton";
import SegmentedControl from "./SegmentedControl";
import { normalizeWysiwygImageUrl, normalizeWysiwygLinkUrl } from "./wysiwygEditorUrls";

export type WysiwygEditorMode = "visual" | "source";

export type WysiwygEditorToken = {
  value: string;
  label?: string;
};

export type WysiwygEditorHandle = {
  focus: () => void;
  insertText: (value: string) => boolean;
  insertToken: (token: WysiwygEditorToken | string) => boolean;
};

export type WysiwygEditorLabels = {
  editor: string;
  visual: string;
  source: string;
  blockStyle: string;
  paragraph: string;
  heading1: string;
  heading2: string;
  heading3: string;
  undo: string;
  redo: string;
  bold: string;
  italic: string;
  underline: string;
  strike: string;
  inlineCodeLabel: string;
  bulletList: string;
  numberedList: string;
  quote: string;
  insertLink: string;
  removeLink: string;
  insertImage: string;
  clearFormatting: string;
  linkText: string;
  linkUrlLabel: string;
  imageUrlLabel: string;
  alternativeText: string;
  apply: string;
  cancel: string;
  invalidLink: string;
  invalidImage: string;
  sourceWarning: string;
};

export type WysiwygEditorProps = {
  value: string;
  onChange: (value: string) => void;
  disabled?: boolean;
  defaultMode?: WysiwygEditorMode;
  allowSourceMode?: boolean;
  ariaLabel?: string;
  className?: string;
  minHeight?: number | string;
  sourceRows?: number;
  labels?: Partial<WysiwygEditorLabels>;
  onFocus?: () => void;
};

type BlockStyle = "paragraph" | "heading-1" | "heading-2" | "heading-3";

const INLINE_TOKEN_NAME = "govoplanInlineToken";
const TOKEN_INPUT_PATTERN = /((?:\{\{[^{}\r\n]+\}\}|\$\{[^{}\r\n]+\}))$/;
const TOKEN_PASTE_PATTERN = /(?:\{\{[^{}\r\n]+\}\}|\$\{[^{}\r\n]+\})/g;
const TOKEN_SCAN_PATTERN = /(?:\{\{[^{}\r\n]+\}\}|\$\{[^{}\r\n]+\})/g;

const DEFAULT_LABELS: WysiwygEditorLabels = {
  editor: "i18n:govoplan-core.rich_text_editor.b56157a6",
  visual: "i18n:govoplan-core.visual.770d690e",
  source: "i18n:govoplan-core.html_source.1d4100b2",
  blockStyle: "i18n:govoplan-core.block_style.c050e2eb",
  paragraph: "i18n:govoplan-core.paragraph.05058e0b",
  heading1: "i18n:govoplan-core.heading_1.fdc7d47a",
  heading2: "i18n:govoplan-core.heading_2.6542d386",
  heading3: "i18n:govoplan-core.heading_3.9694d18f",
  undo: "i18n:govoplan-core.undo.39fc7212",
  redo: "i18n:govoplan-core.redo.471b94d4",
  bold: "i18n:govoplan-core.bold.19e07430",
  italic: "i18n:govoplan-core.italic.1616e2e5",
  underline: "i18n:govoplan-core.underline.39773aa3",
  strike: "i18n:govoplan-core.strikethrough.a93b9a68",
  inlineCodeLabel: "i18n:govoplan-core.inline_code.6f3a4263",
  bulletList: "i18n:govoplan-core.bullet_list.660d9fbe",
  numberedList: "i18n:govoplan-core.numbered_list.8294ea4f",
  quote: "i18n:govoplan-core.block_quote.b3399cd3",
  insertLink: "i18n:govoplan-core.insert_link.4d4ac9a1",
  removeLink: "i18n:govoplan-core.remove_link.8d48d1f9",
  insertImage: "i18n:govoplan-core.insert_image.c8141eb5",
  clearFormatting: "i18n:govoplan-core.clear_formatting.6c8a26c4",
  linkText: "i18n:govoplan-core.link_text.502f872d",
  linkUrlLabel: "i18n:govoplan-core.link_url.08738f6e",
  imageUrlLabel: "i18n:govoplan-core.image_url.35c80f96",
  alternativeText: "i18n:govoplan-core.alternative_text.8d87ae1b",
  apply: "i18n:govoplan-core.apply.cfea419c",
  cancel: "i18n:govoplan-core.cancel.77dfd213",
  invalidLink: "i18n:govoplan-core.enter_a_valid_http_https_mail_or_phone_link.45447137",
  invalidImage: "i18n:govoplan-core.enter_a_valid_http_https_cid_or_raster_data_image.2cec4b3c",
  sourceWarning: "i18n:govoplan-core.this_html_uses_markup_outside_the_visual_editor.5adb2a3c"
};

const InlineToken = Node.create({
  name: INLINE_TOKEN_NAME,
  group: "inline",
  inline: true,
  atom: true,
  selectable: true,

  addAttributes() {
    return {
      value: { default: "" },
      label: { default: "" }
    };
  },

  parseHTML() {
    return [{
      tag: "span[data-govoplan-token]",
      getAttrs: (element) => {
        if (!(element instanceof HTMLElement)) return false;
        const value = element.dataset.govoplanToken || "";
        if (!value) return false;
        return {
          value,
          label: element.dataset.govoplanTokenLabel || tokenLabel(value)
        };
      }
    }];
  },

  renderHTML({ node, HTMLAttributes }) {
    const value = String(node.attrs.value || "");
    const label = String(node.attrs.label || tokenLabel(value));
    return [
      "span",
      mergeAttributes(HTMLAttributes, {
        class: "wysiwyg-inline-token",
        "data-govoplan-token": value,
        "data-govoplan-token-label": label,
        contenteditable: "false"
      }),
      label
    ];
  },

  addInputRules() {
    return [nodeInputRule({
      find: TOKEN_INPUT_PATTERN,
      type: this.type,
      getAttributes: (match) => tokenAttributes(match[1] || match[0])
    })];
  },

  addPasteRules() {
    return [nodePasteRule({
      find: TOKEN_PASTE_PATTERN,
      type: this.type,
      getAttributes: (match) => tokenAttributes(match[0])
    })];
  }
});

const WysiwygEditor = forwardRef<WysiwygEditorHandle, WysiwygEditorProps>(function WysiwygEditor({
  value,
  onChange,
  disabled = false,
  defaultMode = "visual",
  allowSourceMode = true,
  ariaLabel,
  className = "",
  minHeight = 300,
  sourceRows = 16,
  labels,
  onFocus
}, forwardedRef) {
  const { translateText } = usePlatformLanguage();
  const translatedLabels = useMemo(
    () => translateLabels({ ...DEFAULT_LABELS, ...labels }, translateText),
    [labels, translateText]
  );
  const translatedAriaLabel = translateText(ariaLabel || translatedLabels.editor);
  const [mode, setMode] = useState<WysiwygEditorMode>(() => {
    if (!allowSourceMode) return "visual";
    if (defaultMode === "source" || hasUnsupportedVisualMarkup(value)) return "source";
    return "visual";
  });
  const [linkDialogOpen, setLinkDialogOpen] = useState(false);
  const [linkUrl, setLinkUrl] = useState("");
  const [linkText, setLinkText] = useState("");
  const [linkError, setLinkError] = useState("");
  const [imageDialogOpen, setImageDialogOpen] = useState(false);
  const [imageUrl, setImageUrl] = useState("");
  const [imageAlt, setImageAlt] = useState("");
  const [imageError, setImageError] = useState("");
  const [editingImage, setEditingImage] = useState(false);
  const sourceRef = useRef<HTMLTextAreaElement | null>(null);
  const onChangeRef = useRef(onChange);
  const onFocusRef = useRef(onFocus);
  const valueRef = useRef(value);
  const appliedValueRef = useRef(value);
  const modeWasChosenRef = useRef(false);
  const linkSelectionRef = useRef({ from: 0, to: 0 });
  onChangeRef.current = onChange;
  onFocusRef.current = onFocus;
  valueRef.current = value;

  const editor = useEditor({
    extensions: [
      StarterKit.configure({
        heading: { levels: [1, 2, 3, 4, 5, 6] },
        link: {
          openOnClick: false,
          autolink: true,
          defaultProtocol: "https",
          HTMLAttributes: {
            rel: "noopener noreferrer",
            target: "_blank"
          }
        }
      }),
      Image.configure({ allowBase64: true }),
      InlineToken
    ],
    content: decorateTokensForEditor(value),
    editable: !disabled,
    immediatelyRender: true,
    shouldRerenderOnTransaction: false,
    editorProps: {
      attributes: {
        "aria-label": translatedAriaLabel,
        "aria-multiline": "true",
        class: "wysiwyg-prosemirror",
        role: "textbox"
      }
    },
    onFocus: () => onFocusRef.current?.(),
    onUpdate: ({ editor: currentEditor }) => {
      const nextValue = editorHtmlValue(currentEditor);
      appliedValueRef.current = nextValue;
      onChangeRef.current(nextValue);
    }
  });

  const toolbarState = useEditorState({
    editor,
    selector: ({ editor: currentEditor }) => ({
      blockStyle: activeBlockStyle(currentEditor),
      bold: currentEditor.isActive("bold"),
      italic: currentEditor.isActive("italic"),
      underline: currentEditor.isActive("underline"),
      strike: currentEditor.isActive("strike"),
      code: currentEditor.isActive("code"),
      bulletList: currentEditor.isActive("bulletList"),
      numberedList: currentEditor.isActive("orderedList"),
      quote: currentEditor.isActive("blockquote"),
      link: currentEditor.isActive("link"),
      canUndo: currentEditor.can().chain().undo().run(),
      canRedo: currentEditor.can().chain().redo().run()
    })
  });

  const unsupportedSource = useMemo(() => hasUnsupportedVisualMarkup(value), [value]);
  const editorStyle = {
    "--wysiwyg-editor-min-height": typeof minHeight === "number" ? `${minHeight}px` : minHeight
  } as CSSProperties;

  useEffect(() => {
    editor.setEditable(!disabled);
  }, [disabled, editor]);

  useEffect(() => {
    editor.setOptions({
      editorProps: {
        attributes: {
          "aria-label": translatedAriaLabel,
          "aria-multiline": "true",
          class: "wysiwyg-prosemirror",
          role: "textbox"
        }
      }
    });
  }, [editor, translatedAriaLabel]);

  useEffect(() => {
    if (value === appliedValueRef.current) return;
    appliedValueRef.current = value;
    if (mode === "visual") {
      editor.commands.setContent(decorateTokensForEditor(value), { emitUpdate: false });
    }
  }, [editor, mode, value]);

  useEffect(() => {
    if (
      !allowSourceMode
      || mode !== "visual"
      || modeWasChosenRef.current
      || !hasUnsupportedVisualMarkup(value)
    ) return;
    setMode("source");
  }, [allowSourceMode, mode, value]);

  useImperativeHandle(forwardedRef, () => ({
    focus() {
      if (mode === "source") sourceRef.current?.focus();
      else editor.commands.focus();
    },
    insertText(text) {
      if (disabled || !text) return false;
      if (mode === "source") return insertIntoSource(text, valueRef.current, sourceRef.current, onChangeRef.current);
      return editor.chain().focus().insertContent(text).run();
    },
    insertToken(token) {
      if (disabled) return false;
      const normalized = typeof token === "string" ? tokenAttributes(token) : tokenAttributes(token.value, token.label);
      if (!normalized.value) return false;
      if (mode === "source") {
        return insertIntoSource(normalized.value, valueRef.current, sourceRef.current, onChangeRef.current);
      }
      return editor.chain().focus().insertContent({
        type: INLINE_TOKEN_NAME,
        attrs: normalized
      }).run();
    }
  }), [disabled, editor, mode]);

  function selectMode(nextMode: WysiwygEditorMode) {
    if (nextMode === mode || (!allowSourceMode && nextMode === "source")) return;
    modeWasChosenRef.current = true;
    if (nextMode === "visual") {
      appliedValueRef.current = valueRef.current;
      editor.commands.setContent(decorateTokensForEditor(valueRef.current), { emitUpdate: false });
      setMode("visual");
      window.requestAnimationFrame(() => editor.commands.focus("end"));
      return;
    }
    const nextValue = editorHtmlValue(editor);
    appliedValueRef.current = nextValue;
    if (nextValue !== valueRef.current) onChangeRef.current(nextValue);
    setMode("source");
    window.requestAnimationFrame(() => sourceRef.current?.focus());
  }

  function changeSource(event: ChangeEvent<HTMLTextAreaElement>) {
    const nextValue = event.target.value;
    appliedValueRef.current = nextValue;
    onChangeRef.current(nextValue);
  }

  function changeBlockStyle(nextStyle: BlockStyle) {
    if (nextStyle === "paragraph") {
      editor.chain().focus().setParagraph().run();
      return;
    }
    const level = Number(nextStyle.slice(-1)) as 1 | 2 | 3;
    editor.chain().focus().setHeading({ level }).run();
  }

  function openLinkDialog() {
    if (toolbarState.link) editor.chain().focus().extendMarkRange("link").run();
    const { from, to } = editor.state.selection;
    const attributes = editor.getAttributes("link");
    linkSelectionRef.current = { from, to };
    setLinkUrl(String(attributes.href || ""));
    setLinkText(from === to ? "" : editor.state.doc.textBetween(from, to, " "));
    setLinkError("");
    setLinkDialogOpen(true);
  }

  function closeLinkDialog() {
    setLinkDialogOpen(false);
    setLinkError("");
  }

  function applyLink() {
    const href = normalizeWysiwygLinkUrl(linkUrl);
    if (!href) {
      setLinkError(translatedLabels.invalidLink);
      return;
    }
    const { from, to } = linkSelectionRef.current;
    if (from === to) {
      editor.chain().focus().setTextSelection(from).insertContent({
        type: "text",
        text: linkText.trim() || href,
        marks: [{ type: "link", attrs: { href } }]
      }).run();
    } else {
      editor.chain().focus().setTextSelection({ from, to }).setLink({ href }).run();
    }
    closeLinkDialog();
  }

  function openImageDialog() {
    const isEditing = editor.isActive("image");
    const attributes = isEditing ? editor.getAttributes("image") : {};
    setEditingImage(isEditing);
    setImageUrl(String(attributes.src || ""));
    setImageAlt(String(attributes.alt || ""));
    setImageError("");
    setImageDialogOpen(true);
  }

  function closeImageDialog() {
    setImageDialogOpen(false);
    setImageError("");
  }

  function applyImage() {
    const src = normalizeWysiwygImageUrl(imageUrl);
    if (!src) {
      setImageError(translatedLabels.invalidImage);
      return;
    }
    const attributes = { src, alt: imageAlt.trim() || undefined };
    if (editingImage) editor.chain().focus().updateAttributes("image", attributes).run();
    else editor.chain().focus().setImage(attributes).run();
    closeImageDialog();
  }

  function preserveSelection(event: MouseEvent<HTMLButtonElement>) {
    event.preventDefault();
  }

  const rootClassName = [
    "wysiwyg-editor",
    disabled ? "is-disabled" : "",
    mode === "source" ? "is-source-mode" : "",
    className
  ].filter(Boolean).join(" ");

  return (
    <div className={rootClassName} style={editorStyle} role="group" aria-label={translatedAriaLabel}>
      <div className="wysiwyg-editor-header">
        {mode === "visual" && (
          <div className="wysiwyg-editor-toolbar" role="toolbar" aria-label={translatedLabels.editor}>
            <div className="wysiwyg-toolbar-group">
              <IconButton
                variant="ghost"
                className="wysiwyg-toolbar-button"
                label={translatedLabels.undo}
                icon={<Undo2 />}
                disabled={disabled || !toolbarState.canUndo}
                onMouseDown={preserveSelection}
                onClick={() => editor.chain().focus().undo().run()}
              />
              <IconButton
                variant="ghost"
                className="wysiwyg-toolbar-button"
                label={translatedLabels.redo}
                icon={<Redo2 />}
                disabled={disabled || !toolbarState.canRedo}
                onMouseDown={preserveSelection}
                onClick={() => editor.chain().focus().redo().run()}
              />
            </div>
            <label className="wysiwyg-block-style">
              <span className="sr-only">{translatedLabels.blockStyle}</span>
              <select
                aria-label={translatedLabels.blockStyle}
                value={toolbarState.blockStyle}
                disabled={disabled}
                onChange={(event) => changeBlockStyle(event.target.value as BlockStyle)}
              >
                <option value="paragraph">{translatedLabels.paragraph}</option>
                <option value="heading-1">{translatedLabels.heading1}</option>
                <option value="heading-2">{translatedLabels.heading2}</option>
                <option value="heading-3">{translatedLabels.heading3}</option>
              </select>
            </label>
            <div className="wysiwyg-toolbar-group">
              <ToolbarButton active={toolbarState.bold} disabled={disabled} label={translatedLabels.bold} icon={<Bold />} onMouseDown={preserveSelection} onClick={() => editor.chain().focus().toggleBold().run()} />
              <ToolbarButton active={toolbarState.italic} disabled={disabled} label={translatedLabels.italic} icon={<Italic />} onMouseDown={preserveSelection} onClick={() => editor.chain().focus().toggleItalic().run()} />
              <ToolbarButton active={toolbarState.underline} disabled={disabled} label={translatedLabels.underline} icon={<Underline />} onMouseDown={preserveSelection} onClick={() => editor.chain().focus().toggleUnderline().run()} />
              <ToolbarButton active={toolbarState.strike} disabled={disabled} label={translatedLabels.strike} icon={<Strikethrough />} onMouseDown={preserveSelection} onClick={() => editor.chain().focus().toggleStrike().run()} />
              <ToolbarButton active={toolbarState.code} disabled={disabled} label={translatedLabels.inlineCodeLabel} icon={<Code2 />} onMouseDown={preserveSelection} onClick={() => editor.chain().focus().toggleCode().run()} />
            </div>
            <div className="wysiwyg-toolbar-group">
              <ToolbarButton active={toolbarState.bulletList} disabled={disabled} label={translatedLabels.bulletList} icon={<List />} onMouseDown={preserveSelection} onClick={() => editor.chain().focus().toggleBulletList().run()} />
              <ToolbarButton active={toolbarState.numberedList} disabled={disabled} label={translatedLabels.numberedList} icon={<ListOrdered />} onMouseDown={preserveSelection} onClick={() => editor.chain().focus().toggleOrderedList().run()} />
              <ToolbarButton active={toolbarState.quote} disabled={disabled} label={translatedLabels.quote} icon={<Quote />} onMouseDown={preserveSelection} onClick={() => editor.chain().focus().toggleBlockquote().run()} />
            </div>
            <div className="wysiwyg-toolbar-group">
              <IconButton variant="ghost" className="wysiwyg-toolbar-button" label={translatedLabels.insertLink} icon={<Link2 />} disabled={disabled} onMouseDown={preserveSelection} onClick={openLinkDialog} />
              <IconButton variant="ghost" className="wysiwyg-toolbar-button" label={translatedLabels.removeLink} icon={<Link2Off />} disabled={disabled || !toolbarState.link} onMouseDown={preserveSelection} onClick={() => editor.chain().focus().unsetLink().run()} />
              <IconButton variant="ghost" className="wysiwyg-toolbar-button" label={translatedLabels.insertImage} icon={<ImagePlus />} disabled={disabled} onMouseDown={preserveSelection} onClick={openImageDialog} />
              <IconButton variant="ghost" className="wysiwyg-toolbar-button" label={translatedLabels.clearFormatting} icon={<RemoveFormatting />} disabled={disabled} onMouseDown={preserveSelection} onClick={() => editor.chain().focus().unsetAllMarks().clearNodes().run()} />
            </div>
          </div>
        )}
        {allowSourceMode && (
          <SegmentedControl
            className="wysiwyg-editor-mode"
            size="content"
            width="inline"
            ariaLabel={translatedLabels.editor}
            value={mode}
            onChange={selectMode}
            options={[
              { id: "visual", label: translatedLabels.visual },
              { id: "source", label: translatedLabels.source }
            ]}
          />
        )}
      </div>

      {mode === "visual" ? (
        <EditorContent editor={editor} className="wysiwyg-editor-content" data-i18n-skip="true" />
      ) : (
        <>
          {unsupportedSource && <p className="wysiwyg-editor-source-note">{translatedLabels.sourceWarning}</p>}
          <textarea
            ref={sourceRef}
            className="wysiwyg-editor-source"
            rows={sourceRows}
            value={value}
            disabled={disabled}
            aria-label={translatedLabels.source}
            onFocus={() => onFocusRef.current?.()}
            onChange={changeSource}
          />
        </>
      )}

      <Dialog
        open={linkDialogOpen}
        className="wysiwyg-editor-dialog"
        title={translatedLabels.insertLink}
        onClose={closeLinkDialog}
        footer={(
          <>
            <Button onClick={closeLinkDialog}>{translatedLabels.cancel}</Button>
            <Button variant="primary" onClick={applyLink}>{translatedLabels.apply}</Button>
          </>
        )}
      >
        <div className="form-grid">
          {linkSelectionRef.current.from === linkSelectionRef.current.to && (
            <FormField label={translatedLabels.linkText}>
              <input value={linkText} onChange={(event) => setLinkText(event.target.value)} />
            </FormField>
          )}
          <FormField label={translatedLabels.linkUrlLabel}>
            <input value={linkUrl} inputMode="url" onChange={(event) => setLinkUrl(event.target.value)} />
          </FormField>
          {linkError && <p className="wysiwyg-editor-dialog-error" role="alert">{linkError}</p>}
        </div>
      </Dialog>

      <Dialog
        open={imageDialogOpen}
        className="wysiwyg-editor-dialog"
        title={translatedLabels.insertImage}
        onClose={closeImageDialog}
        footer={(
          <>
            <Button onClick={closeImageDialog}>{translatedLabels.cancel}</Button>
            <Button variant="primary" onClick={applyImage}>{translatedLabels.apply}</Button>
          </>
        )}
      >
        <div className="form-grid">
          <FormField label={translatedLabels.imageUrlLabel}>
            <input value={imageUrl} inputMode="url" onChange={(event) => setImageUrl(event.target.value)} />
          </FormField>
          <FormField label={translatedLabels.alternativeText}>
            <input value={imageAlt} onChange={(event) => setImageAlt(event.target.value)} />
          </FormField>
          {imageError && <p className="wysiwyg-editor-dialog-error" role="alert">{imageError}</p>}
        </div>
      </Dialog>
    </div>
  );
});

export default WysiwygEditor;

function ToolbarButton({
  active,
  label,
  icon,
  disabled,
  onMouseDown,
  onClick
}: {
  active: boolean;
  label: string;
  icon: React.ReactNode;
  disabled: boolean;
  onMouseDown: (event: MouseEvent<HTMLButtonElement>) => void;
  onClick: () => void;
}) {
  return (
    <IconButton
      variant="ghost"
      className={`wysiwyg-toolbar-button ${active ? "is-active" : ""}`}
      label={label}
      icon={icon}
      disabled={disabled}
      aria-pressed={active}
      onMouseDown={onMouseDown}
      onClick={onClick}
    />
  );
}

function activeBlockStyle(editor: Editor): BlockStyle {
  if (editor.isActive("heading", { level: 1 })) return "heading-1";
  if (editor.isActive("heading", { level: 2 })) return "heading-2";
  if (editor.isActive("heading", { level: 3 })) return "heading-3";
  return "paragraph";
}

function editorHtmlValue(editor: Editor): string {
  if (editor.isEmpty) return "";
  return restoreTokensFromEditor(editor.getHTML());
}

function tokenAttributes(value: string, label?: string): WysiwygEditorToken {
  const normalizedValue = value.trim();
  return {
    value: normalizedValue,
    label: label?.trim() || tokenLabel(normalizedValue)
  };
}

function tokenLabel(value: string): string {
  if ((value.startsWith("{{") && value.endsWith("}}")) || (value.startsWith("${") && value.endsWith("}"))) {
    return value.slice(2, value.startsWith("{{") ? -2 : -1).trim();
  }
  return value;
}

function insertIntoSource(
  insertedValue: string,
  currentValue: string,
  element: HTMLTextAreaElement | null,
  onChange: (value: string) => void
): boolean {
  const start = element?.selectionStart ?? currentValue.length;
  const end = element?.selectionEnd ?? currentValue.length;
  const nextValue = `${currentValue.slice(0, start)}${insertedValue}${currentValue.slice(end)}`;
  onChange(nextValue);
  window.requestAnimationFrame(() => {
    element?.focus();
    const cursor = start + insertedValue.length;
    element?.setSelectionRange(cursor, cursor);
  });
  return true;
}

function decorateTokensForEditor(html: string): string {
  if (!html || typeof DOMParser === "undefined") return html;
  const document = new DOMParser().parseFromString(`<body>${html}</body>`, "text/html");
  const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
  const textNodes: Text[] = [];
  let current = walker.nextNode();
  while (current) {
    const textNode = current as Text;
    if (!textNode.parentElement?.closest("[data-govoplan-token], script, style")) textNodes.push(textNode);
    current = walker.nextNode();
  }

  for (const textNode of textNodes) {
    const text = textNode.nodeValue || "";
    TOKEN_SCAN_PATTERN.lastIndex = 0;
    if (!TOKEN_SCAN_PATTERN.test(text)) continue;
    TOKEN_SCAN_PATTERN.lastIndex = 0;
    const fragment = document.createDocumentFragment();
    let offset = 0;
    for (const match of text.matchAll(TOKEN_SCAN_PATTERN)) {
      const index = match.index ?? 0;
      if (index > offset) fragment.append(document.createTextNode(text.slice(offset, index)));
      const value = match[0];
      const token = document.createElement("span");
      token.dataset.govoplanToken = value;
      token.dataset.govoplanTokenLabel = tokenLabel(value);
      token.textContent = tokenLabel(value);
      fragment.append(token);
      offset = index + value.length;
    }
    if (offset < text.length) fragment.append(document.createTextNode(text.slice(offset)));
    textNode.replaceWith(fragment);
  }
  return document.body.innerHTML;
}

function restoreTokensFromEditor(html: string): string {
  if (!html || typeof DOMParser === "undefined") return html;
  const document = new DOMParser().parseFromString(`<body>${html}</body>`, "text/html");
  for (const token of document.body.querySelectorAll<HTMLElement>("[data-govoplan-token]")) {
    token.replaceWith(document.createTextNode(token.dataset.govoplanToken || token.textContent || ""));
  }
  return document.body.innerHTML;
}

function hasUnsupportedVisualMarkup(html: string): boolean {
  if (!html.trim() || typeof DOMParser === "undefined") return false;
  const document = new DOMParser().parseFromString(`<body>${html}</body>`, "text/html");
  const allowedTags = new Set([
    "A", "BLOCKQUOTE", "BR", "CODE", "DEL", "EM", "H1", "H2", "H3", "H4", "H5", "H6",
    "HR", "I", "IMG", "LI", "OL", "P", "PRE", "S", "STRIKE", "STRONG", "U", "UL"
  ]);
  const allowedAttributes: Record<string, Set<string>> = {
    A: new Set(["href", "rel", "target", "title"]),
    IMG: new Set(["alt", "height", "src", "title", "width"])
  };
  for (const element of document.body.querySelectorAll("*")) {
    if (!allowedTags.has(element.tagName)) return true;
    const allowedForTag = allowedAttributes[element.tagName] ?? new Set<string>();
    for (const attribute of Array.from(element.attributes)) {
      if (!allowedForTag.has(attribute.name.toLowerCase())) return true;
    }
  }
  return false;
}

function translateLabels(labels: WysiwygEditorLabels, translateText: (value: string) => string): WysiwygEditorLabels {
  return Object.fromEntries(
    Object.entries(labels).map(([key, value]) => [key, translateText(value)])
  ) as WysiwygEditorLabels;
}
