<script setup lang="ts">
import { computed, h, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { ExperimentOutlined, ThunderboltOutlined } from "@ant-design/icons-vue";
import type { KolVariableDefinition, KolVariableType } from "@geo/shared-types";
import { Modal } from "ant-design-vue";

import {
  buildKolPlaceholderToken,
  createKolVariableDefinition,
} from "@/lib/kol-placeholders";

const props = defineProps<{
  content: string;
  variables: KolVariableDefinition[];
  aiBusy: boolean;
  selectionAiOpen?: boolean;
}>();

const emit = defineEmits<{
  (e: "update:content", value: string): void;
  (e: "update:variables", value: KolVariableDefinition[]): void;
  (e: "ai-generate", description: string): void;
  (e: "ai-optimize-selection", value: PromptSelectionAiSnapshot): void;
  (e: "focus-variable", key: string): void;
}>();

const { t } = useI18n();
const aiDescription = ref("");
const textareaRef = ref<HTMLTextAreaElement | null>(null);
const highlightRef = ref<HTMLDivElement | null>(null);
const dropCaret = ref({
  visible: false,
  left: 0,
  top: 0,
  height: 24,
});
const selectionAiAction = ref({
  visible: false,
  buttonX: 0,
  buttonY: 0,
  start: 0,
  end: 0,
  selectedText: "",
  x: 0,
  y: 0,
  width: 0,
  placement: "bottom" as EditorAiAssistPlacement,
  boundary: createViewportBoundary(),
});
const dragInsertIndex = ref<number | null>(null);
let syncingScroll = false;

type PointerSelectionState = {
  key: string | null;
  pointerId: number;
  startX: number;
  startY: number;
};

type OverlaySelectionRange = {
  start: number;
  end: number;
  direction: "forward" | "backward";
};

type ClientPoint = {
  clientX: number;
  clientY: number;
};

type CaretVisualRect = {
  left: number;
  top: number;
  height: number;
};

type EditorScrollState = {
  top: number;
  left: number;
};

type EditorAiAssistBoundary = {
  left: number;
  top: number;
  right: number;
  bottom: number;
};

type EditorAiAssistPlacement = "bottom" | "top";

type PromptSelectionAiSnapshot = {
  start: number;
  end: number;
  selectedText: string;
  x: number;
  y: number;
  width: number;
  placement: EditorAiAssistPlacement;
  boundary: EditorAiAssistBoundary;
};

let pointerSelectionState: PointerSelectionState | null = null;
let dragScrollLock: EditorScrollState | null = null;

const editorPlaceholder = "在这里输入提示词内容，可以使用 {{变量名}} 引用左侧定义的变量";
const highlightTokenRE = /\{\{\s*([^{}]+?)\s*\}\}/g;
const AI_SELECTION_ACTION_SIZE = 44;
const AI_SELECTION_ACTION_GAP = 10;
const AI_SELECTION_PANEL_GAP = 18;
const AI_SELECTION_PANEL_PREFERRED_HEIGHT = 320;
const AI_SELECTION_HIGHLIGHT_NAME = "kol-prompt-ai-optimize-selection";

const highlightedContent = computed(() => {
  const text = props.content ?? "";
  if (!text) {
    return `<span class="editor-placeholder">${escapeHtml(editorPlaceholder)}</span>`;
  }

  let html = "";
  let lastIndex = 0;

  for (const match of text.matchAll(highlightTokenRE)) {
    const matchIndex = match.index ?? 0;
    const token = match[0] ?? "";
    const key = match[1] ?? "";
    const keyOffset = token.indexOf(key);
    const prefix = keyOffset >= 0 ? token.slice(0, keyOffset) : token;
    const suffix = keyOffset >= 0 ? token.slice(keyOffset + key.length) : "";
    const keyStart = matchIndex + Math.max(keyOffset, 0);

    html += escapeHtml(text.slice(lastIndex, matchIndex));
    html += [
      `<span class="token-highlight">`,
      escapeHtml(prefix),
      `<span class="token-highlight__content" data-kol-key="${escapeAttribute(key)}" data-kol-start="${keyStart}">${escapeHtml(key)}</span>`,
      escapeHtml(suffix),
      `</span>`,
    ].join("");
    lastIndex = matchIndex + token.length;
  }

  html += escapeHtml(text.slice(lastIndex));
  return html;
});
const hasPromptContent = computed(() => Boolean(props.content?.trim()));

onMounted(() => {
  window.addEventListener("dragend", hideDropCaret);
  window.addEventListener("drop", hideDropCaret);
  window.addEventListener("pointerdown", handleWindowPointerDown);
  window.addEventListener("resize", handleViewportChange);
});

onBeforeUnmount(() => {
  window.removeEventListener("dragend", hideDropCaret);
  window.removeEventListener("drop", hideDropCaret);
  window.removeEventListener("pointerdown", handleWindowPointerDown);
  window.removeEventListener("resize", handleViewportChange);
  clearSelectionAiHighlight();
});

watch(
  () => props.selectionAiOpen,
  (open) => {
    if (!open) {
      clearSelectionAiHighlight();
    } else if (selectionAiAction.value.selectedText) {
      syncSelectionAiHighlight(selectionAiAction.value.start, selectionAiAction.value.end);
    }
  },
);

watch(
  () => props.content,
  () => {
    if (!props.selectionAiOpen) {
      hideSelectionAiAction(true);
    }
  },
);

function onDragEnter(event: DragEvent) {
  if (!isKolVariableDrag(event)) {
    return;
  }

  event.preventDefault();
  lockEditorScrollForDrag();
  restoreDragScrollLock();
  updateDragInsertionPoint(event);
  restoreDragScrollLock();
}

function onDragOver(event: DragEvent) {
  if (!isKolVariableDrag(event)) {
    return;
  }

  event.preventDefault();
  if (event.dataTransfer) {
    event.dataTransfer.dropEffect = "copy";
  }
  lockEditorScrollForDrag();
  restoreDragScrollLock();
  updateDragInsertionPoint(event);
  restoreDragScrollLock();
}

function onDragLeave(event: DragEvent) {
  const nextTarget = event.relatedTarget;
  const currentTarget = event.currentTarget;
  if (
    nextTarget instanceof Node &&
    currentTarget instanceof HTMLElement &&
    currentTarget.contains(nextTarget)
  ) {
    return;
  }
  if (currentTarget instanceof HTMLElement && isPointInsideElement(event, currentTarget)) {
    return;
  }

  hideDropCaret();
}

function onDrop(event: DragEvent) {
  event.preventDefault();
  const type = event.dataTransfer?.getData("application/kol-variable-type") as KolVariableType;
  if (!type) {
    hideDropCaret();
    return;
  }

  const insertionIndex =
    dragInsertIndex.value ??
    (highlightRef.value ? getCaretIndexFromPoint(highlightRef.value, event) : props.content.length);
  const lockedScroll = dragScrollLock ?? readEditorScrollState();
  hideDropCaret();
  restoreEditorScroll(lockedScroll);

  const labelValue = ref("");

  Modal.confirm({
    title: t("kol.manage.variable.label"),
    content: () => {
      return h("div", { style: { marginTop: "16px" } }, [
        h("input", {
          class: "ant-input",
          placeholder: t("common.inputPlease"),
          value: labelValue.value,
          onInput: (e: Event) => {
            labelValue.value = (e.target as HTMLInputElement).value;
          },
        }),
      ]);
    },
    onOk: () => {
      const key = labelValue.value.trim() || t(`kol.manage.variable.${type}`);
      const existingVariable = props.variables.find((variable) => variable.key === key);

      if (!existingVariable) {
        const newVar: KolVariableDefinition = createKolVariableDefinition(key, {
          type,
        });
        emit("update:variables", [...props.variables, newVar]);
      }

      insertToken(buildKolPlaceholderToken(key), insertionIndex, lockedScroll);
    },
  });
}

function isKolVariableDrag(event: DragEvent): boolean {
  const types = Array.from(event.dataTransfer?.types ?? []);
  return types.includes("application/kol-variable-type");
}

function updateDragInsertionPoint(event: DragEvent) {
  const highlight = highlightRef.value;
  if (!highlight) {
    return;
  }

  const insertIndex = getCaretIndexFromPoint(highlight, event);
  dragInsertIndex.value = insertIndex;
  updateDropCaretPosition(insertIndex);
}

function lockEditorScrollForDrag() {
  if (!dragScrollLock) {
    dragScrollLock = readEditorScrollState();
  }
}

function restoreDragScrollLock() {
  if (dragScrollLock) {
    restoreEditorScroll(dragScrollLock);
  }
}

function updateDropCaretPosition(insertIndex: number) {
  const highlight = highlightRef.value;
  if (!highlight) {
    return;
  }

  const caretRect = getCaretVisualRectAtIndex(highlight, insertIndex);
  const highlightRect = highlight.getBoundingClientRect();
  const height = Math.max(caretRect.height, getEditorLineHeight(highlight) * 0.9);

  dropCaret.value = {
    visible: true,
    left: clampNumber(caretRect.left - highlightRect.left, 0, highlight.clientWidth),
    top: clampNumber(
      caretRect.top - highlightRect.top,
      0,
      Math.max(highlight.clientHeight - height, 0),
    ),
    height,
  };
}

function hideDropCaret() {
  dropCaret.value = {
    ...dropCaret.value,
    visible: false,
  };
  dragInsertIndex.value = null;
  dragScrollLock = null;
}

function insertToken(token: string, insertionIndex?: number, scrollState = readEditorScrollState()) {
  if (!textareaRef.value) return;
  
  const el = textareaRef.value;
  const text = props.content;
  const hasExplicitInsertionIndex = typeof insertionIndex === "number";
  const start = hasExplicitInsertionIndex ? clampOffset(insertionIndex, text.length) : el.selectionStart;
  const end = hasExplicitInsertionIndex ? start : el.selectionEnd;
  
  const newContent = text.substring(0, start) + token + text.substring(end);
  emit("update:content", newContent);
  
  // Set cursor position after update
  setTimeout(() => {
    focusTextareaWithoutScroll(el);
    el.setSelectionRange(start + token.length, start + token.length);
    restoreTextareaScroll(scrollState.top, scrollState.left);
  }, 0);
}

function handleAiGenerate() {
  if (hasPromptContent.value) return;
  if (!aiDescription.value.trim()) return;
  emit("ai-generate", aiDescription.value);
}

function handleContentInput(event: Event) {
  emit("update:content", (event.target as HTMLTextAreaElement).value);
  hideSelectionAiAction(true);
  syncScrollFromTextarea();
}

function scheduleTextareaSelectionSync() {
  window.setTimeout(() => {
    syncTextareaSelectionAction();
  }, 0);
}

function syncTextareaSelectionAction() {
  const textarea = textareaRef.value;
  if (!textarea) {
    return;
  }

  showSelectionAiAction(textarea.selectionStart, textarea.selectionEnd);
}

function handleHighlightPointerDown(event: PointerEvent) {
  hideSelectionAiAction(!props.selectionAiOpen);
  pointerSelectionState = {
    key: getPlaceholderKeyFromTarget(event.target),
    pointerId: event.pointerId,
    startX: event.clientX,
    startY: event.clientY,
  };
}

function handleHighlightPointerUp(event: PointerEvent) {
  const textarea = textareaRef.value;
  const highlight = highlightRef.value;
  const pointerState = pointerSelectionState;
  pointerSelectionState = null;

  if (!textarea || !highlight || !pointerState || pointerState.pointerId !== event.pointerId) {
    return;
  }

  const overlaySelection = getOverlaySelectionRange(highlight);
  if (overlaySelection) {
    const scrollTop = textarea.scrollTop;
    const scrollLeft = textarea.scrollLeft;

    textarea.setSelectionRange(
      overlaySelection.start,
      overlaySelection.end,
      overlaySelection.direction,
    );
    focusTextareaWithoutScroll(textarea);
    restoreTextareaScroll(scrollTop, scrollLeft);
    showSelectionAiAction(overlaySelection.start, overlaySelection.end);
    return;
  }

  const movedDistance =
    Math.abs(event.clientX - pointerState.startX) + Math.abs(event.clientY - pointerState.startY);
  if (movedDistance > 6) {
    return;
  }

  event.preventDefault();
  const cursorPos = getCaretIndexFromPoint(highlight, event);
  const scrollTop = textarea.scrollTop;
  const scrollLeft = textarea.scrollLeft;

  textarea.setSelectionRange(cursorPos, cursorPos);
  focusTextareaWithoutScroll(textarea);
  restoreTextareaScroll(scrollTop, scrollLeft);
  hideSelectionAiAction(!props.selectionAiOpen);

  if (pointerState.key) {
    emit("focus-variable", pointerState.key);
  }
}

function handleHighlightPointerCancel() {
  pointerSelectionState = null;
}

function showSelectionAiAction(start: number, end: number) {
  const selectionStart = Math.min(start, end);
  const selectionEnd = Math.max(start, end);
  const selectedText = props.content.slice(selectionStart, selectionEnd);
  if (selectionStart === selectionEnd || !selectedText.trim()) {
    hideSelectionAiAction(!props.selectionAiOpen);
    return;
  }

  const highlight = highlightRef.value;
  if (!highlight) {
    return;
  }

  const rect = getTextRangeVisualRect(highlight, selectionStart, selectionEnd);
  if (!rect) {
    return;
  }

  const panelFrame = resolveSelectionAiPanelFrame(rect);
  const preferredButtonTop = rect.top - AI_SELECTION_ACTION_SIZE - AI_SELECTION_ACTION_GAP;
  const buttonY =
    preferredButtonTop >= 12 ? preferredButtonTop : rect.bottom + AI_SELECTION_ACTION_GAP;

  selectionAiAction.value = {
    visible: !props.selectionAiOpen,
    buttonX: clampNumber(
      rect.left + rect.width / 2,
      AI_SELECTION_ACTION_SIZE / 2 + 12,
      window.innerWidth - AI_SELECTION_ACTION_SIZE / 2 - 12,
    ),
    buttonY: clampNumber(
      buttonY,
      12,
      window.innerHeight - AI_SELECTION_ACTION_SIZE - 12,
    ),
    start: selectionStart,
    end: selectionEnd,
    selectedText,
    x: panelFrame.x,
    y: panelFrame.y,
    width: panelFrame.width,
    placement: panelFrame.placement,
    boundary: panelFrame.boundary,
  };
  syncSelectionAiHighlight(selectionStart, selectionEnd);
}

function hideSelectionAiAction(clearHighlight: boolean) {
  selectionAiAction.value = {
    ...selectionAiAction.value,
    visible: false,
  };
  if (clearHighlight) {
    clearSelectionAiHighlight();
  }
}

function openSelectionAiPanel() {
  const action = selectionAiAction.value;
  if (!action.selectedText.trim()) {
    return;
  }

  emit("ai-optimize-selection", {
    start: action.start,
    end: action.end,
    selectedText: action.selectedText,
    x: action.x,
    y: action.y,
    width: action.width,
    placement: action.placement,
    boundary: action.boundary,
  });
  hideSelectionAiAction(false);
}

function handleWindowPointerDown(event: PointerEvent) {
  const target = event.target;
  if (target instanceof Node) {
    if (textareaRef.value?.contains(target) || highlightRef.value?.contains(target)) {
      return;
    }
  }

  if (target instanceof Element && target.closest(".prompt-selection-ai-action")) {
    return;
  }

  hideSelectionAiAction(!props.selectionAiOpen);
}

function handleViewportChange() {
  hideSelectionAiAction(!props.selectionAiOpen);
}

function getPlaceholderKeyFromTarget(target: EventTarget | null): string | null {
  const targetElement =
    target instanceof HTMLElement
      ? target
      : target instanceof Text
        ? target.parentElement
        : null;

  return targetElement?.closest<HTMLElement>("[data-kol-key]")?.dataset.kolKey?.trim() || null;
}

function hasOverlaySelection(container: HTMLElement): boolean {
  const selection = window.getSelection();
  if (!selection || selection.isCollapsed) {
    return false;
  }

  const anchorNode = selection.anchorNode;
  const focusNode = selection.focusNode;
  return !!anchorNode && !!focusNode && container.contains(anchorNode) && container.contains(focusNode);
}

function getOverlaySelectionRange(container: HTMLElement): OverlaySelectionRange | null {
  if (!hasOverlaySelection(container)) {
    return null;
  }

  const selection = window.getSelection();
  if (!selection) {
    return null;
  }

  const anchorIndex = resolveCaretIndex(container, selection.anchorNode, selection.anchorOffset);
  const focusIndex = resolveCaretIndex(container, selection.focusNode, selection.focusOffset);
  if (anchorIndex === null || focusIndex === null) {
    return null;
  }

  return {
    start: Math.min(anchorIndex, focusIndex),
    end: Math.max(anchorIndex, focusIndex),
    direction: anchorIndex <= focusIndex ? "forward" : "backward",
  };
}

function getCaretIndexFromPoint(container: HTMLElement, point: ClientPoint): number {
  const browserIndex = getBrowserCaretIndex(container, point);
  if (browserIndex !== null) {
    return clampOffset(browserIndex, props.content.length);
  }

  const approximateIndex = getApproximateCaretIndex(container, point.clientX, point.clientY);
  return clampOffset(approximateIndex, props.content.length);
}

function getBrowserCaretIndex(
  container: HTMLElement,
  point: ClientPoint,
): number | null {
  if ("caretPositionFromPoint" in document) {
    const position = document.caretPositionFromPoint(point.clientX, point.clientY);
    const index = resolveCaretIndex(container, position?.offsetNode ?? null, position?.offset);
    if (index !== null) {
      return index;
    }
  }

  if ("caretRangeFromPoint" in document) {
    const range = document.caretRangeFromPoint(point.clientX, point.clientY);
    const index = resolveCaretIndex(container, range?.startContainer ?? null, range?.startOffset);
    if (index !== null) {
      return index;
    }
  }

  return null;
}

function getCaretVisualRectAtIndex(container: HTMLElement, targetIndex: number): CaretVisualRect {
  const fallbackRect = getFallbackCaretVisualRect(container);
  const position = getTextPositionAtIndex(container, targetIndex);
  if (!position) {
    return fallbackRect;
  }

  const collapsedRange = document.createRange();
  collapsedRange.setStart(position.node, position.offset);
  collapsedRange.collapse(true);
  const collapsedRect = firstUsableRangeRect(collapsedRange);
  if (collapsedRect) {
    return {
      left: collapsedRect.left,
      top: collapsedRect.top,
      height: collapsedRect.height,
    };
  }

  const characterRect = getAdjacentCharacterRect(position.node, position.offset);
  if (characterRect) {
    return characterRect;
  }

  return fallbackRect;
}

function getTextRangeVisualRect(container: HTMLElement, start: number, end: number): DOMRect | null {
  const startPosition = getTextPositionAtIndex(container, start);
  const endPosition = getTextPositionAtIndex(container, end);
  if (!startPosition || !endPosition) {
    return null;
  }

  const range = document.createRange();
  range.setStart(startPosition.node, startPosition.offset);
  range.setEnd(endPosition.node, endPosition.offset);
  const rects = Array.from(range.getClientRects()).filter(
    (rect) => rect.width > 0 || rect.height > 0,
  );
  if (rects.length === 0) {
    const rect = range.getBoundingClientRect();
    return rect.width > 0 || rect.height > 0 ? rect : null;
  }

  return combineRects(rects);
}

function combineRects(rects: DOMRect[]): DOMRect {
  const first = rects[0];
  return rects.slice(1).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(first.left, first.top, Math.max(first.width, 1), Math.max(first.height, 1)),
  );
}

