package com.correx.apps.server.health

import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.HealthDegradedEvent
import com.correx.core.events.events.HealthRestoredEvent
import com.correx.core.events.events.HealthSubject
import com.correx.core.events.events.NewEvent
import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
import org.slf4j.LoggerFactory
import java.util.UUID

/**
 * The reserved session that carries system-global facts (health checks) that belong to no user
 * workflow. Filtered out of operator-facing session listings; it exists only so global events
 * have a home in the per-session event log and flow on the global stream.
 */
val SYSTEM_SESSION: SessionId = SessionId("__system__")

/**
 * Small, paranoid, continuous health watch (observability-spec §4). Each tick polls every
 * [HealthProbe] and, comparing against the last recorded status per subject, appends a
 * [HealthDegradedEvent] / [HealthRestoredEvent] only on a transition (hysteresis — a probe that
 * keeps reporting the same status writes nothing). Health is thus event-sourced: observable,
 * replayable (Invariant #8/#9, the probe runs only live), and renderable via [HealthProjection].
 *
 * [initialStatuses] seeds the last-status map from the projection at start so a server restart
 * does not re-emit a Degraded that the log already records.
 */
class HealthMonitor(
    private val eventStore: EventStore,
    private val probes: List<HealthProbe>,
    private val intervalMs: Long,
    private val clock: Clock = Clock.System,
    private val sessionId: SessionId = SYSTEM_SESSION,
    initialStatuses: Map<HealthSubject, HealthStatus> = emptyMap(),
) {
    private val log = LoggerFactory.getLogger(HealthMonitor::class.java)
    private val lastStatus: MutableMap<HealthSubject, HealthStatus> = HashMap(initialStatuses)

    /** Launches the polling loop on [scope]; cancelling the scope stops the monitor. */
    fun start(scope: CoroutineScope): Job = scope.launch {
        log.info("HealthMonitor: starting ({} probe(s), every {} ms)", probes.size, intervalMs)
        while (isActive) {
            runCatching { tick() }.onFailure { log.warn("HealthMonitor tick failed: {}", it.message) }
            delay(intervalMs)
        }
    }

    /** One poll of every probe. Visible for deterministic testing without the loop/delay. */
    suspend fun tick() {
        for (probe in probes) {
            val observation = runCatching { probe.observe() }
                .getOrElse { log.warn("Health probe {} failed: {}", probe.subject, it.message); null }
                ?: continue
            val previous = lastStatus[probe.subject] ?: HealthStatus.HEALTHY
            if (observation.status == previous) continue
            append(observation)
            lastStatus[probe.subject] = observation.status
        }
    }

    private suspend fun append(observation: HealthObservation) {
        val payload = when (observation.status) {
            HealthStatus.DEGRADED -> HealthDegradedEvent(
                sessionId = sessionId,
                subject = observation.subject,
                metric = observation.metric,
                observedValue = observation.observedValue,
                threshold = observation.threshold,
                detail = observation.detail,
            )
            HealthStatus.HEALTHY -> HealthRestoredEvent(
                sessionId = sessionId,
                subject = observation.subject,
                metric = observation.metric,
                observedValue = observation.observedValue,
                detail = observation.detail,
            )
        }
        log.info("Health {} -> {}: {}", observation.subject, observation.status, observation.detail)
        eventStore.append(
            NewEvent(
                metadata = EventMetadata(
                    eventId = EventId(UUID.randomUUID().toString()),
                    sessionId = sessionId,
                    timestamp = clock.now(),
                    schemaVersion = 1,
                    causationId = null,
                    correlationId = null,
                ),
                payload = payload,
            ),
        )
    }
}
