import { useCallback, useEffect, useMemo, useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from 'react';
import { FileText, Music2, Video } from 'lucide-react';

export type PlaygroundMentionUploadKind = 'audio' | 'file' | 'image' | 'video';

export interface PlaygroundMentionUpload {
  id: string;
  kind: PlaygroundMentionUploadKind;
  name: string;
  url: string;
}

const resourceTokenPrefix = '<<<playground-resource:';
const resourceTokenSuffix = '>>>';
const resourceTokenPattern = /<<<playground-resource:([^>]+)>>>/g;
const resourceTokenWithSpacePattern = /<<<playground-resource:([^>]+)>>>\s?/g;

export function buildPlaygroundResourceToken(id: string) {
  return `${resourceTokenPrefix}${id}${resourceTokenSuffix}`;
}

export function removeInvalidPlaygroundResourceTokens(raw: string, uploads: PlaygroundMentionUpload[]) {
  if (!raw.includes(resourceTokenPrefix)) return raw;
  const validIds = new Set(uploads.map((item) => item.id));
  return raw.replace(resourceTokenWithSpacePattern, (full, id: string) => validIds.has(id) ? full : '');
}

export function replacePlaygroundResourceTokens(
  raw: string,
  uploads: PlaygroundMentionUpload[],
  mode: 'image' | 'video',
) {
  if (!raw.includes(resourceTokenPrefix)) return raw;
  const byId = new Map(uploads.map((item) => [item.id, item]));
  return raw.replace(resourceTokenPattern, (full, id: string) => {
    const item = byId.get(id);
    if (!item) return '';
    return resourcePromptLabel(item, uploads, mode);
  });
}

export function replacePlaygroundResourceTokensForDisplay(raw: string, uploads: PlaygroundMentionUpload[]) {
  if (!raw.includes(resourceTokenPrefix)) return raw;
  const byId = new Map(uploads.map((item) => [item.id, item]));
  const usedIds = new Set<string>();
  let fallbackIndex = 0;
  return raw.replace(resourceTokenPattern, (full, id: string) => {
    let item = byId.get(id);
    if (item) {
      usedIds.add(item.id);
    } else {
      while (fallbackIndex < uploads.length && usedIds.has(uploads[fallbackIndex]!.id)) {
        fallbackIndex += 1;
      }
      item = uploads[fallbackIndex];
      fallbackIndex += 1;
      if (item) usedIds.add(item.id);
    }
    if (!item) return '@资产';
    return mentionDisplayLabel(item, uploads);
  });
}

export function PlaygroundPromptMentionInput(props: {
  disabled?: boolean;
  placeholder: string;
  uploads: PlaygroundMentionUpload[];
  value: string;
  onChange: (value: string) => void;
}) {
  const editableRef = useRef<HTMLDivElement>(null);
  const dropdownRef = useRef<HTMLDivElement>(null);
  const blurTimerRef = useRef<number | undefined>(undefined);
  const isComposingRef = useRef(false);
  const [text, setText] = useState(props.value);
  const [focused, setFocused] = useState(false);
  const [hasEditableContent, setHasEditableContent] = useState(() => promptTextHasContent(props.value));
  const [mentionOpen, setMentionOpen] = useState(false);
  const [mentionAtIndex, setMentionAtIndex] = useState(-1);
  const [mentionSearch, setMentionSearch] = useState('');
  const [highlightIndex, setHighlightIndex] = useState(0);
  const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0, placement: 'bottom' as 'bottom' | 'top' });
  const showPlaceholder = !hasEditableContent;

  const uploadSignature = useMemo(
    () => props.uploads.map((item) => `${item.id}:${item.kind}:${item.name}:${item.url}`).join('|'),
    [props.uploads],
  );

  const mentionItems = useMemo(() => props.uploads.map((item, index) => ({
    item,
    label: mentionDisplayLabel(item, props.uploads),
    searchText: `${item.name} ${mentionDisplayLabel(item, props.uploads)} ${uploadKindChinese(item.kind)} 资产 素材 resource asset ${index + 1}`.toLowerCase(),
    token: buildPlaygroundResourceToken(item.id),
  })), [props.uploads]);

  const filteredMentionItems = useMemo(() => {
    const keyword = mentionSearch.trim().toLowerCase();
    if (!keyword) return mentionItems;
    return mentionItems.filter((item) => item.searchText.includes(keyword));
  }, [mentionItems, mentionSearch]);

  useEffect(() => {
    if (props.value === text) {
      setHasEditableContent(promptTextHasContent(props.value));
      return;
    }
    setText(props.value);
    setHasEditableContent(promptTextHasContent(props.value));
    if (!isComposingRef.current) {
      requestAnimationFrame(() => renderToEditable(props.value));
    }
  }, [focused, props.value, text]);

  useEffect(() => {
    const cleaned = removeInvalidPlaygroundResourceTokens(props.value, props.uploads);
    if (cleaned !== props.value) {
      setText(cleaned);
      setHasEditableContent(promptTextHasContent(cleaned));
      props.onChange(cleaned);
      requestAnimationFrame(() => renderToEditable(cleaned));
      return;
    }
    if (props.value !== text) {
      setText(props.value);
      setHasEditableContent(promptTextHasContent(props.value));
    }
    if (!focused) {
      requestAnimationFrame(() => renderToEditable(props.value));
    }
  }, [uploadSignature]);

  useEffect(() => {
    requestAnimationFrame(() => renderToEditable(text));
    return () => {
      if (blurTimerRef.current) window.clearTimeout(blurTimerRef.current);
    };
  }, []);

  const updateMentionDropdownPosition = useCallback(() => {
    const editable = editableRef.current;
    const root = editable?.parentElement;
    if (!editable || !root) return;
    const rootRect = root.getBoundingClientRect();
    const caretRect = getCaretClientRect(editable) ?? editable.getBoundingClientRect();
    const dropdown = dropdownRef.current;
    const viewportPadding = 8;
    const gap = 6;
    const dropdownWidth = Math.min(dropdown?.offsetWidth ?? 320, Math.max(180, window.innerWidth - viewportPadding * 2));
    const dropdownHeight = Math.min(dropdown?.offsetHeight ?? 220, Math.max(120, window.innerHeight - viewportPadding * 2));
    const minLeft = viewportPadding - rootRect.left;
    const maxLeft = window.innerWidth - viewportPadding - dropdownWidth - rootRect.left;
    const left = clamp(caretRect.left - rootRect.left, minLeft, Math.max(minLeft, maxLeft));
    const belowTop = caretRect.bottom + gap;
    const aboveTop = caretRect.top - dropdownHeight - gap;
    const shouldOpenAbove = belowTop + dropdownHeight > window.innerHeight - viewportPadding && aboveTop >= viewportPadding;
    const viewportTop = shouldOpenAbove
      ? Math.max(viewportPadding, aboveTop)
      : Math.min(belowTop, window.innerHeight - viewportPadding - dropdownHeight);
    setDropdownPosition({
      top: viewportTop - rootRect.top,
      left,
      placement: shouldOpenAbove ? 'top' : 'bottom',
    });
  }, []);

  useEffect(() => {
    if (!mentionOpen) return;
    const frame = window.requestAnimationFrame(updateMentionDropdownPosition);
    const handleViewportChange = () => updateMentionDropdownPosition();
    window.addEventListener('resize', handleViewportChange);
    window.addEventListener('scroll', handleViewportChange, true);
    return () => {
      window.cancelAnimationFrame(frame);
      window.removeEventListener('resize', handleViewportChange);
      window.removeEventListener('scroll', handleViewportChange, true);
    };
  }, [filteredMentionItems.length, mentionOpen, mentionSearch, updateMentionDropdownPosition]);

  function renderToEditable(nextText = text, caret?: number) {
    const editable = editableRef.current;
    if (!editable) return;
    editable.innerHTML = textToHtml(nextText, props.uploads);
    setHasEditableContent(promptTextHasContent(nextText));
    if (typeof caret === 'number') {
      editable.focus();
      setCaretOffset(editable, caret);
    }
  }

  function commitFromEditable(shouldInspectMention: boolean, event?: InputEvent) {
    const editable = editableRef.current;
    if (!editable || props.disabled) return;
    const nextText = serializeEditableToPlainText(editable);
    setHasEditableContent(promptTextHasContent(nextText));
    if (isComposingRef.current || event?.isComposing) return;
    setText(nextText);
    props.onChange(nextText);
    if (!shouldInspectMention) return;
    inspectMentionTrigger(nextText, getCaretOffset(editable), event);
  }

  function inspectMentionTrigger(nextText: string, caret: number, event?: InputEvent) {
    const beforeCaret = nextText.slice(0, caret);
    const atIndex = Math.max(beforeCaret.lastIndexOf('@'), beforeCaret.lastIndexOf('＠'));
    if (atIndex < 0) {
      closeMention();
      return;
    }
    const search = beforeCaret.slice(atIndex + 1);
    if (/\s/.test(search)) {
      closeMention();
      return;
    }
    const inputData = event?.data ?? '';
    if (!mentionOpen && search.length > 0 && inputData !== '@' && inputData !== '＠') {
      closeMention();
      return;
    }
    setMentionAtIndex(atIndex);
    setMentionSearch(search);
    setMentionOpen(true);
    setHighlightIndex(0);
    requestAnimationFrame(updateMentionDropdownPosition);
  }

  function closeMention() {
    setMentionOpen(false);
    setMentionSearch('');
    setMentionAtIndex(-1);
    setHighlightIndex(0);
  }

  function applyMention(token: string) {
    const editable = editableRef.current;
    if (!editable || mentionAtIndex < 0) return;
    const currentText = serializeEditableToPlainText(editable);
    const replaceEnd = Math.max(mentionAtIndex + 1, mentionAtIndex + 1 + mentionSearch.length);
    const before = currentText.slice(0, mentionAtIndex);
    const after = currentText.slice(replaceEnd);
    const nextText = `${before}${token} ${after}`;
    const nextCaret = before.length + token.length + 1;
    setText(nextText);
    setHasEditableContent(promptTextHasContent(nextText));
    props.onChange(nextText);
    closeMention();
    requestAnimationFrame(() => renderToEditable(nextText, nextCaret));
  }

  function handleKeyDown(event: ReactKeyboardEvent<HTMLDivElement>) {
    if (props.disabled) {
      event.preventDefault();
      return;
    }
    if (!mentionOpen || !filteredMentionItems.length) return;
    if (event.key === 'ArrowDown') {
      event.preventDefault();
      setHighlightIndex((current) => (current + 1) % filteredMentionItems.length);
      return;
    }
    if (event.key === 'ArrowUp') {
      event.preventDefault();
      setHighlightIndex((current) => (current - 1 + filteredMentionItems.length) % filteredMentionItems.length);
      return;
    }
    if (event.key === 'Enter') {
      event.preventDefault();
      const item = filteredMentionItems[Math.min(highlightIndex, filteredMentionItems.length - 1)];
      if (item) applyMention(item.token);
      return;
    }
    if (event.key === 'Escape') {
      event.preventDefault();
      closeMention();
    }
  }

  return (
    <div className="promptMentionInput">
      <div
        ref={editableRef}
        className="promptMentionEditable"
        contentEditable={!props.disabled}
        role="textbox"
        aria-label={props.placeholder}
        aria-multiline="true"
        suppressContentEditableWarning
        onBlur={() => {
          blurTimerRef.current = window.setTimeout(() => {
            setFocused(false);
            closeMention();
            commitFromEditable(false);
          }, 120);
        }}
        onCompositionEnd={() => {
          isComposingRef.current = false;
          setHasEditableContent(editablePromptTextHasContent(editableRef.current));
          requestAnimationFrame(() => commitFromEditable(true));
        }}
        onCompositionStart={() => {
          isComposingRef.current = true;
        }}
        onFocus={() => {
          setFocused(true);
          if (blurTimerRef.current) window.clearTimeout(blurTimerRef.current);
        }}
        onInput={(event) => commitFromEditable(true, event.nativeEvent instanceof InputEvent ? event.nativeEvent : undefined)}
        onKeyDown={handleKeyDown}
        onPaste={(event) => {
          if (props.disabled) return;
          event.preventDefault();
          const plainText = event.clipboardData.getData('text/plain');
          document.execCommand('insertText', false, plainText);
        }}
      />
      {showPlaceholder && <div className="promptMentionPlaceholder" aria-hidden="true">{props.placeholder}</div>}
      {mentionOpen && (
        <div
          ref={dropdownRef}
          className="promptMentionDropdown"
          data-placement={dropdownPosition.placement}
          style={{ left: dropdownPosition.left, top: dropdownPosition.top }}
          onMouseDown={(event) => event.preventDefault()}
        >
          {filteredMentionItems.length ? (
            filteredMentionItems.map((candidate, index) => (
              <button
                type="button"
                className="promptMentionItem"
                data-active={index === highlightIndex}
                key={candidate.item.id}
                onMouseEnter={() => setHighlightIndex(index)}
                onMouseDown={(event) => {
                  event.preventDefault();
                  applyMention(candidate.token);
                }}
              >
                <MentionThumb item={candidate.item} />
                <span>
                  <strong>{candidate.label}</strong>
                  <small>{candidate.item.name}</small>
                </span>
                <em>{uploadKindChinese(candidate.item.kind)}</em>
              </button>
            ))
          ) : (
            <div className="promptMentionEmpty">暂无可引用资源</div>
          )}
        </div>
      )}
    </div>
  );
}

