/**
 * Markdown rule engine.
 *
 * Parses each paragraph through a Lezer AST walk:
 *   1. Block rules collect BlockDecoration from tree block nodes
 *      (headings, blockquotes, tasks, callouts).
 *   2. Inline rules collect TokenDecoration from tree inline nodes
 *      (bold, links, tags, etc.).
 *
 * All positions in decorationRanges are absolute within docText.
 */

import type { Parser, Tree } from "@lezer/common"
import { TreeFragment } from "@lezer/common"
import { createLogger } from "@/lib/logger"
import { createBlockRules } from "@/lib/markdown-rules/block-rules"
import { createInlineRules } from "@/lib/markdown-rules/inline-rules"
import type {
  BlockDecoration,
  BlockRule,
  MarkdownRule,
  ParseContext,
  ParsedDecorations,
  PropertyInfo,
} from "@/lib/markdown-rules/types"

const log = createLogger("ParseEngine")

// ── Rule index builders ───────────────────────────────────────────────

function buildRuleIndex(
  rules: ReadonlyArray<MarkdownRule<unknown>>,
): ReadonlyMap<string, ReadonlyArray<MarkdownRule<unknown>>> {
  const index = new Map<string, MarkdownRule<unknown>[]>()
  for (const rule of rules) {
    for (const nodeType of rule.nodeTypes ?? []) {
      const bucket = index.get(nodeType)
      if (bucket) bucket.push(rule)
      else index.set(nodeType, [rule])
    }
  }
  return index
}

function buildBlockRuleIndex(
  rules: ReadonlyArray<BlockRule>,
): ReadonlyMap<string, ReadonlyArray<BlockRule>> {
  const index = new Map<string, BlockRule[]>()
  for (const rule of rules) {
    for (const nodeType of rule.nodeTypes) {
      const bucket = index.get(nodeType)
      if (bucket) bucket.push(rule)
      else index.set(nodeType, [rule])
    }
  }
  return index
}

// ── Property parsing (single-line) ───────────────────────────────────

const PROPERTY_RE = /^(\w[\w-]*)::([^\n]*)/

function parseProperties(text: string): PropertyInfo[] {
  const match = text.match(PROPERTY_RE)
  if (!match) return []
  // biome-ignore lint/style/noNonNullAssertion: match confirmed by PROPERTY_RE
  const key = match[1]!
  // biome-ignore lint/style/noNonNullAssertion: match confirmed by PROPERTY_RE
  const rawValue = match[2]!
  const keyEnd = key.length
  const delimiterEnd = keyEnd + 2
  return [
    {
      key,
      value: rawValue.trim(),
      keyRange: [0, keyEnd],
      delimiterRange: [keyEnd, delimiterEnd],
      valueRange: [delimiterEnd, delimiterEnd + rawValue.length],
    },
  ]
}

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

// ── Engine ───────────────────────────────────────────────────────────

export class MarkdownRuleEngine {
  private readonly inlineRules: ReadonlyArray<MarkdownRule<unknown>>
  private readonly inlineRuleIndex: ReadonlyMap<
    string,
    ReadonlyArray<MarkdownRule<unknown>>
  >
  private readonly blockRules: ReadonlyArray<BlockRule>
  private readonly blockRuleIndex: ReadonlyMap<string, ReadonlyArray<BlockRule>>

  /**
   * @param options.extraInlineRules - Additional inline rules appended after
   *   the built-in ones (bold, italic, code, links, refs, tags, math, highlight).
   * @param options.extraBlockRules - Additional block rules appended after
   *   the built-in ones (headings, blockquotes, tasks, tables, callouts, hr).
   */
  constructor(options?: {
    extraInlineRules?: MarkdownRule<unknown>[]
    extraBlockRules?: BlockRule[]
  }) {
    this.inlineRules = [
      ...createInlineRules(),
      ...(options?.extraInlineRules ?? []),
    ]
    this.inlineRuleIndex = buildRuleIndex(this.inlineRules)
    this.blockRules = [
      ...createBlockRules(),
      ...(options?.extraBlockRules ?? []),
    ]
    this.blockRuleIndex = buildBlockRuleIndex(this.blockRules)
  }

  parse(
    docText: string,
    markdownParser: Parser,
    oldTree?: Tree,
  ): ParsedDecorations & { tree: Tree } {
    const content: ParsedDecorations["content"] = []
    const blocks: BlockDecoration[] = []
    const properties: PropertyInfo[] = []

    if (!docText) {
      return {
        content,
        properties,
        blocks,
        tree: markdownParser.parse(docText),
      }
    }

    // ── Phase 1: Lezer AST walk ─────────────────────────────────────
    const ctx: ParseContext = { doc: docText }
    const startTime = performance.now()
    const fragments = oldTree ? TreeFragment.addTree(oldTree) : undefined
    const tree: Tree = markdownParser.parse(docText, fragments)
    const parseTime = performance.now() - startTime

    const visitedNodeTypes: string[] = []

    tree.iterate({
      enter: (ref) => {
        const node = ref.node
        const nodeType = node.type.name
        visitedNodeTypes.push(nodeType)

        log.info("ENTER", {
          type: nodeType,
          from: node.from,
          to: node.to,
          text: JSON.stringify(
            node.to - node.from <= 80
              ? docText.slice(node.from, node.to)
              : "...",
          ),
        })

        const blockCandidates = this.blockRuleIndex.get(nodeType)
        if (blockCandidates) {
          for (const rule of blockCandidates) {
            if (rule.match(node, ctx)) {
              const token = rule.run(node, ctx)
              if (token) blocks.push(token)
              break
            }
          }
        }
      },
      leave: (ref) => {
        const node = ref.node
        const nodeType = node.type.name

        log.info("LEAVE", { type: nodeType })

        if (this.blockRuleIndex.has(nodeType)) return

        const inlineCandidates = this.inlineRuleIndex.get(nodeType)
        if (inlineCandidates) {
          for (const rule of inlineCandidates) {
            if (rule.match(node, ctx)) {
              const token = rule.run(node, ctx)
              if (token) {
                content.push(token as ParsedDecorations["content"][number])
              }
              break
            }
          }
        }
      },
    })

    // ── Phase 2: Property extraction (per line) ────────────────────
    const lines = docText.split("\n")
    let lineOffset = 0
    for (const line of lines) {
      const lineProps = parseProperties(line)
      for (const prop of lineProps) {
        prop.keyRange[0] += lineOffset
        prop.keyRange[1] += lineOffset
        prop.delimiterRange[0] += lineOffset
        prop.delimiterRange[1] += lineOffset
        prop.valueRange[0] += lineOffset
        prop.valueRange[1] += lineOffset
        properties.push(prop)
      }
      lineOffset += line.length + 1
    }

    const totalTime = performance.now() - startTime
    log.debug("parse", {
      docLen: docText.length,
      parseTimeMs: parseTime.toFixed(2),
      totalTimeMs: totalTime.toFixed(2),
      blocks: blocks.length,
      inlineTokens: content.length,
      properties: properties.length,
      incremental: !!oldTree,
      tree: visitedNodeTypes.join("→"),
      blockTypes: blocks.map((b: any) => b.type).join(","),
      inlineTypes: content.map((c: any) => c.type).join(","),
    })
    return { content, properties, blocks, tree }
  }
}
