package com.correx.apps.server.inference

import com.correx.core.context.model.CompressionMetadata
import com.correx.core.context.model.ContextEntry
import com.correx.core.context.model.ContextLayer
import com.correx.core.context.model.ContextPack
import com.correx.core.context.model.EntryRole
import com.correx.core.events.events.ReviewFinding
import com.correx.core.events.events.ReviewSeverity
import com.correx.core.events.events.ReviewVerdict
import com.correx.core.events.types.ContextEntryId
import com.correx.core.events.types.ContextPackId
import com.correx.core.events.types.InferenceRequestId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.inference.GenerationConfig
import com.correx.core.inference.InferenceRequest
import com.correx.core.inference.InferenceRouter
import com.correx.core.inference.ModelCapability
import com.correx.core.inference.ResponseFormat
import com.correx.core.kernel.orchestration.ReviewOutcome
import com.correx.core.kernel.orchestration.SemanticReviewer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.slf4j.LoggerFactory
import java.nio.file.Path
import java.util.UUID
import kotlin.io.path.readText

private const val MAX_FILE_CHARS = 8_000
private const val MAX_TOTAL_CHARS = 24_000
// Reviewer runs on a reasoning-capable model (gemma) that spends a large token budget thinking
// before it emits the final JSON verdict. 1024 truncated it mid-reasoning → empty content →
// parse degraded to PASS. Give it real headroom so it reaches the JSON.
private const val REVIEW_MAX_TOKENS = 8_192

/**
 * Gate 3 reviewer implementation: reads the stage's actually-produced files from the workspace (the
 * FileWritten manifest — never blind context), asks a review-capable model for PR-comment-style
 * findings as JSON, and maps them to [ReviewOutcome]. Talks to the provider directly with a
 * self-contained prompt (like [summarizeWithInference]) rather than the orchestrator's contextPack
 * machinery. Any failure (unroutable, timeout, unparseable output) degrades to a PASS with no
 * findings — the reviewer is advisory and must never wedge a stage on its own error.
 */