function promptTextHasContent(raw: string) {
  return raw.trim().length > 0;
}

function editablePromptTextHasContent(editable: HTMLElement | null) {
  return editable ? promptTextHasContent(serializeEditableToPlainText(editable)) : false;
}

function MentionThumb(props: { item: PlaygroundMentionUpload }) {
  if (props.item.kind === 'image') {
    return <img className="promptMentionThumb" src={props.item.url} alt="" draggable={false} />;
  }
  if (props.item.kind === 'video') {
    return <video className="promptMentionThumb" src={props.item.url} muted playsInline preload="metadata" />;
  }
  if (props.item.kind === 'audio') {
    return <Music2 className="promptMentionThumbIcon" size={16} />;
  }
  return <FileText className="promptMentionThumbIcon" size={16} />;
}

function textToHtml(raw: string, uploads: PlaygroundMentionUpload[]) {
  const uploadById = new Map(uploads.map((item) => [item.id, item]));
  const parts: string[] = [];
  let lastIndex = 0;
  let match: RegExpExecArray | null;
  const re = new RegExp(resourceTokenPattern);
  while ((match = re.exec(raw)) !== null) {
    if (match.index > lastIndex) {
      parts.push(escapeHtml(raw.slice(lastIndex, match.index)));
    }
    const token = match[0] ?? '';
    const id = match[1] ?? '';
    const item = uploadById.get(id);
    parts.push(item ? mentionChipHtml(token, item, uploads) : '');
    lastIndex = match.index + token.length;
  }
  if (lastIndex < raw.length) {
    parts.push(escapeHtml(raw.slice(lastIndex)));
  }
  return parts.join('').replace(/\n/g, '<br>');
}

