<script setup lang="ts">
import {
  AlignCenterOutlined,
  AlignLeftOutlined,
  AlignRightOutlined,
  BoldOutlined,
  CodeOutlined,
  DeleteOutlined,
  InsertRowAboveOutlined,
  InsertRowBelowOutlined,
  InsertRowLeftOutlined,
  InsertRowRightOutlined,
  ItalicOutlined,
  StrikethroughOutlined,
  OrderedListOutlined,
  MinusOutlined,
  PictureOutlined,
  RedoOutlined,
  TableOutlined,
  UnorderedListOutlined,
} from "@ant-design/icons-vue";
import { message } from "ant-design-vue";
import { commandsCtx, editorViewCtx, parserCtx } from "@milkdown/kit/core";
import { redoCommand, undoCommand } from "@milkdown/kit/plugin/history";
import { Slice } from "@milkdown/kit/prose/model";
import { NodeSelection, TextSelection, type Selection } from "@milkdown/kit/prose/state";
import type { EditorView } from "@milkdown/kit/prose/view";
import {
  addBlockTypeCommand,
  insertHrCommand,
  toggleEmphasisCommand,
  toggleStrongCommand,
  wrapInBlockquoteCommand,
  wrapInBulletListCommand,
  wrapInHeadingCommand,
  wrapInOrderedListCommand,
} from "@milkdown/kit/preset/commonmark";
import {
  addColAfterCommand,
  addColBeforeCommand,
  addRowAfterCommand,
  addRowBeforeCommand,
  deleteSelectedCellsCommand,
  insertTableCommand,
  selectColCommand,
  selectRowCommand,
  setAlignCommand,
  toggleStrikethroughCommand,
} from "@milkdown/kit/preset/gfm";
import type { Ctx } from "@milkdown/kit/ctx";
import { callCommand, replaceAll } from "@milkdown/kit/utils";
import { Crepe, CrepeFeature } from "@milkdown/crepe";
// NOTE: import each common stylesheet explicitly instead of the aggregate
// `theme/common/style.css`. The aggregate transitively `@import`s `latex.css`,
// which pulls in `katex/dist/katex.min.css` and emits ~59 KaTeX font files
// into dist/assets/fonts even though we disable CrepeFeature.Latex. We don't
// render math, so skip latex.css to drop the fonts from the bundle.
import "@milkdown/crepe/theme/common/prosemirror.css";
import "@milkdown/crepe/theme/common/reset.css";
import "@milkdown/crepe/theme/common/block-edit.css";
import "@milkdown/crepe/theme/common/code-mirror.css";
import "@milkdown/crepe/theme/common/cursor.css";
import "@milkdown/crepe/theme/common/image-block.css";
import "@milkdown/crepe/theme/common/link-tooltip.css";
import "@milkdown/crepe/theme/common/list-item.css";
import "@milkdown/crepe/theme/common/placeholder.css";
import "@milkdown/crepe/theme/common/toolbar.css";
import "@milkdown/crepe/theme/common/table.css";
import "@milkdown/crepe/theme/common/top-bar.css";
import "@milkdown/crepe/theme/frame.css";
import {
  computed,
  nextTick,
  onBeforeUnmount,
  onMounted,
  ref,
  shallowRef,
  watch,
  type ComponentPublicInstance,
} from "vue";
import { useI18n } from "vue-i18n";

import EditorActionMenu from "@/components/editor-common/EditorActionMenu.vue";
import EditorAiAssistPanel from "@/components/editor-common/EditorAiAssistPanel.vue";
import EditorGridSizePicker from "@/components/editor-common/EditorGridSizePicker.vue";
import EditorSelectionFrame from "@/components/editor-common/EditorSelectionFrame.vue";
import CoverPickerModal from "@/components/CoverPickerModal.vue";
import MarkdownPreview from "@/components/MarkdownPreview.vue";
import {
  imagesApi,
  streamArticleSelectionOptimize,
  type ArticleSelectionOptimizeStreamEvent,
} from "@/lib/api";
import { formatError } from "@/lib/errors";
import {
  richImageBlockFeature,
  richImageBlockSchema,
  type RichImageAlign,
} from "@/lib/milkdown/richImageBlock";

const props = defineProps<{
  articleId: number;
  title: string;
  modelValue: string;
  disabled?: boolean;
  uploadImage?: (file: File) => Promise<string>;
}>();

const emit = defineEmits<{
  "update:title": [value: string];
  "update:modelValue": [value: string];
}>();

const { t } = useI18n();
const crepeRef = shallowRef<Crepe | null>(null);
const syncingExternalMarkdown = ref(false);
const editorRootRef = ref<HTMLElement | null>(null);
const editorRef = shallowRef<MilkdownEditorInstance | null>(null);
const loading = ref(true);
const surfaceRef = ref<HTMLElement | null>(null);
const tablePickerRef = ref<HTMLElement | null>(null);

const AI_OPTIMIZE_PANEL_GAP = 18;
const AI_OPTIMIZE_SELECTION_HIGHLIGHT_NAME = "article-editor-ai-optimize-selection";
const AI_OPTIMIZE_TOOLBAR_ICON = `
  <svg
    class="svgfont"
    aria-hidden="true"
    viewBox="0 0 1024 1024"
    width="1em"
    height="1em"
    fill="currentColor"
  >
    <path d="M716.571429 332l167.428571-167.428571-61.142857-61.142857-167.428571 167.428571zm255.428571-167.428571q0 15.428571-10.285714 25.714286l-734.857143 734.857143q-10.285714 10.285714-25.714286 10.285714t-25.714286-10.285714l-113.142857-113.142857q-10.285714-10.285714-10.285714-25.714286t10.285714-25.714286l734.857143-734.857143q10.285714-10.285714 25.714286-10.285714t25.714286 10.285714l113.142857 113.142857q10.285714 10.285714 10.285714 25.714286zm-772-108.571429l56 17.142857-56 17.142857-17.142857 56-17.142857-56-56-17.142857 56-17.142857 17.142857-56zm200 92.571429l112 34.285714-112 34.285714-34.285714 112-34.285714-112-112-34.285714 112-34.285714 34.285714-112zm531.428571 273.142857l56 17.142857-56 17.142857-17.142857 56-17.142857-56-56-17.142857 56-17.142857 17.142857-56zm-365.714286-365.714286l56 17.142857-56 17.142857-17.142857 56-17.142857-56-56-17.142857 56-17.142857 17.142857-56z"></path>
  </svg>
`;

type TableContextMenuAction =
  | "add-row-before"
  | "add-row-after"
  | "delete-row"
  | "add-col-before"
  | "add-col-after"
  | "delete-col"
  | "align-left"
  | "align-center"
  | "align-right"
  | "delete-table";

type ImageResizeHandle = "nw" | "ne" | "sw" | "se";

type ImageContextMenuAction =
  | "replace-image"
  | "toggle-caption"
  | "reset-size"
  | "align-left"
  | "align-center"
  | "align-right"
  | "delete-image";

type ImagePickerAction = "insert" | "replace";

type AiOptimizeStatus = "idle" | "generating" | "completed" | "error";
type EditorAiAssistPlacement = "bottom" | "top";

type TableContextMenuState = {
  open: boolean;
  x: number;
  y: number;
  rowIndex: number;
  colIndex: number;
  tableStart: number;
  tableInnerPos: number;
  tableNodeSize: number;
};

type SelectedImageState = {
  open: boolean;
  x: number;
  y: number;
  width: number;
  height: number;
  nodePos: number;
  nodeSize: number;
  src: string;
  assetId: number | null;
  align: RichImageAlign;
};

type AiOptimizePanelState = {
  open: boolean;
  x: number;
  y: number;
  width: number;
  placement: EditorAiAssistPlacement;
  from: number;
  to: number;
  selectedText: string;
};

type TextSelectionSnapshot = {
  from: number;
  to: number;
  text: string;
  rect: DOMRect;
};

type ImageContextMenuState = {
  open: boolean;
  x: number;
  y: number;
  nodePos: number;
  nodeSize: number;
  src: string;
};

type SelectedImageSnapshot = {
  host: HTMLElement;
  wrapper: HTMLElement;
  image: HTMLImageElement;
  rect: DOMRect;
  nodePos: number;
  nodeSize: number;
  src: string;
  assetId: number | null;
  align: RichImageAlign;
};

