package com.correx.apps.server.metrics

import com.correx.core.events.events.ApprovalDecisionResolvedEvent
import com.correx.core.events.events.ApprovalRequestedEvent
import com.correx.core.events.events.BriefEchoMismatchEvent
import com.correx.core.events.events.CapabilityGapReflectedEvent
import com.correx.core.events.events.CapabilityGapVerdict
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.InferenceCompletedEvent
import com.correx.core.events.events.InferenceFailedEvent
import com.correx.core.events.events.InferenceTimeoutEvent
import com.correx.core.events.events.LowQualityExtractionEvent
import com.correx.core.events.events.RepoKnowledgeHitsFilteredEvent
import com.correx.core.events.events.RepoKnowledgeRetrievedEvent
import com.correx.core.events.events.SourceFetchedEvent
import com.correx.core.events.events.StageFailedEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.events.ToolExecutionCompletedEvent
import com.correx.core.events.events.ToolExecutionFailedEvent
import com.correx.core.events.events.ToolExecutionRejectedEvent
import com.correx.core.events.events.WorkflowFailedEvent
import com.correx.core.sessions.projections.Projection
import kotlinx.datetime.Instant

/**
 * Pure fold deriving [MetricsState] from a session's event log
 * (observability-spec §2: observability-as-projection — no separate trace store, no OTel).
 *
 * Every metric here is replayable: a metric added later can be recomputed against events
 * recorded before it existed. This projection records no new events and reads only events
 * other bounded contexts already emit; it is a disposable read-model (Hard Invariant #2).
 */
class MetricsProjection : Projection<MetricsState> {

    override fun initial(): MetricsState = MetricsState()

    override fun apply(state: MetricsState, event: StoredEvent): MetricsState {
        val at = event.metadata.timestamp
        val spanned = state.copy(
            firstEventAt = state.firstEventAt ?: at,
            lastEventAt = at,
            eventCount = state.eventCount + 1,
        )
        return when (val payload = event.payload) {
            is InferenceCompletedEvent -> spanned.applyInferenceCompleted(payload)
            is InferenceFailedEvent -> spanned.bumpFailure { it.copy(inferenceFailures = it.inferenceFailures + 1) }
            is InferenceTimeoutEvent -> spanned.bumpFailure { it.copy(inferenceTimeouts = it.inferenceTimeouts + 1) }
            is ToolExecutionCompletedEvent -> spanned.applyToolCompleted(payload)
            is ToolExecutionFailedEvent -> spanned.bumpFailure { it.copy(toolFailures = it.toolFailures + 1) }
            is ToolExecutionRejectedEvent -> spanned.bumpFailure { it.copy(toolRejections = it.toolRejections + 1) }
            is ApprovalRequestedEvent -> spanned.applyApprovalRequested(payload, at)
            is ApprovalDecisionResolvedEvent -> spanned.applyApprovalResolved(payload)
            is StageFailedEvent -> spanned.bumpFailure { it.copy(stageFailures = it.stageFailures + 1) }
            is WorkflowFailedEvent -> spanned.bumpFailure { it.copy(workflowFailures = it.workflowFailures + 1) }
            else -> spanned.applyQualityEvent(payload)
        }
    }

    // Signal-quality events other bounded contexts emit. Split out of apply() so its when stays
    // under the cyclomatic-complexity bar; unmatched payloads fall through unchanged.
    private fun MetricsState.applyQualityEvent(payload: EventPayload): MetricsState =
        when (payload) {
            is SourceFetchedEvent -> bumpQuality { it.copy(sourceFetches = it.sourceFetches + 1) }
            is LowQualityExtractionEvent ->
                bumpQuality { it.copy(lowQualityExtractions = it.lowQualityExtractions + 1) }
            is RepoKnowledgeRetrievedEvent ->
                bumpQuality { it.copy(retrievedHits = it.retrievedHits + payload.hits.size) }
            is RepoKnowledgeHitsFilteredEvent ->
                bumpQuality { it.copy(droppedHits = it.droppedHits + payload.dropped.size) }
            is BriefEchoMismatchEvent -> bumpQuality { it.copy(briefDriftCount = it.briefDriftCount + 1) }
            is CapabilityGapReflectedEvent ->
                if (payload.verdict == CapabilityGapVerdict.NEEDS_TOOL) {
                    bumpQuality { it.copy(capabilityGapCount = it.capabilityGapCount + 1) }
                } else {
                    this
                }
            else -> this
        }