function resolveSelectionAiPanelFrame(anchorRect: DOMRect): { x: number; y: number; width: number; placement: EditorAiAssistPlacement; boundary: EditorAiAssistBoundary } {
  const viewportPadding = 16;
  const panelPadding = 24;
  const preferredWidth = 1120;
  const surfaceRect = highlightRef.value?.getBoundingClientRect();
  const boundary = normalizeEditorBoundary(surfaceRect);
  const maxWidth = Math.max(240, boundary.right - boundary.left - viewportPadding * 2);
  const widthFromSurface = surfaceRect
    ? Math.max(240, surfaceRect.width - panelPadding * 2)
    : preferredWidth;
  const availableBelow = boundary.bottom - anchorRect.bottom - AI_SELECTION_PANEL_GAP - viewportPadding;
  const availableAbove = anchorRect.top - boundary.top - AI_SELECTION_PANEL_GAP - viewportPadding;
  const placement: EditorAiAssistPlacement =
    availableBelow >= AI_SELECTION_PANEL_PREFERRED_HEIGHT || availableBelow >= availableAbove
      ? "bottom"
      : "top";

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

function createViewportBoundary(): EditorAiAssistBoundary {
  return {
    left: 0,
    top: 0,
    right: window.innerWidth,
    bottom: window.innerHeight,
  };
}

function normalizeEditorBoundary(rect: DOMRect | null | undefined): EditorAiAssistBoundary {
  if (!rect) {
    return createViewportBoundary();
  }

  const left = clampNumber(rect.left, 0, window.innerWidth);
  const top = clampNumber(rect.top, 0, window.innerHeight);
  const right = clampNumber(rect.right, left, window.innerWidth);
  const bottom = clampNumber(rect.bottom, top, window.innerHeight);

  if (right - left < 1 || bottom - top < 1) {
    return createViewportBoundary();
  }

  return { left, top, right, bottom };
}

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 clearSelectionAiHighlight() {
  getCSSHighlightsRegistry()?.delete(AI_SELECTION_HIGHLIGHT_NAME);
}

function syncSelectionAiHighlight(start: number, end: number) {
  clearSelectionAiHighlight();
  const registry = getCSSHighlightsRegistry();
  const HighlightCtor = window.Highlight as (new (...ranges: Range[]) => unknown) | undefined;
  const highlight = highlightRef.value;
  if (!registry || !HighlightCtor || !highlight) {
    return;
  }

  const startPosition = getTextPositionAtIndex(highlight, start);
  const endPosition = getTextPositionAtIndex(highlight, end);
  if (!startPosition || !endPosition) {
    return;
  }

  const range = document.createRange();
  range.setStart(startPosition.node, startPosition.offset);
  range.setEnd(endPosition.node, endPosition.offset);
  registry.set(AI_SELECTION_HIGHLIGHT_NAME, new HighlightCtor(range));
}

function getTextPositionAtIndex(
  container: HTMLElement,
  targetIndex: number,
): { node: Text; offset: number } | null {
  const textNodes = getTextNodes(container);
  if (textNodes.length === 0) {
    return null;
  }

  let currentIndex = 0;
  for (const textNode of textNodes) {
    const textLength = textNode.textContent?.length ?? 0;
    if (targetIndex <= currentIndex + textLength) {
      return {
        node: textNode,
        offset: clampNumber(targetIndex - currentIndex, 0, textLength),
      };
    }
    currentIndex += textLength;
  }

  const lastNode = textNodes[textNodes.length - 1];
  return {
    node: lastNode,
    offset: lastNode.textContent?.length ?? 0,
  };
}

function firstUsableRangeRect(range: Range): DOMRect | null {
  const rects = Array.from(range.getClientRects());
  const rect = rects.find((item) => item.height > 0);
  if (rect) {
    return rect;
  }

  const boundingRect = range.getBoundingClientRect();
  return boundingRect.height > 0 ? boundingRect : null;
}

function getAdjacentCharacterRect(node: Text, offset: number): CaretVisualRect | null {
  const textLength = node.textContent?.length ?? 0;
  const range = document.createRange();

  if (offset > 0) {
    range.setStart(node, offset - 1);
    range.setEnd(node, offset);
    const rect = firstUsableRangeRect(range);
    if (rect) {
      return {
        left: rect.right,
        top: rect.top,
        height: rect.height,
      };
    }
  }

  if (offset < textLength) {
    range.setStart(node, offset);
    range.setEnd(node, offset + 1);
    const rect = firstUsableRangeRect(range);
    if (rect) {
      return {
        left: rect.left,
        top: rect.top,
        height: rect.height,
      };
    }
  }

  return null;
}

function getFallbackCaretVisualRect(container: HTMLElement): CaretVisualRect {
  const rect = container.getBoundingClientRect();
  const style = window.getComputedStyle(container);
  const paddingLeft = Number.parseFloat(style.paddingLeft) || 0;
  const paddingTop = Number.parseFloat(style.paddingTop) || 0;

  return {
    left: rect.left + paddingLeft,
    top: rect.top + paddingTop,
    height: getEditorLineHeight(container),
  };
}

function getEditorLineHeight(container: HTMLElement): number {
  const style = window.getComputedStyle(container);
  const lineHeight = Number.parseFloat(style.lineHeight);
  if (Number.isFinite(lineHeight)) {
    return lineHeight;
  }

  const fontSize = Number.parseFloat(style.fontSize) || 16;
  return fontSize * 1.6;
}

function clampNumber(value: number, min: number, max: number): number {
  if (!Number.isFinite(value)) {
    return min;
  }
  return Math.min(Math.max(value, min), max);
}

function isPointInsideElement(point: ClientPoint, element: HTMLElement): boolean {
  const rect = element.getBoundingClientRect();
  return (
    point.clientX >= rect.left &&
    point.clientX <= rect.right &&
    point.clientY >= rect.top &&
    point.clientY <= rect.bottom
  );
}

function resolveCaretIndex(
  container: HTMLElement,
  node: Node | null,
  offset?: number,
): number | null {
  if (!node || (node !== container && !container.contains(node))) {
    return null;
  }

  const range = document.createRange();
  range.setStart(container, 0);
  range.setEnd(node, offset ?? 0);
  return range.toString().length;
}

function getApproximateCaretIndex(
  container: HTMLElement,
  clientX: number,
  clientY: number,
): number {
  const textNodes = getTextNodes(container);
  if (textNodes.length === 0) {
    return 0;
  }

  const range = document.createRange();
  let indexOffset = 0;
  let bestIndex = 0;
  let bestDistance = Number.POSITIVE_INFINITY;
  let sawLineMatch = false;

  for (const textNode of textNodes) {
    const textLength = textNode.textContent?.length ?? 0;

    for (let index = 0; index < textLength; index += 1) {
      range.setStart(textNode, index);
      range.setEnd(textNode, index + 1);
      const rect = range.getBoundingClientRect();

      if (!rect.width && !rect.height) {
        continue;
      }

      const isSameLine = clientY >= rect.top && clientY <= rect.bottom;
      const midpoint = rect.left + rect.width / 2;

      if (isSameLine) {
        sawLineMatch = true;
        if (clientX <= midpoint) {
          return indexOffset + index;
        }
        bestIndex = indexOffset + index + 1;
        continue;
      }

      const deltaY =
        clientY < rect.top
          ? rect.top - clientY
          : clientY > rect.bottom
            ? clientY - rect.bottom
            : 0;
      const deltaX =
        clientX < rect.left
          ? rect.left - clientX
          : clientX > rect.right
            ? clientX - rect.right
            : 0;
      const distance = deltaY * deltaY + deltaX * deltaX;

      if (distance < bestDistance) {
        bestDistance = distance;
        bestIndex = clientX <= midpoint ? indexOffset + index : indexOffset + index + 1;
      }
    }

    indexOffset += textLength;
  }

  if (sawLineMatch) {
    return bestIndex;
  }

  return bestIndex;
}

function getTextNodes(container: HTMLElement): Text[] {
  const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, {
    acceptNode(node) {
      if (!(node instanceof Text)) {
        return NodeFilter.FILTER_REJECT;
      }
      return node.textContent?.length ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
    },
  });
  const nodes: Text[] = [];

  while (walker.nextNode()) {
    const currentNode = walker.currentNode;
    if (currentNode instanceof Text) {
      nodes.push(currentNode);
    }
  }

  return nodes;
}

