import { Plugin, PluginKey, Selection } from '@milkdown/prose/state'
import { $prose, $ctx, $markSchema } from '@milkdown/kit/utils'
import { parserCtx, serializerCtx } from '@milkdown/kit/core'
import { Node as ProseNode, Schema, Slice } from '@milkdown/prose/model'
import type { Ctx, MarkType } from '@milkdown/kit/core'
import type { Transaction } from '@milkdown/prose/model'
import { Decoration, DecorationSet, type EditorView } from '@milkdown/prose/view'
import { extractDocBlockContextFromMarkdown } from '../utils/docBlock.js'
import { extractWebSearchContextFromMarkdown, WEB_SEARCH_NODE_TYPE } from '../utils/webSearch.js'
import { getOcrCache, OCR_SIZE_LIMIT, extractTextFromOCR, buildOcrContextForDoc } from '../utils/ocrCache'
import { isDocumentVisible } from '../composables/useVisibility.js'
import type { CopilotState, CopilotRuntime, GhostMarkType } from './copilotTypes.js'

const COPILOT_PLUGIN_KEY = new PluginKey('milkdown-copilot')
const DEBOUNCE_MS = 1000
const SIZE_LIMIT = OCR_SIZE_LIMIT
const DOC_SIZE_LIMIT = 32 * 1024  // 文档块32KB限制
const FALLBACK_BLOCK_SEPARATOR = '\n\n'
const FALLBACK_LEAF_TEXT = '\n'

interface CopilotState {
  from: number
  to: number
  suggestion: string
}

interface CopilotConfig {
  fetchSuggestion: (prefix: string, suffix: string, languageId: string, signal?: AbortSignal) => Promise<string>
  debounceMs?: number
}

interface CopilotRuntime {
  enabled: boolean
  debounceTimer: ReturnType<typeof setTimeout> | null
  abortController: AbortController | null
  ctx: Ctx
  requestSeq: number
  docVersion: number
}

const initialState: CopilotState = {
  from: 0,
  to: 0,
  suggestion: ''
}

const runtimeByView = new WeakMap<EditorView, CopilotRuntime>()

export const copilotConfigCtx = $ctx<CopilotConfig, 'copilotConfig'>({
  fetchSuggestion: async () => '',
  debounceMs: DEBOUNCE_MS
}, 'copilotConfig')

export const copilotGhostMark = $markSchema('copilot_ghost', () => ({
  excludes: '',
  inclusive: true,
  parseDOM: [{ tag: 'span[data-copilot-ghost]' }],
  toDOM: () => ['span', { 'data-copilot-ghost': '', class: 'copilot-ghost-text' }, 0],
  parseMarkdown: {
    match: () => false,
    runner: () => { }
  },
  toMarkdown: {
    match: (mark) => mark.type.name === 'copilot_ghost',
    runner: () => { }
  }
}))

function clearRuntimeRequests(runtime: CopilotRuntime, invalidateRequest = true, abortReason = 'abort') {
  if (runtime.debounceTimer) {
    clearTimeout(runtime.debounceTimer)
    runtime.debounceTimer = null
  }

  if (runtime.abortController) {
    runtime.abortController.abort(abortReason)
    runtime.abortController = null
  }

  if (invalidateRequest) {
    runtime.requestSeq += 1
  }
}

function getGhostState(view: EditorView): CopilotState | null {
  const state = COPILOT_PLUGIN_KEY.getState(view.state) as CopilotState | undefined
  if (!state || !state.suggestion || state.from >= state.to) return null
  return state
}

function getGhostRange(view: EditorView): { from: number; to: number } | null {
  const state = getGhostState(view)
  if (!state) return null
  return { from: state.from, to: state.to }
}

function hasGhostText(view: EditorView): boolean {
  return getGhostRange(view) !== null
}

function clearGhostText(view: EditorView): boolean {
  const range = getGhostRange(view)
  if (!range) return false

  const maxPos = view.state.doc.content.size
  const from = Math.max(0, Math.min(range.from, maxPos))
  const to = Math.max(from, Math.min(range.to, maxPos))
  if (to <= from) return false

  const tr = view.state.tr
    .delete(from, to)
    .setMeta(COPILOT_PLUGIN_KEY, { ...initialState })
  view.dispatch(tr)
  return true
}