type MilkdownEditorInstance = Awaited<ReturnType<Crepe["create"]>>;

function createDefaultTableContextMenuState(): TableContextMenuState {
  return {
    open: false,
    x: 0,
    y: 0,
    rowIndex: 0,
    colIndex: 0,
    tableStart: 0,
    tableInnerPos: 0,
    tableNodeSize: 0,
  };
}

function createDefaultSelectedImageState(): SelectedImageState {
  return {
    open: false,
    x: 0,
    y: 0,
    width: 0,
    height: 0,
    nodePos: 0,
    nodeSize: 0,
    src: "",
    assetId: null,
    align: "center",
  };
}

function createDefaultImageContextMenuState(): ImageContextMenuState {
  return {
    open: false,
    x: 0,
    y: 0,
    nodePos: 0,
    nodeSize: 0,
    src: "",
  };
}

function createDefaultAiOptimizePanelState(): AiOptimizePanelState {
  return {
    open: false,
    x: 0,
    y: 0,
    width: 0,
    placement: "bottom",
    from: 0,
    to: 0,
    selectedText: "",
  };
}

function createCrepe(root: HTMLElement): Crepe {
  const crepe = new Crepe({
    root,
    defaultValue: props.modelValue || "",
    features: {
      [CrepeFeature.TopBar]: false,
      [CrepeFeature.Toolbar]: true,
      [CrepeFeature.Latex]: false,
      [CrepeFeature.ImageBlock]: false,
      [CrepeFeature.BlockEdit]: true,
      [CrepeFeature.Placeholder]: false,
    },
    featureConfigs: {
      [CrepeFeature.Toolbar]: {
        buildToolbar: (builder) => {
          builder.addGroup("ai-optimize", "AI Optimize").addItem("ai-optimize", {
            icon: AI_OPTIMIZE_TOOLBAR_ICON,
            active: () => false,
            onRun: (ctx) => {
              openAiOptimizePanel(ctx);
            },
          });
        },
      },
    },
  });
  crepe.addFeature(richImageBlockFeature, {
    onUpload: props.uploadImage,
    proxyDomURL: (url) => url,
    maxWidth: 960,
    maxHeight: 720,
    blockCaptionPlaceholderText: "Write image caption",
  });

  crepe.setReadonly(!!props.disabled);
  crepe.on((listener) => {
    listener.markdownUpdated((_ctx, markdown) => {
      if (syncingExternalMarkdown.value) {
        return;
      }

      emit("update:modelValue", markdown);
    });
    listener.selectionUpdated((_ctx, selection) => {
      scheduleSelectedImageSync(selection);
    });
  });

  crepeRef.value = crepe;
  return crepe;
}

function getEditor(): MilkdownEditorInstance | null {
  return editorRef.value;
}

const editorDisabled = computed(() => props.disabled || loading.value);
const tablePickerOpen = ref(false);
const imagePickerOpen = ref(false);
const imagePickerAction = ref<ImagePickerAction>("insert");
const imagePickerCurrentUrl = ref("");
const imagePickerCurrentAssetId = ref<number | null>(null);
const tableContextMenu = ref<TableContextMenuState>(createDefaultTableContextMenuState());
const selectedImage = ref<SelectedImageState>(createDefaultSelectedImageState());
const imageContextMenu = ref<ImageContextMenuState>(createDefaultImageContextMenuState());
const aiOptimizePanel = ref<AiOptimizePanelState>(createDefaultAiOptimizePanelState());
const aiOptimizeInstruction = ref("");
const aiOptimizePreview = ref("");
const aiOptimizeError = ref("");
const aiOptimizeStatus = ref<AiOptimizeStatus>("idle");

let selectedImageHostElement: HTMLElement | null = null;
let selectedImageWrapperElement: HTMLElement | null = null;
let selectedImageElement: HTMLImageElement | null = null;
let selectedImageSyncFrame = 0;
let imageDragActive = false;
let selectedImageResizeObserver: ResizeObserver | null = null;
let selectedImageLoadHandler: (() => void) | null = null;
let aiOptimizeAbortController: AbortController | null = null;
let aiOptimizeIgnoreNextWindowPointerDown = false;

const EDITOR_ACTION_MENU_DROPDOWN_SELECTOR = ".editor-action-menu__dropdown-overlay";

const tableContextHeaderRow = computed(() => tableContextMenu.value.rowIndex === 0);
const aiOptimizeStreaming = computed(() => aiOptimizeStatus.value === "generating");
const aiOptimizeHasPreview = computed(() => aiOptimizePreview.value.trim().length > 0);
const aiOptimizeUiText = computed(() => ({
  title: t("article.editor.aiOptimize.title"),
  promptPlaceholder: t("article.editor.aiOptimize.promptPlaceholder"),
  generating: t("article.editor.aiOptimize.generating"),
  failed: t("article.editor.aiOptimize.messages.failed"),
  disclaimer: t("article.editor.aiOptimize.disclaimer"),
  regenerate: t("article.editor.aiOptimize.regenerate"),
  close: t("article.editor.aiOptimize.close"),
  replace: t("article.editor.aiOptimize.replace"),
}));

const tableContextMenuGroups = computed(() => [
  {
    key: "operations",
    columns: 4,
    items: [
      {
        key: "rows",
        label: "行操作",
        icon: InsertRowAboveOutlined,
        children: [
          {
            key: "add-row-before",
            label: t("article.editor.tableMenu.addRowBefore"),
            icon: InsertRowAboveOutlined,
            disabled: tableContextHeaderRow.value,
          },
          {
            key: "add-row-after",
            label: t("article.editor.tableMenu.addRowAfter"),
            icon: InsertRowBelowOutlined,
          },
          {
            key: "delete-row",
            label: t("article.editor.tableMenu.deleteRow"),
            icon: DeleteOutlined,
            danger: true,
            disabled: tableContextHeaderRow.value,
          },
        ],
      },
      {
        key: "columns",
        label: "列操作",
        icon: InsertRowLeftOutlined,
        children: [
          {
            key: "add-col-before",
            label: t("article.editor.tableMenu.addColBefore"),
            icon: InsertRowLeftOutlined,
          },
          {
            key: "add-col-after",
            label: t("article.editor.tableMenu.addColAfter"),
            icon: InsertRowRightOutlined,
          },
          {
            key: "delete-col",
            label: t("article.editor.tableMenu.deleteCol"),
            icon: DeleteOutlined,
            danger: true,
          },
        ],
      },
      {
        key: "align",
        label: "对齐方式",
        icon: AlignCenterOutlined,
        children: [
          {
            key: "align-left",
            label: t("article.editor.tableMenu.alignLeft"),
            icon: AlignLeftOutlined,
          },
          {
            key: "align-center",
            label: t("article.editor.tableMenu.alignCenter"),
            icon: AlignCenterOutlined,
          },
          {
            key: "align-right",
            label: t("article.editor.tableMenu.alignRight"),
            icon: AlignRightOutlined,
          },
        ],
      },
      {
        key: "delete-table",
        label: t("article.editor.tableMenu.deleteTable"),
        icon: DeleteOutlined,
        danger: true,
      },
    ],
  },
]);

const imageContextMenuGroups = computed(() => [
  {
    key: "operations",
    columns: 4,
    items: [
      {
        key: "image",
        label: "图片操作",
        icon: PictureOutlined,
        children: [
          {
            key: "replace-image",
            label: t("article.editor.imageMenu.replace"),
            icon: PictureOutlined,
          },
          {
            key: "toggle-caption",
            label: t("article.editor.imageMenu.caption"),
            icon: CodeOutlined,
          },
        ],
      },
      {
        key: "align",
        label: "对齐方式",
        icon: AlignCenterOutlined,
        children: [
          {
            key: "align-left",
            label: t("article.editor.imageMenu.alignLeft"),
            icon: AlignLeftOutlined,
            active: selectedImage.value.align === "left",
          },
          {
            key: "align-center",
            label: t("article.editor.imageMenu.alignCenter"),
            icon: AlignCenterOutlined,
            active: selectedImage.value.align === "center",
          },
          {
            key: "align-right",
            label: t("article.editor.imageMenu.alignRight"),
            icon: AlignRightOutlined,
            active: selectedImage.value.align === "right",
          },
        ],
      },
      {
        key: "reset-size",
        label: t("article.editor.imageMenu.resetSize"),
        icon: RedoOutlined,
      },
      {
        key: "delete-image",
        label: t("article.editor.imageMenu.delete"),
        icon: DeleteOutlined,
        danger: true,
      },
    ],
  },
]);