function clampOffset(offset: number, textLength: number): number {
  if (!Number.isFinite(offset)) {
    return 0;
  }
  return Math.min(Math.max(offset, 0), textLength);
}

function focusTextareaWithoutScroll(textarea: HTMLTextAreaElement) {
  try {
    textarea.focus({ preventScroll: true });
  } catch {
    textarea.focus();
  }
}

function restoreTextareaScroll(scrollTop: number, scrollLeft: number) {
  restoreEditorScroll({ top: scrollTop, left: scrollLeft });

  requestAnimationFrame(() => {
    restoreEditorScroll({ top: scrollTop, left: scrollLeft });
  });
}

function readEditorScrollState(): EditorScrollState {
  return {
    top: highlightRef.value?.scrollTop ?? textareaRef.value?.scrollTop ?? 0,
    left: highlightRef.value?.scrollLeft ?? textareaRef.value?.scrollLeft ?? 0,
  };
}

function restoreEditorScroll(state: EditorScrollState) {
  if (textareaRef.value) {
    textareaRef.value.scrollTop = state.top;
    textareaRef.value.scrollLeft = state.left;
  }
  if (highlightRef.value) {
    highlightRef.value.scrollTop = state.top;
    highlightRef.value.scrollLeft = state.left;
  }
  if (dropCaret.value.visible && dragInsertIndex.value !== null) {
    updateDropCaretPosition(dragInsertIndex.value);
  }
}