function getCursorBeforeGhostInsert(tr: Transaction, from: number): number {
  const mapped = tr.mapping.map(from, -1)
  return Math.max(0, Math.min(mapped, tr.doc.content.size))
}

function insertParsedMarkdownSlice(
  tr: Transaction,
  from: number,
  parsedDoc: ProseNode
): { from: number; to: number } | null {
  if (parsedDoc.content.size <= 0) return null

  const insertPos = tr.mapping.map(from, -1)
  const parsedSlice = Slice.maxOpen(parsedDoc.content)
  if (!parsedSlice || parsedSlice.size <= 0) return null

  tr.replaceRange(insertPos, insertPos, parsedSlice)
  const startPos = tr.mapping.map(insertPos, -1)
  const endPos = tr.mapping.map(insertPos, 1)
  if (endPos <= startPos) return null
  return { from: startPos, to: endPos }
}

function createGhostDecorations(doc: ProseNode, from: number, to: number) {
  if (to <= from) return DecorationSet.empty

  const decorations = [
    Decoration.inline(from, to, { class: 'copilot-ghost-text', 'data-copilot-ghost': '' })
  ]

  doc.nodesBetween(from, to, (node, pos) => {
    if (!node.isBlock) return true
    const nodeFrom = pos
    const nodeTo = pos + node.nodeSize
    if (nodeFrom >= from && nodeTo <= to) {
      decorations.push(Decoration.node(nodeFrom, nodeTo, { class: 'copilot-ghost-block' }))
    }
    return true
  })

  return DecorationSet.create(doc, decorations)
}

function addGhostMarksToTextNodes(tr: Transaction, from: number, to: number, markType: MarkType) {
  tr.doc.nodesBetween(from, to, (node, pos) => {
    if (!node.isText || node.nodeSize <= 0) return true

    const start = Math.max(pos, from)
    const end = Math.min(pos + node.nodeSize, to)
    if (end <= start) return true

    tr.addMark(start, end, markType.create())
    return true
  })
}

function normalizeSuggestionText(raw: string): string {
  if (!raw) return raw

  let text = raw.replace(/\r\n?/g, '\n')
  const trimmed = text.trim()

  if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
    try {
      const parsed = JSON.parse(trimmed)
      if (typeof parsed === 'string') {
        text = parsed.replace(/\r\n?/g, '\n')
      }
    } catch {
      // Not valid JSON, keep original
    }
  }

  return text
}

function sanitizeLanguageId(value: string): string {
  if (!value) return ''
  const trimmed = value.trim()
  if (!trimmed) return ''
  let safe = ''
  for (const ch of trimmed) {
    if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch === '-' || ch === '_' || ch === '+' || ch === '.') {
      safe += ch
    }
  }
  return safe.slice(0, 32).toLowerCase()
}

function getCodeBlockLanguage(node: ProseNode): string {
  const candidates = [node.attrs?.language, node.attrs?.lang, node.attrs?.info]
  for (const value of candidates) {
    if (typeof value === 'string' && value.trim()) {
      return value.trim()
    }
  }
  return ''
}

function getCursorLanguageId(view: EditorView): string {
  const fallback = 'markdown'
  const { $from } = view.state.selection

  for (let depth = $from.depth; depth > 0; depth -= 1) {
    const node = $from.node(depth)
    const typeName = node.type?.name || ''

    if (typeName === 'math_inline' || typeName === 'math_block' || typeName === 'latex' || typeName === 'math') {
      return 'latex'
    }

    if (typeName === 'code_block' || typeName === 'codeBlock' || typeName === 'code_fence' || typeName === 'fence') {
      const lang = sanitizeLanguageId(getCodeBlockLanguage(node))
      if (!lang) return fallback
      if (lang === 'tex' || lang === 'latex' || lang === 'katex') return 'latex'
      return lang
    }
  }

  return fallback
}

