import { Editor, mergeAttributes } from "@tiptap/core";
import StarterKit from "@tiptap/starter-kit";
import Image from "@tiptap/extension-image";
import { NodeSelection } from "@tiptap/pm/state";

const PORTFOLIO_IMAGE_TYPE = "portfolioImage";

function parseNumber(value) {
  return Number.parseInt(String(value || "0"), 10) || 0;
}

function parseStoredFileFromUrl(src) {
  if (!src) {
    return null;
  }

  let path = String(src);

  try {
    path = new URL(path, window.location.origin).pathname;
  } catch {
    // Relative paths are still matched as-is.
  }

  const match = path.match(/\/comn\/file\/view\/([^/?#]+)\/(\d+)/);
  if (!match) {
    return null;
  }

  return {
    atchFileIdntfNo: decodeURIComponent(match[1]),
    fileSn: parseNumber(match[2]),
  };
}

const PortfolioImage = Image.extend({
  name: PORTFOLIO_IMAGE_TYPE,
  group: "block",
  draggable: true,
  selectable: true,

  addAttributes() {
    return {
      ...this.parent?.(),
      atchFileIdntfNo: {
        default: "",
        parseHTML: (element) => {
          const storedFile = parseStoredFileFromUrl(element.getAttribute("src"));
          return (
            element.getAttribute("data-atch-file-idntf-no") ||
            storedFile?.atchFileIdntfNo ||
            ""
          );
        },
        renderHTML: (attributes) =>
          attributes.atchFileIdntfNo
            ? { "data-atch-file-idntf-no": attributes.atchFileIdntfNo }
            : {},
      },
      fileSn: {
        default: 0,
        parseHTML: (element) => {
          const dataFileSn = parseNumber(element.getAttribute("data-file-sn"));
          if (dataFileSn > 0) {
            return dataFileSn;
          }

          return parseStoredFileFromUrl(element.getAttribute("src"))?.fileSn || 0;
        },
        renderHTML: (attributes) =>
          attributes.fileSn > 0 ? { "data-file-sn": String(attributes.fileSn) } : {},
      },
      isRepresentative: {
        default: false,
        parseHTML: (element) =>
          element.getAttribute("data-is-representative") === "true",
        renderHTML: (attributes) =>
          attributes.isRepresentative ? { "data-is-representative": "true" } : {},
      },
    };
  },

  parseHTML() {
    return [{ tag: "img[src]" }];
  },

  renderHTML({ HTMLAttributes }) {
    return ["img", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
  },

  addNodeView() {
    return ({ node, editor, getPos }) => {
      let currentNode = node;
      const figure = document.createElement("figure");
      const image = document.createElement("img");

      figure.className = "iotdoor-editor-image-node";
      figure.appendChild(image);

      const selectImageNode = (event) => {
        event.preventDefault();
        event.stopPropagation();

        if (typeof getPos !== "function") {
          return;
        }

        const position = getPos();
        if (typeof position !== "number") {
          return;
        }

        const transaction = editor.state.tr.setSelection(
          NodeSelection.create(editor.state.doc, position),
        );

        editor.view.dispatch(transaction);
        editor.view.focus();
      };

      const syncNode = () => {
        image.src = currentNode.attrs.src || "";
        image.alt = currentNode.attrs.alt || "";
        figure.classList.toggle(
          "is-representative",
          currentNode.attrs.isRepresentative === true,
        );
      };

      syncNode();
      figure.addEventListener("mousedown", selectImageNode);

      return {
        dom: figure,
        update(updatedNode) {
          if (updatedNode.type.name !== currentNode.type.name) {
            return false;
          }

          currentNode = updatedNode;
          syncNode();
          return true;
        },
        selectNode() {
          figure.classList.add("ProseMirror-selectednode");
        },
        deselectNode() {
          figure.classList.remove("ProseMirror-selectednode");
        },
        destroy() {
          figure.removeEventListener("mousedown", selectImageNode);
        },
      };
    };
  },
});

function getSelectedImagePosition(editor) {
  const { selection } = editor.state;

  if (
    selection instanceof NodeSelection &&
    selection.node.type.name === PORTFOLIO_IMAGE_TYPE
  ) {
    return selection.from;
  }

  return null;
}

function getRepresentativeImage(editor) {
  let representative = null;

  editor.state.doc.descendants((node) => {
    if (node.type.name !== PORTFOLIO_IMAGE_TYPE) {
      return true;
    }

    if (node.attrs.isRepresentative) {
      representative = node.attrs;
      return false;
    }

    return true;
  });

  return representative;
}

function findImagePosition(editor, matcher) {
  let foundPosition = null;

  editor.state.doc.descendants((node, position) => {
    if (node.type.name !== PORTFOLIO_IMAGE_TYPE) {
      return true;
    }

    if (matcher(node, position)) {
      foundPosition = position;
      return false;
    }

    return true;
  });

  return foundPosition;
}

function setRepresentativeAtPosition(editor, targetPosition) {
  const transaction = editor.state.tr;
  let changed = false;

  editor.state.doc.descendants((node, position) => {
    if (node.type.name !== PORTFOLIO_IMAGE_TYPE) {
      return true;
    }

    const shouldBeRepresentative = position === targetPosition;
    const storedFile = parseStoredFileFromUrl(node.attrs.src);
    const nextAtchFileIdntfNo =
      node.attrs.atchFileIdntfNo || storedFile?.atchFileIdntfNo || "";
    const nextFileSn = parseNumber(node.attrs.fileSn) || storedFile?.fileSn || 0;
    const nextAttrs = {
      ...node.attrs,
      atchFileIdntfNo: nextAtchFileIdntfNo,
      fileSn: nextFileSn,
      isRepresentative: shouldBeRepresentative,
    };

    if (
      Boolean(node.attrs.isRepresentative) !== shouldBeRepresentative ||
      node.attrs.atchFileIdntfNo !== nextAtchFileIdntfNo ||
      parseNumber(node.attrs.fileSn) !== nextFileSn
    ) {
      transaction.setNodeMarkup(position, undefined, nextAttrs);
      changed = true;
    }

    return true;
  });

  if (changed) {
    editor.view.dispatch(transaction);
  }

  return changed;
}

function syncToolbar(editor, toolbar) {
  toolbar
    .querySelectorAll("button[data-editor-action]")
    .forEach((button) => {
      const action = button.dataset.editorAction;
      let active = false;

      if (action === "paragraph") {
        active = editor.isActive("paragraph");
      } else if (action === "heading") {
        active = editor.isActive("heading", {
          level: parseNumber(button.dataset.level),
        });
      } else if (action === "bold") {
        active = editor.isActive("bold");
      } else if (action === "italic") {
        active = editor.isActive("italic");
      } else if (action === "strike") {
        active = editor.isActive("strike");
      } else if (action === "bulletList") {
        active = editor.isActive("bulletList");
      } else if (action === "orderedList") {
        active = editor.isActive("orderedList");
      } else if (action === "blockquote") {
        active = editor.isActive("blockquote");
      }

      button.classList.toggle("is-active", active);
    });
}

function syncHiddenFields(editor, hiddenInput, representativeIdInput, representativeSnInput) {
  hiddenInput.value = editor.getHTML();

  const representative = getRepresentativeImage(editor);
  representativeIdInput.value = representative?.atchFileIdntfNo || "";
  representativeSnInput.value = representative?.fileSn ? String(representative.fileSn) : "";
}

async function uploadImage(file, options, form) {
  const formData = new FormData();
  formData.append("file", file);
  formData.append("directory", options.uploadDirectory);
  formData.append("description", options.uploadDescription);

  const headers = {};
  const csrfHeader = form.dataset.csrfHeader;
  const csrfToken = form.dataset.csrfToken;

  if (csrfHeader && csrfToken) {
    headers[csrfHeader] = csrfToken;
  }

  const response = await fetch(options.uploadUrl, {
    method: "POST",
    headers,
    body: formData,
  });

  if (!response.ok) {
    throw new Error("이미지 업로드에 실패했습니다.");
  }

  return response.json();
}

function ensureLegacyRepresentative(editor, form) {
  const currentUrl = form.dataset.currentThumbnailUrl || "";
  const currentAtchFileIdntfNo =
    form.dataset.currentThumbnailAtchFileIdntfNo || "";
  const currentFileSn = parseNumber(form.dataset.currentThumbnailFileSn);

  if (!currentAtchFileIdntfNo || currentFileSn <= 0) {
    return;
  }

  if (getRepresentativeImage(editor)) {
    return;
  }

  const existingPosition = findImagePosition(
    editor,
    (node) =>
      node.attrs.atchFileIdntfNo === currentAtchFileIdntfNo &&
      parseNumber(node.attrs.fileSn) === currentFileSn,
  );

  if (existingPosition !== null) {
    setRepresentativeAtPosition(editor, existingPosition);
    return;
  }

  if (!currentUrl) {
    return;
  }

  editor.commands.insertContent({
    type: PORTFOLIO_IMAGE_TYPE,
    attrs: {
      src: currentUrl,
      alt: "대표 이미지",
      atchFileIdntfNo: currentAtchFileIdntfNo,
      fileSn: currentFileSn,
      isRepresentative: true,
    },
  });
}

function attachToolbarActions(editor, toolbar, fileInput) {
  toolbar.addEventListener("mousedown", (event) => {
    if (event.target.closest("button[data-editor-action]")) {
      event.preventDefault();
    }
  });

  toolbar.addEventListener("click", (event) => {
    const button = event.target.closest("button[data-editor-action]");
    if (!button) {
      return;
    }

    const action = button.dataset.editorAction;
    const chain = editor.chain().focus();

    if (action === "paragraph") {
      chain.setParagraph().run();
      return;
    }

    if (action === "heading") {
      chain.toggleHeading({ level: parseNumber(button.dataset.level) }).run();
      return;
    }

    if (action === "bold") {
      chain.toggleBold().run();
      return;
    }

    if (action === "italic") {
      chain.toggleItalic().run();
      return;
    }

    if (action === "strike") {
      chain.toggleStrike().run();
      return;
    }

    if (action === "bulletList") {
      chain.toggleBulletList().run();
      return;
    }

    if (action === "orderedList") {
      chain.toggleOrderedList().run();
      return;
    }

    if (action === "blockquote") {
      chain.toggleBlockquote().run();
      return;
    }

    if (action === "horizontalRule") {
      chain.setHorizontalRule().run();
      return;
    }

    if (action === "undo") {
      chain.undo().run();
      return;
    }

    if (action === "redo") {
      chain.redo().run();
      return;
    }

    if (action === "imageUpload") {
      fileInput.click();
      return;
    }

    if (action === "setRepresentative") {
      const selectedPosition = getSelectedImagePosition(editor);
      if (selectedPosition === null) {
        window.alert("대표 이미지로 지정할 이미지를 먼저 선택해주세요.");
        return;
      }

      setRepresentativeAtPosition(editor, selectedPosition);
    }
  });
}

function initPortfolioEditor(options) {
  const form = document.querySelector(options.formSelector);
  const editorElement = document.querySelector(options.editorSelector);
  const toolbar = document.querySelector(options.toolbarSelector);
  const hiddenInput = document.querySelector(options.hiddenInputSelector);
  const fileInput = document.querySelector(options.fileInputSelector);
  const representativeIdInput = document.querySelector(
    options.representativeIdSelector,
  );
  const representativeSnInput = document.querySelector(
    options.representativeSnSelector,
  );

  if (
    !form ||
    !editorElement ||
    !toolbar ||
    !hiddenInput ||
    !fileInput ||
    !representativeIdInput ||
    !representativeSnInput
  ) {
    return null;
  }

  const editor = new Editor({
    element: editorElement,
    extensions: [StarterKit, PortfolioImage],
    content: hiddenInput.value || "<p></p>",
    editorProps: {
      attributes: {
        class: "ProseMirror",
      },
    },
    onCreate({ editor: createdEditor }) {
      ensureLegacyRepresentative(createdEditor, form);
      syncHiddenFields(
        createdEditor,
        hiddenInput,
        representativeIdInput,
        representativeSnInput,
      );
      syncToolbar(createdEditor, toolbar);
    },
    onUpdate({ editor: updatedEditor }) {
      syncHiddenFields(
        updatedEditor,
        hiddenInput,
        representativeIdInput,
        representativeSnInput,
      );
      syncToolbar(updatedEditor, toolbar);
    },
    onSelectionUpdate({ editor: selectedEditor }) {
      syncToolbar(selectedEditor, toolbar);
    },
  });

  attachToolbarActions(editor, toolbar, fileInput);

  fileInput.addEventListener("change", async () => {
    if (!fileInput.files?.length) {
      return;
    }

    const [file] = fileInput.files;

    try {
      const payload = await uploadImage(file, options, form);
      const shouldBeRepresentative = !getRepresentativeImage(editor);

      editor
        .chain()
        .focus()
        .insertContent({
          type: PORTFOLIO_IMAGE_TYPE,
          attrs: {
            src: payload.url,
            alt: payload.orgnlFileNm || file.name,
            atchFileIdntfNo: payload.atchFileIdntfNo,
            fileSn: payload.fileSn,
            isRepresentative: shouldBeRepresentative,
          },
        })
        .run();
    } catch (error) {
      window.alert(error.message || "이미지 업로드에 실패했습니다.");
    } finally {
      fileInput.value = "";
    }
  });

  form.addEventListener("submit", (event) => {
    syncHiddenFields(editor, hiddenInput, representativeIdInput, representativeSnInput);
    if (!representativeIdInput.value || !representativeSnInput.value) {
      event.preventDefault();
      window.alert("대표 이미지로 지정할 본문 이미지를 선택해주세요.");
    }
  });

  return editor;
}

export { initPortfolioEditor };