function syncScrollFromTextarea() {
  if (!textareaRef.value || !highlightRef.value) {
    return;
  }

  if (dragScrollLock) {
    restoreDragScrollLock();
    return;
  }

  if (syncingScroll) {
    return;
  }

  syncingScroll = true;
  highlightRef.value.scrollTop = textareaRef.value.scrollTop;
  highlightRef.value.scrollLeft = textareaRef.value.scrollLeft;
  if (dropCaret.value.visible && dragInsertIndex.value !== null) {
    updateDropCaretPosition(dragInsertIndex.value);
  }
  requestAnimationFrame(() => {
    syncingScroll = false;
  });
}

function syncScrollFromHighlight() {
  if (!textareaRef.value || !highlightRef.value) {
    return;
  }

  if (dragScrollLock) {
    restoreDragScrollLock();
    return;
  }

  if (syncingScroll) {
    return;
  }

  syncingScroll = true;
  textareaRef.value.scrollTop = highlightRef.value.scrollTop;
  textareaRef.value.scrollLeft = highlightRef.value.scrollLeft;
  if (dropCaret.value.visible && dragInsertIndex.value !== null) {
    updateDropCaretPosition(dragInsertIndex.value);
  }
  requestAnimationFrame(() => {
    syncingScroll = false;
  });
}