async function insertGhostText(view: EditorView, suggestion: string, from: number, ctx: Ctx) {
  if (!suggestion) return

  const markType = view.state.schema.marks.copilot_ghost

  if (!markType) {
    return
  }

  try {
    const parser = ctx.get(parserCtx)
    const parsedDoc = await parser(suggestion)

    if (!parsedDoc || parsedDoc.content.size <= 0) {
      insertPlainText(view, suggestion, from, markType)
      return
    }

    const tr = view.state.tr
    const insertedRange = insertParsedMarkdownSlice(tr, from, parsedDoc)

    if (!insertedRange) {
      insertPlainText(view, suggestion, from, markType)
      return
    }

    addGhostMarksToTextNodes(tr, insertedRange.from, insertedRange.to, markType)
    tr.setSelection(Selection.near(tr.doc.resolve(getCursorBeforeGhostInsert(tr, from)), -1))
    tr.setMeta(COPILOT_PLUGIN_KEY, { from: insertedRange.from, to: insertedRange.to, suggestion })
    view.dispatch(tr)
  } catch {
    insertPlainText(view, suggestion, from, markType)
  }
}

function insertPlainText(view: EditorView, suggestion: string, from: number, markType: MarkType) {
  const tr = view.state.tr
  tr.insertText(suggestion, from)
  const endPos = from + suggestion.length
  tr.addMark(from, endPos, markType.create())
  tr.setSelection(Selection.near(tr.doc.resolve(getCursorBeforeGhostInsert(tr, from)), -1))
  tr.setMeta(COPILOT_PLUGIN_KEY, { from, to: endPos, suggestion })
  view.dispatch(tr)
}

function serializeRangeToMarkdown(
  doc: ProseNode,
  from: number,
  to: number,
  schema: Schema,
  serializer: (content: ProseNode) => string
): string {
  if (from >= to) return ''
  const slice = doc.slice(from, to)
  if (slice.content.size <= 0) return ''
  const sliceDoc = schema.topNodeType.createAndFill(undefined, slice.content)
  return sliceDoc ? serializer(sliceDoc) : doc.textBetween(from, to, FALLBACK_BLOCK_SEPARATOR, FALLBACK_LEAF_TEXT)
}

function doFetchSuggestion(
  view: EditorView,
  runtime: CopilotRuntime,
  pos: number,
  prefix: string,
  suffix: string,
  requestSeq: number,
  requestDocVersion: number
) {
  const config = runtime.ctx.get(copilotConfigCtx.key)

  // Skip AI completion when tab is hidden (energy saving)
  if (!isDocumentVisible()) return

  if (runtime.abortController) {
    runtime.abortController.abort('superseded')
    runtime.abortController = null
  }

  const controller = new AbortController()
  runtime.abortController = controller

  const languageId = getCursorLanguageId(view)
  config.fetchSuggestion(prefix, suffix, languageId, controller.signal)
    .then((suggestion) => {
      if (!runtime.enabled) return
      if (runtime.requestSeq !== requestSeq) return
      if (runtime.docVersion !== requestDocVersion) return
      if (view.state.selection.from !== pos || view.state.selection.to !== pos) return

      const normalizedSuggestion = normalizeSuggestionText(suggestion)
      if (normalizedSuggestion) {
        insertGhostText(view, normalizedSuggestion, pos, runtime.ctx)
      }
    })
    .catch((e: any) => {
      if (e?.name !== 'AbortError') {
        // Log error silently
      }
    })
    .finally(() => {
      if (runtime.abortController === controller) {
        runtime.abortController = null
      }
    })
}

