package com.correx.core.validation.artifact

import com.correx.core.artifacts.kind.FileWrittenArtifact
import com.correx.core.artifacts.kind.ProcessResultArtifact
import com.correx.core.artifacts.kind.TypedArtifactSlot
import com.correx.core.events.types.ArtifactId
import com.correx.core.validation.model.ValidationContext
import com.correx.core.validation.model.ValidationIssue
import com.correx.core.validation.model.ValidationSection
import com.correx.core.validation.model.ValidationSeverity
import com.correx.core.validation.pipeline.Validator
import kotlinx.serialization.json.Json
import java.nio.file.Path

class ArtifactPayloadValidator : Validator {

    override suspend fun validate(context: ValidationContext): ValidationSection {
        val issues = mutableListOf<ValidationIssue>()

        for ((_, stageConfig) in context.graph.stages) {
            for (slot in stageConfig.produces) {
                validateSlot(slot, context.producedArtifactContent, issues)
            }
        }

        return ValidationSection(name = "artifact_payload", issues = issues)
    }

    private suspend fun validateSlot(
        slot: TypedArtifactSlot,
        artifactContent: Map<String, String>,
        issues: MutableList<ValidationIssue>,
    ) {
        val payloadStr = artifactContent[slot.name.value] ?: return

        when (slot.kind.id) {
            "file_written" ->
                validateSlotAsFileWritten(slot.name, slot.kind.id, payloadStr, issues)
            "process_result" ->
                validateSlotAsProcessResult(slot.name, slot.kind.id, payloadStr, issues)
            // Config-declared kinds: validate generically against the declared schema, so an
            // LLM-emitted custom kind is checked against its shape rather than trusted.
            else ->
                validateSlotAgainstSchema(slot, payloadStr, issues)
        }
    }

    private fun validateSlotAgainstSchema(
        slot: TypedArtifactSlot,
        payloadStr: String,
        issues: MutableList<ValidationIssue>,
    ) {
        val json = runCatching { Json.parseToJsonElement(payloadStr) }.getOrElse {
            issues += decodeFailedIssue(slot.name, slot.kind.id, it.message)
            return
        }
        JsonSchemaValidator.validate(slot.kind.deriveJsonSchema(), json)
            .forEach { issues += semanticIssue(slot.name, it) }
    }

    private fun validateSlotAsFileWritten(
        slotName: ArtifactId,
        kindId: String,
        payloadStr: String,
        issues: MutableList<ValidationIssue>,
    ) {
        val artifact = runCatching {
            Json.decodeFromString(FileWrittenArtifact.serializer(), payloadStr)
        }.getOrElse {
            issues += decodeFailedIssue(slotName, kindId, it.message)
            return
        }
        validateFileWritten(artifact, slotName, issues)
    }

    private fun validateSlotAsProcessResult(
        slotName: ArtifactId,
        kindId: String,
        payloadStr: String,
        issues: MutableList<ValidationIssue>,
    ) {
        val artifact = runCatching {
            Json.decodeFromString(ProcessResultArtifact.serializer(), payloadStr)
        }.getOrElse {
            issues += decodeFailedIssue(slotName, kindId, it.message)
            return
        }
        validateProcessResult(artifact, slotName, issues)
    }

    private fun validateFileWritten(
        artifact: FileWrittenArtifact,
        name: ArtifactId,
        issues: MutableList<ValidationIssue>,
    ) {
        if (artifact.path.isBlank()) issues += semanticIssue(name, "path is blank")
        if (Path.of(artifact.path).isAbsolute) issues += semanticIssue(name, "path must be relative")
        if (artifact.path.contains("..")) issues += semanticIssue(name, "path contains '..'")
        if (artifact.contentHash.isBlank()) issues += semanticIssue(name, "contentHash is blank")
        // content non-empty cannot be checked here: FileWrittenArtifact is the canonical (CAS) form
        // which contains only the hash, not the raw content bytes.
        if (!Regex("^0?[0-7]{3,4}$").matches(artifact.mode)) {
            issues += semanticIssue(name, "mode '${artifact.mode}' is not a valid octal permission string")
        }
    }

    private fun validateProcessResult(
        artifact: ProcessResultArtifact,
        name: ArtifactId,
        issues: MutableList<ValidationIssue>,
    ) {
        if (artifact.command.isBlank()) issues += semanticIssue(name, "command is blank")
    }

    private fun decodeFailedIssue(name: ArtifactId, kindId: String, errorMessage: String?) = ValidationIssue(
        code = "ARTIFACT_DECODE_FAILED",
        message = "Artifact '${name.value}' failed to decode as $kindId: $errorMessage",
        severity = ValidationSeverity.ERROR,
    )

    private fun semanticIssue(name: ArtifactId, message: String) = ValidationIssue(
        code = "ARTIFACT_SEMANTIC_INVALID",
        message = "Artifact '${name.value}': $message",
        severity = ValidationSeverity.ERROR,
    )
}
