import { TextSelection } from "prosemirror-state"
import type { EditorView } from "prosemirror-view"
import { MarkdownRuleEngine, parseMarkdown } from "@/lib/markdown-parser"
import type {
  BlockDecoration,
  DecorationRange,
  ParsedDecorations,
  TaskState,
} from "@/lib/markdown-rules"
import { schema } from "@/lib/schema"

const engine = new MarkdownRuleEngine()

// ── Config ────────────────────────────────────────────────────────

interface FormatConfig {
  parseType: string
  delim: string
}

const FORMAT_CONFIGS = {
  bold: { parseType: "strong", delim: "**" },
  italic: { parseType: "em", delim: "*" },
  code: { parseType: "code", delim: "`" },
  strikethrough: { parseType: "strike", delim: "~~" },
  highlight: { parseType: "highlight", delim: "^^" },
  inlineMath: { parseType: "inline-math", delim: "$" },
} as const

function getFormat(key: string): FormatConfig | undefined {
  return FORMAT_CONFIGS[key as keyof typeof FORMAT_CONFIGS]
}

// ── Public interface ──────────────────────────────────────────────

export interface FormattingHandlers {
  toggleBold: () => void
  toggleItalic: () => void
  toggleCode: () => void
  toggleStrikethrough: () => void
  toggleHighlight: () => void
  insertLink: (url?: string) => void
  insertInlineMath: () => void
  insertBlockMath: () => void
  insertPageRef: (pageName: string, alias?: string) => void
  insertBlockRef: (blockId: string) => void
  insertTag: (tag: string) => void
  setHeading: (level: number | null) => void
  toggleTask: (state?: string) => void
  isActive: (format: string) => boolean
  getActiveHeading: () => number | null
  getActiveTask: () => string | null
}

// ── Block helpers ─────────────────────────────────────────────────

/** Find the current textblock and return its start offset + text */
function currentBlock(
  view: EditorView,
): { blockStart: number; text: string } | null {
  const { $from } = view.state.selection
  if (!$from.parent.isTextblock) return null
  return {
    blockStart: $from.before() + 1,
    text: $from.parent.textContent,
  }
}

// ── isActive ──────────────────────────────────────────────────────

export function isFormatActive(view: EditorView, format: string): boolean {
  const config = getFormat(format)
  if (!config) return false
  const block = currentBlock(view)
  if (!block) return false
  const cursorInBlock = view.state.selection.$from.parentOffset
  const parsed = parseMarkdown(block.text, engine)
  for (const token of parsed.content) {
    if (token.type === config.parseType) {
      for (const range of token.decorationRanges) {
        if (
          range.type === "content" &&
          range.from <= cursorInBlock &&
          cursorInBlock <= range.to
        ) {
          return true
        }
      }
    }
  }
  return false
}

// ── Find token at cursor in parsed result ──────────────────────────

function findActiveToken(
  parsed: ParsedDecorations,
  config: FormatConfig,
  cursorInBlock: number,
): { ranges: DecorationRange[] } | null {
  for (const token of parsed.content) {
    if (token.type === config.parseType) {
      for (const range of token.decorationRanges) {
        if (
          range.type === "content" &&
          range.from <= cursorInBlock &&
          cursorInBlock <= range.to
        ) {
          return { ranges: token.decorationRanges }
        }
      }
    }
  }
  return null
}

// ── Toggle inline format ──────────────────────────────────────────

function toggleFormat(view: EditorView, config: FormatConfig) {
  const block = currentBlock(view)
  if (!block) return

  const cursorInBlock = view.state.selection.$from.parentOffset
  const parsed = parseMarkdown(block.text, engine)
  const active = findActiveToken(parsed, config, cursorInBlock)

  // Unwrap
  if (active) {
    const tr = view.state.tr
    const hidden = active.ranges.filter((r) => r.type === "hidden")
    hidden.sort((a, b) => b.from - a.from)
    for (const range of hidden) {
      tr.delete(block.blockStart + range.from, block.blockStart + range.to)
    }
    view.dispatch(tr)
    return
  }

  const { from, to, empty } = view.state.selection

  // Wrap selection
  if (!empty) {
    const selected = view.state.doc.textBetween(from, to)
    const tr = view.state.tr
    tr.replaceWith(
      from,
      to,
      view.state.schema.text(config.delim + selected + config.delim),
    )
    const cursorPos =
      from + config.delim.length + selected.length + config.delim.length
    tr.setSelection(TextSelection.create(tr.doc, cursorPos))
    view.dispatch(tr)
    return
  }

  // Insert empty delimiters
  const cursor = view.state.selection.head
  const tr = view.state.tr
  tr.replaceWith(
    cursor,
    cursor,
    view.state.schema.text(config.delim + config.delim),
  )
  tr.setSelection(TextSelection.create(tr.doc, cursor + config.delim.length))
  view.dispatch(tr)
}

