import type { EditorState } from "prosemirror-state"
import { Plugin, TextSelection } from "prosemirror-state"
import type { EditorView } from "prosemirror-view"
import { createLogger } from "@/lib/logger"
import { schema } from "@/lib/schema"

const log = createLogger("AutoClose")

export interface AutoCloseRule {
  name: string
  handler: (
    state: EditorState,
    view: EditorView,
    from: number,
    to: number,
    text: string,
  ) => boolean
  /** For pair rules: opening and closing characters used by backspace delete-pair */
  pair?: { open: string; close: string }
}

function isTextBlock(state: EditorState, pos: number) {
  const $pos = state.doc.resolve(pos)
  return !$pos.parent.type.spec.code
}

export function autoClosePlugin(rules: AutoCloseRule[]) {
  const pairs: NonNullable<AutoCloseRule["pair"]>[] = []
  for (const rule of rules) {
    if (rule.pair) pairs.push(rule.pair)
  }

  const pairLookup = new Map<string, string>()
  for (const p of pairs) {
    pairLookup.set(p.open, p.close)
  }

  return new Plugin<unknown>({
    props: {
      handleTextInput(view, from, to, text) {
        if (text.length !== 1) return false
        if (!isTextBlock(view.state, from)) return false
        for (const rule of rules) {
          if (rule.handler(view.state, view, from, to, text)) {
            return true
          }
        }
        return false
      },
      handleDOMEvents: {
        keydown(view, event) {
          if (event.key !== "Backspace" || pairs.length === 0) return false
          const { from, to, empty } = view.state.selection
          if (!empty || from !== to || from < 1) return false

          // textBetween is safe here even near paragraph boundaries:
          // if |from| points to the start of a paragraph (right after the
          // parent node), from-1 resolves to the parent's end position and
          // textBetween returns "" rather than throwing.
          const prev = view.state.doc.textBetween(from - 1, from)
          const next = view.state.doc.textBetween(from, from + 1)

          if (prev && next && pairLookup.get(prev) === next) {
            event.preventDefault()
            log.debug("delete pair", { open: prev, close: next })
            const tr = view.state.tr.delete(from - 1, from + 1)
            view.dispatch(tr)
            return true
          }
          return false
        },
      },
    },
  })
}

// ── Pair rule factory ──────────────────────────────────────────

interface PairRuleOptions {
  /** Context regex: only auto-close if text before cursor matches */
  context?: RegExp
  /** Disable skip-over for the closing character */
  noSkip?: boolean
}

export function createPairRule(
  open: string,
  close: string,
  options?: PairRuleOptions,
): AutoCloseRule {
  return {
    name: `pair-${open.charCodeAt(0).toString(36)}`,
    handler(state, view, from, to, text) {
      // ── Open character: auto-close or wrap selection ──
      if (text === open) {
        if (from !== to) {
          // Wrap selection — no context check; the user's intent is clear
          const selected = state.doc.textBetween(from, to)
          const tr = state.tr.replaceWith(
            from,
            to,
            state.schema.text(open + selected + close),
          )
          const cursorPos = from + open.length + selected.length + close.length
          tr.setSelection(TextSelection.create(tr.doc, cursorPos))
          view.dispatch(tr)
          view.focus()
          return true
        }

        // Context check: only auto-close if preceded by allowed chars
        if (options?.context) {
          const before = state.doc.textBetween(Math.max(0, from - 1), from)
          if (before && !options.context.test(before)) return false
        }

        // Auto-close: insert open + close, cursor between
        log.debug("pair auto-close", { open, close })
        const tr = state.tr.replaceWith(
          from,
          from,
          state.schema.text(open + close),
        )
        const cursorPos = from + open.length
        tr.setSelection(TextSelection.create(tr.doc, cursorPos))
        view.dispatch(tr)
        view.focus()
        return true
      }

      // ── Close character: skip-over (only when no selection) ──
      if (text === close && !options?.noSkip && from === to) {
        const ahead = state.doc.textBetween(from, from + close.length)
        if (ahead === close) {
          log.debug("pair skip-over", { close })
          const tr = view.state.tr.setSelection(
            TextSelection.create(view.state.doc, from + close.length),
          )
          view.dispatch(tr)
          return true
        }
        return false
      }

      return false
    },
    pair: { open, close },
  }
}