function syncMarkdownFromProps(nextValue: string): void {
  const editor = getEditor();
  const crepe = crepeRef.value;
  if (!editor || !crepe) {
    return;
  }

  const resolvedValue = nextValue || "";
  if (crepe.getMarkdown() === resolvedValue) {
    return;
  }

  clearSelectedImageState();
  closeImageContextMenu();
  closeAiOptimizePanel();
  syncingExternalMarkdown.value = true;
  editor.action(replaceAll(resolvedValue, true));
  queueMicrotask(() => {
    syncingExternalMarkdown.value = false;
  });
}

watch(
  () => props.disabled,
  (value) => {
    crepeRef.value?.setReadonly(!!value);
    if (value) {
      closeTablePicker();
      closeTableContextMenu();
      clearSelectedImageState();
      closeImageContextMenu();
      imagePickerOpen.value = false;
      closeAiOptimizePanel();
    }
  },
  { immediate: true },
);

watch(
  [() => props.modelValue, loading],
  ([value, editorLoading]) => {
    if (editorLoading) {
      return;
    }

    syncMarkdownFromProps(value);
  },
  { immediate: true },
);

async function mountEditor(): Promise<void> {
  const root = editorRootRef.value;
  if (!root || crepeRef.value) {
    return;
  }

  loading.value = true;

  const crepe = createCrepe(root);

  try {
    editorRef.value = await crepe.create();
  } catch (error) {
    crepeRef.value = null;
    console.error(error);
  } finally {
    loading.value = false;
  }
}

onBeforeUnmount(() => {
  stopAiOptimizeGeneration();
  if (selectedImageSyncFrame) {
    cancelAnimationFrame(selectedImageSyncFrame);
    selectedImageSyncFrame = 0;
  }
  detachSelectedImageDomSync();
  window.removeEventListener("pointerdown", handleWindowPointerDown);
  window.removeEventListener("resize", handleWindowResize);
  window.removeEventListener("keydown", handleWindowKeydown);
  editorRef.value = null;
  void crepeRef.value?.destroy().catch(console.error);
  crepeRef.value = null;
});

onMounted(() => {
  window.addEventListener("pointerdown", handleWindowPointerDown);
  window.addEventListener("resize", handleWindowResize);
  window.addEventListener("keydown", handleWindowKeydown);
  void mountEditor();
});

function runCommand(command: { key: unknown }, payload?: unknown): void {
  if (editorDisabled.value) {
    return;
  }

  const editor = getEditor();
  if (!editor) {
    return;
  }

  editor.action(callCommand(command.key as never, payload as never));
}

function runEditorAction(action: (ctx: Ctx) => void): void {
  if (editorDisabled.value) {
    return;
  }

  const editor = getEditor();
  if (!editor) {
    return;
  }

  editor.action(action);
}

function resolveImageAssetId(value: unknown): number | null {
  const resolved = Number.parseInt(String(value ?? "").trim(), 10);
  if (!Number.isInteger(resolved) || resolved <= 0) {
    return null;
  }
  return resolved;
}

function insertImageBlockAtCursor(payload: { src: string; assetId?: number | null }): void {
  const resolvedSrc = payload.src.trim();
  if (!resolvedSrc) {
    return;
  }

  runEditorAction((ctx) => {
    const commands = ctx.get(commandsCtx);
    commands.call(addBlockTypeCommand.key, {
      nodeType: richImageBlockSchema.type(ctx),
      attrs: {
        src: resolvedSrc,
        caption: "",
        assetId: payload.assetId ?? 0,
        ratio: 1,
        width: 0,
        height: 0,
        align: "center",
      },
    });

    requestAnimationFrame(() => {
      ctx.get(editorViewCtx).focus();
    });
  });
}

function insertImage(): void {
  if (editorDisabled.value) {
    return;
  }

  closeTablePicker();
  closeTableContextMenu();
  closeImageContextMenu();
  imagePickerAction.value = "insert";
  imagePickerCurrentUrl.value = "";
  imagePickerCurrentAssetId.value = null;
  imagePickerOpen.value = true;
}

async function uploadBodyImageToLibrary(file: File): Promise<{ url: string; fileName: string; assetId: number }> {
  const uploaded = await imagesApi.upload(file);
  return {
    url: uploaded.url,
    fileName: uploaded.name,
    assetId: uploaded.id,
  };
}

function handleImagePicked(payload: { url: string; fileName: string; assetId?: number | null }): void {
  const assetId = payload.assetId ?? null;
  if (imagePickerAction.value === "replace" && selectedImage.value.open) {
    updateSelectedImageAttributes({
      src: payload.url,
      assetId: assetId ?? 0,
      ratio: 1,
      width: 0,
      height: 0,
    });
  } else {
    insertImageBlockAtCursor({
      src: payload.url,
      assetId,
    });
  }

  imagePickerAction.value = "insert";
  imagePickerCurrentUrl.value = "";
  imagePickerCurrentAssetId.value = null;
}

function clamp(value: number, min: number, max: number): number {
  return Math.min(Math.max(value, min), max);
}

function resolveImageAlign(value: unknown): RichImageAlign {
  const normalized = String(value ?? "").trim().toLowerCase();
  if (normalized === "left" || normalized === "right") {
    return normalized;
  }
  return "center";
}

function closeTablePicker(): void {
  tablePickerOpen.value = false;
}

function assignTablePickerRef(element: Element | ComponentPublicInstance | null): void {
  tablePickerRef.value = element instanceof HTMLElement ? element : null;
}

function assignSurfaceRef(element: Element | ComponentPublicInstance | null): void {
  surfaceRef.value = element instanceof HTMLElement ? element : null;
}

function assignEditorRootRef(element: Element | ComponentPublicInstance | null): void {
  editorRootRef.value = element instanceof HTMLElement ? element : null;
}

function closeTableContextMenu(): void {
  tableContextMenu.value = createDefaultTableContextMenuState();
}

function resolveSurfaceRelativeRect(rect: DOMRect): { x: number; y: number; width: number; height: number } | null {
  const surface = surfaceRef.value;
  if (!surface) {
    return null;
  }

  const surfaceRect = surface.getBoundingClientRect();

  return {
    x: rect.left - surfaceRect.left + surface.scrollLeft,
    y: rect.top - surfaceRect.top + surface.scrollTop,
    width: rect.width,
    height: rect.height,
  };
}

function detachSelectedImageDomSync(): void {
  selectedImageResizeObserver?.disconnect();
  selectedImageResizeObserver = null;

  if (selectedImageElement && selectedImageLoadHandler) {
    selectedImageElement.removeEventListener("load", selectedImageLoadHandler);
  }
  selectedImageLoadHandler = null;
}

function queueSelectedImageBoundsSync(): void {
  requestAnimationFrame(() => {
    syncSelectedImageBoundsFromDom();
    requestAnimationFrame(() => {
      syncSelectedImageBoundsFromDom();
    });
  });
}

function attachSelectedImageDomSync(): void {
  detachSelectedImageDomSync();

  if (!selectedImageElement) {
    return;
  }

  selectedImageLoadHandler = () => {
    queueSelectedImageBoundsSync();
  };
  selectedImageElement.addEventListener("load", selectedImageLoadHandler);

  if (typeof ResizeObserver !== "undefined") {
    selectedImageResizeObserver = new ResizeObserver(() => {
      syncSelectedImageBoundsFromDom();
    });
    selectedImageResizeObserver.observe(selectedImageElement);
  }

  queueSelectedImageBoundsSync();
}

function clearSelectedImageState(): void {
  detachSelectedImageDomSync();
  selectedImage.value = createDefaultSelectedImageState();
  selectedImageHostElement = null;
  selectedImageWrapperElement = null;
  selectedImageElement = null;
}

function closeImageContextMenu(): void {
  imageContextMenu.value = createDefaultImageContextMenuState();
}

function resolveEditorDOMRange(view: EditorView, from: number, to: number): Range | null {
  try {
    const start = view.domAtPos(from);
    const end = view.domAtPos(to);
    const range = view.dom.ownerDocument.createRange();
    range.setStart(start.node, start.offset);
    range.setEnd(end.node, end.offset);
    return range;
  } catch {
    return null;
  }
}

