import { createApp, h, reactive } from 'vue'
import { remarkCtx, schemaCtx, serializerCtx, type Ctx } from '@milkdown/kit/core'
import { $ctx, $node, $prose, $remark, $view } from '@milkdown/kit/utils'
import { Plugin, PluginKey, Selection } from '@milkdown/prose/state'
import { type Node as ProseNode, Slice, type Schema } from '@milkdown/prose/model'
import { Decoration, DecorationSet, type EditorView, type NodeView } from '@milkdown/prose/view'
import { ParserState } from '@milkdown/transformer'
import ProBlockCrepe from '../components/ProBlockCrepe.vue'
import { extractDocBlockContextFromMarkdown } from '../utils/docBlock.js'
import { buildOcrContextForDoc } from '../utils/ocrCache'
import { normalizeProAcceptMarkdown, splitPlainTextFallbackBlocks } from '../utils/proAccept.js'
import { PRO_BLOCK_NODE_TYPE, PRO_DISPLAY_LABEL, PRO_TRIGGER_TEXT, parseProBlockSyntax, serializeProBlockSyntax } from '../utils/proBlock.js'
import { extractWebSearchContextFromMarkdown } from '../utils/webSearch.js'

const PRO_BLOCK_INPUT_PLUGIN_KEY = new PluginKey('milkdown-pro-block-input')
const PRO_BLOCK_HIGHLIGHT_PLUGIN_KEY = new PluginKey<DecorationSet>('milkdown-pro-block-highlight')
const PRO_BLOCK_INPUT_META = 'pro-block-input-meta'
const PRO_CONTEXT_LIMIT = 32 * 1024
const FALLBACK_BLOCK_SEPARATOR = '\n\n'
const FALLBACK_LEAF_TEXT = '\n'

interface ProBlockConfig {
  fetchSuggestionStream: (payload: {
    prefix: string
    suffix: string
    languageId: string
    instruction: string
    signal?: AbortSignal
    onChunk?: (chunk: string) => void
    onEvent?: (event: string, data?: Record<string, any>) => void
  }) => Promise<string>
  t: (key: string) => string
  showError: (message: string) => void
}

interface ProRequestPayload {
  prefix: string
  suffix: string
  languageId: string
  instruction?: string
  blocked: boolean
}

function serializeRangeToMarkdown(
  doc: ProseNode,
  from: number,
  to: number,
  schema: Schema,
  serializer: (content: ProseNode) => string
): string {
  if (from >= to) return ''
  const fallback = doc.textBetween(from, to, FALLBACK_BLOCK_SEPARATOR, FALLBACK_LEAF_TEXT)
  if (typeof serializer !== 'function') return fallback

  const slice = doc.slice(from, to)
  if (slice.content.size <= 0) return ''

  try {
    const sliceDoc = schema.topNodeType.createAndFill(undefined, slice.content)
    return sliceDoc ? serializer(sliceDoc) : fallback
  } catch {
    return fallback
  }
}

function transformProBlockChildren(node: any) {
  if (!node || !Array.isArray(node.children)) return

  node.children = node.children.map((child: any) => {
    const replacement = parseProParagraphNode(child)
    if (replacement) return replacement
    transformProBlockChildren(child)
    return child
  })
}

function parseProParagraphNode(node: any) {
  if (!node || node.type !== 'paragraph' || !Array.isArray(node.children)) return null
  if (node.children.length !== 1) return null

  const textNode = node.children[0]
  if (!textNode || textNode.type !== 'text' || typeof textNode.value !== 'string') return null

  const parsed = parseProBlockSyntax(textNode.value)
  if (!parsed) return null

  return {
    type: PRO_BLOCK_NODE_TYPE,
    instruction: parsed.instruction,
    autoStart: false,
  }
}

