import type { SyntaxNode } from "@lezer/common"
import { GFM, parser } from "@lezer/markdown"
import { nanoid } from "nanoid"
import { contentBlockId } from "@/lib/block-id"
import { createMarkdownExtensions } from "@/lib/markdown-extensions"

export interface EditorBlockData {
  id: string
  depth: number
  content: string
}

// ── ID utilities ──────────────────────────────────────────────────

/**
 * Generate a new block identifier using a 6-character NanoID.
 * Not every block needs an ID — only blocks referenced via ((id)) do.
 */
export function generateBlockId(): string {
  return nanoid(6)
}

/**
 * Regex matching a line whose trimmed content is `id:: <value>`.
 * Scanned from the end of a block's content lines.
 */
const ID_LINE_RE = /^\s*id::\s*(\S+)\s*$/

/**
 * Extract an embedded block ID from content lines.
 * Scans from the last line upward looking for `id:: <uuid>`. If found,
 * returns the ID — the `id::` line is NOT removed from content (it's a
 * visible property line that survives round-trips automatically).
 *
 * NOTE: This operates on raw content lines without knowledge of code
 * fences (```). A line like `  id:: abc-123` inside a code block (e.g.
 * YAML, config file) will be falsely matched. In practice this is benign
 * because the matched ID still uniquely identifies the block — the
 * false match doesn't corrupt content — but it means the code-fence
 * content cannot declare its own `id::` property separate from the
 * block's identity.
 */
export function extractEmbeddedId(content: string): string | null {
  const lines = content.split("\n")
  for (let i = lines.length - 1; i >= 0; i--) {
    const line = lines[i] as string
    const match = line.match(ID_LINE_RE)
    if (match) return match[1] as string
  }
  return null
}

/**
 * Stamp a persistent `id::` property onto a block's content.
 * This is called when a block becomes referenced (by a page ref, block
 * ref, backlink, etc.) — making its ID immutable across re-parses.
 * No-op if the block already has an `id::` line.
 *
 * The `id::` line uses continuation indentation (`(depth + 1) * 2` spaces)
 * so it remains inside the list item during GFM parsing.
 */
export function stampBlockId(block: EditorBlockData): void {
  if (extractEmbeddedId(block.content)) return
  const indent = "  ".repeat(block.depth + 1)
  block.content += `\n${indent}id:: ${block.id}`
}

const markdownParser = parser.configure([GFM, ...createMarkdownExtensions()])

/**
 * Find the start of the line containing `pos` in `md`.
 */
function lineStart(md: string, pos: number): number {
  const idx = md.lastIndexOf("\n", pos - 1)
  return idx === -1 ? 0 : idx + 1
}

/**
 * Given a ListItem SyntaxNode, find where its "own content" ends —
 * i.e., the start of any nested BulletList/List child.
 * If there's no nested list, the content spans the entire node.
 */
function ownContentEnd(node: SyntaxNode): number {
  let child = node.firstChild
  while (child) {
    if (
      child.type.name === "BulletList" ||
      child.type.name === "List" ||
      child.type.name === "OrderedList"
    ) {
      return child.from
    }
    child = child.nextSibling
  }
  return node.to
}

/**
 * Parse markdown into an array of editor blocks.
 *
 * Each block corresponds to one ListItem in the Lezer AST.
 * Depth is computed from the indentation before `*`.
 * Content is everything after `* `, excluding nested child list items.
 *
 * If `existing` is provided, block IDs are preserved by position (matching
 * parsed index to `existing[i].id`). New IDs are generated for any surplus
 * parsed blocks. This prevents unnecessary Block remounts when content
 * changes externally but structural identity is unchanged.
 */
export function splitMarkdownIntoBlocks(
  md: string,
  idFn: (content: string, index: number) => string = contentBlockId,
  existing?: Pick<EditorBlockData, "id">[],
): EditorBlockData[] {
  if (!md.trim()) return []

  const tree = markdownParser.parse(md)
  const cursor = tree.cursor()

  // Collect all ListItem SyntaxNodes.
  const listItems: SyntaxNode[] = []
  do {
    if (cursor.type.name === "ListItem") {
      listItems.push(cursor.node)
    }
  } while (cursor.next())

  // Sort by position
  listItems.sort((a, b) => a.from - b.from)

  return listItems.map((node, i) => {
    const leadingSpaces = node.from - lineStart(md, node.from)
    const depth = leadingSpaces / 2
    const end = ownContentEnd(node)

    // Content: skip "* " (2 chars) after the bullet
    const contentStart = node.from + 2
    const content = md.slice(contentStart, end).trimEnd()

    // Strip GFM continuation indentation from continuation lines.
    // This mirrors serializeBlocks which adds it back on write.
    const continuationIndent = "  ".repeat(depth + 1)
    const lines = content.split("\n")
    const cleaned = lines
      .map((line, i) => {
        if (i === 0) return line
        return line.startsWith(continuationIndent)
          ? line.slice(continuationIndent.length)
          : line
      })
      .join("\n")

    // Priority 1: embedded id:: in content (survives any repositioning)
    const embeddedId = extractEmbeddedId(cleaned)
    if (embeddedId) {
      return { id: embeddedId, depth, content: cleaned }
    }

    // Priority 2: position-based preservation (prevents remount in-session)
    if (existing?.[i]?.id) {
      return { id: existing[i].id as string, depth, content: cleaned }
    }

    // Priority 3: fresh ID (deterministic by default via contentBlockId)
    return { id: idFn(cleaned, i), depth, content: cleaned }
  })
}

/**
 * Serialize an array of editor blocks back to markdown.
 *
 * Continuation lines (second and subsequent lines within a single block's
 * content) are indented by `(depth + 1) * 2` spaces so the markdown stays
 * structurally valid — continuation paragraphs remain inside the list item.
 */
export function serializeBlocks(blocks: EditorBlockData[]): string {
  return blocks
    .map((block) => {
      const indent = "  ".repeat(block.depth)
      const prefix = `${indent}* `
      const lines = block.content.split("\n")
      const continuationIndent = " ".repeat((block.depth + 1) * 2)
      return lines
        .map((line, i) => {
          if (i === 0) return `${prefix}${line}`
          if (!line.trim()) return line
          const trimmed = line.trimStart()
          return `${continuationIndent}${trimmed}`
        })
        .join("\n")
    })
    .join("\n")
}
