import { useCallback, useRef, useEffect } from 'react'
import {
  Editor,
  rootCtx,
  defaultValueCtx,
  editorViewCtx,
  prosePluginsCtx
} from '@milkdown/core'
import { replaceAll as milkdownReplaceAll } from '@milkdown/utils'
import { commonmark } from '@milkdown/preset-commonmark'
import { gfm } from '@milkdown/preset-gfm'
import { history } from '@milkdown/plugin-history'
import { listener, listenerCtx } from '@milkdown/plugin-listener'
import { indent } from '@milkdown/plugin-indent'
import { trailing } from '@milkdown/plugin-trailing'
import { clipboard } from '@milkdown/plugin-clipboard'
import { Plugin, PluginKey, TextSelection } from '@milkdown/prose/state'
import { Decoration, DecorationSet } from '@milkdown/prose/view'
import type { EditorState } from '@milkdown/prose/state'

// --- Auto-pair plugin (D2) ---

const PAIRS: Record<string, string> = { '(': ')', '[': ']', '{': '}', '"': '"', "'": "'", '`': '`' }

function createAutoPairPlugin(): Plugin {
  return new Plugin({
    props: {
      handleTextInput(view, from, to, text) {
        // 选中文本时用配对符号包裹
        if (from !== to && PAIRS[text]) {
          const close = PAIRS[text]
          const { tr } = view.state
          tr.insertText(text + view.state.doc.textBetween(from, to) + close, from, to)
          view.dispatch(tr)
          return true
        }
        return false
      },
      handleKeyDown(view, event) {
        const char = event.key

        // Backspace: 删除配对符号（光标在两个配对符号中间时）
        // 必须在 PAIRS 检查之前处理，因为 'Backspace' 不在 PAIRS 映射中
        if (char === 'Backspace') {
          const { state } = view
          const { from, to } = state.selection
          if (from !== to || from < 2) return false
          const before = state.doc.textBetween(from - 1, from)
          const after = state.doc.textBetween(from, from + 1)
          if (PAIRS[before] === after) {
            const tr = state.tr.delete(from - 1, from + 1)
            view.dispatch(tr)
            return true
          }
          return false
        }

        // 处理配对符号的自动闭合
        if (!PAIRS[char]) return false

        const { state } = view
        const { from, to } = state.selection

        if (from !== to) {
          // 有选区时：包裹
          const close = PAIRS[char]
          const tr = state.tr.insertText(char + state.doc.textBetween(from, to) + close, from, to)
          view.dispatch(tr)
          return true
        }

        // 无选区：插入配对并光标置中
        const close = PAIRS[char]
        const tr = state.tr.insertText(char + close, from)
        // 光标置于中间
        tr.setSelection(
          TextSelection.create(tr.doc, from + 1)
        )
        view.dispatch(tr)
        return true
      }
    }
  })
}

// --- Search highlight plugin ---

export interface SearchMatch {
  from: number
  to: number
}

export interface SearchPluginState {
  matches: SearchMatch[]
  currentIndex: number
  query: string
}

export const searchPluginKey = new PluginKey<SearchPluginState>('search-highlight')

function createSearchPlugin(): Plugin<SearchPluginState> {
  return new Plugin<SearchPluginState>({
    key: searchPluginKey,
    state: {
      init(): SearchPluginState {
        return { matches: [], currentIndex: -1, query: '' }
      },
      apply(tr, value): SearchPluginState {
        const meta = tr.getMeta(searchPluginKey) as SearchPluginState | undefined
        if (meta) return meta
        // When the document changes, map existing match positions through the change
        // but only if there's an active search
        if (tr.docChanged && value.query) {
          // The SearchReplace component will re-search when doc changes,
          // but we map positions so decorations stay roughly correct in the interim
          return { ...value }
        }
        return value
      }
    },
    props: {
      decorations(state: EditorState) {
        const pluginState = searchPluginKey.getState(state)
        if (!pluginState || pluginState.matches.length === 0) {
          return null
        }

        const decos: Decoration[] = pluginState.matches.map((match, index) => {
          const cls =
            index === pluginState.currentIndex
              ? 'search-match-active'
              : 'search-match-highlight'
          return Decoration.inline(match.from, match.to, { class: cls })
        })

        return DecorationSet.create(state.doc, decos)
      }
    }
  })
}

// --- Hook ---

interface UseMilkdownOptions {
  content: string
  onChange: (value: string) => void
  darkMode: boolean
}