function findProBlockReplacements(doc: ProseNode) {
  const replacements: Array<{ from: number; to: number; instruction: string; autoStart: boolean }> = []

  doc.descendants((node, pos) => {
    if (node.type.name !== 'paragraph' || node.childCount !== 1) return true

    const firstChild = node.firstChild
    if (!firstChild?.isText) return true

    const parsed = parseProBlockSyntax(node.textContent)
    if (!parsed) return true

    replacements.push({
      from: pos,
      to: pos + node.nodeSize,
      instruction: parsed.instruction,
      autoStart: parsed.autoStart,
    })

    return false
  })

  return replacements
}

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

  const decorations = [
    Decoration.inline(from, to, { class: 'pro-block-accepted-highlight' }),
  ]

  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: 'pro-block-accepted-block' }))
    }
    return true
  })

  return DecorationSet.create(doc, decorations)
}

function getTransactionInsertedRange(tr: any, from: number, to: number, beforeSize: number) {
  const deleteSize = Math.max(0, to - from)
  const insertedSize = Math.max(0, tr.doc.content.size - beforeSize + deleteSize)
  const startPos = Math.max(0, Math.min(from, tr.doc.content.size))
  const endPos = Math.max(startPos, Math.min(startPos + insertedSize, tr.doc.content.size))
  if (endPos <= startPos) return null
  return { from: startPos, to: endPos }
}

function replaceWithParsedMarkdownSlice(tr: any, from: number, to: number, parsedDoc: ProseNode) {
  if (!parsedDoc || parsedDoc.content.size <= 0) return null

  const parsedSlice = new Slice(parsedDoc.content, 0, 0)
  if (!parsedSlice || parsedSlice.size <= 0) return null

  const beforeSize = tr.doc.content.size
  try {
    tr.replaceRange(from, to, parsedSlice)
  } catch {
    return null
  }
  return getTransactionInsertedRange(tr, from, to, beforeSize)
}

function parseMarkdownWithCurrentSchema(ctx: Ctx, markdown: string) {
  const schema = ctx.get(schemaCtx)
  const remark = ctx.get(remarkCtx)
  const parser = ParserState.create(schema, remark)
  return parser(markdown)
}

function replaceWithTextFallback(
  tr: any,
  from: number,
  to: number,
  schema: Schema,
  source: string
) {
  const text = String(source || '')
  if (!text.trim()) return null

  const paragraphType = schema.nodes.paragraph
  const codeBlockType = schema.nodes.code_block || schema.nodes.codeBlock
  const beforeSize = tr.doc.content.size

  try {
    if (codeBlockType && text.includes('\n')) {
      tr.replaceWith(from, to, codeBlockType.create(null, schema.text(text)))
      return getTransactionInsertedRange(tr, from, to, beforeSize)
    }

    if (!paragraphType) return null

    const paragraphs = splitPlainTextFallbackBlocks(text)
    const nodes = paragraphs.map((block) => paragraphType.create(null, schema.text(block)))
    if (nodes.length === 0) return null

    tr.replaceWith(from, to, nodes)
    return getTransactionInsertedRange(tr, from, to, beforeSize)
  } catch {
    return null
  }
}

function insertProBlockNode(view: EditorView, autoStart: boolean) {
  const proBlockType = view.state.schema.nodes[PRO_BLOCK_NODE_TYPE]
  if (!proBlockType) return false

  const { from, to } = view.state.selection
  const blockNode = proBlockType.create({ instruction: '', autoStart })
  const tr = view.state.tr.replaceRangeWith(from, to, blockNode)
  const nextPos = Math.min(from + blockNode.nodeSize, tr.doc.content.size)
  tr.setSelection(Selection.near(tr.doc.resolve(nextPos), 1))
  view.dispatch(tr.scrollIntoView())
  view.focus()
  return true
}