function escapeHtml(value: string): string {
  return value
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;");
}

function escapeAttribute(value: string): string {
  return escapeHtml(value).replaceAll('"', "&quot;");
}
</script>

<template>
  <div
    class="kol-prompt-edit-area"
    @dragenter="onDragEnter"
    @dragover="onDragOver"
    @dragleave="onDragLeave"
    @drop="onDrop"
  >
    <div v-if="!hasPromptContent || aiBusy" class="ai-assist-bar">
      <a-input
        v-model:value="aiDescription"
        class="ai-input"
        :placeholder="t('kol.manage.editor.aiPlaceholder')"
        :disabled="aiBusy"
        @press-enter="handleAiGenerate"
      >
        <template #prefix>
          <ExperimentOutlined style="color: #722ed1" />
        </template>
      </a-input>
      <a-button
        v-if="!hasPromptContent"
        type="primary"
        class="ai-btn"
        :loading="aiBusy"
        @click="handleAiGenerate"
      >
        <template #icon><ThunderboltOutlined /></template>
        {{ t('kol.manage.editor.aiGenerate') }}
      </a-button>
      <span v-if="aiBusy" class="ai-status-text">AI 正在处理中...</span>
    </div>

    <div class="editor-container" :class="{ 'editor-container--drop-active': dropCaret.visible }">
      <div
        ref="highlightRef"
        class="prompt-highlight"
        aria-hidden="true"
        v-html="highlightedContent"
        @pointerdown="handleHighlightPointerDown"
        @pointerup="handleHighlightPointerUp"
        @pointercancel="handleHighlightPointerCancel"
        @scroll="syncScrollFromHighlight"
      ></div>
      <textarea
        ref="textareaRef"
        class="prompt-textarea"
        :value="content"
        :placeholder="editorPlaceholder"
        @input="handleContentInput"
        @select="scheduleTextareaSelectionSync"
        @mouseup="scheduleTextareaSelectionSync"
        @keyup="scheduleTextareaSelectionSync"
        @scroll="syncScrollFromTextarea"
      ></textarea>
      <div
        v-if="dropCaret.visible"
        class="prompt-drop-caret"
        :style="{
          height: `${dropCaret.height}px`,
          transform: `translate3d(${dropCaret.left}px, ${dropCaret.top}px, 0)`,
        }"
      ></div>
    </div>
    <Teleport to="body">
      <button
        v-if="selectionAiAction.visible"
        type="button"
        class="prompt-selection-ai-action"
        :style="{
          transform: `translate3d(${selectionAiAction.buttonX}px, ${selectionAiAction.buttonY}px, 0) translateX(-50%)`,
        }"
        :title="t('kol.manage.editor.aiOptimize')"
        @pointerdown.prevent.stop
        @click.prevent.stop="openSelectionAiPanel"
      >
        <svg
          class="prompt-selection-ai-action__icon"
          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>
      </button>
    </Teleport>
  </div>