function getCSSHighlightsRegistry():
  | {
      set: (name: string, value: unknown) => void;
      delete: (name: string) => void;
    }
  | null {
  const cssWithHighlights = globalThis.CSS as typeof globalThis.CSS & {
    highlights?: {
      set: (name: string, value: unknown) => void;
      delete: (name: string) => void;
    };
  };

  return cssWithHighlights.highlights ?? null;
}

function clearAiOptimizeSelectionHighlight(): void {
  getCSSHighlightsRegistry()?.delete(AI_OPTIMIZE_SELECTION_HIGHLIGHT_NAME);
}

function syncAiOptimizeSelectionHighlight(from: number, to: number): void {
  clearAiOptimizeSelectionHighlight();

  const registry = getCSSHighlightsRegistry();
  const HighlightCtor = window.Highlight as (new (...ranges: Range[]) => unknown) | undefined;
  if (!registry || !HighlightCtor) {
    return;
  }

  const editor = getEditor();
  if (!editor) {
    return;
  }

  editor.action((ctx) => {
    const view = ctx.get(editorViewCtx);
    const range = resolveEditorDOMRange(view, from, to);
    if (!range) {
      return;
    }

    registry.set(AI_OPTIMIZE_SELECTION_HIGHLIGHT_NAME, new HighlightCtor(range));
  });
}

function resetAiOptimizeOutput(): void {
  aiOptimizePreview.value = "";
  aiOptimizeError.value = "";
  aiOptimizeStatus.value = "idle";
}

function stopAiOptimizeGeneration(): void {
  aiOptimizeAbortController?.abort();
  aiOptimizeAbortController = null;

  if (aiOptimizeStatus.value === "generating") {
    aiOptimizeStatus.value = aiOptimizeHasPreview.value ? "completed" : "idle";
    aiOptimizeError.value = "";
  }
}

function closeAiOptimizePanel(): void {
  stopAiOptimizeGeneration();
  clearAiOptimizeSelectionHighlight();
  aiOptimizePanel.value = createDefaultAiOptimizePanelState();
  aiOptimizeInstruction.value = "";
  resetAiOptimizeOutput();
}

function resolveTextSelectionSnapshot(ctx: Ctx): TextSelectionSnapshot | null {
  const view = ctx.get(editorViewCtx);
  const { selection, doc } = view.state;
  if (!(selection instanceof TextSelection) || selection.empty) {
    return null;
  }

  const text = doc.textBetween(selection.from, selection.to, "\n\n");
  if (!text.trim()) {
    return null;
  }

  const range = resolveEditorDOMRange(view, selection.from, selection.to);
  const rangeRects = range
    ? Array.from(range.getClientRects()).filter((rect) => rect.width > 0 || rect.height > 0)
    : [];
  const rangeRect =
    rangeRects.length > 0
      ? rangeRects.reduce(
          (combined, rect) => {
            const left = Math.min(combined.left, rect.left);
            const top = Math.min(combined.top, rect.top);
            const right = Math.max(combined.right, rect.right);
            const bottom = Math.max(combined.bottom, rect.bottom);
            return new DOMRect(left, top, Math.max(right - left, 1), Math.max(bottom - top, 1));
          },
          new DOMRect(
            rangeRects[0].left,
            rangeRects[0].top,
            Math.max(rangeRects[0].width, 1),
            Math.max(rangeRects[0].height, 1),
          ),
        )
      : range?.getBoundingClientRect();
  const rect =
    rangeRect && (rangeRect.width > 0 || rangeRect.height > 0)
      ? rangeRect
      : (() => {
          const start = view.coordsAtPos(selection.from);
          const end = view.coordsAtPos(selection.to);
          const left = Math.min(start.left, end.left);
          const right = Math.max(start.right, end.right);
          const top = Math.min(start.top, end.top);
          const bottom = Math.max(start.bottom, end.bottom);
          return new DOMRect(left, top, Math.max(right - left, 1), Math.max(bottom - top, 1));
        })();

  return {
    from: selection.from,
    to: selection.to,
    text,
    rect,
  };
}

function resolveAiOptimizePanelFrame(anchorRect: DOMRect): { x: number; y: number; width: number; placement: EditorAiAssistPlacement } {
  const viewportPadding = 16;
  const panelPadding = 24;
  const maxWidth = Math.max(240, window.innerWidth - viewportPadding * 2);
  const preferredWidth = 1120;

  const surfaceRect = surfaceRef.value?.getBoundingClientRect();
  const boundary = surfaceRect ?? new DOMRect(0, 0, window.innerWidth, window.innerHeight);
  const widthFromSurface = surfaceRect ? Math.max(240, surfaceRect.width - panelPadding * 2) : preferredWidth;
  const width = Math.min(maxWidth, preferredWidth, widthFromSurface);
  const availableBelow = boundary.bottom - anchorRect.bottom - AI_OPTIMIZE_PANEL_GAP - viewportPadding;
  const availableAbove = anchorRect.top - boundary.top - AI_OPTIMIZE_PANEL_GAP - viewportPadding;
  const placement: EditorAiAssistPlacement =
    availableBelow >= 320 || availableBelow >= availableAbove ? "bottom" : "top";

  return {
    x: anchorRect.left + anchorRect.width / 2,
    y:
      placement === "top"
        ? Math.min(boundary.bottom - viewportPadding, anchorRect.top - AI_OPTIMIZE_PANEL_GAP)
        : Math.max(boundary.top + viewportPadding, anchorRect.bottom + AI_OPTIMIZE_PANEL_GAP),
    width,
    placement,
  };
}

function openAiOptimizePanel(ctx: Ctx): void {
  if (editorDisabled.value) {
    return;
  }

  const snapshot = resolveTextSelectionSnapshot(ctx);
  if (!snapshot) {
    message.warning(t("article.editor.aiOptimize.messages.selectionRequired"));
    return;
  }

  closeTablePicker();
  closeTableContextMenu();
  clearSelectedImageState();
  closeImageContextMenu();
  stopAiOptimizeGeneration();
  const frame = resolveAiOptimizePanelFrame(snapshot.rect);

  aiOptimizePanel.value = {
    open: true,
    x: frame.x,
    y: frame.y,
    width: frame.width,
    placement: frame.placement,
    from: snapshot.from,
    to: snapshot.to,
    selectedText: snapshot.text,
  };
  aiOptimizeInstruction.value = "";
  resetAiOptimizeOutput();
  aiOptimizeIgnoreNextWindowPointerDown = true;

  window.setTimeout(() => {
    aiOptimizeIgnoreNextWindowPointerDown = false;
  }, 0);

  void nextTick(() => {
    syncAiOptimizeSelectionHighlight(snapshot.from, snapshot.to);
  });
}

function handleAiOptimizeStreamEvent(event: ArticleSelectionOptimizeStreamEvent): void {
  if (event.type === "start") {
    aiOptimizeStatus.value = "generating";
    aiOptimizeError.value = "";
    return;
  }

  if (event.type === "delta") {
    aiOptimizeStatus.value = "generating";
    aiOptimizeError.value = "";
    if (typeof event.content === "string") {
      aiOptimizePreview.value = event.content;
    } else if (typeof event.delta === "string") {
      aiOptimizePreview.value += event.delta;
    }
    return;
  }

  if (event.type === "completed") {
    aiOptimizeStatus.value = "completed";
    aiOptimizeError.value = "";
    if (typeof event.content === "string") {
      aiOptimizePreview.value = event.content;
    }
    return;
  }

  if (event.type === "error") {
    aiOptimizeStatus.value = aiOptimizeHasPreview.value ? "completed" : "error";
    aiOptimizeError.value = event.error || t("article.editor.aiOptimize.messages.failed");
    if (typeof event.content === "string" && event.content.trim()) {
      aiOptimizePreview.value = event.content;
    }
    if (!aiOptimizeHasPreview.value) {
      message.error(aiOptimizeError.value);
    }
  }
}