function mentionChipHtml(token: string, item: PlaygroundMentionUpload, uploads: PlaygroundMentionUpload[]) {
  const label = mentionDisplayLabel(item, uploads);
  const thumb = item.kind === 'image'
    ? `<img class="promptMentionChipThumb" src="${escapeAttr(item.url)}" alt="" draggable="false">`
    : item.kind === 'video'
      ? `<video class="promptMentionChipThumb" src="${escapeAttr(item.url)}" muted preload="metadata" playsinline></video>`
      : `<span class="promptMentionChipThumb promptMentionChipThumbPlaceholder">${escapeHtml(uploadKindShort(item.kind))}</span>`;
  return `<span contenteditable="false" class="promptMentionChip" data-token="${escapeAttr(token)}">${thumb}<span>${escapeHtml(label)}</span></span>`;
}

function getCaretClientRect(editable: HTMLElement) {
  const selection = document.getSelection();
  if (!selection || selection.rangeCount === 0) return null;
  const originalRange = selection.getRangeAt(0);
  if (!editable.contains(originalRange.startContainer)) return null;
  const collapsedRange = originalRange.cloneRange();
  collapsedRange.collapse(true);
  let rect = collapsedRange.getBoundingClientRect();
  if (rect.width || rect.height) return rect;
  const marker = document.createElement('span');
  marker.textContent = '\u200b';
  const restoreRange = originalRange.cloneRange();
  collapsedRange.insertNode(marker);
  rect = marker.getBoundingClientRect();
  marker.remove();
  selection.removeAllRanges();
  selection.addRange(restoreRange);
  return rect.width || rect.height ? rect : null;
}

