/**
 * Pattern detection plugin for ProseMirror.
 *
 * Detects trigger patterns at cursor position and emits events:
 * - "/" at word boundary → trigger("slash") for command menu
 * - "[[" at word boundary → trigger("page-ref") for page suggestions
 * - "((" at word boundary → trigger("block-ref") for block suggestions
 * - "#" at word boundary → trigger("tag") for tag suggestions
 *
 * Emits "cancelled" when pattern session ends without completion.
 */

import {
  type EditorState,
  Plugin,
  PluginKey,
  TextSelection,
  type Transaction,
} from "prosemirror-state"
import type { EditorView } from "prosemirror-view"
import { inlineToString } from "@/lib/content-model"
import { createLogger } from "@/lib/logger"

export type PatternKind = "reference" | "embed" | "command" | "tag"
export type CloseReason = "completed" | "cancelled"
export type Termination = "delimiter" | "boundary" | "manual"

export interface PatternSpec {
  start: string
  end?: string
  kind: PatternKind
  termination: Termination
  priority?: number
}

const PATTERN_SPECS = [
  {
    start: "[[",
    end: "]]",
    kind: "reference" as PatternKind,
    termination: "delimiter" as Termination,
    priority: 3,
  },
  {
    start: "((",
    end: "))",
    kind: "embed" as PatternKind,
    termination: "delimiter" as Termination,
    priority: 3,
  },
  {
    start: "/",
    kind: "command" as PatternKind,
    termination: "boundary" as Termination,
    priority: 2,
  },
  {
    start: "#",
    kind: "tag" as PatternKind,
    termination: "boundary" as Termination,
    priority: 1,
  },
].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)) satisfies PatternSpec[]

export interface PatternSession {
  id: string
  trigger: string
  kind: PatternKind
  query: string
  range: { from: number; to: number }
  position: { x: number; y: number }
}

export interface PatternOpenPayload {
  id: string
  trigger: string
  kind: PatternKind
  query: string
  range: { from: number; to: number }
  position: { x: number; y: number }
  respond: (opts: {
    text: string
    mode?: "partial" | "replace" | "append"
  }) => void
  close: (reason?: CloseReason) => void
}

export interface PatternUpdatePayload {
  id: string
  trigger: string
  kind: PatternKind
  query: string
  range: { from: number; to: number }
  position: { x: number; y: number }
}

export interface PatternClosePayload {
  id: string
  reason: CloseReason
}

export interface PatternCallbacks {
  onPatternOpen: (payload: PatternOpenPayload) => void
  onPatternUpdate: (payload: PatternUpdatePayload) => void
  onPatternClose: (payload: PatternClosePayload) => void
}

export const patternPluginKey = new PluginKey<PatternSession | null>("pattern")

export function getPatternSession(state: EditorState): PatternSession | null {
  return patternPluginKey.getState(state) ?? null
}
const forceCloseMetaKey = "pattern-force-close"

/**
 * Extract text before cursor in the current paragraph.
 */
function getTextBeforeCursor(
  state: EditorState,
): { textBefore: string } | null {
  const { $head } = state.selection
  const parentNode = $head.parent
  if (!parentNode?.isTextblock) return null

  // Use inlineToString to account for hard_break nodes (textContent omits them)
  const textBefore = inlineToString(parentNode).slice(0, $head.parentOffset)

  return { textBefore }
}

function findPattern(
  state: EditorState,
  prevSession: PatternSession | null,
  specs: PatternSpec[],
): PatternSession | null {
  const { $head } = state.selection
  const parentNode = $head.parent
  if (!parentNode?.isTextblock) return null

  const info = getTextBeforeCursor(state)
  if (!info) return null

  const { textBefore } = info

  for (const spec of specs) {
    const match = matchPattern(textBefore, spec)
    if (!match) continue

    const { startIdx, trigger, query } = match

    // Don't open tag palette on bare # — it's likely a heading attempt.
    if (spec.kind === "tag" && query.length === 0) continue

    // Don't open delimiter patterns ([[, (() followed immediately by a space
    // — the user is not starting a reference.
    if (spec.termination === "delimiter" && /^\s/.test(query)) continue

    const startPos = $head.pos - textBefore.length + startIdx
    if (isPatternCompleted(spec, query)) return null

    // For delimiter-terminated patterns, check if the closing delimiter
    // For delimiter-terminated patterns (page/block refs), check if the
    // closing delimiter exists anywhere in the paragraph after the trigger.
    // This prevents an active session when the pattern is already complete
    // but the cursor is inside it (e.g. navigating into [[Page Ref]]).
    if (spec.termination === "delimiter" && spec.end) {
      const parentText = parentNode.textContent
      const triggerIdx = parentText.indexOf(trigger)
      if (triggerIdx >= 0) {
        const textFromTrigger = parentText.slice(triggerIdx + trigger.length)
        if (textFromTrigger.includes(spec.end)) return null
      }
    }

    if (spec.termination === "boundary" && startIdx > 0) {
      const charBefore = textBefore[startIdx - 1]
      if (charBefore && !/\s/.test(charBefore)) continue
    }

    const id =
      prevSession &&
      prevSession.trigger === trigger &&
      prevSession.range.from === startPos
        ? prevSession.id
        : `pattern-${Math.random().toString(36).slice(2, 8)}`

    return {
      id,
      trigger,
      kind: spec.kind,
      query,
      range: { from: startPos, to: startPos + trigger.length + query.length },
      position: { x: 0, y: 0 },
    }
  }
  return null
}