async function generateAiOptimizePreview(): Promise<void> {
  if (!aiOptimizePanel.value.open || editorDisabled.value) {
    return;
  }

  const instruction = aiOptimizeInstruction.value.trim();
  if (!instruction) {
    message.warning(t("article.editor.aiOptimize.messages.promptRequired"));
    return;
  }

  stopAiOptimizeGeneration();
  aiOptimizePreview.value = "";
  aiOptimizeError.value = "";
  aiOptimizeStatus.value = "generating";

  const controller = new AbortController();
  aiOptimizeAbortController = controller;

  try {
    await streamArticleSelectionOptimize(
      props.articleId,
      {
        title: props.title,
        markdown_content: props.modelValue,
        selected_text: aiOptimizePanel.value.selectedText,
        instruction,
      },
      {
        onEvent: handleAiOptimizeStreamEvent,
      },
      controller.signal,
    );

    if (!controller.signal.aborted && aiOptimizeStatus.value === "generating") {
      aiOptimizeStatus.value = aiOptimizeHasPreview.value ? "completed" : "error";
    }
  } catch (error) {
    if (controller.signal.aborted) {
      return;
    }

    aiOptimizeStatus.value = aiOptimizeHasPreview.value ? "completed" : "error";
    aiOptimizeError.value = formatError(error);
    if (!aiOptimizeHasPreview.value) {
      message.error(aiOptimizeError.value);
    }
  } finally {
    if (aiOptimizeAbortController === controller) {
      aiOptimizeAbortController = null;
    }
  }
}

function applyAiOptimizeReplacement(): void {
  const replacement = aiOptimizePreview.value.trim();
  if (!aiOptimizePanel.value.open || !replacement) {
    if (!replacement) {
      message.warning(t("article.editor.aiOptimize.messages.empty"));
    }
    return;
  }

  const target = { ...aiOptimizePanel.value };
  let replaced = false;

  runEditorAction((ctx) => {
    const view = ctx.get(editorViewCtx);
    const currentText = view.state.doc.textBetween(target.from, target.to, "\n\n");
    if (currentText !== target.selectedText) {
      message.warning(t("article.editor.aiOptimize.messages.selectionChanged"));
      return;
    }

    const parser = ctx.get(parserCtx);
    const selection = TextSelection.create(view.state.doc, target.from, target.to);

    try {
      const replacementDoc = parser(replacement);
      const replacementSlice = Slice.maxOpen(replacementDoc.content);
      const transaction = view.state.tr.setSelection(selection).replaceSelection(replacementSlice).scrollIntoView();
      view.dispatch(transaction);
    } catch {
      view.dispatch(view.state.tr.insertText(replacement, target.from, target.to).scrollIntoView());
    }

    replaced = true;
    requestAnimationFrame(() => {
      view.focus();
    });
  });

  if (replaced) {
    closeAiOptimizePanel();
  }
}

function resolveSelectedImageSnapshot(targetSelection?: Selection | null): SelectedImageSnapshot | null {
  const editor = getEditor();
  if (!editor) {
    return null;
  }

  let snapshot: SelectedImageSnapshot | null = null;

  editor.action((ctx) => {
    const view = ctx.get(editorViewCtx);
    const selection = targetSelection ?? view.state.selection;
    if (!(selection instanceof NodeSelection) || selection.node.type.name !== "image-block") {
      return;
    }

    const nodeDom = view.nodeDOM(selection.from);
    const host =
      nodeDom instanceof HTMLElement && nodeDom.classList.contains("milkdown-image-block")
        ? nodeDom
        : (view.dom.querySelector(".milkdown-image-block.selected") as HTMLElement | null);

    if (!host) {
      return;
    }

    const wrapper = host.querySelector(".image-wrapper");
    const image = wrapper?.querySelector("img");
    if (!(wrapper instanceof HTMLElement) || !(image instanceof HTMLImageElement)) {
      return;
    }

    const rect = image.getBoundingClientRect();
    if (!rect.width || !rect.height) {
      return;
    }

    snapshot = {
      host,
      wrapper,
      image,
      rect,
      nodePos: selection.from,
      nodeSize: selection.node.nodeSize,
      src: String(selection.node.attrs.src ?? ""),
      assetId: resolveImageAssetId(selection.node.attrs.assetId),
      align: resolveImageAlign(selection.node.attrs.align),
    };
  });

  return snapshot;
}

function applySelectedImageSnapshot(snapshot: SelectedImageSnapshot | null): void {
  if (!snapshot) {
    clearSelectedImageState();
    closeImageContextMenu();
    return;
  }

  selectedImageHostElement = snapshot.host;
  selectedImageWrapperElement = snapshot.wrapper;
  selectedImageElement = snapshot.image;
  const surfaceRect = resolveSurfaceRelativeRect(snapshot.rect);
  if (!surfaceRect) {
    clearSelectedImageState();
    closeImageContextMenu();
    return;
  }

  selectedImage.value = {
    open: true,
    x: surfaceRect.x,
    y: surfaceRect.y,
    width: surfaceRect.width,
    height: surfaceRect.height,
    nodePos: snapshot.nodePos,
    nodeSize: snapshot.nodeSize,
    src: snapshot.src,
    assetId: snapshot.assetId,
    align: snapshot.align,
  };
  attachSelectedImageDomSync();

  if (imageContextMenu.value.open && imageContextMenu.value.nodePos !== snapshot.nodePos) {
    closeImageContextMenu();
  }
}

function syncSelectedImageBoundsFromDom(): void {
  const frameElement = selectedImageElement ?? selectedImageWrapperElement;
  if (!selectedImage.value.open || !frameElement) {
    return;
  }

  if (!document.body.contains(frameElement)) {
    clearSelectedImageState();
    closeImageContextMenu();
    return;
  }

  const rect = frameElement.getBoundingClientRect();
  if (!rect.width || !rect.height) {
    return;
  }

  const surfaceRect = resolveSurfaceRelativeRect(rect);
  if (!surfaceRect) {
    return;
  }

  selectedImage.value = {
    ...selectedImage.value,
    x: surfaceRect.x,
    y: surfaceRect.y,
    width: surfaceRect.width,
    height: surfaceRect.height,
  };
}

function scheduleSelectedImageSync(selection?: Selection | null): void {
  if (imageDragActive) {
    return;
  }

  if (selectedImageSyncFrame) {
    cancelAnimationFrame(selectedImageSyncFrame);
  }

  selectedImageSyncFrame = requestAnimationFrame(() => {
    selectedImageSyncFrame = 0;
    applySelectedImageSnapshot(resolveSelectedImageSnapshot(selection));
  });
}

function queueSelectedImageSync(): void {
  requestAnimationFrame(() => {
    requestAnimationFrame(() => {
      scheduleSelectedImageSync();
    });
  });
}

function insertTableFromSelection(row: number, col: number): void {
  runCommand(insertTableCommand, { row, col });
  closeTablePicker();
}

function toggleTablePicker(): void {
  if (editorDisabled.value) {
    return;
  }

  if (tablePickerOpen.value) {
    closeTablePicker();
    return;
  }

  closeTableContextMenu();
  tablePickerOpen.value = true;
}

function handleWindowPointerDown(event: PointerEvent): void {
  if (aiOptimizeIgnoreNextWindowPointerDown) {
    return;
  }

  if (
    !tablePickerOpen.value &&
    !tableContextMenu.value.open &&
    !imageContextMenu.value.open &&
    !(aiOptimizePanel.value.open && aiOptimizeStatus.value === "idle")
  ) {
    return;
  }

  const target = event.target;
  if (target instanceof Node) {
    if (tablePickerOpen.value && tablePickerRef.value?.contains(target)) {
      return;
    }
  }

  if (isEditorActionMenuDropdownTarget(target)) {
    return;
  }

  if (tablePickerOpen.value) {
    closeTablePicker();
  }

  if (tableContextMenu.value.open) {
    closeTableContextMenu();
  }

  if (imageContextMenu.value.open) {
    closeImageContextMenu();
  }

  if (aiOptimizePanel.value.open && aiOptimizeStatus.value === "idle") {
    closeAiOptimizePanel();
  }
}

function isEditorActionMenuDropdownTarget(target: EventTarget | null): boolean {
  if (target instanceof Element) {
    return target.closest(EDITOR_ACTION_MENU_DROPDOWN_SELECTOR) !== null;
  }

  if (target instanceof Node) {
    return target.parentElement?.closest(EDITOR_ACTION_MENU_DROPDOWN_SELECTOR) !== null;
  }

  return false;
}

