package com.correx.infrastructure.tools.extract

import org.jsoup.Jsoup

/**
 * Deterministic HTML → clean markdown extraction (research-workflow-spec §5). Zero inference,
 * zero network: structural strip + density extraction + content-type dispatch. The cleaned
 * markdown is the canonical fetch result destined for CAS; [ExtractionResult.extractorVersion]
 * is pinned so replay reads recorded artifacts and never re-extracts.
 *
 * Content-type dispatch (§5.3): HTML is extracted; JSON / plain text pass through unchanged;
 * everything else (binary, media, pdf) is [ExtractionQuality.REJECTED] at the header. Output below
 * [minContentLength] is [ExtractionQuality.LOW_QUALITY] so the workflow routes around dead sources
 * (JS-rendered SPAs, paywalls) instead of feeding empty pages to synthesis.
 */
class HtmlMarkdownExtractor(
    private val minContentLength: Int = DEFAULT_MIN_CONTENT_LENGTH,
) {
    fun extract(body: String, contentType: String?): ExtractionResult =
        when (classify(contentType)) {
            ContentKind.HTML -> classifyQuality(MarkdownRenderer.render(MainContentSelector.select(Jsoup.parse(body))))
            ContentKind.PASSTHROUGH -> classifyQuality(body.trim())
            ContentKind.UNSUPPORTED -> ExtractionResult(
                markdown = "",
                quality = ExtractionQuality.REJECTED,
                extractorVersion = VERSION,
                reason = "unsupported content-type: ${contentType ?: "unknown"}",
            )
        }

    private fun classifyQuality(markdown: String): ExtractionResult {
        val low = markdown.length < minContentLength
        return ExtractionResult(
            markdown = markdown,
            quality = if (low) ExtractionQuality.LOW_QUALITY else ExtractionQuality.OK,
            extractorVersion = VERSION,
            reason = if (low) "extracted ${markdown.length} chars, below minimum $minContentLength" else null,
        )
    }

    /**
     * Whether a body of this content-type can be extracted at all — the header check a fetcher runs
     * before downloading the body (§3: "binary/media rejected at the header"). Single source of
     * truth for the content-type policy, shared with the fetch tool.
     */
    fun supports(contentType: String?): Boolean = classify(contentType) != ContentKind.UNSUPPORTED

    private fun classify(contentType: String?): ContentKind {
        val type = contentType?.substringBefore(';')?.trim()?.lowercase()
        return when {
            type.isNullOrBlank() -> ContentKind.HTML // best effort: most fetched bodies are HTML
            type.contains("json") -> ContentKind.PASSTHROUGH
            type in HTML_TYPES -> ContentKind.HTML
            type.startsWith("text/") -> ContentKind.PASSTHROUGH
            else -> ContentKind.UNSUPPORTED
        }
    }

    private enum class ContentKind { HTML, PASSTHROUGH, UNSUPPORTED }

    companion object {
        /** Bump on any heuristic change. Pinned into fetch events; old artifacts keep their meaning. */
        const val VERSION = "html-md-1"
        const val DEFAULT_MIN_CONTENT_LENGTH = 200

        private val HTML_TYPES = setOf(
            "text/html", "application/xhtml+xml", "application/xml", "text/xml",
        )
    }
}