</template>

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

.kol-prompt-edit-area {
  flex: 1;
  display: flex;
  flex-direction: column;
  background: #fff;
  min-width: 0;
}

.ai-assist-bar {
  padding: 12px 16px;
  border-bottom: 1px solid #f0f0f0;
  display: flex;
  gap: 8px;
  background: #f9f0ff;
}

.ai-input {
  flex: 1;
}

.ai-btn {
  flex-shrink: 0;
}

.prompt-selection-ai-action {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 1320;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border: 1px solid rgba(15, 23, 42, 0.08);
  border-radius: 14px;
  background: #ffffff;
  color: #8c8c8c;
  box-shadow:
    0 10px 30px rgba(15, 23, 42, 0.18),
    0 0 0 1px rgba(15, 23, 42, 0.04);
  cursor: pointer;
  transition:
    color 0.16s ease,
    box-shadow 0.16s ease;
}

.prompt-selection-ai-action:hover {
  color: #1677ff;
  box-shadow:
    0 14px 34px rgba(22, 119, 255, 0.2),
    0 0 0 1px rgba(22, 119, 255, 0.16);
}

.prompt-selection-ai-action__icon {
  width: 22px;
  height: 22px;
}

.ai-status-text {
  display: inline-flex;
  align-items: center;
  color: #722ed1;
  font-size: 13px;
  white-space: nowrap;
}

