package com.onthelevel.feature.level

import com.onthelevel.core.sensors.DisplayDeadband
import com.onthelevel.core.sensors.EdgeCalibration
import com.onthelevel.core.sensors.Ema
import com.onthelevel.core.sensors.GravitySample
import com.onthelevel.core.sensors.LevelMode
import com.onthelevel.core.sensors.LockDetector
import com.onthelevel.core.sensors.OrientationMath
import com.onthelevel.core.sensors.SurfaceCalibration
import com.onthelevel.core.sensors.SurfacePresentation
import com.onthelevel.core.sensors.SurfacePresentationDetector
import com.onthelevel.core.sensors.SettlingDetector

/**
 * One reading through the raw → stable pipeline (BRIEF.md values 1 and 2 of 3).
 * Value 3 — the spring-animated display value that moves the tactile vial — is
 * NOT yet implemented; this scaffold renders the deadbanded stable value directly.
 */
data class LevelReading(
    val mode: LevelMode,
    /** Deadbanded stable magnitude for the big readout, in degrees. */
    val displayPrimaryDegrees: Double,
    /** Surface: pitch when it is meaningful. Edge: signed level deviation. */
    val secondaryADegrees: Double?,
    /** Surface: roll when it is meaningful. Edge: plumb lean. */
    val secondaryBDegrees: Double?,
    /** Stable calibrated Surface axes for the visual instrument; null outside Surface mode. */
    val stableSurfacePitchDegrees: Double? = null,
    val stableSurfaceRollDegrees: Double? = null,
    /** Stable (pre-deadband) Surface tilt magnitude; drives the audio tick rate. Null for Edge. */
    val stableTiltDegrees: Double? = null,
    /** Surface-only presentation state; null for Edge mode. */
    val surfacePresentation: SurfacePresentation? = null,
    /** False when the device is not physically in the selected mode's geometry. */
    val placementOk: Boolean,
    /** Surface-only shared motion gate; true while the phone is still settling. */
    val isSettling: Boolean = false,
    val isLocked: Boolean,
    val fireFeedback: Boolean,
)

/**
 * Stateful per-collection pipeline: vector-space calibration → angle derivation →
 * EMA smoothing → lock detection → display deadband. Calibration is applied to the
 * gravity VECTOR before any display angle is derived, so it stays a reference
 * orientation rather than a scalar offset. Created fresh when mode or calibration
 * changes; the LockDetector is shared across recreations so the haptic debounce
 * survives mode switches.
 *
 * Invalid Edge placement (e.g. Edge selected but the phone lying flat) suppresses
 * lock detection — asin(gy) reads near zero there too, and locking on it would be
 * a lie. Surface lock is self-gating: only a near-zero surface tilt can acquire it,
 * while its magnitude remains visible at every orientation.
 */
class LevelPipeline(
    private val mode: LevelMode,
    private val surfaceCalibration: SurfaceCalibration,
    private val edgeCalibration: EdgeCalibration,
    private val lockDetector: LockDetector,
) {
    private val emaPrimary = Ema(SMOOTHING_TAU_SECONDS)
    private val emaA = Ema(SMOOTHING_TAU_SECONDS)
    private val emaB = Ema(SMOOTHING_TAU_SECONDS)
    private val primaryDeadband = DisplayDeadband()
    private val aDeadband = DisplayDeadband()
    private val bDeadband = DisplayDeadband()
    private val surfacePresentationDetector = SurfacePresentationDetector()
    private val settlingDetector = SettlingDetector()
    private var lastTimestampNanos: Long? = null

    fun process(g: GravitySample): LevelReading {
        val dtSeconds = lastTimestampNanos?.let { (g.timestampNanos - it) / 1e9 } ?: 0.0
        lastTimestampNanos = g.timestampNanos
        // Sensor timestamps are monotonic; using them (not wall clock) keeps the
        // lock state machine deterministic under recorded traces.
        val nowMillis = g.timestampNanos / 1_000_000
        val placementOk = OrientationMath.isPlacementValid(g, mode)

        return when (mode) {
            LevelMode.SURFACE -> {
                val corrected = surfaceCalibration.apply(g)
                val pitch = emaA.update(OrientationMath.surfacePitchDegrees(corrected), dtSeconds)
                val roll = emaB.update(OrientationMath.surfaceRollDegrees(corrected), dtSeconds)
                val magnitude = emaPrimary.update(
                    OrientationMath.surfaceTiltMagnitudeDegrees(corrected),
                    dtSeconds,
                )
                val displayMagnitude = primaryDeadband.update(magnitude)
                val presentation = surfacePresentationDetector.update(displayMagnitude)
                val settling = settlingDetector.update(pitch, roll, g.timestampNanos)
                // Velocity-aware lock: ease into center → quick confirm; fly across → nothing.
                val lock = lockDetector.update(magnitude, settling.movementRateDegreesPerSecond, nowMillis)
                LevelReading(
                    mode = mode,
                    displayPrimaryDegrees = displayMagnitude,
                    secondaryADegrees = if (presentation == SurfacePresentation.FACE_UP) {
                        aDeadband.update(pitch)
                    } else {
                        null
                    },
                    secondaryBDegrees = if (presentation == SurfacePresentation.FACE_UP) {
                        bDeadband.update(roll)
                    } else {
                        null
                    },
                    stableTiltDegrees = magnitude,
                    stableSurfacePitchDegrees = pitch,
                    stableSurfaceRollDegrees = roll,
                    surfacePresentation = presentation,
                    placementOk = placementOk,
                    isSettling = settling.isSettling,
                    isLocked = lock.isLocked,
                    fireFeedback = lock.fireFeedback,
                )
            }
            LevelMode.EDGE -> {
                val corrected = edgeCalibration.apply(g)
                val level = emaPrimary.update(
                    OrientationMath.edgeLevelDegrees(corrected),
                    dtSeconds,
                )
                val lean = emaB.update(
                    OrientationMath.edgePlumbLeanDegrees(corrected),
                    dtSeconds,
                )
                // Edge has no movement-rate signal yet: null → classic fixed-dwell lock.
                val lock = lockDetector.update(
                    if (placementOk) level else Double.MAX_VALUE,
                    null,
                    nowMillis,
                )
                val displayLevel = primaryDeadband.update(level)
                LevelReading(
                    mode = mode,
                    displayPrimaryDegrees = displayLevel,
                    secondaryADegrees = displayLevel,
                    secondaryBDegrees = bDeadband.update(lean),
                    placementOk = placementOk,
                    isLocked = lock.isLocked,
                    fireFeedback = lock.fireFeedback,
                )
            }
        }
    }

    private companion object {
        const val SMOOTHING_TAU_SECONDS = 0.15 // TODO(tune) against recorded traces
    }
}