// ── Exported handlers ─────────────────────────────────────────────

export function toggleBold(view: EditorView) {
  toggleFormat(view, FORMAT_CONFIGS.bold)
}

export function toggleItalic(view: EditorView) {
  toggleFormat(view, FORMAT_CONFIGS.italic)
}

export function toggleCode(view: EditorView) {
  toggleFormat(view, FORMAT_CONFIGS.code)
}

export function toggleStrikethrough(view: EditorView) {
  toggleFormat(view, FORMAT_CONFIGS.strikethrough)
}

export function toggleHighlight(view: EditorView) {
  toggleFormat(view, FORMAT_CONFIGS.highlight)
}

export function insertInlineMath(view: EditorView) {
  toggleFormat(view, FORMAT_CONFIGS.inlineMath)
}

export function insertBlockMath(view: EditorView) {
  const { $from } = view.state.selection
  const init = "$$\n\n$$"
  const mathBlock = schema.nodes.math_block.create(null, [schema.text(init)])

  if ($from.parent.content.size === 0) {
    const pos = $from.before()
    const tr = view.state.tr.replaceWith(
      pos,
      pos + $from.parent.nodeSize,
      mathBlock,
    )
    const cursorPos = pos + 1 + "$$".length
    tr.setSelection(TextSelection.create(tr.doc, cursorPos))
    view.dispatch(tr)
    view.focus()
    return
  }

  const pos = $from.after()
  const tr = view.state.tr.insert(pos, mathBlock)
  const cursorPos = pos + 1 + "$$".length
  tr.setSelection(TextSelection.create(tr.doc, cursorPos))
  view.dispatch(tr)
  view.focus()
}

// ── insertLink ────────────────────────────────────────────────────

export function insertLink(view: EditorView, url?: string) {
  const { from, to, empty } = view.state.selection
  const placeholderUrl = url ?? "url"
  const placeholderText = "link"

  if (!empty) {
    const selected = view.state.doc.textBetween(from, to)
    const text = `[${selected}](${placeholderUrl})`
    const tr = view.state.tr
    tr.replaceWith(from, to, view.state.schema.text(text))
    if (!url) {
      const urlStart = from + selected.length + 3
      const urlEnd = urlStart + placeholderUrl.length
      tr.setSelection(TextSelection.create(tr.doc, urlStart, urlEnd))
    } else {
      tr.setSelection(TextSelection.create(tr.doc, from + text.length))
    }
    view.dispatch(tr)
    return
  }

  const cursor = view.state.selection.head
  const text = `[${placeholderText}](${placeholderUrl})`
  const tr = view.state.tr
  tr.replaceWith(cursor, cursor, view.state.schema.text(text))
  if (!url) {
    const textStart = cursor + 1
    const textEnd = textStart + placeholderText.length
    tr.setSelection(TextSelection.create(tr.doc, textStart, textEnd))
  } else {
    tr.setSelection(TextSelection.create(tr.doc, cursor + text.length))
  }
  view.dispatch(tr)
}

// ── insertPageRef ─────────────────────────────────────────────────

export function insertPageRef(
  view: EditorView,
  pageName: string,
  alias?: string,
) {
  if (!pageName) return
  // The explicit pageName wins over any selection text — unlike insertLink
  // which uses the selection as link text, this handler is called after the
  // user has already chosen a page from a suggestion menu, so the chosen
  // name is authoritative and the selection is just a position marker.
  const { from, to, empty } = view.state.selection
  const ref = alias ? `[[${pageName}|${alias}]]` : `[[${pageName}]]`

  if (!empty) {
    const tr = view.state.tr
    tr.replaceWith(from, to, view.state.schema.text(ref))
    tr.setSelection(TextSelection.create(tr.doc, from + ref.length))
    view.dispatch(tr)
    return
  }

  const cursor = view.state.selection.head
  const tr = view.state.tr
  tr.replaceWith(cursor, cursor, view.state.schema.text(ref))
  tr.setSelection(TextSelection.create(tr.doc, cursor + ref.length))
  view.dispatch(tr)
}

// ── insertBlockRef ─────────────────────────────────────────────────

export function insertBlockRef(view: EditorView, blockId: string) {
  if (!blockId) return
  const { from, to, empty } = view.state.selection
  const ref = `((${blockId}))`

  if (!empty) {
    const tr = view.state.tr
    tr.replaceWith(from, to, view.state.schema.text(ref))
    tr.setSelection(TextSelection.create(tr.doc, from + ref.length))
    view.dispatch(tr)
    return
  }

  const cursor = view.state.selection.head
  const tr = view.state.tr
  tr.replaceWith(cursor, cursor, view.state.schema.text(ref))
  tr.setSelection(TextSelection.create(tr.doc, cursor + ref.length))
  view.dispatch(tr)
}

// ── insertTag ──────────────────────────────────────────────────────