.editor-container {
  flex: 1;
  position: relative;
  display: flex;
  overflow: hidden;
  transition: background-color 0.18s ease;
}

.editor-container--drop-active {
  background: linear-gradient(90deg, rgba(22, 119, 255, 0.035), rgba(114, 46, 209, 0.035));
}

.prompt-highlight {
  position: absolute;
  inset: 0;
  z-index: 2;
  overflow: auto;
  padding: 24px;
  white-space: pre-wrap;
  overflow-wrap: break-word;
  word-wrap: break-word;
  word-break: normal;
  font-family: inherit;
  font-size: 16px;
  line-height: 1.6;
  letter-spacing: normal;
  color: #262626;
  cursor: text;
  user-select: text;
  box-sizing: border-box;
}

.prompt-textarea {
  position: relative;
  z-index: 1;
  flex: 1;
  border: none;
  resize: none;
  padding: 24px;
  background: transparent;
  font-family: inherit;
  font-size: 16px;
  line-height: 1.6;
  overflow-wrap: break-word;
  word-wrap: break-word;
  word-break: normal;
  letter-spacing: normal;
  outline: none;
  color: transparent;
  caret-color: #262626;
  box-sizing: border-box;
}

.prompt-textarea::placeholder {
  color: transparent;
}

.prompt-drop-caret {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 5;
  width: 2px;
  min-height: 20px;
  border-radius: 999px;
  background: #1677ff;
  box-shadow:
    0 0 0 3px rgba(22, 119, 255, 0.12),
    0 0 18px rgba(22, 119, 255, 0.35);
  pointer-events: none;
  transition:
    transform 0.12s cubic-bezier(0.22, 1, 0.36, 1),
    height 0.12s ease,
    opacity 0.12s ease;
  animation: promptDropCaretBlink 0.9s ease-in-out infinite;
}

.prompt-drop-caret::before,
.prompt-drop-caret::after {
  position: absolute;
  left: 50%;
  width: 12px;
  height: 2px;
  border-radius: 999px;
  background: #1677ff;
  content: "";
  transform: translateX(-50%);
}

.prompt-drop-caret::before {
  top: -1px;
}

.prompt-drop-caret::after {
  bottom: -1px;
}

@keyframes promptDropCaretBlink {
  0%,
  100% {
    opacity: 0.45;
  }

  50% {
    opacity: 1;
  }
}

.prompt-highlight :deep(.token-highlight) {
  color: #eb2f96;
}

.prompt-highlight :deep(.token-highlight__content) {
  cursor: text;
}

.prompt-highlight :deep(.editor-placeholder) {
  color: #bfbfbf;
}

</style>