function replaceParagraphWithProBlock(
  view: EditorView,
  from: number,
  to: number,
  instruction: string,
  autoStart: boolean
) {
  const proBlockType = view.state.schema.nodes[PRO_BLOCK_NODE_TYPE]
  if (!proBlockType) return false

  const blockNode = proBlockType.create({
    instruction,
    autoStart,
  })
  const tr = view.state.tr.replaceWith(from, to, blockNode)
  const nextPos = Math.min(from + blockNode.nodeSize, tr.doc.content.size)
  tr.setMeta(PRO_BLOCK_INPUT_META, true)
  tr.setSelection(Selection.near(tr.doc.resolve(nextPos), 1))
  view.dispatch(tr.scrollIntoView())
  view.focus()
  return true
}

function tryHandleProTriggerTextInput(view: EditorView, insertedText: string) {
  if (!insertedText || !insertedText.includes(']')) return false

  const { state } = view
  const { $from, from, to } = state.selection
  const paragraph = $from.parent
  if (!paragraph || paragraph.type.name !== 'paragraph') return false
  if (!$from.sameParent(state.selection.$to)) return false

  const paragraphDepth = $from.depth
  const paragraphStart = $from.start(paragraphDepth)
  const startOffset = from - paragraphStart
  const endOffset = to - paragraphStart
  const nextText = `${paragraph.textContent.slice(0, startOffset)}${insertedText}${paragraph.textContent.slice(endOffset)}`
  const parsed = parseProBlockSyntax(nextText)
  if (!parsed || !insertedText.includes(']')) return false

  const blockFrom = $from.before(paragraphDepth)
  const blockTo = blockFrom + paragraph.nodeSize
  return replaceParagraphWithProBlock(view, blockFrom, blockTo, parsed.instruction, parsed.autoStart)
}

function isAbortError(error: unknown) {
  return Boolean(error && typeof error === 'object' && 'name' in error && (error as { name?: string }).name === 'AbortError')
}

export const proBlockConfigCtx = $ctx<ProBlockConfig, 'proBlockConfig'>({
  fetchSuggestionStream: async () => '',
  t: (key: string) => key,
  showError: () => {},
}, 'proBlockConfig')

class ProBlockNodeView implements NodeView {
  node: ProseNode
  view: EditorView
  getPos: (() => number) | boolean
  dom: HTMLElement
  app: ReturnType<typeof createApp> | null = null
  props: Record<string, any>
  ctx: Ctx
  config: ProBlockConfig
  abortController: AbortController | null = null
  requestSeq = 0
  candidates: string[] = []
  activeCandidateIndex = -1
  destroyed = false
  highlightTimer: ReturnType<typeof setTimeout> | null = null

  constructor(
    node: ProseNode,
    view: EditorView,
    getPos: (() => number) | boolean,
    ctx: Ctx,
    config: ProBlockConfig
  ) {
    this.node = node
    this.view = view
    this.getPos = getPos
    this.ctx = ctx
    this.config = config
    this.dom = document.createElement('div')
    this.dom.className = 'pro-block-node-view'
    this.props = reactive({
      stage: 'idle',
      previewContent: '',
      activeContent: '',
      errorMessage: '',
      isBusy: false,
      isThinking: false,
      title: PRO_DISPLAY_LABEL,
      instruction: node.attrs.instruction || '',
      instructionExpanded: false,
      activeVersion: 0,
      versionCount: 0,
      canAccept: false,
      t: (key: string) => this.config.t(key),
      activateAction: () => {
        void this.startThinking(false)
      },
      cancelAction: () => {
        this.cancelThinking()
      },
      discardAction: () => {
        this.discardResult()
      },
      redoAction: () => {
        void this.startThinking(true)
      },
      acceptAction: () => {
        void this.acceptResult()
      },
      prevVersionAction: () => {
        this.selectCandidate(this.activeCandidateIndex - 1)
      },
      nextVersionAction: () => {
        this.selectCandidate(this.activeCandidateIndex + 1)
      },
      toggleInstructionAction: () => {
        this.props.instructionExpanded = !this.props.instructionExpanded
      },
      updateInstructionAction: (value: string) => {
        this.updateInstruction(value)
      },
    })
    this.mount()
    queueMicrotask(() => {
      if (!this.destroyed) {
        this.consumeAutoStart()
      }
    })
  }