function serializeEditableToPlainText(editable: HTMLElement) {
  const parts: string[] = [];
  const walk = (node: Node) => {
    if (node.nodeType === Node.TEXT_NODE) {
      parts.push(node.textContent ?? '');
      return;
    }
    if (node.nodeType !== Node.ELEMENT_NODE) return;
    const element = node as HTMLElement;
    const token = element.getAttribute('data-token');
    if (token) {
      parts.push(token);
      return;
    }
    if (element.tagName === 'BR') {
      parts.push('\n');
      return;
    }
    if (element.tagName === 'DIV' && parts.length && !parts[parts.length - 1]?.endsWith('\n')) {
      parts.push('\n');
    }
    element.childNodes.forEach(walk);
  };
  editable.childNodes.forEach(walk);
  return parts.join('');
}

function getCaretOffset(editable: HTMLElement) {
  const selection = document.getSelection();
  if (!selection || selection.rangeCount === 0) return 0;
  const range = selection.getRangeAt(0);
  let offset = 0;
  const walk = (node: Node): boolean => {
    if (node.nodeType === Node.TEXT_NODE) {
      const textLength = (node.textContent ?? '').length;
      if (node === range.startContainer) {
        offset += range.startOffset;
        return true;
      }
      offset += textLength;
      return false;
    }
    if (node.nodeType !== Node.ELEMENT_NODE) return false;
    const element = node as HTMLElement;
    const token = element.getAttribute('data-token');
    if (token) {
      if (node === range.startContainer || element.contains(range.startContainer)) {
        offset += range.startOffset > 0 ? token.length : 0;
        return true;
      }
      offset += token.length;
      return false;
    }
    if (element.tagName === 'BR') {
      offset += 1;
      return false;
    }
    for (const child of Array.from(element.childNodes)) {
      if (walk(child)) return true;
    }
    return false;
  };
  for (const child of Array.from(editable.childNodes)) {
    if (walk(child)) break;
  }
  return offset;
}

