package com.correx.apps.server.health

import com.correx.core.events.events.HealthSubject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import java.nio.file.Files
import java.nio.file.Path

private const val MILLIS_PER_MINUTE = 60_000.0
private const val BYTES_PER_KIB = 1024.0

/**
 * Watches the on-disk footprint of the event log + CAS (observability-spec §4: "disk watermark").
 * Append-only storage grows monotonically, so two thresholds matter: absolute size (the warning
 * that arrives before the 2am surprise) and growth rate (when log compaction stops being
 * theoretical). Size takes priority over growth when both cross.
 *
 * The directory walk is the environment observation; it runs only live in the monitor loop. The
 * prior-sample state used to compute the growth rate is per-run and never affects replay.
 */
class DiskWatermarkProbe(
    private val monitoredPaths: List<Path>,
    private val warnBytes: Long,
    private val warnGrowthBytesPerMin: Long,
    private val clock: Clock = Clock.System,
) : HealthProbe {

    override val subject: HealthSubject = HealthSubject.DISK

    private data class Sample(val bytes: Long, val at: Instant)

    private var lastSample: Sample? = null

    override suspend fun observe(): HealthObservation = withContext(Dispatchers.IO) {
        val now = clock.now()
        val totalBytes = monitoredPaths.sumOf { sizeOf(it) }
        val growthPerMin = growthBytesPerMin(totalBytes, now)
        lastSample = Sample(totalBytes, now)

        when {
            totalBytes >= warnBytes -> degraded(
                metric = "total_bytes",
                value = totalBytes,
                threshold = warnBytes,
                detail = "event log + CAS at ${humanBytes(totalBytes)} (warn ≥ ${humanBytes(warnBytes)})",
            )
            growthPerMin >= warnGrowthBytesPerMin -> degraded(
                metric = "growth_bytes_per_min",
                value = growthPerMin,
                threshold = warnGrowthBytesPerMin,
                detail = "storage growing ${humanBytes(growthPerMin)}/min " +
                    "(warn ≥ ${humanBytes(warnGrowthBytesPerMin)}/min)",
            )
            else -> HealthObservation(
                subject = subject,
                status = HealthStatus.HEALTHY,
                metric = "total_bytes",
                observedValue = totalBytes,
                threshold = warnBytes,
                detail = "event log + CAS at ${humanBytes(totalBytes)}",
            )
        }
    }

    private fun degraded(metric: String, value: Long, threshold: Long, detail: String) =
        HealthObservation(subject, HealthStatus.DEGRADED, metric, value, threshold, detail)

    /** Bytes-per-minute since the previous sample; 0 on the first tick (no baseline yet). */
    private fun growthBytesPerMin(totalBytes: Long, now: Instant): Long {
        val prev = lastSample ?: return 0L
        val elapsedMin = (now - prev.at).inWholeMilliseconds.toDouble() / MILLIS_PER_MINUTE
        return if (elapsedMin <= 0.0) {
            0L
        } else {
            ((totalBytes - prev.bytes).toDouble() / elapsedMin).toLong().coerceAtLeast(0L)
        }
    }

    private fun sizeOf(path: Path): Long {
        if (!Files.exists(path)) return 0L
        return runCatching {
            Files.walk(path).use { stream ->
                stream.filter { Files.isRegularFile(it) }
                    .mapToLong { runCatching { Files.size(it) }.getOrDefault(0L) }
                    .sum()
            }
        }.getOrDefault(0L)
    }
}

private val UNITS = listOf("B", "KiB", "MiB", "GiB", "TiB")

private fun humanBytes(bytes: Long): String {
    if (bytes < BYTES_PER_KIB) return "$bytes B"
    var value = bytes.toDouble()
    var unit = 0
    while (value >= BYTES_PER_KIB && unit < UNITS.size - 1) {
        value /= BYTES_PER_KIB
        unit++
    }
    return "%.1f %s".format(value, UNITS[unit])
}
