package com.correx.core.validation.artifact

import com.correx.core.artifacts.kind.JsonSchema
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive

/**
 * Pure, deterministic extraction ladder for LLM artifact output. Turns near-miss text
 * (prose-wrapped JSON, code fences, trailing commas, unquoted keys) into a canonical, schema-shaped
 * [JsonObject], or reports why it couldn't. No coroutines, no inference — so replay recomputes it
 * (CLAUDE.md #8) and it records no events.
 *
 * The rare LLM-repair rung (spec §5/§8) is intentionally NOT here: it touches inference and is
 * driven by the orchestrator when this returns [Unresolved].
 *
 * See docs/specs/2026-07-04-artifact-emission-pipeline.md.
 */
class ArtifactExtractionPipeline {

    sealed interface ExtractionResult {
        /** [repaired] = the raw text was NOT already clean; a repair transform was needed to resolve it. */
        data class Resolved(val canonicalJson: JsonObject, val repaired: Boolean) : ExtractionResult
        data class Unresolved(
            val classification: ArtifactFailure,
            val bestCandidate: String?, // most-repaired text, handed to the LLM-repair rung
            val detail: String,
        ) : ExtractionResult
    }

    fun run(raw: String, schema: JsonSchema): ExtractionResult {
        val trimmed = raw.trim()
        // Fast path: already-clean, schema-valid JSON needs no repair (and emits no repair event).
        parse(trimmed)?.let { direct ->
            val canonical = canonicalize(direct, schema)
            if (JsonSchemaValidator.validate(schema, canonical).isEmpty()) {
                return ExtractionResult.Resolved(canonical, repaired = false)
            }
        }
        var candidate = trimmed
        for (repair in ArtifactRepairs.ladder) {
            candidate = repair(candidate)
            val parsed = parse(candidate) ?: continue
            val canonical = canonicalize(parsed, schema)
            if (JsonSchemaValidator.validate(schema, canonical).isEmpty()) {
                return ExtractionResult.Resolved(canonical, repaired = true)
            }
        }
        val best = parse(candidate) ?: parse(trimmed)
        val issues = best?.let { JsonSchemaValidator.validate(schema, canonicalize(it, schema)) } ?: emptyList()
        return ExtractionResult.Unresolved(
            classification = classify(candidate, best, issues),
            bestCandidate = candidate,
            detail = issues.takeIf { it.isNotEmpty() }?.joinToString("; ")
                ?: "could not extract a JSON object from the output",
        )
    }

    /** Fill in schema-declared defaults for absent properties. */
    private fun canonicalize(obj: JsonObject, schema: JsonSchema): JsonObject {
        val filled = obj.toMutableMap()
        for ((key, prop) in schema.properties) {
            if (key !in filled && prop.default != null) filled[key] = JsonPrimitive(prop.default)
        }
        return JsonObject(filled)
    }

    private fun parse(text: String): JsonObject? =
        runCatching { JSON.parseToJsonElement(text) as? JsonObject }.getOrNull()

    private companion object {
        val JSON = Json { ignoreUnknownKeys = true; isLenient = true }
    }
}