function scheduleFetch(view: EditorView, runtime: CopilotRuntime, pos: number) {
  if (!runtime.enabled) return

  const doc = view.state.doc
  const schema = view.state.schema

  const serializer = runtime.ctx.get(serializerCtx)
  let prefixMarkdown = ''
  let suffixMarkdown = ''

  try {
    prefixMarkdown = serializeRangeToMarkdown(doc, 0, pos, schema, serializer)
    if (!prefixMarkdown) {
      prefixMarkdown = doc.textBetween(0, pos, FALLBACK_BLOCK_SEPARATOR, FALLBACK_LEAF_TEXT)
    }

    suffixMarkdown = serializeRangeToMarkdown(doc, pos, doc.content.size, schema, serializer)
    if (!suffixMarkdown) {
      suffixMarkdown = doc.textBetween(pos, doc.content.size, FALLBACK_BLOCK_SEPARATOR, FALLBACK_LEAF_TEXT)
    }
  } catch {
    prefixMarkdown = doc.textBetween(0, pos, FALLBACK_BLOCK_SEPARATOR, FALLBACK_LEAF_TEXT)
    suffixMarkdown = doc.textBetween(pos, doc.content.size, FALLBACK_BLOCK_SEPARATOR, FALLBACK_LEAF_TEXT)
  }

  // 构建上下文：OCR内容 + 上传文档内容
  const ocrContext = buildOcrContextForDoc(doc, 100)
  
  // 从markdown中提取文档块内容用于AI补全上下文
  const docContext = extractDocBlockContextFromMarkdown(prefixMarkdown + suffixMarkdown, 500)
  const webSearchContext = extractWebSearchContextFromMarkdown(prefixMarkdown + suffixMarkdown, 500)
  
  // 组合所有上下文到prefix前面
  const fullPrefixWithContext = [ocrContext, docContext, webSearchContext, prefixMarkdown].filter(Boolean).join('\n\n')
  
  const totalTextLen = (prefixMarkdown + suffixMarkdown).length
  const contextLen = fullPrefixWithContext.length - prefixMarkdown.length
  const totalWithContext = totalTextLen + contextLen

  // 使用32KB限制（文档上下文）
  const overLimit = totalWithContext > DOC_SIZE_LIMIT

  if (overLimit) {
    setCopilotEnabled(view, false)
    return
  }

  if (runtime.debounceTimer) {
    clearTimeout(runtime.debounceTimer)
    runtime.debounceTimer = null
  }

  const debounceMs = runtime.ctx.get(copilotConfigCtx.key).debounceMs ?? DEBOUNCE_MS
  const requestSeq = runtime.requestSeq + 1
  runtime.requestSeq = requestSeq
  const requestDocVersion = runtime.docVersion

  // 使用包含文档上下文的prefix
  runtime.debounceTimer = setTimeout(() => {
    runtime.debounceTimer = null
    doFetchSuggestion(view, runtime, pos, fullPrefixWithContext, suffixMarkdown, requestSeq, requestDocVersion)
  }, debounceMs)
}

function acceptSuggestion(view: EditorView) {
  const range = getGhostRange(view)
  if (!range) return false

  const tr = view.state.tr
  const doc = tr.doc
  const maxPos = doc.content.size
  const from = Math.max(0, Math.min(range.from, maxPos))
  const to = Math.max(from, Math.min(range.to, maxPos))
  if (to <= from) return false
  const markType = view.state.schema.marks.copilot_ghost
  if (!markType) return false

  doc.nodesBetween(from, to, (node, pos) => {
    if (node.marks.some((m: any) => m.type === markType)) {
      tr.removeMark(pos, pos + node.nodeSize, markType)
    }
  })

  const endPos = Math.min(to, tr.doc.content.size)
  tr.setSelection(Selection.near(tr.doc.resolve(endPos)))
  tr.setMeta(COPILOT_PLUGIN_KEY, { ...initialState })
  view.dispatch(tr)
  return true
}

function rejectSuggestion(view: EditorView) {
  if (!hasGhostText(view)) return false
  return clearGhostText(view)
}

export function clearGhostSuggestion(view: EditorView): boolean {
  return clearGhostText(view)
}

