package com.correx.apps.server.metrics

import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.SessionId
import com.correx.core.sessions.projections.replay.DefaultEventReplayer
import kotlinx.serialization.Serializable

private const val MS_PER_SECOND = 1000.0
private const val PERCENT = 100.0

@Serializable
data class ProviderStatsDto(
    val provider: String,
    val completedCount: Long,
    val totalLatencyMs: Long,
    val promptTokens: Long,
    val completionTokens: Long,
    val tokensPerSecond: Double,
)

@Serializable
data class ToolStatsDto(
    val toolName: String,
    val completedCount: Long,
    val totalDurationMs: Long,
)

@Serializable
data class TierApprovalStatsDto(
    val tier: String,
    val requestedCount: Long,
    val resolvedCount: Long,
    val totalWaitMs: Long,
    val avgWaitMs: Long,
)

/**
 * Signal-quality view: raw counts plus the two derived ratios an operator reads. Extraction
 * quality is the share of research fetches that cleared the quality bar; retrieval precision is
 * the share of repo-knowledge hits kept (not filtered below the similarity threshold). Both are
 * 1.0 when there's nothing to judge (no fetches / no retrieval), so a clean run reads as clean.
 */
@Serializable
data class QualityStatsDto(
    val sourceFetches: Long = 0,
    val lowQualityExtractions: Long = 0,
    val extractionQualityRate: Double = 1.0,
    val retrievedHits: Long = 0,
    val droppedHits: Long = 0,
    val retrievalPrecision: Double = 1.0,
    val briefDriftCount: Long = 0,
    val capabilityGapCount: Long = 0,
)

/**
 * Render-friendly view of a session's metrics. Raw sums plus the derived ratios the operator
 * actually reads; the time-accounting percentages split session wall time into inference /
 * tool execution / awaiting-approval, with the remainder ("other": context build, orchestration,
 * idle) recoverable as `100 − (inference + tool + approvalWait)`.
 */
@Serializable
data class MetricsReport(
    val sessionId: String,
    val eventCount: Long,
    val sessionDurationMs: Long,
    val inferenceCount: Long,
    val inferenceMs: Long,
    val promptTokens: Long,
    val completionTokens: Long,
    val tokensPerSecond: Double,
    val perProvider: List<ProviderStatsDto>,
    val toolCount: Long,
    val toolMs: Long,
    val perTool: List<ToolStatsDto>,
    val approvalsRequested: Long,
    val approvalsResolved: Long,
    val approvalsPending: Long,
    val approvalWaitMs: Long,
    val avgApprovalWaitMs: Long,
    val perTier: List<TierApprovalStatsDto>,
    val failures: FailureMetrics,
    val quality: QualityStatsDto = QualityStatsDto(),
    val inferencePct: Double,
    val toolPct: Double,
    val approvalWaitPct: Double,
)

/**
 * Rebuilds [MetricsState] by replaying a session through [MetricsProjection], then derives the
 * read-side ratios into a [MetricsReport]. Replay-only: no network, no live inference
 * (Hard Invariant #8).
 */
class MetricsInspectionService(private val eventStore: EventStore) {

    private val replayer = DefaultEventReplayer(eventStore, MetricsProjection())

    fun inspect(sessionId: SessionId): MetricsReport {
        val state = replayer.rebuild(sessionId)
        return toReport(sessionId, state)
    }

    private fun toReport(sessionId: SessionId, s: MetricsState): MetricsReport {
        val durationMs = sessionDurationMs(s)
        return MetricsReport(
            sessionId = sessionId.value,
            eventCount = s.eventCount,
            sessionDurationMs = durationMs,
            inferenceCount = s.inference.completedCount,
            inferenceMs = s.inference.totalLatencyMs,
            promptTokens = s.inference.promptTokens,
            completionTokens = s.inference.completionTokens,
            tokensPerSecond = tokensPerSecond(s.inference.completionTokens, s.inference.totalLatencyMs),
            perProvider = s.inference.perProvider.entries
                .sortedByDescending { it.value.totalLatencyMs }
                .map { (provider, m) ->
                    ProviderStatsDto(
                        provider = provider,
                        completedCount = m.completedCount,
                        totalLatencyMs = m.totalLatencyMs,
                        promptTokens = m.promptTokens,
                        completionTokens = m.completionTokens,
                        tokensPerSecond = tokensPerSecond(m.completionTokens, m.totalLatencyMs),
                    )
                },
            toolCount = s.tools.completedCount,
            toolMs = s.tools.totalDurationMs,
            perTool = s.tools.perTool.entries
                .sortedByDescending { it.value.totalDurationMs }
                .map { (name, m) -> ToolStatsDto(name, m.completedCount, m.totalDurationMs) },
            approvalsRequested = s.approvals.requestedCount,
            approvalsResolved = s.approvals.resolvedCount,
            approvalsPending = s.pendingApprovals.size.toLong(),
            approvalWaitMs = s.approvals.totalWaitMs,
            avgApprovalWaitMs = avg(s.approvals.totalWaitMs, s.approvals.resolvedCount),
            perTier = s.approvals.perTier.entries
                .sortedBy { it.key }
                .map { (tier, m) ->
                    TierApprovalStatsDto(
                        tier = tier,
                        requestedCount = m.requestedCount,
                        resolvedCount = m.resolvedCount,
                        totalWaitMs = m.totalWaitMs,
                        avgWaitMs = avg(m.totalWaitMs, m.resolvedCount),
                    )
                },
            failures = s.failures,
            quality = qualityStats(s.quality),
            inferencePct = pct(s.inference.totalLatencyMs, durationMs),
            toolPct = pct(s.tools.totalDurationMs, durationMs),
            approvalWaitPct = pct(s.approvals.totalWaitMs, durationMs),
        )
    }

    private fun sessionDurationMs(s: MetricsState): Long {
        val first = s.firstEventAt ?: return 0L
        val last = s.lastEventAt ?: return 0L
        return (last - first).inWholeMilliseconds.coerceAtLeast(0L)
    }

    private fun tokensPerSecond(completionTokens: Long, latencyMs: Long): Double =
        if (latencyMs <= 0L) 0.0 else completionTokens.toDouble() / (latencyMs / MS_PER_SECOND)

    private fun pct(part: Long, whole: Long): Double =
        if (whole <= 0L) 0.0 else (part.toDouble() / whole.toDouble()) * PERCENT

    private fun avg(total: Long, count: Long): Long =
        if (count <= 0L) 0L else total / count

    private fun qualityStats(q: QualityMetrics): QualityStatsDto {
        val keptExtractions = (q.sourceFetches - q.lowQualityExtractions).coerceAtLeast(0L)
        val totalHits = q.retrievedHits + q.droppedHits
        return QualityStatsDto(
            sourceFetches = q.sourceFetches,
            lowQualityExtractions = q.lowQualityExtractions,
            // 1.0 when nothing was fetched — no evidence of low quality reads as clean.
            extractionQualityRate = if (q.sourceFetches <= 0L) 1.0 else keptExtractions.toDouble() / q.sourceFetches,
            retrievedHits = q.retrievedHits,
            droppedHits = q.droppedHits,
            retrievalPrecision = if (totalHits <= 0L) 1.0 else q.retrievedHits.toDouble() / totalHits,
            briefDriftCount = q.briefDriftCount,
            capabilityGapCount = q.capabilityGapCount,
        )
    }
}