// ── Toggle rule factory (multi-char delimiters: **, ~~) ────────

interface ToggleRuleOptions {
  wordBoundary?: boolean
}

export function createToggleRule(
  delimiter: string,
  _options?: ToggleRuleOptions,
): AutoCloseRule {
  if (delimiter.length < 2) {
    throw new Error(
      `createToggleRule requires a multi-character delimiter, got "${delimiter}"`,
    )
  }

  const lastChar = delimiter.charAt(delimiter.length - 1)
  const prefix = delimiter.slice(0, -1)

  return {
    name: `toggle-${delimiter.charCodeAt(0).toString(36)}`,
    handler(state, view, from, to, text) {
      if (text !== lastChar) return false
      if (!isTextBlock(view.state, from)) return false

      // Skip-over: jump past a matching closing delimiter character
      // one at a time. Runs before the prefix/space checks so the
      // cursor can advance through an existing closing pair.
      if (from === to) {
        const next = state.doc.textBetween(from, from + 1)
        if (next === lastChar) {
          log.debug("toggle skip-over", { delimiter, char: lastChar })
          const tr = state.tr.setSelection(
            TextSelection.create(state.doc, from + 1),
          )
          view.dispatch(tr)
          view.focus()
          return true
        }
      }

      // Check if previous chars match the delimiter prefix
      const prevChars = state.doc.textBetween(
        Math.max(0, from - prefix.length),
        from,
      )
      if (prevChars !== prefix) return false

      // Only auto-close when preceded by whitespace or start-of-text.
      // This prevents auto-closing mid-word (e.g. "text**" should not
      // become "text****") or right after a completed pair.
      const before = state.doc.textBetween(
        Math.max(0, from - prefix.length - 1),
        from - prefix.length,
      )
      if (before && !/^\s$/.test(before)) return false

      // handleTextInput fires one character at a time, so by the time
      // both ** are typed any prior selection is already collapsed.
      // Wrapping selected text with toggle delimiters requires
      // a keyboard shortcut or toolbar button, not auto-close.
      if (from !== to) return false

      const start = from - prefix.length

      // Insert delimiter pair, cursor at midpoint
      log.debug("toggle auto-close", { delimiter })
      const tr = state.tr.replaceWith(
        start,
        from,
        state.schema.text(delimiter + delimiter),
      )
      const cursorPos = start + delimiter.length
      tr.setSelection(TextSelection.create(tr.doc, cursorPos))
      view.dispatch(tr)
      view.focus()
      return true
    },
  }
}

// ── Structural rules ───────────────────────────────────────────

/** Auto-close ``` (triple backtick) into a code block */
export const tripleBacktickRule: AutoCloseRule = {
  name: "triple-backtick",
  handler(state, view, from, _to, text) {
    if (text !== "`") return false
    const $pos = state.doc.resolve(from)
    if ($pos.parent.type.name !== "paragraph") return false

    const before = state.doc.textBetween(Math.max(0, from - 2), from)
    if (before !== "``") return false

    // Ensure ``` is at paragraph start (only whitespace before)
    const posInPara = from - $pos.before() - 1
    const prefix = $pos.parent.textContent.slice(0, posInPara - 2)
    if (prefix.trim() !== "") return false

    log.info("triple backtick → code block")
    const paraStart = $pos.before()
    const paraEnd = paraStart + $pos.parent.nodeSize
    const init = "```\n\n```"
    const codeBlock = schema.nodes.code_block.create({ language: "" }, [
      schema.text(init),
    ])
    const tr = state.tr.replaceWith(paraStart, paraEnd, codeBlock)
    const cursorPos = paraStart + 1 + "```".length
    tr.setSelection(new TextSelection(tr.doc.resolve(cursorPos)))
    view.dispatch(tr)
    return true
  },
}

/** Auto-close ** (double asterisk → bold) */
export const doubleAsteriskRule: AutoCloseRule = createToggleRule("**")

/** Auto-close ~~ (double tilde → strikethrough) */
export const doubleTildeRule: AutoCloseRule = createToggleRule("~~")

/** Auto-close ^^ (double caret → highlight) */
export const doubleCaretRule: AutoCloseRule = createToggleRule("^^")

