package com.onthelevel.core.sensors

import kotlin.math.abs

/**
 * Level-lock state machine with hysteresis, dwell, and haptic debounce (BRIEF.md
 * §Sensor and measurement architecture). Operates on the stable calibrated measurement —
 * the same value the numeric readout shows, so the lock never disagrees with the number.
 *
 * Acquisition is VELOCITY-AWARE when a movement rate is supplied (Surface): a bubble eased
 * gently into center (≤ [slowRateThresholdDegPerSec]) locks after a short [dwellMillis]
 * confirmation, but a bubble flying across center never accumulates lock time — the dwell
 * timer only runs while inside the zone AND moving slowly, and resets otherwise (so slowing
 * to a stop inside the zone starts a fresh confirmation). This replaces a fixed long dwell,
 * which forced even a slow, deliberate arrival to wait (and delayed the "level" sound).
 *
 * When no rate is supplied (`rateDegPerSec == null`, e.g. Edge), it falls back to the
 * classic fixed [settledDwellMillis] with no velocity gate.
 *
 * The single lock drives sound, the lime visual, and the haptic together — there is no
 * separate audio threshold. Time is injected (callers pass `nowMillis`) for testability.
 */
class LockDetector(
    private val enterThresholdDegrees: Double = DEFAULT_ENTER_DEGREES,
    private val exitThresholdDegrees: Double = DEFAULT_EXIT_DEGREES,
    private val dwellMillis: Long = DEFAULT_DWELL_MILLIS,
    private val settledDwellMillis: Long = DEFAULT_SETTLED_DWELL_MILLIS,
    private val slowRateThresholdDegPerSec: Double = DEFAULT_SLOW_RATE_DEG_PER_SEC,
    private val feedbackDebounceMillis: Long = DEFAULT_FEEDBACK_DEBOUNCE_MILLIS,
) {
    companion object {
        // BRIEF.md: enter at no more than 0.2°, exit at at least 0.35°. The exit threshold
        // doubles as the tolerance stated next to the locked label (Codex review).
        const val DEFAULT_ENTER_DEGREES = 0.2
        const val DEFAULT_EXIT_DEGREES = 0.35
        const val DEFAULT_DWELL_MILLIS = 175L // velocity-gated: short, because a fast crossing can't accumulate
        const val DEFAULT_SETTLED_DWELL_MILLIS = 400L // no-velocity fallback (Edge)
        const val DEFAULT_SLOW_RATE_DEG_PER_SEC = 0.3
        const val DEFAULT_FEEDBACK_DEBOUNCE_MILLIS = 3_000L
    }

    init {
        require(exitThresholdDegrees > enterThresholdDegrees) {
            "Hysteresis requires exit > enter threshold"
        }
    }

    data class Result(
        val isLocked: Boolean,
        /** True exactly once per lock acquisition, and only outside the debounce window. */
        val fireFeedback: Boolean,
    )

    private var locked = false
    private var withinEnterSinceMillis: Long? = null
    private var lastFeedbackAtMillis: Long? = null

    fun update(stableTiltDegrees: Double, rateDegPerSec: Double?, nowMillis: Long): Result {
        val magnitude = abs(stableTiltDegrees)
        var fire = false

        if (!locked) {
            val inZone = magnitude <= enterThresholdDegrees
            // With a velocity signal, require slow motion to accumulate; a fast center
            // crossing (rate above the threshold) never builds lock time.
            val slowEnough = rateDegPerSec == null || rateDegPerSec <= slowRateThresholdDegPerSec
            val requiredDwell = if (rateDegPerSec == null) settledDwellMillis else dwellMillis
            if (inZone && slowEnough) {
                val since = withinEnterSinceMillis ?: nowMillis.also { withinEnterSinceMillis = it }
                if (nowMillis - since >= requiredDwell) {
                    locked = true
                    val last = lastFeedbackAtMillis
                    if (last == null || nowMillis - last >= feedbackDebounceMillis) {
                        fire = true
                        lastFeedbackAtMillis = nowMillis
                    }
                }
            } else {
                // Out of zone or moving too fast: reset, so a fresh slow-and-centered spell
                // starts the confirmation dwell over.
                withinEnterSinceMillis = null
            }
        } else if (magnitude >= exitThresholdDegrees) {
            locked = false
            withinEnterSinceMillis = null
        }

        return Result(isLocked = locked, fireFeedback = fire)
    }

    fun reset() {
        locked = false
        withinEnterSinceMillis = null
        // lastFeedbackAtMillis survives reset on purpose: switching modes must not
        // defeat the haptic debounce.
    }
}
