package com.correx.infrastructure.tools.extract

import org.jsoup.nodes.Element
import org.jsoup.nodes.Node
import org.jsoup.nodes.TextNode

private val HEADINGS = setOf("h1", "h2", "h3", "h4", "h5", "h6")
private val INLINE_PASSTHROUGH = setOf("span", "small", "mark", "sub", "sup", "u", "abbr", "label")
private val LIST_TAGS = setOf("ul", "ol")
private val WHITESPACE = Regex("[ \\t]+")
private val BLANK_RUN = Regex("\n{3,}")

/**
 * Converts a cleaned main-content DOM subtree to markdown (§5.2: "Output is markdown, preserving
 * headings and tables — structure the synthesis model needs"). Block elements become markdown
 * blocks; inline elements (links, emphasis, code) render in place via [InlineRenderer]. Pure,
 * deterministic.
 */
internal object MarkdownRenderer {

    fun render(root: Element): String {
        val blocks = mutableListOf<String>()
        appendBlocks(root, blocks)
        return normalize(blocks)
    }

    private fun appendBlocks(container: Element, out: MutableList<String>) {
        for (node in container.childNodes()) {
            when (node) {
                is TextNode -> node.text().trim().takeIf { it.isNotEmpty() }?.let(out::add)
                is Element -> appendElement(node, out)
            }
        }
    }

    @Suppress("CyclomaticComplexMethod")
    private fun appendElement(el: Element, out: MutableList<String>) {
        when (val tag = el.normalName()) {
            in HEADINGS -> heading(tag, el)?.let(out::add)
            "p" -> InlineRenderer.inline(el).takeIf { it.isNotBlank() }?.let(out::add)
            "pre" -> codeBlock(el)?.let(out::add)
            "blockquote" -> blockquote(el)?.let(out::add)
            "ul" -> renderList(el, ordered = false, indent = "")?.let(out::add)
            "ol" -> renderList(el, ordered = true, indent = "")?.let(out::add)
            "table" -> TableRenderer.render(el)?.let(out::add)
            "hr" -> out.add("---")
            "a", "strong", "b", "em", "i", "code", in INLINE_PASSTHROUGH ->
                InlineRenderer.inline(el).takeIf { it.isNotBlank() }?.let(out::add)
            else -> appendBlocks(el, out) // containers (div/section/article/main/…) recurse
        }
    }

    private fun heading(tag: String, el: Element): String? {
        val text = InlineRenderer.inline(el)
        if (text.isBlank()) return null
        return "#".repeat(tag.last().digitToInt()) + " " + text
    }

    private fun codeBlock(el: Element): String? {
        val code = el.wholeText().trimEnd('\n', ' ')
        return if (code.isBlank()) null else "```\n$code\n```"
    }

    private fun blockquote(el: Element): String? {
        val inner = InlineRenderer.inline(el).trim()
        return if (inner.isBlank()) null else inner.lines().joinToString("\n") { "> $it" }
    }

    private fun renderList(list: Element, ordered: Boolean, indent: String): String? {
        val items = list.children().filter { it.normalName() == "li" }
        if (items.isEmpty()) return null
        val out = mutableListOf<String>()
        items.forEachIndexed { index, li ->
            val marker = if (ordered) "${index + 1}. " else "- "
            InlineRenderer.inlineSkippingLists(li).takeIf { it.isNotBlank() }?.let { out.add(indent + marker + it) }
            li.children()
                .filter { it.normalName() in LIST_TAGS }
                .forEach { nested -> renderList(nested, nested.normalName() == "ol", "$indent  ")?.let(out::add) }
        }
        return out.takeIf { it.isNotEmpty() }?.joinToString("\n")
    }

    private fun normalize(blocks: List<String>): String {
        val deduped = mutableListOf<String>()
        blocks.map { it.trim() }.filter { it.isNotEmpty() }.forEach { block ->
            if (deduped.lastOrNull() != block) deduped.add(block) // drop consecutive repeats the strip missed
        }
        return deduped.joinToString("\n\n").replace(BLANK_RUN, "\n\n").trim()
    }
}

/** Markdown table rendering (header row + separator + body), split out to keep each unit focused. */
internal object TableRenderer {

    fun render(table: Element): String? {
        val grid = table.select("tr")
            .map { tr -> tr.select("th, td").map { cell(it) } }
            .filter { it.isNotEmpty() }
        if (grid.isEmpty()) return null
        val width = grid.maxOf { it.size }
        return buildString {
            append(row(grid.first().pad(width))).append('\n')
            append(row(grid.first().map { "---" }.pad(width, "---"))).append('\n')
            grid.drop(1).forEach { append(row(it.pad(width))).append('\n') }
        }.trimEnd()
    }

    private fun cell(el: Element): String = InlineRenderer.inline(el).replace("|", "\\|").ifBlank { " " }
    private fun row(cells: List<String>): String = "| " + cells.joinToString(" | ") + " |"
    private fun List<String>.pad(width: Int, fill: String = " "): List<String> =
        this + List((width - size).coerceAtLeast(0)) { fill }
}

/** Inline span rendering: links, emphasis, code, line breaks, images. */
internal object InlineRenderer {

    fun inline(el: Element): String = collapseSpaces(
        buildString { for (node in el.childNodes()) append(inlineNode(node)) },
    ).trim()

    /** Inline content of a list item, skipping nested lists (those render as indented sub-lists). */
    fun inlineSkippingLists(el: Element): String = collapseSpaces(
        buildString {
            for (node in el.childNodes()) {
                if (node is Element && node.normalName() in LIST_TAGS) continue
                append(inlineNode(node))
            }
        },
    ).trim()

    private fun inlineNode(node: Node): String = when (node) {
        is TextNode -> node.text()
        is Element -> inlineElement(node)
        else -> ""
    }

    private fun inlineElement(el: Element): String = when (el.normalName()) {
        "a" -> link(el)
        "strong", "b" -> inline(el).let { if (it.isBlank()) "" else "**$it**" }
        "em", "i" -> inline(el).let { if (it.isBlank()) "" else "*$it*" }
        "code" -> el.text().let { if (it.isBlank()) "" else "`$it`" }
        "br" -> "\n"
        "img" -> image(el)
        else -> inline(el)
    }

    private fun link(el: Element): String {
        val href = el.attr("href").trim()
        val text = inline(el)
        return if (href.isNotBlank() && text.isNotBlank()) "[$text]($href)" else text
    }

    private fun image(el: Element): String {
        val src = el.attr("src").trim()
        return if (src.isBlank()) "" else "![${el.attr("alt")}]($src)"
    }

    private fun collapseSpaces(text: String): String =
        text.replace(WHITESPACE, " ").lineSequence().joinToString("\n") { it.trim() }.trim()
}