  mount() {
    this.app = createApp({
      render: () => h(ProBlockCrepe, { ...this.props }),
    })
    this.app.mount(this.dom)
  }

  getPosValue() {
    if (typeof this.getPos === 'function') {
      try {
        const pos = this.getPos()
        if (typeof pos === 'number') return pos
      } catch {
        // Ignore and fall back to DOM lookup.
      }
    }

    try {
      const pos = this.view.posAtDOM(this.dom, 0)
      return typeof pos === 'number' ? pos : undefined
    } catch {
      return undefined
    }
  }

  updateAttrs(patch: Record<string, any>) {
    const pos = this.getPosValue()
    if (pos === undefined) return
    const nextAttrs = { ...this.node.attrs, ...patch }
    this.view.dispatch(this.view.state.tr.setNodeMarkup(pos, undefined, nextAttrs))
  }

  setStage(stage: 'idle' | 'queued' | 'thinking' | 'streaming' | 'done' | 'error' | 'cancelled', previewContent = '') {
    this.props.stage = stage
    this.props.previewContent = previewContent ? normalizeProAcceptMarkdown(previewContent) : ''
    this.props.isBusy = stage === 'queued' || stage === 'thinking' || stage === 'streaming'
    this.props.isThinking = stage === 'thinking'
    this.refreshCandidateProps()
  }

  refreshCandidateProps() {
    this.props.activeContent = this.candidates[this.activeCandidateIndex] || ''
    this.props.activeVersion = Math.max(0, this.activeCandidateIndex)
    this.props.versionCount = this.candidates.length
    this.props.canAccept = !this.props.isBusy && Boolean(this.props.activeContent || this.props.previewContent) && this.props.stage !== 'error'
  }

  selectCandidate(index: number) {
    if (index < 0 || index >= this.candidates.length) return
    this.activeCandidateIndex = index
    this.props.errorMessage = ''
    this.setStage('done', '')
  }

  setResult(content: string) {
    const normalized = normalizeProAcceptMarkdown(content)
    if (!normalized) return
    this.candidates.push(normalized)
    if (this.candidates.length > 5) {
      this.candidates = this.candidates.slice(this.candidates.length - 5)
    }
    this.activeCandidateIndex = this.candidates.length - 1
    this.refreshCandidateProps()
  }

  clearResult() {
    this.candidates = []
    this.activeCandidateIndex = -1
    this.props.previewContent = ''
    this.refreshCandidateProps()
  }

  buildRequestPayload(): ProRequestPayload {
    const pos = this.getPosValue()
    if (pos === undefined) {
      return {
        prefix: '',
        suffix: '',
        languageId: 'markdown',
        blocked: true,
      }
    }

    const doc = this.view.state.doc
    const schema = this.view.state.schema
    const serializer = this.ctx.get(serializerCtx)
    const prefixMarkdown = serializeRangeToMarkdown(doc, 0, pos, schema, serializer)
      || doc.textBetween(0, pos, FALLBACK_BLOCK_SEPARATOR, FALLBACK_LEAF_TEXT)
    const suffixStart = Math.min(pos + this.node.nodeSize, doc.content.size)
    const suffixMarkdown = serializeRangeToMarkdown(doc, suffixStart, doc.content.size, schema, serializer)
      || doc.textBetween(suffixStart, doc.content.size, FALLBACK_BLOCK_SEPARATOR, FALLBACK_LEAF_TEXT)
    const ocrContext = buildOcrContextForDoc(doc, 120)
    const docContext = extractDocBlockContextFromMarkdown(`${prefixMarkdown}\n\n${suffixMarkdown}`, 1600)
    const webSearchContext = extractWebSearchContextFromMarkdown(`${prefixMarkdown}\n\n${suffixMarkdown}`, 1600)
    const fullPrefix = [ocrContext, docContext, webSearchContext, prefixMarkdown].filter(Boolean).join('\n\n')

    return {
      prefix: fullPrefix,
      suffix: suffixMarkdown,
      languageId: 'markdown',
      instruction: this.props.instruction || '',
      blocked: fullPrefix.length + suffixMarkdown.length > PRO_CONTEXT_LIMIT,
    }
  }