/**
 * Auto-close $ for inline math and $$ → math_block conversion.
 *
 * - Single $ at word boundary: inserts $$ with cursor between (inline math)
 * - Second $ at paragraph start on an existing $$: converts paragraph to math_block
 * - $ at end of inline math content: skip-over (move cursor past closing $)
 * - Backspace between $$: delete both
 */
export const dollarSignRule: AutoCloseRule = {
  name: "dollar-sign",
  handler(state, view, from, to, text) {
    if (text !== "$") return false
    const $pos = state.doc.resolve(from)
    if ($pos.parent.type.spec.code) return false

    const prevChar = state.doc.textBetween(Math.max(0, from - 1), from)

    // ── Skip-over: cursor right before a closing $ → jump past it ──
    if (from === to && prevChar !== "$") {
      const next = state.doc.textBetween(from, from + 1)
      if (next === "$") {
        const tr = state.tr.setSelection(
          TextSelection.create(state.doc, from + 1),
        )
        view.dispatch(tr)
        return true
      }
    }

    // ── $$ at paragraph start → math_block node ──
    if (prevChar === "$") {
      const posInPara = from - $pos.before() - 1
      const prefix = $pos.parent.textContent.slice(0, posInPara - 1)
      if (prefix.trim() === "") {
        log.info("double dollar → math block")
        const paraStart = $pos.before()
        const paraEnd = paraStart + $pos.parent.nodeSize
        const init = "$$\n\n$$"
        const mathBlock = schema.nodes.math_block.create(null, [
          schema.text(init),
        ])
        const tr = state.tr.replaceWith(paraStart, paraEnd, mathBlock)
        const cursorPos = paraStart + 1 + "$$".length
        tr.setSelection(new TextSelection(tr.doc.resolve(cursorPos)))
        view.dispatch(tr)
        return true
      }
      return false
    }

    // ── Single $: auto-close (inline math) with context check ──
    const context = /^$|[\s([{:>]/
    const before = state.doc.textBetween(Math.max(0, from - 1), from)
    if (before && !context.test(before)) return false

    // Wrap selection
    if (from !== to) {
      const selected = state.doc.textBetween(from, to)
      const tr = state.tr.replaceWith(
        from,
        to,
        state.schema.text(`$${selected}$`),
      )
      tr.setSelection(
        TextSelection.create(tr.doc, from + 1 + selected.length + 1),
      )
      view.dispatch(tr)
      return true
    }

    // Auto-close
    log.debug("dollar auto-close", {})
    const tr = state.tr.replaceWith(from, from, state.schema.text("$$"))
    tr.setSelection(TextSelection.create(tr.doc, from + 1))
    view.dispatch(tr)
    return true
  },
  pair: { open: "$", close: "$" },
}

/** Auto-close _ (single underscore → italic/emphasis) */
export const singleUnderscoreRule: AutoCloseRule = {
  name: "single-underscore",
  handler(state, view, from, to, text) {
    if (text !== "_") return false
    const $pos = state.doc.resolve(from)
    if ($pos.parent.type.spec.code) return false

    // Wrap selection — no boundary check; the user's intent is clear
    if (from !== to) {
      const selected = state.doc.textBetween(from, to)
      const tr = state.tr.replaceWith(
        from,
        to,
        state.schema.text(`_${selected}_`),
      )
      const cursorPos = from + 1 + selected.length + 1
      tr.setSelection(TextSelection.create(tr.doc, cursorPos))
      view.dispatch(tr)
      view.focus()
      return true
    }

    // Word-boundary check: don't auto-close mid-word
    const prev = state.doc.textBetween(Math.max(0, from - 1), from)
    if (/\w/.test(prev)) return false

    log.info("single underscore → italic")
    const tr = state.tr.replaceWith(from, from, state.schema.text("__"))
    const resolved = tr.doc.resolve(from + 1)
    tr.setSelection(new TextSelection(resolved))
    view.dispatch(tr)
    view.focus()
    return true
  },
}

/**
 * Redirect spaces typed inside a toggle pair (e.g. inside **text**)
 * to after the closing delimiter. If the next character is typed,
 * both the space and the character move back into the pair.
 */
// (removed — revisit later)