function setCaretOffset(editable: HTMLElement, targetOffset: number) {
  const selection = document.getSelection();
  if (!selection) return;
  let offset = 0;
  let targetNode: Node | null = null;
  let targetNodeOffset = 0;
  const walk = (node: Node): boolean => {
    if (node.nodeType === Node.TEXT_NODE) {
      const textLength = (node.textContent ?? '').length;
      if (offset + textLength >= targetOffset) {
        targetNode = node;
        targetNodeOffset = Math.max(0, targetOffset - offset);
        return true;
      }
      offset += textLength;
      return false;
    }
    if (node.nodeType !== Node.ELEMENT_NODE) return false;
    const element = node as HTMLElement;
    const token = element.getAttribute('data-token');
    if (token) {
      if (offset + token.length >= targetOffset) {
        targetNode = node;
        targetNodeOffset = targetOffset <= offset ? 0 : 1;
        return true;
      }
      offset += token.length;
      return false;
    }
    if (element.tagName === 'BR') {
      if (offset + 1 >= targetOffset) {
        targetNode = node;
        targetNodeOffset = 0;
        return true;
      }
      offset += 1;
      return false;
    }
    for (const child of Array.from(element.childNodes)) {
      if (walk(child)) return true;
    }
    return false;
  };
  for (const child of Array.from(editable.childNodes)) {
    if (walk(child)) break;
  }
  if (!targetNode) {
    targetNode = editable;
    targetNodeOffset = editable.childNodes.length;
  }
  const range = document.createRange();
  range.setStart(targetNode, targetNode.nodeType === Node.TEXT_NODE ? targetNodeOffset : Math.min(targetNodeOffset, targetNode.childNodes.length));
  range.collapse(true);
  selection.removeAllRanges();
  selection.addRange(range);
}

function mentionDisplayLabel(item: PlaygroundMentionUpload, uploads: PlaygroundMentionUpload[]) {
  return `@${uploadKindChinese(item.kind)} ${uploadKindIndex(item, uploads)}`;
}

function resourcePromptLabel(item: PlaygroundMentionUpload, uploads: PlaygroundMentionUpload[], mode: 'image' | 'video') {
  const kind = mode === 'image' ? 'image' : uploadKindEnglish(item.kind);
  return `${kind} ${uploadKindIndex(item, uploads)}`;
}

function uploadKindIndex(item: PlaygroundMentionUpload, uploads: PlaygroundMentionUpload[]) {
  const sameKind = uploads.filter((upload) => upload.kind === item.kind);
  return Math.max(1, sameKind.findIndex((upload) => upload.id === item.id) + 1);
}

function uploadKindChinese(kind: PlaygroundMentionUploadKind) {
  if (kind === 'image') return '图像';
  if (kind === 'video') return '视频';
  if (kind === 'audio') return '音频';
  return '文件';
}

function uploadKindEnglish(kind: PlaygroundMentionUploadKind) {
  if (kind === 'image') return 'image';
  if (kind === 'video') return 'video';
  if (kind === 'audio') return 'audio';
  return 'file';
}

function uploadKindShort(kind: PlaygroundMentionUploadKind) {
  if (kind === 'image') return '图';
  if (kind === 'video') return '视';
  if (kind === 'audio') return '音';
  return '文';
}

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

function escapeHtml(value: string) {
  return value
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;');
}

function escapeAttr(value: string) {
  return escapeHtml(value).replace(/"/g, '&quot;');
}