class SemanticReviewerImpl(
    private val inferenceRouter: InferenceRouter,
    private val modelId: String? = null,
) : SemanticReviewer {

    override suspend fun review(
        sessionId: SessionId,
        stageId: StageId,
        workspaceRoot: Path,
        files: List<String>,
        objective: String,
    ): ReviewOutcome {
        val bundle = renderFiles(workspaceRoot, files)
        if (bundle.isBlank()) return ReviewOutcome(ReviewVerdict.PASS, emptyList())

        return runCatching {
            val provider = inferenceRouter.route(stageId, setOf(ModelCapability.General), modelId)
            val prompt = buildPrompt(objective, bundle)
            val request = InferenceRequest(
                requestId = InferenceRequestId(UUID.randomUUID().toString()),
                sessionId = sessionId,
                stageId = stageId,
                contextPack = pack(sessionId, stageId, prompt),
                generationConfig = GenerationConfig(temperature = 0.2, topP = 0.9, maxTokens = REVIEW_MAX_TOKENS),
                responseFormat = ResponseFormat.Text,
            )
            parse(provider.infer(request).text)
        }.getOrElse {
            log.warn(
                "[SemanticReviewer] review failed session={} stage={}: {}",
                sessionId.value, stageId.value, it.message,
            )
            ReviewOutcome(ReviewVerdict.PASS, emptyList())
        }
    }

    private fun renderFiles(workspaceRoot: Path, files: List<String>): String {
        val sb = StringBuilder()
        for (rel in files) {
            if (sb.length >= MAX_TOTAL_CHARS) break
            val text = runCatching { workspaceRoot.resolve(rel).readText() }.getOrNull()
            if (text != null) {
                sb.append("=== FILE: ").append(rel).append(" ===\n")
                    .append(text.take(MAX_FILE_CHARS)).append("\n\n")
            }
        }
        return sb.toString().take(MAX_TOTAL_CHARS)
    }

    private fun buildPrompt(objective: String, bundle: String): String =
        """
        You are a senior code reviewer performing a PR review. The deterministic checks (compile,
        contract, build) have ALREADY passed — do NOT report anything a compiler or linter would catch.
        Review ONLY for correctness bugs, logic errors, security issues, and clear design/smell problems.

        STAGE OBJECTIVE:
        $objective

        FILES PRODUCED:
        $bundle

        Respond with ONLY a JSON object, no prose, no markdown fence:
        {"verdict":"PASS|WARN|FAIL","findings":[
          {"severity":"CRITICAL|MAJOR|MINOR|NIT","confidence":0.0-1.0,"category":"correctness|smell|architecture|naming|security",
           "target":"path/to/file.ext","message":"what is wrong","suggestedFix":"how to fix (optional)","correctness":true|false}
        ]}
        Set correctness=true only when the code is actually WRONG (a confirmable bug), false for
        subjective/suboptimal opinions. Use verdict FAIL only when at least one correctness finding is
        high-confidence. Empty findings + PASS when the code is sound.
        """.trimIndent()

    private fun pack(sessionId: SessionId, stageId: StageId, prompt: String) = ContextPack(
        id = ContextPackId(UUID.randomUUID().toString()),
        sessionId = sessionId,
        stageId = stageId,
        layers = mapOf(
            ContextLayer.L0 to listOf(
                ContextEntry(
                    id = ContextEntryId(UUID.randomUUID().toString()),
                    layer = ContextLayer.L0,
                    content = prompt,
                    sourceType = "semantic_review",
                    sourceId = "reviewer",
                    tokenEstimate = prompt.length / 4,
                    role = EntryRole.USER,
                ),
            ),
        ),
        budgetUsed = prompt.length / 4,
        budgetLimit = prompt.length / 4,
        compressionMetadata = CompressionMetadata(),
    )

    private fun parse(raw: String): ReviewOutcome {
        val pass = ReviewOutcome(ReviewVerdict.PASS, emptyList())
        val dto = extractJson(raw)
            ?.let { json -> runCatching { lenient.decodeFromString(ReviewDto.serializer(), json) }.getOrNull() }
            ?: return pass
        val verdict = runCatching { ReviewVerdict.valueOf(dto.verdict.uppercase()) }.getOrDefault(ReviewVerdict.PASS)
        val findings = dto.findings.map { f ->
            ReviewFinding(
                severity = runCatching { ReviewSeverity.valueOf(f.severity.uppercase()) }
                    .getOrDefault(ReviewSeverity.MINOR),
                confidence = f.confidence.coerceIn(0.0, 1.0),
                category = f.category.ifBlank { "correctness" },
                target = f.target,
                message = f.message,
                suggestedFix = f.suggestedFix?.takeIf { it.isNotBlank() },
                correctness = f.correctness,
            )
        }
        return ReviewOutcome(verdict, findings)
    }

    /** Strip an optional ```json fence and slice from the first { to the last } — tolerant of prose wrap. */
    private fun extractJson(raw: String): String? {
        val start = raw.indexOf('{')
        val end = raw.lastIndexOf('}')
        return if (start in 0 until end) raw.substring(start, end + 1) else null
    }

    @Serializable
    private data class ReviewDto(
        val verdict: String = "PASS",
        val findings: List<FindingDto> = emptyList(),
    )

    @Serializable
    private data class FindingDto(
        val severity: String = "MINOR",
        val confidence: Double = 0.5,
        val category: String = "correctness",
        val target: String = "",
        val message: String = "",
        @SerialName("suggestedFix") val suggestedFix: String? = null,
        val correctness: Boolean = false,
    )

    companion object {
        private val log = LoggerFactory.getLogger(SemanticReviewerImpl::class.java)
        private val lenient = Json { ignoreUnknownKeys = true; isLenient = true }
    }
}
