package com.fabledsword.minstrel.connectivity

internal const val ESCALATE_AFTER_MS = 120_000L
internal const val CORROBORATION_WINDOW_MS = 30_000L
internal const val CORROBORATION_OP_THRESHOLD = 2

/**
 * Minimum gap between op failures for them to count as SEPARATE evidence
 * (#1209).
 *
 * A link handoff fails every in-flight request at once, so a burst is one
 * event producing N failures — not N independent observations that the server
 * is gone. Without this, two simultaneous failures corroborated each other
 * straight to Unreachable, and ServerDown makes OfflineGatedDataSource refuse
 * every uncached track. The app declined to play music that would have played
 * fine, for a blip that had already resolved.
 *
 * 3s is comfortably above the sub-second window an OS handoff occupies while
 * still letting a genuine outage corroborate within seconds once a client
 * retries. The sustained-time backstop covers the case where nothing retries
 * at all — and if nothing is asking, a late ServerDown costs nothing.
 */
internal const val CORROBORATION_MIN_SPACING_MS = 3_000L

/**
 * Pure reachability state machine. No Android, no coroutines, no real clock —
 * every entry point takes `nowMs`, so it is fully deterministic and unit-
 * testable. [NetworkStatusController] wires real time + signals around it.
 *
 * Reachability (independent of the device link):
 *  - Reachable   last evidence says the server answered.
 *  - Unstable    a probe failed; arbitration/escalation pending.
 *  - Unreachable corroborated or sustained failure.
 *
 * [health] folds the device link over that: no link → Offline; otherwise the
 * reachability maps Reachable→Healthy, Unstable→Unstable, Unreachable→ServerDown.
 *
 * Principle: **success is self-proving, failure is ambiguous.** [onSuccess]
 * (a real server byte-read or API 2xx, or a successful /healthz) snaps straight
 * back to Reachable. A failure only escalates when a /healthz probe corroborates
 * it ([onProbeFailure]) — either via fresh op-failure corroboration or the
 * sustained-time backstop.
 */
class ReachabilityMachine {

    private enum class Reachability { Reachable, Unstable, Unreachable }

    private var linkUp = true
    private var reachability = Reachability.Reachable
    private var failureStreakStartMs: Long? = null
    private val recentOpFailures = ArrayDeque<Long>()

    fun onLinkChange(up: Boolean) {
        linkUp = up
        // Link transitions don't reset reachability — a restored link keeps the
        // last-known server reachability until a fresh probe/op result arrives.
        if (!up) recentOpFailures.clear()
    }

    /** A real successful server op (stream read, API 2xx) or a successful /healthz. */
    fun onSuccess() {
        reachability = Reachability.Reachable
        failureStreakStartMs = null
        recentOpFailures.clear()
    }

    /**
     * A real network op failed. Ambiguous on its own — records corroboration.
     *
     * Failures arriving within [CORROBORATION_MIN_SPACING_MS] of the last
     * recorded one are dropped rather than stacked: see that constant for why
     * a burst must not corroborate itself.
     */
    fun onOpFailure(nowMs: Long) {
        pruneOpFailures(nowMs)
        val last = recentOpFailures.lastOrNull()
        if (last != null && nowMs - last < CORROBORATION_MIN_SPACING_MS) return
        recentOpFailures.addLast(nowMs)
    }

    /** A /healthz probe failed — the arbiter. Escalates per corroboration/backstop. */
    fun onProbeFailure(nowMs: Long) {
        pruneOpFailures(nowMs)
        if (reachability == Reachability.Reachable) {
            reachability = Reachability.Unstable
            failureStreakStartMs = nowMs
        }
        if (reachability == Reachability.Unstable && shouldEscalate(nowMs)) {
            reachability = Reachability.Unreachable
        }
    }

    fun health(): ServerHealth = when {
        !linkUp -> ServerHealth.Offline
        reachability == Reachability.Reachable -> ServerHealth.Healthy
        reachability == Reachability.Unstable -> ServerHealth.Unstable
        else -> ServerHealth.ServerDown
    }

    private fun shouldEscalate(nowMs: Long): Boolean {
        val corroborated = recentOpFailures.size >= CORROBORATION_OP_THRESHOLD
        val sustained =
            failureStreakStartMs?.let { nowMs - it >= ESCALATE_AFTER_MS } ?: false
        return corroborated || sustained
    }

    private fun pruneOpFailures(nowMs: Long) {
        while (recentOpFailures.isNotEmpty() &&
            nowMs - recentOpFailures.first() > CORROBORATION_WINDOW_MS
        ) {
            recentOpFailures.removeFirst()
        }
    }
}