  consumeAutoStart() {
    if (!this.node.attrs.autoStart) return
    this.updateAttrs({ autoStart: false })
    void this.startThinking(false)
  }

  abortActive(reason = 'abort') {
    if (!this.abortController) return
    this.abortController.abort(reason)
    this.abortController = null
  }

  cancelThinking() {
    const draft = String(this.props.previewContent || '').trim()
    this.abortActive('cancelled')
    if (draft) {
      this.setResult(draft)
    }
    this.props.errorMessage = ''
    this.setStage(draft ? 'cancelled' : 'idle', '')
  }

  discardResult() {
    this.abortActive('discard')
    this.clearResult()
    this.props.errorMessage = ''
    this.setStage('idle', '')
    this.updateAttrs({ autoStart: false })
  }

  updateInstruction(value: string) {
    if (this.props.isBusy) return
    const next = String(value || '')
    if (next === this.props.instruction) return
    this.props.instruction = next
    this.clearResult()
    this.props.errorMessage = ''
    this.setStage('idle', '')
    this.updateAttrs({ instruction: next, autoStart: false })
  }

  async startThinking(_isRedo: boolean) {
    if (this.props.isBusy) {
      this.abortActive('restart')
    }
    let requestSeq = this.requestSeq
    try {
      const payload = this.buildRequestPayload()
      if (payload.blocked) {
        this.config.showError('PRO 模式上下文超过 32KB，无法继续思考。')
        return
      }

      requestSeq = this.requestSeq + 1
      this.requestSeq = requestSeq
      this.abortController = new AbortController()
      this.props.errorMessage = ''
      this.setStage('queued', '')

      const result = await this.config.fetchSuggestionStream({
        ...payload,
        signal: this.abortController.signal,
        instruction: this.props.instruction || '',
        onChunk: (chunk) => {
          if (this.destroyed || this.requestSeq !== requestSeq) return
          const nextContent = `${this.props.previewContent || ''}${chunk}`
          this.setStage('streaming', nextContent)
        },
        onEvent: (event, data) => {
          if (this.destroyed || this.requestSeq !== requestSeq) return
          if (event === 'queued') {
            this.setStage('queued', '')
            return
          }
          if (event === 'started') {
            this.setStage('thinking', this.props.previewContent || '')
            return
          }
          if (event === 'thinking') {
            this.setStage('thinking', this.props.previewContent || '')
            return
          }
          if (event === 'chunk') {
            // 'chunk' events are handled by onChunk callback
            return
          }
          if (event === 'done') {
            // When done, update stage to 'done' and clear any error messages
            this.setStage('done', '')
            this.props.errorMessage = ''
            return
          }
          if (event === 'error') {
            this.props.errorMessage = String(data?.error || this.config.t('proErrorActionable') || 'PRO 模式生成失败，请重试。')
            this.setStage('error', this.props.previewContent || '')
          }
        },
      })

      if (this.destroyed || this.requestSeq !== requestSeq) return

      const content = normalizeProAcceptMarkdown(result || this.props.previewContent || '')
      if (!content) {
        this.props.errorMessage = this.config.t('proEmptyResult') || 'PRO 返回空结果，请重试或缩短上下文。'
        this.setStage('error', '')
        return
      }
      this.setResult(content)
      this.setStage('done', '')
      this.updateAttrs({ autoStart: false })
    } catch (error) {
      if (this.destroyed || this.requestSeq !== requestSeq) return
      if (isAbortError(error)) {
        const draft = String(this.props.previewContent || '').trim()
        if (draft && this.props.stage !== 'cancelled') {
          this.setResult(draft)
          this.setStage('cancelled', '')
        } else if (!draft && this.props.stage !== 'cancelled') {
          this.setStage(this.props.activeContent ? 'done' : 'idle', '')
        }
      } else {
        this.props.errorMessage = error instanceof Error
          ? error.message
          : (this.config.t('proErrorActionable') || 'PRO 模式生成失败，请重试。')
        this.setStage('error', this.props.previewContent || '')
      }
    } finally {
      if (this.requestSeq === requestSeq) {
        this.abortController = null
      }
    }
  }