function matchPattern(
  textBefore: string,
  spec: PatternSpec,
): { startIdx: number; trigger: string; query: string } | null {
  const start = spec.start
  const idx = textBefore.lastIndexOf(start)
  if (idx === -1) return null

  const afterTrigger = textBefore.slice(idx + start.length)

  // For delimiter-terminated patterns ([[, ((), the query includes everything
  // until the closing delimiter — spaces are allowed in page/block names.
  // For boundary patterns (/, #), the query must be a single word.
  if (spec.termination === "delimiter") {
    return { startIdx: idx, trigger: start, query: afterTrigger }
  }

  const queryMatch = afterTrigger.match(/^[^\s\n]*/)
  if (!queryMatch) return null
  const query = queryMatch[0]
  if (afterTrigger !== query) return null

  return { startIdx: idx, trigger: start, query }
}

function isPatternCompleted(spec: PatternSpec, query: string): boolean {
  if (spec.termination === "delimiter" && spec.end) {
    return query.includes(spec.end)
  }
  if (spec.termination === "boundary") {
    return !/^[^\s\n]*$/.test(query)
  }
  return false
}

function getCurrentRange(
  state: EditorState,
  trigger: string,
): { from: number; to: number } | null {
  const $head = state.selection.$head
  if (!$head.parent.isTextblock) return null

  const info = getTextBeforeCursor(state)
  if (!info) return null

  const startIdx = info.textBefore.lastIndexOf(trigger)
  if (startIdx === -1) return null

  const startPos = $head.pos - info.textBefore.length + startIdx
  const query = info.textBefore.slice(startIdx + trigger.length)

  return { from: startPos, to: startPos + trigger.length + query.length }
}

function isPageRefComplete(query: string): boolean {
  return query.includes("]]")
}

function isBlockRefComplete(query: string): boolean {
  return query.includes(")")
}

class PatternView {
  private view: EditorView
  private callbacks: PatternCallbacks
  private specs: PatternSpec[]
  private currentSession: PatternSession | null = null
  private manualClose = false
  private log = createLogger("PatternPlugin")

  constructor(
    view: EditorView,
    callbacks: PatternCallbacks,
    specs: PatternSpec[],
  ) {
    this.view = view
    this.callbacks = callbacks
    this.specs = specs
  }

  update(view: EditorView) {
    const state = view.state
    const newSession = patternPluginKey.getState(state)
    const prevSession = this.currentSession

    if (newSession && !prevSession) {
      this.open(newSession)
    } else if (
      newSession &&
      prevSession &&
      newSession.query !== prevSession.query
    ) {
      this.updateQuery(newSession)
    } else if (!newSession && prevSession) {
      this.close(prevSession)
    }

    this.currentSession = newSession || null
  }

  private open(session: PatternSession) {
    this.log.info("open", {
      kind: session.kind,
      trigger: session.trigger,
      query: session.query,
    })
    const pos = this.view.coordsAtPos(session.range.from)
    const payload: PatternOpenPayload = {
      id: session.id,
      trigger: session.trigger,
      kind: session.kind,
      query: session.query,
      range: session.range,
      position: { x: pos.left, y: pos.top },
      respond: (opts) => this.respond(session.id, opts),
      close: (reason = "completed") => this.forceClose(session.id, reason),
    }
    this.callbacks.onPatternOpen(payload)
  }

