import { createApp, h, reactive } from 'vue'
import { Plugin, PluginKey } from '@milkdown/prose/state'
import { $ctx, $node, $prose, $remark, $view } from '@milkdown/kit/utils'
import type { Node as ProseNode } from '@milkdown/prose/model'
import type { EditorView, NodeView } from '@milkdown/prose/view'
import UploadBlockCrepe from '../components/UploadBlockCrepe.vue'
import {
  DEFAULT_UPLOAD_BLOCK_TYPES,
  UPLOAD_BLOCK_NODE_TYPE,
  getUploadBlockAccept,
  getUploadBlockMenuOptions,
  normalizeUploadBlockTypes,
  parseUploadBlockSyntax,
  serializeUploadBlockSyntax,
} from '../utils/uploadBlock.js'

const UPLOAD_BLOCK_INPUT_PLUGIN_KEY = new PluginKey('milkdown-upload-block-input')
const UPLOAD_BLOCK_INPUT_META = 'upload-block-input-meta'

interface UploadBlockConfig {
  requestUpload: (payload: { file: File; allowedTypes: string[]; from: number; to: number }) => Promise<void>
}

export const uploadBlockConfigCtx = $ctx<UploadBlockConfig, 'uploadBlockConfig'>({
  requestUpload: async () => {},
}, 'uploadBlockConfig')

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

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

function parseUploadParagraphNode(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 = parseUploadBlockSyntax(textNode.value)
  if (!parsed) return null

  return {
    type: UPLOAD_BLOCK_NODE_TYPE,
    allowedTypes: parsed.allowedTypes,
  }
}

function findUploadBlockReplacements(doc: ProseNode) {
  const replacements: Array<{ from: number; to: number; allowedTypes: string[] }> = []

  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 = parseUploadBlockSyntax(node.textContent)
    if (!parsed) return true

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

    return false
  })

  return replacements
}

class UploadBlockNodeView implements NodeView {
  node: ProseNode
  view: EditorView
  getPos: (() => number) | boolean
  dom: HTMLElement
  app: ReturnType<typeof createApp> | null = null
  props: Record<string, any>
  requestUpload: UploadBlockConfig['requestUpload']

  constructor(node: ProseNode, view: EditorView, getPos: (() => number) | boolean, requestUpload: UploadBlockConfig['requestUpload']) {
    this.node = node
    this.view = view
    this.getPos = getPos
    this.requestUpload = requestUpload
    this.dom = document.createElement('div')
    this.dom.className = 'upload-block-node-view'

    const allowedTypes = normalizeUploadBlockTypes(node.attrs.allowedTypes)

    this.props = reactive({
      allowedTypes,
      accept: getUploadBlockAccept(allowedTypes),
      menuOptions: getUploadBlockMenuOptions(allowedTypes),
      onUploadRequest: (file: File) => this.handleUploadRequest(file),
    })

    this.mount()
  }

  mount() {
    this.app = createApp({
      render: () => h(UploadBlockCrepe, 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
    }
  }

  async handleUploadRequest(file: File) {
    const pos = this.getPosValue()
    if (pos === undefined || !file) return

    await this.requestUpload({
      file,
      allowedTypes: normalizeUploadBlockTypes(this.node.attrs.allowedTypes),
      from: pos,
      to: pos + this.node.nodeSize,
    })
  }

  update(node: ProseNode) {
    if (node.type !== this.node.type) return false
    this.node = node

    const allowedTypes = normalizeUploadBlockTypes(node.attrs.allowedTypes)
    this.props.allowedTypes = allowedTypes
    this.props.accept = getUploadBlockAccept(allowedTypes)
    this.props.menuOptions = getUploadBlockMenuOptions(allowedTypes)
    return true
  }

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

  ignoreMutation() {
    return true
  }

  destroy() {
    this.app?.unmount()
    this.app = null
  }
}

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

export const uploadBlockNode = $node(UPLOAD_BLOCK_NODE_TYPE, () => ({
  group: 'block',
  atom: true,
  isolating: true,
  selectable: true,
  draggable: false,
  marks: '',
  attrs: {
    allowedTypes: { default: [...DEFAULT_UPLOAD_BLOCK_TYPES] },
  },
  parseDOM: [
    {
      tag: 'div[data-upload-block="true"]',
      getAttrs: (dom) => ({
        allowedTypes: normalizeUploadBlockTypes(
          String((dom as HTMLElement).getAttribute('data-upload-types') || '')
            .split(',')
            .map((item) => item.trim())
            .filter(Boolean)
        ),
      }),
    },
  ],
  toDOM: (node) => [
    'div',
    {
      'data-upload-block': 'true',
      'data-upload-types': normalizeUploadBlockTypes(node.attrs.allowedTypes).join(','),
    },
  ],
  parseMarkdown: {
    match: (node) => node.type === UPLOAD_BLOCK_NODE_TYPE,
    runner: (state, node, type) => {
      state.addNode(type, {
        allowedTypes: normalizeUploadBlockTypes(node.allowedTypes),
      })
    },
  },
  toMarkdown: {
    match: (node) => node.type.name === UPLOAD_BLOCK_NODE_TYPE,
    runner: (state, node) => {
      state.addNode('paragraph', [{
        type: 'text',
        value: serializeUploadBlockSyntax(node.attrs.allowedTypes),
      }])
    },
  },
  leafText: (node) => serializeUploadBlockSyntax(node.attrs.allowedTypes),
}))

export const uploadBlockView = $view(uploadBlockNode, (ctx) => {
  const requestUpload = ctx.get(uploadBlockConfigCtx.key).requestUpload
  return (node, view, getPos) => new UploadBlockNodeView(node, view, getPos, requestUpload)
})

export const uploadBlockInputPlugin = $prose(() => {
  return new Plugin({
    key: UPLOAD_BLOCK_INPUT_PLUGIN_KEY,
    appendTransaction: (transactions, _oldState, newState) => {
      if (!transactions.some((transaction) => transaction.docChanged)) return null
      if (transactions.some((transaction) => transaction.getMeta(UPLOAD_BLOCK_INPUT_META))) return null

      const uploadBlockType = newState.schema.nodes[UPLOAD_BLOCK_NODE_TYPE]
      if (!uploadBlockType) return null

      const replacements = findUploadBlockReplacements(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,
          uploadBlockType.create({
            allowedTypes: normalizeUploadBlockTypes(replacement.allowedTypes),
          })
        )
      }

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