  async acceptResult() {
    if (this.props.isBusy) return

    const source = normalizeProAcceptMarkdown(this.props.activeContent || this.props.previewContent || '')
    if (!source) return

    const pos = this.getPosValue()
    if (pos === undefined) return

    const from = pos
    const to = pos + this.node.nodeSize
    const tr = this.view.state.tr
    let insertedRange: { from: number; to: number } | null = null

    try {
      const parsedDoc = parseMarkdownWithCurrentSchema(this.ctx, source)
      insertedRange = replaceWithParsedMarkdownSlice(tr, from, to, parsedDoc)
    } catch (e) {
      console.error('PRO block parse failed:', e)
      insertedRange = null
    }

    if (!insertedRange) {
      insertedRange = replaceWithTextFallback(tr, from, to, this.view.state.schema, source)
      if (!insertedRange) {
        this.props.errorMessage = this.config.t('proAcceptParseError') || '无法接受当前 PRO 结果，请重试生成。'
        this.setStage('error', source)
        return
      }
    }

    const endPos = Math.min(insertedRange.to, tr.doc.content.size)
    tr.setSelection(Selection.near(tr.doc.resolve(endPos), 1))
    tr.setMeta(PRO_BLOCK_HIGHLIGHT_PLUGIN_KEY, { set: insertedRange })
    this.view.dispatch(tr.scrollIntoView())
    this.view.focus()

    if (this.highlightTimer) {
      clearTimeout(this.highlightTimer)
    }
    this.highlightTimer = setTimeout(() => {
      if (this.destroyed) return
      this.view.dispatch(this.view.state.tr.setMeta(PRO_BLOCK_HIGHLIGHT_PLUGIN_KEY, { clear: true }))
    }, 1000)
  }

  update(node: ProseNode) {
    if (node.type !== this.node.type) return false
    this.node = node
    this.props.instruction = node.attrs.instruction || ''

    if (!this.props.isBusy) {
      this.refreshCandidateProps()
      if (!this.props.activeContent && !this.props.errorMessage) {
        this.props.previewContent = ''
        this.props.stage = 'idle'
      }
    }

    return true
  }

  stopEvent(event: Event) {
    const target = event.target as Node | null
    return Boolean(target && this.dom.contains(target))
  }

  ignoreMutation() {
    return true
  }

  destroy() {
    this.destroyed = true
    this.abortActive('destroy')
    if (this.highlightTimer) {
      clearTimeout(this.highlightTimer)
      this.highlightTimer = null
    }
    this.app?.unmount()
    this.app = null
  }
}

export const proBlockRemark = $remark('proBlockRemark', () => () => {
  return (tree: any) => {
    transformProBlockChildren(tree)
  }
})