  private updateQuery(session: PatternSession) {
    this.log.debug("update", { kind: session.kind, query: session.query })
    const pos = this.view.coordsAtPos(session.range.from)
    this.callbacks.onPatternUpdate({
      id: session.id,
      trigger: session.trigger,
      kind: session.kind,
      query: session.query,
      range: session.range,
      position: { x: pos.left, y: pos.top },
    })
  }

  private close(session: PatternSession) {
    this.log.info("close", {
      kind: session.kind,
      reason: this.manualClose ? "completed" : "checking",
    })
    if (this.manualClose) {
      this.callbacks.onPatternClose({ id: session.id, reason: "completed" })
      this.manualClose = false
      return
    }

    const spec = this.specs.find((s) => s.start === session.trigger)
    if (!spec) {
      this.callbacks.onPatternClose({ id: session.id, reason: "cancelled" })
      return
    }

    const $head = this.view.state.selection.$head
    if (!$head.parent.isTextblock) {
      this.callbacks.onPatternClose({ id: session.id, reason: "cancelled" })
      return
    }

    const info = getTextBeforeCursor(this.view.state)
    if (!info) {
      this.callbacks.onPatternClose({ id: session.id, reason: "cancelled" })
      return
    }

    const startIdx = info.textBefore.lastIndexOf(session.trigger)
    if (startIdx === -1) {
      this.callbacks.onPatternClose({ id: session.id, reason: "cancelled" })
      return
    }

    const startPos = $head.pos - info.textBefore.length + startIdx
    if (startPos !== session.range.from) {
      this.callbacks.onPatternClose({ id: session.id, reason: "cancelled" })
      return
    }

    const query = info.textBefore.slice(startIdx + session.trigger.length)

    let isComplete = false
    if (spec.start === "[[") {
      isComplete = isPageRefComplete(query)
    } else if (spec.start === "((") {
      isComplete = isBlockRefComplete(query)
    } else {
      isComplete = isPatternCompleted(spec, query)
    }

    if (isComplete) {
      this.callbacks.onPatternClose({ id: session.id, reason: "completed" })
    } else {
      this.callbacks.onPatternClose({ id: session.id, reason: "cancelled" })
    }
  }

  private respond(
    sessionId: string,
    opts: {
      text: string
      mode?: "partial" | "replace" | "append"
      cursor?: number | "end"
    },
  ) {
    const state = this.view.state
    const session = patternPluginKey.getState(state)
    if (!session || session.id !== sessionId) return

    const currentRange = getCurrentRange(state, session.trigger)
    if (!currentRange) return

    const text = opts.mode === "append" ? session.query + opts.text : opts.text
    const tr = state.tr.insertText(text, currentRange.from, currentRange.to)

    if (opts.cursor !== undefined) {
      if (opts.cursor === "end") {
        const pos = currentRange.from + text.length
        tr.setSelection(TextSelection.create(tr.doc, pos))
      } else {
        tr.setSelection(TextSelection.create(tr.doc, opts.cursor))
      }
    }

    this.view.dispatch(tr)
  }

  private forceClose(sessionId: string, reason: CloseReason = "completed") {
    const state = this.view.state
    const session = patternPluginKey.getState(state)
    if (!session || session.id !== sessionId) return

    this.manualClose = true
    const tr = state.tr.setMeta(forceCloseMetaKey, reason)
    this.view.dispatch(tr)
  }

  destroy() {}
}

/**
 * Create a ProseMirror plugin that detects trigger patterns at the cursor
 * and emits open/update/close lifecycle events via `callbacks`.
 *
 * @param callbacks - Event handlers for pattern session lifecycle.
 * @param extraPatterns - Additional pattern specs appended after the built-in
 *   ones ([[, ((, /, #). Merged and sorted by priority.
 */
export function createPatternPlugin(
  callbacks: PatternCallbacks,
  extraPatterns?: PatternSpec[],
): Plugin<PatternSession | null> {
  const allSpecs = [...PATTERN_SPECS, ...(extraPatterns ?? [])].sort(
    (a, b) => (b.priority ?? 0) - (a.priority ?? 0),
  )

  return new Plugin<PatternSession | null>({
    key: patternPluginKey,
    state: {
      init: () => null,
      apply(
        tr: Transaction,
        prevSession: PatternSession | null,
        _oldState: EditorState,
        newState: EditorState,
      ) {
        if (tr.getMeta(forceCloseMetaKey)) return null
        return findPattern(newState, prevSession, allSpecs)
      },
    },
    view: (editorView: EditorView) =>
      new PatternView(editorView, callbacks, allSpecs),
  })
}