export const copilotPlugin = $prose((ctx) => new Plugin<CopilotState>({
  key: COPILOT_PLUGIN_KEY,
  state: {
    init: () => ({ ...initialState }),
    apply: (tr, value) => {
      const meta = tr.getMeta(COPILOT_PLUGIN_KEY)
      if (meta !== undefined) {
        return meta
      }

      if (tr.docChanged && value.suggestion && value.from < value.to) {
        const fromResult = tr.mapping.mapResult(value.from, -1)
        const toResult = tr.mapping.mapResult(value.to, 1)
        const mappedFrom = Math.max(0, fromResult.pos)
        const mappedTo = Math.max(mappedFrom, toResult.pos)
        if (mappedTo <= mappedFrom) {
          return { ...initialState }
        }
        return { ...value, from: mappedFrom, to: mappedTo }
      }

      return value
    }
  },
  props: {
    decorations: (state) => {
      const ghost = COPILOT_PLUGIN_KEY.getState(state) as CopilotState | undefined
      if (!ghost || !ghost.suggestion || ghost.from >= ghost.to) return null

      const maxPos = state.doc.content.size
      const from = Math.max(0, Math.min(ghost.from, maxPos))
      const to = Math.max(from, Math.min(ghost.to, maxPos))
      if (to <= from) return null
      return createGhostDecorations(state.doc, from, to)
    },
    handleKeyDown: (view, event) => {
      const hasGhost = hasGhostText(view)

      if (event.key === 'Tab' && hasGhost) {
        event.preventDefault()
        return acceptSuggestion(view)
      }

      if (event.key === 'Escape' && hasGhost) {
        event.preventDefault()
        return rejectSuggestion(view)
      }

      if (hasGhost && event.key !== 'Shift' && event.key !== 'Control' && event.key !== 'Alt' && event.key !== 'Meta') {
        clearGhostText(view)
      }

      return false
    },
    handleTextInput: (view) => {
      if (hasGhostText(view)) {
        clearGhostText(view)
      }
      return false
    },
    handlePaste: (view) => {
      if (hasGhostText(view)) {
        clearGhostText(view)
      }
      return false
    },
    handleDrop: (view) => {
      if (hasGhostText(view)) {
        clearGhostText(view)
      }
      return false
    },
    handleClick: (view, pos) => {
      const range = getGhostRange(view)
      if (!range) return false

      if (pos >= range.from && pos <= range.to) {
        return acceptSuggestion(view)
      }

      clearGhostText(view)
      return false
    },
    handleDOMEvents: {
      compositionstart: (view) => {
        if (hasGhostText(view)) {
          clearGhostText(view)
        }
        return false
      },
      beforeinput: (view, event) => {
        if (!hasGhostText(view)) return false
        const inputType = (event as InputEvent).inputType || ''
        if (
          inputType.startsWith('insert') ||
          inputType.startsWith('delete') ||
          inputType.startsWith('format')
        ) {
          clearGhostText(view)
        }
        return false
      }
    }
  },
  view: (view) => {
    let activeView = view
    let activeDom = view.dom
    const runtime: CopilotRuntime = {
      enabled: true,
      debounceTimer: null,
      abortController: null,
      ctx,
      requestSeq: 0,
      docVersion: 0
    }
    runtimeByView.set(view, runtime)

    const onKeydownCapture = (event: KeyboardEvent) => {
      if (!hasGhostText(activeView)) return

      if (event.key === 'Tab') {
        event.preventDefault()
        event.stopPropagation()
        event.stopImmediatePropagation?.()
        acceptSuggestion(activeView)
        return
      }

      if (event.key === 'Escape') {
        event.preventDefault()
        event.stopPropagation()
        event.stopImmediatePropagation?.()
        rejectSuggestion(activeView)
      }
    }

    const onPointerDownCapture = (event: MouseEvent) => {
      if (!hasGhostText(activeView)) return
      const targetNode = event.target instanceof Node ? event.target : null
      const target = targetNode instanceof Element ? targetNode : targetNode?.parentElement
      if (!target) return

      if (target.closest('[data-copilot-ghost]') || target.closest('.copilot-ghost-block')) {
        event.preventDefault()
        event.stopPropagation()
        event.stopImmediatePropagation?.()
        acceptSuggestion(activeView)
      }
    }

    const bindDomListeners = (dom: HTMLElement) => {
      dom.addEventListener('keydown', onKeydownCapture, true)
      dom.addEventListener('mousedown', onPointerDownCapture, true)
    }

    const unbindDomListeners = (dom: HTMLElement) => {
      dom.removeEventListener('keydown', onKeydownCapture, true)
      dom.removeEventListener('mousedown', onPointerDownCapture, true)
    }

    bindDomListeners(activeDom)

    return {
      update: (nextView, prevState) => {
        if (nextView.dom !== activeDom) {
          unbindDomListeners(activeDom)
          activeDom = nextView.dom
          bindDomListeners(activeDom)
        }

        activeView = nextView
        const docChanged = !nextView.state.doc.eq(prevState.doc)
        const selectionChanged = !nextView.state.selection.eq(prevState.selection)

        if (docChanged) {
          runtime.docVersion += 1
        }

        if (!docChanged && !selectionChanged) {
          return
        }

        const prevGhost = COPILOT_PLUGIN_KEY.getState(prevState) as CopilotState | undefined
        const nextGhost = COPILOT_PLUGIN_KEY.getState(nextView.state) as CopilotState | undefined
        const prevHasGhost = Boolean(prevGhost?.suggestion && prevGhost.from < prevGhost.to)
        const nextHasGhost = Boolean(nextGhost?.suggestion && nextGhost.from < nextGhost.to)
        if (docChanged && prevHasGhost && nextHasGhost) {
          clearGhostText(nextView)
          clearRuntimeRequests(runtime, true, 'superseded')
          return
        }

        const ghostRange = getGhostRange(nextView)
        if (ghostRange) {
          const { from, to } = nextView.state.selection
          const overlapsGhost = from < ghostRange.to && to > ghostRange.from
          const alreadyAtGhostEnd = from === to && from === ghostRange.to
          if (overlapsGhost && !alreadyAtGhostEnd) {
            const endPos = Math.min(ghostRange.to, nextView.state.doc.content.size)
            const tr = nextView.state.tr.setSelection(Selection.near(nextView.state.doc.resolve(endPos), 1))
            nextView.dispatch(tr)
          }
          return
        }

        const { from, to } = nextView.state.selection
        if (from !== to) {
          clearRuntimeRequests(runtime, true, 'manual')
          return
        }

        scheduleFetch(nextView, runtime, from)
      },
      destroy: () => {
        unbindDomListeners(activeDom)
        clearRuntimeRequests(runtime, true, 'destroy')
        runtimeByView.delete(view)
      }
    }
  }
}))