export const proBlockNode = $node(PRO_BLOCK_NODE_TYPE, () => ({
  group: 'block',
  atom: true,
  isolating: true,
  selectable: true,
  draggable: false,
  marks: '',
  attrs: {
    instruction: { default: '' },
    autoStart: { default: false },
  },
  parseDOM: [
    {
      tag: 'div[data-pro-block="true"]',
      getAttrs: (dom) => ({
        instruction: (dom as HTMLElement).getAttribute('data-pro-instruction') || '',
        autoStart: ((dom as HTMLElement).getAttribute('data-pro-autostart') || '') === 'true',
      }),
    },
  ],
  toDOM: (node) => [
    'div',
    {
      'data-pro-block': 'true',
      'data-pro-instruction': node.attrs.instruction || '',
      'data-pro-autostart': String(Boolean(node.attrs.autoStart)),
    },
  ],
  parseMarkdown: {
    match: (node) => node.type === PRO_BLOCK_NODE_TYPE,
    runner: (state, node, type) => {
      state.addNode(type, {
        instruction: String(node.instruction || ''),
        autoStart: false,
      })
    },
  },
  toMarkdown: {
    match: (node) => node.type.name === PRO_BLOCK_NODE_TYPE,
    runner: (state, node) => {
      state.addNode('paragraph', [{
        type: 'text',
        value: serializeProBlockSyntax(node.attrs.instruction),
      }])
    },
  },
  leafText: (node) => serializeProBlockSyntax(node.attrs.instruction),
}))

export const proBlockView = $view(proBlockNode, (ctx) => {
  const config = ctx.get(proBlockConfigCtx.key)
  return (node, view, getPos) => new ProBlockNodeView(node, view, getPos, ctx, config)
})

export const proBlockInputPlugin = $prose(() => {
  return new Plugin({
    key: PRO_BLOCK_INPUT_PLUGIN_KEY,
    props: {
      handleTextInput: (view, _from, _to, text) => {
        return tryHandleProTriggerTextInput(view, text)
      },
      handleKeyDown: (view, event) => {
        const pressed = (event.ctrlKey || event.metaKey) && event.shiftKey && event.key.toLowerCase() === 'p'
        if (!pressed) return false
        event.preventDefault()
        return insertProBlockNode(view, false)
      },
    },
    appendTransaction: (transactions, _oldState, newState) => {
      if (!transactions.some((transaction) => transaction.docChanged)) return null
      if (transactions.some((transaction) => transaction.getMeta(PRO_BLOCK_INPUT_META))) return null

      const proBlockType = newState.schema.nodes[PRO_BLOCK_NODE_TYPE]
      if (!proBlockType) return null

      const replacements = findProBlockReplacements(newState.doc)
      if (replacements.length === 0) return null

      let tr = newState.tr

      for (let index = replacements.length - 1; index >= 0; index -= 1) {
        const replacement = replacements[index]
        tr = tr.replaceWith(
          replacement.from,
          replacement.to,
          proBlockType.create({
            instruction: replacement.instruction,
            autoStart: replacement.autoStart,
          })
        )
      }

      if (!tr.docChanged) return null
      tr.setMeta(PRO_BLOCK_INPUT_META, true)
      return tr
    },
  })
})

export const proBlockHighlightPlugin = $prose(() => {
  return new Plugin<DecorationSet>({
    key: PRO_BLOCK_HIGHLIGHT_PLUGIN_KEY,
    state: {
      init: () => DecorationSet.empty,
      apply: (tr, value) => {
        const meta = tr.getMeta(PRO_BLOCK_HIGHLIGHT_PLUGIN_KEY)
        if (meta?.clear) {
          return DecorationSet.empty
        }
        if (meta?.set) {
          return createHighlightDecorations(tr.doc, meta.set.from, meta.set.to)
        }
        return value.map(tr.mapping, tr.doc)
      },
    },
    props: {
      decorations: (state) => PRO_BLOCK_HIGHLIGHT_PLUGIN_KEY.getState(state),
    },
  })
})

export function insertProBlockAtSelection(view: EditorView, autoStart = false) {
  return insertProBlockNode(view, autoStart)
}

export { PRO_TRIGGER_TEXT }
