package com.correx.apps.server.freestyle

import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.ExecutionPlanLockedEvent
import com.correx.core.events.events.ExecutionPlanRejectedEvent
import com.correx.core.events.events.NewEvent
import com.correx.core.events.events.PlanLintCompletedEvent
import com.correx.infrastructure.workflow.PlanLintResult
import com.correx.infrastructure.workflow.PlanLinter
import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId
import com.correx.core.kernel.execution.WorkflowResult
import com.correx.core.kernel.orchestration.OrchestrationConfig
import com.correx.core.transitions.graph.WorkflowGraph
import com.correx.infrastructure.workflow.ExecutionPlanCompiler
import kotlinx.datetime.Clock
import org.slf4j.LoggerFactory
import java.util.UUID

/**
 * Drives the second phase of a freestyle workflow: reads the validated execution_plan,
 * compiles it, emits [ExecutionPlanLockedEvent], and invokes phase-2 orchestration.
 *
 * [runPhase2] is a functional seam so the driver can be tested without the full
 * [DefaultSessionOrchestrator] constructor chain.
 */
class FreestyleDriver(
    private val eventStore: EventStore,
    private val compiler: ExecutionPlanCompiler,
    private val planContent: (SessionId) -> String?,
    private val config: OrchestrationConfig,
    private val runPhase2: suspend (SessionId, WorkflowGraph, OrchestrationConfig) -> WorkflowResult,
    private val requestPlanApproval: suspend (SessionId, String) -> Boolean = { _, _ -> true },
) {
    suspend fun lockAndRun(sessionId: SessionId) {
        val json = planContent(sessionId) ?: run {
            log.warn("freestyle: no execution_plan content for session={}", sessionId.value)
            emitRejected(sessionId, "no execution_plan content produced by planning phase", "missing_content")
            return
        }
        val graph = runCatching { compiler.compile(json, "freestyle-${sessionId.value}") }
            .getOrElse {
                log.error("freestyle: plan failed to compile: {}", it.message)
                emitRejected(sessionId, "plan failed to compile: ${it.message}", "compile")
                return
            }
        // Deterministic lint (plan-pipeline-spec §5) before the plan is surfaced/locked: a hard
        // failure (unproduced need, trap state) means the plan would fail at runtime, so reject it
        // now rather than execute a broken plan. Soft findings are recorded for display only.
        val lint = PlanLinter.lint(graph)
        emitPlanLint(sessionId, graph.id, lint)
        if (!lint.passed) {
            val summary = lint.hardFailures.joinToString("; ") {
                "${it.code}${it.stageId?.let { s -> " @$s" } ?: ""}: ${it.detail}"
            }
            log.warn("freestyle: plan failed lint for session={}: {}", sessionId.value, summary)
            emitRejected(sessionId, "plan failed lint: $summary", "lint")
            return
        }
        val approved = requestPlanApproval(sessionId, json)
        if (!approved) {
            log.info("freestyle: execution plan rejected by operator for session={}", sessionId.value)
            emitRejected(sessionId, "rejected by operator at plan review", "operator")
            return
        }
        eventStore.append(
            NewEvent(
                metadata = EventMetadata(
                    eventId = EventId(UUID.randomUUID().toString()),
                    sessionId = sessionId,
                    timestamp = Clock.System.now(),
                    schemaVersion = 1,
                    causationId = null,
                    correlationId = null,
                ),
                payload = ExecutionPlanLockedEvent(
                    sessionId = sessionId,
                    planArtifactId = ArtifactId("execution_plan"),
                    workflowId = graph.id,
                    stageIds = graph.stageIds.map { it.value },
                    startStageId = graph.start.value,
                ),
            ),
        )
        runPhase2(sessionId, graph, config)
    }

    /**
     * Recompiles the stored execution plan for resume-after-restart. Requires the
     * orchestrator's artifact cache to be rehydrated first. Null when the plan is
     * absent or no longer compiles — callers fall back to a route-level error.
     */
    fun compiledGraph(sessionId: SessionId): WorkflowGraph? {
        val json = planContent(sessionId) ?: return null
        return runCatching { compiler.compile(json, "freestyle-${sessionId.value}") }
            .onFailure { log.error("freestyle: plan recompile for resume failed: {}", it.message) }
            .getOrNull()
    }

    private suspend fun emitPlanLint(sessionId: SessionId, candidateId: String, lint: PlanLintResult) {
        eventStore.append(
            NewEvent(
                metadata = EventMetadata(
                    eventId = EventId(UUID.randomUUID().toString()),
                    sessionId = sessionId,
                    timestamp = Clock.System.now(),
                    schemaVersion = 1,
                    causationId = null,
                    correlationId = null,
                ),
                payload = PlanLintCompletedEvent(
                    sessionId = sessionId,
                    candidateId = candidateId,
                    hardFailures = lint.hardFailures,
                    softFindings = lint.softFindings,
                    score = lint.score,
                ),
            ),
        )
    }

    private suspend fun emitRejected(sessionId: SessionId, reason: String, source: String) {
        eventStore.append(
            NewEvent(
                metadata = EventMetadata(
                    eventId = EventId(UUID.randomUUID().toString()),
                    sessionId = sessionId,
                    timestamp = Clock.System.now(),
                    schemaVersion = 1,
                    causationId = null,
                    correlationId = null,
                ),
                payload = ExecutionPlanRejectedEvent(
                    sessionId = sessionId,
                    reason = reason,
                    source = source,
                ),
            ),
        )
    }

    companion object {
        private val log = LoggerFactory.getLogger(FreestyleDriver::class.java)
    }
}