function resolveTableContextFromPointer(clientX: number, clientY: number): Omit<TableContextMenuState, "open" | "x" | "y"> | null {
  const editor = getEditor();
  if (!editor) {
    return null;
  }

  let context: Omit<TableContextMenuState, "open" | "x" | "y"> | null = null;

  editor.action((ctx) => {
    const view = ctx.get(editorViewCtx);
    const position = view.posAtCoords({ left: clientX, top: clientY });
    if (!position) {
      return;
    }

    const resolvedPosition = view.state.doc.resolve(position.inside > -1 ? position.inside : position.pos);

    let tableDepth = -1;
    let rowDepth = -1;
    for (let depth = resolvedPosition.depth; depth > 0; depth -= 1) {
      const nodeName = resolvedPosition.node(depth).type.name;
      if (tableDepth < 0 && nodeName === "table") {
        tableDepth = depth;
      }
      if (rowDepth < 0 && (nodeName === "table_row" || nodeName === "table_header_row")) {
        rowDepth = depth;
      }
    }

    if (tableDepth < 0 || rowDepth < 0) {
      return;
    }

    const tableStart = resolvedPosition.before(tableDepth);
    const tableNode = resolvedPosition.node(tableDepth);

    context = {
      rowIndex: resolvedPosition.index(tableDepth),
      colIndex: resolvedPosition.index(rowDepth),
      tableStart,
      tableInnerPos: tableStart + 1,
      tableNodeSize: tableNode.nodeSize,
    };
  });

  return context;
}

function openTableContextMenu(event: MouseEvent): void {
  const context = resolveTableContextFromPointer(event.clientX, event.clientY);
  if (!context) {
    closeTableContextMenu();
    return;
  }

  closeTablePicker();
  tableContextMenu.value = {
    open: true,
    x: event.clientX,
    y: event.clientY,
    ...context,
  };
}

function resolveImageContextFromTarget(target: EventTarget | null): SelectedImageSnapshot | null {
  if (!(target instanceof HTMLElement)) {
    return null;
  }

  const host = target.closest(".milkdown-image-block");
  if (!(host instanceof HTMLElement)) {
    return null;
  }

  const editor = getEditor();
  if (!editor) {
    return null;
  }

  let snapshot: SelectedImageSnapshot | null = null;

  editor.action((ctx) => {
    const view = ctx.get(editorViewCtx);
    const nodePos = view.posAtDOM(host, 0);
    const node = view.state.doc.nodeAt(nodePos);
    if (!node || node.type.name !== "image-block") {
      return;
    }

    const wrapper = host.querySelector(".image-wrapper");
    const image = wrapper?.querySelector("img");
    if (!(wrapper instanceof HTMLElement) || !(image instanceof HTMLImageElement)) {
      return;
    }

    const selection = NodeSelection.create(view.state.doc, nodePos);
    if (!view.state.selection.eq(selection)) {
      view.dispatch(view.state.tr.setSelection(selection).scrollIntoView());
    }

    snapshot = {
      host,
      wrapper,
      image,
      rect: image.getBoundingClientRect(),
      nodePos,
      nodeSize: node.nodeSize,
      src: String(node.attrs.src ?? ""),
      assetId: resolveImageAssetId(node.attrs.assetId),
      align: resolveImageAlign(node.attrs.align),
    };
  });

  return snapshot;
}

function openImageContextMenu(event: MouseEvent): void {
  const snapshot = resolveImageContextFromTarget(event.target);
  if (!snapshot) {
    closeImageContextMenu();
    return;
  }

  closeTablePicker();
  closeTableContextMenu();
  applySelectedImageSnapshot(snapshot);
  imageContextMenu.value = {
    open: true,
    x: event.clientX,
    y: event.clientY,
    nodePos: snapshot.nodePos,
    nodeSize: snapshot.nodeSize,
    src: snapshot.src,
  };
}

function handleEditorDragStart(event: DragEvent): void {
  const target = event.target;
  if (!(target instanceof HTMLElement) || !target.closest(".milkdown-image-block")) {
    return;
  }

  imageDragActive = true;
  if (selectedImageSyncFrame) {
    cancelAnimationFrame(selectedImageSyncFrame);
    selectedImageSyncFrame = 0;
  }
  closeImageContextMenu();
  clearSelectedImageState();
}

function finishEditorImageDrag(): void {
  if (!imageDragActive) {
    return;
  }

  imageDragActive = false;
  queueSelectedImageSync();
}

function handleEditorDragEnd(): void {
  finishEditorImageDrag();
}

function handleEditorDrop(): void {
  finishEditorImageDrag();
}

function updateSelectedImageAttributes(
  attrs: Partial<Record<"src" | "assetId" | "ratio" | "width" | "height" | "align", string | number>>,
): void {
  if (!selectedImage.value.open) {
    return;
  }

  const { nodePos } = selectedImage.value;
  runEditorAction((ctx) => {
    const view = ctx.get(editorViewCtx);
    let transaction = view.state.tr;
    for (const [attr, value] of Object.entries(attrs)) {
      transaction = transaction.setNodeAttribute(nodePos, attr, value);
    }
    view.dispatch(transaction);
    requestAnimationFrame(() => {
      view.focus();
      scheduleSelectedImageSync();
    });
  });
}

function updateSelectedImageAttribute(
  attr: "src" | "assetId" | "ratio" | "width" | "height" | "align",
  value: string | number,
): void {
  updateSelectedImageAttributes({ [attr]: value });
}

function deleteSelectedImage(): void {
  if (!selectedImage.value.open) {
    return;
  }

  const { nodePos, nodeSize } = selectedImage.value;
  closeImageContextMenu();
  clearSelectedImageState();
  runEditorAction((ctx) => {
    const view = ctx.get(editorViewCtx);
    view.dispatch(view.state.tr.delete(nodePos, nodePos + nodeSize).scrollIntoView());
    requestAnimationFrame(() => {
      view.focus();
    });
  });
}

function toggleSelectedImageCaption(): void {
  if (!selectedImageHostElement) {
    return;
  }

  closeImageContextMenu();
  const toggle = selectedImageHostElement.querySelector(".operation .operation-item");
  if (toggle instanceof HTMLElement) {
    toggle.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, cancelable: true }));
  }
}

function replaceSelectedImage(): void {
  if (!selectedImage.value.open) {
    return;
  }

  closeImageContextMenu();
  imagePickerAction.value = "replace";
  imagePickerCurrentUrl.value = selectedImage.value.src;
  imagePickerCurrentAssetId.value = selectedImage.value.assetId;
  imagePickerOpen.value = true;
}

function runImageContextAction(action: ImageContextMenuAction): void {
  switch (action) {
    case "replace-image":
      replaceSelectedImage();
      break;
    case "toggle-caption":
      toggleSelectedImageCaption();
      break;
    case "reset-size":
      closeImageContextMenu();
      updateSelectedImageAttributes({
        ratio: 1,
        width: 0,
        height: 0,
      });
      break;
    case "align-left":
      closeImageContextMenu();
      updateSelectedImageAttribute("align", "left");
      break;
    case "align-center":
      closeImageContextMenu();
      updateSelectedImageAttribute("align", "center");
      break;
    case "align-right":
      closeImageContextMenu();
      updateSelectedImageAttribute("align", "right");
      break;
    case "delete-image":
      deleteSelectedImage();
      break;
  }
}

function handleTableContextMenuSelect(action: string): void {
  runTableContextAction(action as TableContextMenuAction);
}

function handleImageContextMenuSelect(action: string): void {
  runImageContextAction(action as ImageContextMenuAction);
}