export function useMilkdown({ content, onChange, darkMode }: UseMilkdownOptions) {
  const containerRef = useRef<HTMLDivElement>(null)
  const editorRef = useRef<Editor | null>(null)
  const onChangeRef = useRef(onChange)
  const isExternalUpdate = useRef(false)
  const initialContentRef = useRef(content)
  // H-03: 请求计数器 — 每个 setContent 调用自增，仅抑制匹配的 markdownUpdated 事件
  const setContentRequestId = useRef(0)

  // Keep onChangeRef fresh
  useEffect(() => {
    onChangeRef.current = onChange
  }, [onChange])

  // Initialize editor
  useEffect(() => {
    if (!containerRef.current) return

    const editor = Editor.make()
      .config((ctx) => {
        ctx.set(rootCtx, containerRef.current!)
        ctx.set(defaultValueCtx, initialContentRef.current)

        // Inject search highlight plugin and auto-pair plugin into ProseMirror plugin list
        ctx.update(prosePluginsCtx, (plugins) => [...plugins, createAutoPairPlugin(), createSearchPlugin()])

        // Configure listener for content changes
        const lm = ctx.get(listenerCtx)
        lm.markdownUpdated((_ctx, markdown, prevMarkdown) => {
          if (markdown === prevMarkdown) return
          // H-03: 只有未被外部更新抑制时才触发 onChange。
          // 计数器匹配确保用户按键不会在 setContent 期间被丢弃。
          if (setContentRequestId.current === 0 && !isExternalUpdate.current) {
            onChangeRef.current(markdown)
          }
        })

        // Listen for blur events (used for potential future state saving)
        lm.blur(() => {
          // blur handler placeholder
        })
      })
      .use(commonmark)
      .use(gfm)
      .use(history)
      .use(listener)
      .use(indent)
      .use(trailing)
      .use(clipboard)

    let cancelled = false

    editor.create().then((created) => {
      if (cancelled) {
        // 组件已卸载，立即销毁以避免内存泄漏
        created.destroy()
        return
      }
      editorRef.current = created
    }).catch((err) => {
      // eslint-disable-next-line no-console -- editor creation failure must be reported
      console.error('Milkdown editor creation failed:', err)
    })

    return () => {
      cancelled = true
      editorRef.current?.destroy()
      editorRef.current = null
    }
  }, [])

  // Dark mode: update CSS custom properties on the container
  useEffect(() => {
    const container = containerRef.current
    if (!container) return
    if (darkMode) {
      container.classList.add('milkdown-dark')
    } else {
      container.classList.remove('milkdown-dark')
    }
  }, [darkMode])

  // External content update (tab switch)
  const setContent = useCallback((newContent: string) => {
    const editor = editorRef.current
    if (!editor) return

    // H-03: 递增请求 ID，这样只有本次 replaceAll 触发的 markdownUpdated 会被抑制
    const requestId = ++setContentRequestId.current
    isExternalUpdate.current = true
    try {
      editor.action(milkdownReplaceAll(newContent))
    } catch {
      // replaceAll may fail if editor is not fully ready
    } finally {
      // 仅当没有新的 setContent 启动时才重置标志
      if (setContentRequestId.current === requestId) {
        setContentRequestId.current = 0
        isExternalUpdate.current = false
      }
    }
  }, [])

  // Scroll position management
  const getScrollTop = useCallback((): number => {
    const container = containerRef.current
    if (!container) return 0
    const scroller = container.querySelector('.milkdown .editor') as HTMLElement | null
    return scroller?.scrollTop ?? container.scrollTop ?? 0
  }, [])

  const setScrollTop = useCallback((top: number) => {
    const container = containerRef.current
    if (!container) return
    const scroller = container.querySelector('.milkdown .editor') as HTMLElement | null
    if (scroller) {
      scroller.scrollTop = top
    } else {
      container.scrollTop = top
    }
  }, [])

  // Selection management
  const getSelection = useCallback((): { from: number; to: number } => {
    const editor = editorRef.current
    if (!editor) return { from: 0, to: 0 }
    try {
      return editor.action((ctx) => {
        const view = ctx.get(editorViewCtx)
        if (view?.state?.selection) {
          return { from: view.state.selection.from, to: view.state.selection.to }
        }
        return { from: 0, to: 0 }
      })
    } catch {
      return { from: 0, to: 0 }
    }
  }, [])

  const setSelection = useCallback(() => {
    // ProseMirror selection setting requires the view instance
    // which we access lazily; for now we focus the editor
    const container = containerRef.current
    if (!container) return
    const pmEditor = container.querySelector('.ProseMirror') as HTMLElement | null
    pmEditor?.focus()
  }, [])

  // Get ProseMirror EditorView instance
  const getView = useCallback(() => {
    const editor = editorRef.current
    if (!editor) return null
    try {
      return editor.action((ctx) => ctx.get(editorViewCtx))
    } catch {
      return null
    }
  }, [])

  // Execute a Milkdown action (for toolbar commands)
  const action = useCallback((fn: (editor: Editor) => void) => {
    const editor = editorRef.current
    if (!editor) return
    fn(editor)
  }, [])

  return {
    containerRef,
    editorRef,
    action,
    setContent,
    getScrollTop,
    setScrollTop,
    getSelection,
    setSelection,
    getView
  }
}