    private fun MetricsState.bumpFailure(transform: (FailureMetrics) -> FailureMetrics): MetricsState =
        copy(failures = transform(failures))

    private fun MetricsState.bumpQuality(transform: (QualityMetrics) -> QualityMetrics): MetricsState =
        copy(quality = transform(quality))

    private fun MetricsState.applyInferenceCompleted(e: InferenceCompletedEvent): MetricsState {
        val provider = e.providerId.value
        val prev = inference.perProvider[provider] ?: ProviderInferenceMetrics()
        val updated = prev.copy(
            completedCount = prev.completedCount + 1,
            totalLatencyMs = prev.totalLatencyMs + e.latencyMs,
            promptTokens = prev.promptTokens + e.tokensUsed.promptTokens,
            completionTokens = prev.completionTokens + e.tokensUsed.completionTokens,
        )
        return copy(
            inference = inference.copy(
                completedCount = inference.completedCount + 1,
                totalLatencyMs = inference.totalLatencyMs + e.latencyMs,
                promptTokens = inference.promptTokens + e.tokensUsed.promptTokens,
                completionTokens = inference.completionTokens + e.tokensUsed.completionTokens,
                perProvider = inference.perProvider + (provider to updated),
            ),
        )
    }

    private fun MetricsState.applyToolCompleted(e: ToolExecutionCompletedEvent): MetricsState {
        val durationMs = e.receipt.durationMs
        val prev = tools.perTool[e.toolName] ?: ToolUsageMetrics()
        val updated = prev.copy(
            completedCount = prev.completedCount + 1,
            totalDurationMs = prev.totalDurationMs + durationMs,
        )
        return copy(
            tools = tools.copy(
                completedCount = tools.completedCount + 1,
                totalDurationMs = tools.totalDurationMs + durationMs,
                perTool = tools.perTool + (e.toolName to updated),
            ),
        )
    }

    private fun MetricsState.applyApprovalRequested(e: ApprovalRequestedEvent, at: Instant): MetricsState {
        val tier = e.tier.name
        val prev = approvals.perTier[tier] ?: TierApprovalMetrics()
        return copy(
            approvals = approvals.copy(
                requestedCount = approvals.requestedCount + 1,
                perTier = approvals.perTier + (tier to prev.copy(requestedCount = prev.requestedCount + 1)),
            ),
            pendingApprovals = pendingApprovals + (e.requestId.value to at),
        )
    }

    private fun MetricsState.applyApprovalResolved(e: ApprovalDecisionResolvedEvent): MetricsState {
        val requestedAt = pendingApprovals[e.requestId.value]
        val waitMs = requestedAt
            ?.let { (e.resolutionTimestamp - it).inWholeMilliseconds.coerceAtLeast(0L) }
            ?: 0L
        val tier = e.tier.name
        val prev = approvals.perTier[tier] ?: TierApprovalMetrics()
        return copy(
            approvals = approvals.copy(
                resolvedCount = approvals.resolvedCount + 1,
                totalWaitMs = approvals.totalWaitMs + waitMs,
                perTier = approvals.perTier + (
                    tier to prev.copy(
                        resolvedCount = prev.resolvedCount + 1,
                        totalWaitMs = prev.totalWaitMs + waitMs,
                    )
                    ),
            ),
            pendingApprovals = pendingApprovals - e.requestId.value,
        )
    }
}