function beginSelectedImageResize(event: PointerEvent, handle: ImageResizeHandle): void {
  if (editorDisabled.value || !selectedImageElement || !selectedImage.value.open) {
    return;
  }

  event.preventDefault();
  event.stopPropagation();
  closeImageContextMenu();

  const image = selectedImageElement;
  const startRect = image.getBoundingClientRect();
  const startWidth = startRect.width;
  const startHeight = startRect.height;
  const startDiagonal = Math.hypot(startWidth, startHeight);
  if (!startDiagonal) {
    return;
  }

  const minScale = Math.max(100 / Math.max(startWidth, 1), 100 / Math.max(startHeight, 1));
  const maxScale = Math.min(960 / Math.max(startWidth, 1), 720 / Math.max(startHeight, 1));
  const anchorX = handle.includes("w") ? startRect.right : startRect.left;
  const anchorY = handle.includes("n") ? startRect.bottom : startRect.top;
  const body = document.body;

  const onPointerMove = (moveEvent: PointerEvent) => {
    moveEvent.preventDefault();

    const currentDistance = Math.hypot(moveEvent.clientX - anchorX, moveEvent.clientY - anchorY);
    const scale = clamp(currentDistance / startDiagonal, minScale, maxScale);
    const nextWidth = Number((startWidth * scale).toFixed(2));
    const nextHeight = Number((startHeight * scale).toFixed(2));

    image.dataset.width = String(nextWidth);
    image.dataset.height = String(nextHeight);
    image.style.width = `${nextWidth}px`;
    image.style.height = `${nextHeight}px`;
    syncSelectedImageBoundsFromDom();
  };

  const onPointerUp = () => {
    window.removeEventListener("pointermove", onPointerMove);
    window.removeEventListener("pointerup", onPointerUp);
    body.style.cursor = "";

    const currentWidth = Number(image.dataset.width || image.getBoundingClientRect().width);
    const currentHeight = Number(image.dataset.height || image.getBoundingClientRect().height);
    if (Number.isNaN(currentWidth) || currentWidth <= 0 || Number.isNaN(currentHeight) || currentHeight <= 0) {
      scheduleSelectedImageSync();
      return;
    }

    updateSelectedImageAttributes({
      ratio: 1,
      width: currentWidth,
      height: currentHeight,
    });
  };

  body.style.cursor = handle === "nw" || handle === "se" ? "nwse-resize" : "nesw-resize";
  window.addEventListener("pointermove", onPointerMove);
  window.addEventListener("pointerup", onPointerUp);
}

function handleEditorContextMenu(event: MouseEvent): void {
  if (editorDisabled.value) {
    closeTableContextMenu();
    closeImageContextMenu();
    clearSelectedImageState();
    return;
  }

  const target = event.target;
  if (!(target instanceof HTMLElement)) {
    closeTableContextMenu();
    closeImageContextMenu();
    clearSelectedImageState();
    return;
  }

  if (target.closest(".milkdown-image-block")) {
    event.preventDefault();
    openImageContextMenu(event);
    return;
  }

  if (!target.closest(".milkdown-table-block") || !target.closest("td, th")) {
    closeTableContextMenu();
    closeImageContextMenu();
    clearSelectedImageState();
    return;
  }

  event.preventDefault();
  clearSelectedImageState();
  closeImageContextMenu();
  openTableContextMenu(event);
}

function handleSurfaceScroll(): void {
  closeTableContextMenu();
  closeImageContextMenu();
  syncSelectedImageBoundsFromDom();
  if (aiOptimizePanel.value.open) {
    syncAiOptimizeSelectionHighlight(aiOptimizePanel.value.from, aiOptimizePanel.value.to);
  }
}

function handleWindowResize(): void {
  syncSelectedImageBoundsFromDom();
  if (aiOptimizePanel.value.open) {
    syncAiOptimizeSelectionHighlight(aiOptimizePanel.value.from, aiOptimizePanel.value.to);
  }
}

function handleWindowKeydown(event: KeyboardEvent): void {
  if (!aiOptimizePanel.value.open) {
    return;
  }

  if (event.key === "Escape") {
    event.preventDefault();
    closeAiOptimizePanel();
  }
}

function runTableContextAction(action: TableContextMenuAction): void {
  if (!tableContextMenu.value.open || editorDisabled.value) {
    return;
  }

  const context = { ...tableContextMenu.value };
  closeTableContextMenu();

  runEditorAction((ctx) => {
    const commands = ctx.get(commandsCtx);
    const view = ctx.get(editorViewCtx);

    const selectRow = () =>
      commands.call(selectRowCommand.key, {
        index: context.rowIndex,
        pos: context.tableInnerPos,
      });

    const selectCol = () =>
      commands.call(selectColCommand.key, {
        index: context.colIndex,
        pos: context.tableInnerPos,
      });

    switch (action) {
      case "add-row-before":
        if (context.rowIndex === 0) {
          break;
        }
        selectRow();
        commands.call(addRowBeforeCommand.key);
        break;
      case "add-row-after":
        selectRow();
        commands.call(addRowAfterCommand.key);
        break;
      case "delete-row":
        if (context.rowIndex === 0) {
          break;
        }
        selectRow();
        commands.call(deleteSelectedCellsCommand.key);
        break;
      case "add-col-before":
        selectCol();
        commands.call(addColBeforeCommand.key);
        break;
      case "add-col-after":
        selectCol();
        commands.call(addColAfterCommand.key);
        break;
      case "delete-col":
        selectCol();
        commands.call(deleteSelectedCellsCommand.key);
        break;
      case "align-left":
        selectCol();
        commands.call(setAlignCommand.key, "left");
        break;
      case "align-center":
        selectCol();
        commands.call(setAlignCommand.key, "center");
        break;
      case "align-right":
        selectCol();
        commands.call(setAlignCommand.key, "right");
        break;
      case "delete-table":
        view.dispatch(
          view.state.tr
            .delete(context.tableStart, context.tableStart + context.tableNodeSize)
            .scrollIntoView(),
        );
        break;
    }

    requestAnimationFrame(() => {
      view.focus();
    });
  });
}
</script>