export function insertTag(view: EditorView, tag: string) {
  if (!tag || tag.includes(" ")) return
  // Like insertPageRef, the explicit tag wins over selection text. The
  // handler is called after the user picks a tag from a menu; the selection
  // is just a position marker. If the selected text contains spaces, bail
  // out since that would produce invalid syntax regardless.
  const { from, to, empty } = view.state.selection
  const ref = `#${tag}`

  if (!empty) {
    const selected = view.state.doc.textBetween(from, to)
    if (selected.includes(" ")) return
    const tr = view.state.tr
    tr.replaceWith(from, to, view.state.schema.text(ref))
    tr.setSelection(TextSelection.create(tr.doc, from + ref.length))
    view.dispatch(tr)
    return
  }

  const cursor = view.state.selection.head
  const tr = view.state.tr
  tr.replaceWith(cursor, cursor, view.state.schema.text(ref))
  tr.setSelection(TextSelection.create(tr.doc, cursor + ref.length))
  view.dispatch(tr)
}

// ── Heading state ─────────────────────────────────────────────────

export function getActiveHeading(view: EditorView): number | null {
  const block = currentBlock(view)
  if (!block) return null
  const parsed = parseMarkdown(block.text, engine)
  const heading = parsed.blocks.find((b) => b.type === "heading")
  return heading?.metadata?.level ?? null
}

const HEADING_RE = /^(#{1,6}) ?/

export function setHeading(view: EditorView, level: number | null) {
  const block = currentBlock(view)
  if (!block) return

  const currentLevel = getActiveHeading(view)

  if (level === null || level === currentLevel) {
    const match = HEADING_RE.exec(block.text)
    if (match) {
      const tr = view.state.tr
      tr.delete(block.blockStart, block.blockStart + match[0].length)
      view.dispatch(tr)
    }
    return
  }

  const prefix = `${"#".repeat(level)} `
  const existing = HEADING_RE.exec(block.text)
  if (existing) {
    const tr = view.state.tr
    tr.replaceWith(
      block.blockStart,
      block.blockStart + existing[0].length,
      view.state.schema.text(prefix),
    )
    view.dispatch(tr)
  } else {
    const tr = view.state.tr
    tr.replaceWith(
      block.blockStart,
      block.blockStart,
      view.state.schema.text(prefix),
    )
    view.dispatch(tr)
  }
}

// ── toggleTask ────────────────────────────────────────────────────

const TASK_CYCLE: TaskState[] = ["TODO", "DOING", "DONE"]

function parseCurrentTask(
  view: EditorView,
): { state: TaskState; token: BlockDecoration } | null {
  const block = currentBlock(view)
  if (!block) return null
  const parsed = parseMarkdown(block.text, engine)
  for (const b of parsed.blocks) {
    if (b.type === "task") {
      const ts = b.metadata?.taskState
      if (ts) return { state: ts, token: b }
    }
  }
  return null
}

export function getActiveTask(view: EditorView): string | null {
  return parseCurrentTask(view)?.state ?? null
}

/**
 * Find the start of the task content (after the marker keyword and its
 * trailing separator).  The decoration ranges from createTaskRule give us
 * the marker keyword range and the content range starting at contentStart.
 */
function taskContentStart(token: BlockDecoration): number | null {
  const contentRange = token.decorationRanges.find(
    (r) => r.className === "md-task-content",
  )
  return contentRange?.from ?? null
}

export function toggleTask(view: EditorView, state?: TaskState) {
  const block = currentBlock(view)
  if (!block) return

  const active = parseCurrentTask(view)
  const markerRange = active?.token.decorationRanges.find(
    (r) => r.className === "md-task-marker",
  )

  if (active && markerRange) {
    const markerFrom = block.blockStart + markerRange.from
    const contentFrom = taskContentStart(active.token)
    const markerTo = block.blockStart + markerRange.to
    const deleteTo =
      contentFrom != null ? block.blockStart + contentFrom : markerTo + 1

    if (state) {
      if (state === active.state) {
        const tr = view.state.tr
        tr.delete(markerFrom, deleteTo)
        view.dispatch(tr)
        return
      }
      const tr = view.state.tr
      tr.replaceWith(markerFrom, markerTo, view.state.schema.text(state))
      view.dispatch(tr)
      return
    }

    const idx = TASK_CYCLE.indexOf(active.state)
    if (idx === -1 || idx === TASK_CYCLE.length - 1) {
      const tr = view.state.tr
      tr.delete(markerFrom, deleteTo)
      view.dispatch(tr)
      return
    }

    const next = TASK_CYCLE[idx + 1] as TaskState
    const tr = view.state.tr
    tr.replaceWith(markerFrom, markerTo, view.state.schema.text(next))
    view.dispatch(tr)
    return
  }

  const tr = view.state.tr
  tr.replaceWith(
    block.blockStart,
    block.blockStart,
    view.state.schema.text("TODO "),
  )
  view.dispatch(tr)
}