export { COPILOT_PLUGIN_KEY }

export function isCopilotEnabled(view: EditorView): boolean {
  return runtimeByView.get(view)?.enabled ?? true
}

export function setCopilotEnabled(view: EditorView, value: boolean): void {
  const runtime = runtimeByView.get(view)
  if (!runtime) return

  runtime.enabled = value
  if (!value) {
    clearRuntimeRequests(runtime, true, 'disabled')
  }
}

export function interruptCopilot(view: EditorView): void {
  const runtime = runtimeByView.get(view)
  if (!runtime) return
  clearRuntimeRequests(runtime, true, 'manual')
}

export function checkSizeLimit(view: EditorView): { size: number; overLimit: boolean } {
  let size = view.state.doc.content.size
  view.state.doc.descendants((node) => {
    // 文档块：统计 content 属性
    if (node.type.name === 'doc_block' && node.attrs.content) {
      size += String(node.attrs.content).length
    }
    // PRO 块：统计 instruction 属性
    if (node.type.name === 'pro_block' && node.attrs.instruction) {
      size += String(node.attrs.instruction).length
    }
    // 网络搜索块：统计 content 属性
    if (node.type.name === WEB_SEARCH_NODE_TYPE && node.attrs.content) {
      size += String(node.attrs.content).length
    }
    // 图片节点：统计 alt 属性（ASR 转录文本常存于此）
    if (node.type.name === 'image' && node.attrs.alt) {
      size += String(node.attrs.alt).length
    }
  })
  return { size, overLimit: size > SIZE_LIMIT }
}

export { SIZE_LIMIT }