<template>
  <div
    class="article-editor-canvas"
    :class="{
      'article-editor-canvas--disabled': disabled,
      'article-editor-canvas--ai-optimize-open': aiOptimizePanel.open,
    }"
  >
    <div class="article-editor-canvas__toolbar">
      <a-button size="small" type="text" @click="runCommand(undoCommand)">
        <template #icon><IconFont type="icon-Undo" /></template>
      </a-button>
      <a-button size="small" type="text" @click="runCommand(redoCommand)">
        <template #icon><IconFont type="icon-redo" /></template>
      </a-button>
      <span class="article-editor-canvas__divider"></span>
      <a-button size="small" type="text" @click="runCommand(wrapInHeadingCommand, 1)">H1</a-button>
      <a-button size="small" type="text" @click="runCommand(wrapInHeadingCommand, 2)">H2</a-button>
      <a-button size="small" type="text" @click="runCommand(wrapInHeadingCommand, 3)">H3</a-button>
      <span class="article-editor-canvas__divider"></span>
      <a-button size="small" type="text" @click="runCommand(toggleStrongCommand)">
        <template #icon><BoldOutlined /></template>
      </a-button>
      <a-button size="small" type="text" @click="runCommand(toggleEmphasisCommand)">
        <template #icon><ItalicOutlined /></template>
      </a-button>
      <a-button size="small" type="text" @click="runCommand(toggleStrikethroughCommand)">
        <template #icon><StrikethroughOutlined /></template>
      </a-button>
      <span class="article-editor-canvas__divider"></span>
      <a-button size="small" type="text" style="font-size: 16px; font-weight: bold; line-height: 1" @click="runCommand(wrapInBlockquoteCommand)">
        "
      </a-button>
      <a-button size="small" type="text" @click="runCommand(insertHrCommand)">
        <template #icon><MinusOutlined /></template>
      </a-button>
      <span class="article-editor-canvas__divider"></span>
      <a-button size="small" type="text" @click="runCommand(wrapInBulletListCommand)">
        <template #icon><UnorderedListOutlined /></template>
      </a-button>
      <a-button size="small" type="text" @click="runCommand(wrapInOrderedListCommand)">
        <template #icon><OrderedListOutlined /></template>
      </a-button>

      <div :ref="assignTablePickerRef" class="article-editor-canvas__table-trigger">
        <a-button
          size="small"
          type="text"
          class="article-editor-canvas__toolbar-button"
          :class="{ 'article-editor-canvas__toolbar-button--active': tablePickerOpen }"
          @click="toggleTablePicker"
        >
          <template #icon><TableOutlined /></template>
        </a-button>

        <EditorGridSizePicker
          v-if="tablePickerOpen"
          :empty-label="t('article.editor.tablePicker.empty')"
          :hint="t('article.editor.tablePicker.hint')"
          @select="insertTableFromSelection"
        />
      </div>

      <span class="article-editor-canvas__divider"></span>
      <a-button
        size="small"
        type="text"
        @mousedown.prevent
        @click="insertImage"
      >
        <template #icon><PictureOutlined /></template>
      </a-button>
    </div>

    <div
      :ref="assignSurfaceRef"
      class="article-editor-canvas__surface"
      @contextmenu.capture="handleEditorContextMenu"
      @dragstart.capture="handleEditorDragStart"
      @dragend.capture="handleEditorDragEnd"
      @drop.capture="handleEditorDrop"
      @scroll="handleSurfaceScroll"
    >
      <div class="article-editor-canvas__title-row">
        <a-input
          :value="title"
          size="large"
          :disabled="disabled"
          :bordered="false"
          class="article-editor-canvas__title-input"
          :placeholder="t('article.editor.titlePlaceholder')"
          @update:value="emit('update:title', String($event ?? ''))"
        />
      </div>

      <div :ref="assignEditorRootRef" data-milkdown-root></div>

      <EditorSelectionFrame
        v-if="selectedImage.open"
        :x="selectedImage.x"
        :y="selectedImage.y"
        :width="selectedImage.width"
        :height="selectedImage.height"
        @resize-start="beginSelectedImageResize"
      />

      <div v-if="loading" class="article-editor-canvas__loading">
        <a-skeleton active :paragraph="{ rows: 10 }" />
      </div>
      <div v-if="disabled" class="article-editor-canvas__mask">
        {{ t("article.editor.messages.locked") }}
      </div>
    </div>

    <EditorActionMenu
      v-if="tableContextMenu.open"
      :x="tableContextMenu.x"
      :y="tableContextMenu.y"
      :groups="tableContextMenuGroups"
      variant="inline"
      @select="handleTableContextMenuSelect"
    />

    <EditorActionMenu
      v-if="imageContextMenu.open"
      :x="imageContextMenu.x"
      :y="imageContextMenu.y"
      :groups="imageContextMenuGroups"
      variant="inline"
      @select="handleImageContextMenuSelect"
    />

    <EditorAiAssistPanel
      v-if="aiOptimizePanel.open"
      v-model:prompt="aiOptimizeInstruction"
      :x="aiOptimizePanel.x"
      :y="aiOptimizePanel.y"
      :width="aiOptimizePanel.width"
      :placement="aiOptimizePanel.placement"
      :boundary-element="surfaceRef"
      :status="aiOptimizeStatus"
      :streaming="aiOptimizeStreaming"
      :has-preview="aiOptimizeHasPreview"
      :error="aiOptimizeError"
      :title="aiOptimizeUiText.title"
      :prompt-placeholder="aiOptimizeUiText.promptPlaceholder"
      :continue-placeholder="aiOptimizeUiText.promptPlaceholder"
      :loading-label="aiOptimizeUiText.generating"
      :error-label="aiOptimizeUiText.failed"
      :disclaimer="aiOptimizeUiText.disclaimer"
      :regenerate-label="aiOptimizeUiText.regenerate"
      :discard-label="aiOptimizeUiText.close"
      :apply-label="aiOptimizeUiText.replace"
      @submit="generateAiOptimizePreview"
      @close="closeAiOptimizePanel"
      @reset="resetAiOptimizeOutput"
      @apply="applyAiOptimizeReplacement"
    >
      <template #preview>
        <MarkdownPreview :content="aiOptimizePreview" />
      </template>
    </EditorAiAssistPanel>

    <CoverPickerModal
      v-model:open="imagePickerOpen"
      :article-id="articleId"
      :platform-ids="[]"
      :current-url="imagePickerCurrentUrl"
      :current-asset-id="imagePickerCurrentAssetId"
      :title="t('article.editor.imagePicker.title')"
      :empty-upload-title="t('article.editor.imagePicker.emptyTitle')"
      :empty-upload-hint="t('article.editor.imagePicker.emptyHint')"
      :upload-handler="uploadBodyImageToLibrary"
      @confirmed="handleImagePicked"
    />
  </div>
</template>

<style scoped>
:global(::highlight(article-editor-ai-optimize-selection)) {
  color: inherit;
  background: rgba(156, 163, 175, 0.42);
}

.article-editor-canvas {
  position: relative;
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
  border: 1px solid #edf2fa;
  border-radius: 24px;
  overflow: hidden;
  background:
    radial-gradient(circle at top right, rgba(64, 110, 255, 0.08), transparent 20%),
    linear-gradient(180deg, #ffffff 0%, #fbfcff 100%);
}

.article-editor-canvas__toolbar {
  position: relative;
  z-index: 3;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  padding: 14px 18px;
  border-bottom: 1px solid #eef2f8;
  background: rgba(255, 255, 255, 0.92);
  backdrop-filter: blur(12px);
}

.article-editor-canvas__divider {
  width: 1px;
  height: 20px;
  margin: 0 2px;
  background: #e5ebf5;
}

.article-editor-canvas__table-trigger {
  position: relative;
  display: flex;
  align-items: center;
}

.article-editor-canvas__toolbar :deep(.article-editor-canvas__toolbar-button--active) {
  color: #245bdb;
  background: rgba(47, 107, 255, 0.1);
}

.article-editor-canvas__surface {
  position: relative;
  z-index: 1;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

.article-editor-canvas__title-row {
  position: relative;
  z-index: 0;
  padding: 18px 28px 0;
}

.article-editor-canvas__title-row :deep(.ant-input) {
  height: 68px;
  padding: 0;
  border: 0;
  background: transparent;
  box-shadow: none;
  color: #101828;
  font-size: 30px !important;
  font-weight: 800;
  line-height: 1.15;
}

.article-editor-canvas__title-row :deep(.ant-input::placeholder) {
  color: #98a2b3;
}

.article-editor-canvas__loading {
  position: absolute;
  inset: 0;
  z-index: 2;
  padding: 24px;
  background: rgba(255, 255, 255, 0.92);
}

/* Removed height 100% blocks to allow natural vertical content growth and scrolling */

.article-editor-canvas__surface :deep(.ProseMirror) {
  min-height: 640px;
  padding: 14px 36px 80px;
  background: transparent;
}

.article-editor-canvas__surface :deep(.ProseMirror:focus) {
  outline: none;
}

.article-editor-canvas__surface :deep(.milkdown-image-block) {
  margin: 18px 0;
  text-align: center;
}

.article-editor-canvas__surface :deep(.milkdown-image-block[data-align="left"]) {
  text-align: left;
}

.article-editor-canvas__surface :deep(.milkdown-image-block[data-align="right"]) {
  text-align: right;
}

.article-editor-canvas__surface :deep(.milkdown-image-block > .image-wrapper) {
  display: inline-flex;
  flex-direction: column;
  max-width: 100%;
}

.article-editor-canvas__surface :deep(.milkdown-image-block.selected > .image-wrapper::before) {
  background: transparent;
  border: 0;
}

.article-editor-canvas__surface :deep(.milkdown-image-block img) {
  display: block;
  max-width: min(100%, 960px);
  border-radius: 18px;
}

.article-editor-canvas__surface :deep(.milkdown-image-block .caption-input) {
  width: min(100%, 960px);
  margin-top: 12px;
}

.article-editor-canvas__surface :deep(.milkdown-image-block > .image-wrapper .operation) {
  opacity: 0 !important;
  pointer-events: none;
}

.article-editor-canvas__surface :deep(.milkdown-image-block > .image-wrapper .image-resize-handle) {
  opacity: 0 !important;
  pointer-events: none;
}

.article-editor-canvas__surface :deep(.milkdown-table-block table) {
  width: 100%;
  border-collapse: collapse;
  border-spacing: 0;
  background: #ffffff;
}

.article-editor-canvas__surface :deep(.milkdown-table-block th),
.article-editor-canvas__surface :deep(.milkdown-table-block td) {
  min-width: 140px;
  min-height: 58px;
  padding: 12px 18px;
  border: 2px solid #d7d7d7;
  color: #1f2937;
  caret-color: #000000;
  font-size: 16px;
  line-height: 1.55;
  vertical-align: middle;
  background: #ffffff;
}



.article-editor-canvas__mask {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding-top: 120px;
  color: #475467;
  font-size: 14px;
  background: rgba(255, 255, 255, 0.58);
  backdrop-filter: blur(3px);
}

.article-editor-canvas--disabled .article-editor-canvas__toolbar {
  opacity: 0.56;
}

.article-editor-canvas--disabled .article-editor-canvas__title-row {
  opacity: 0.72;
}

.article-editor-canvas--ai-optimize-open :deep(.milkdown-toolbar) {
  display: none !important;
}
</style>
