package com.onthelevel.core.sensors

import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.emptyFlow

/**
 * Sensor selection order (BRIEF.md): GAME_ROTATION_VECTOR → GRAVITY → low-passed
 * ACCELEROMETER. Game rotation vector is deliberate — it excludes the magnetometer,
 * which lies near the steel this app is used against (AUDIT.md finding 1). No
 * heading is needed; only the gravity direction matters.
 */
class AndroidSensorSource(context: Context) : SensorSource {

    private val sensorManager: SensorManager? =
        context.getSystemService(SensorManager::class.java)

    private val selected: Pair<Sensor, SensorSource.Kind>? = sensorManager?.let { sm ->
        sm.getDefaultSensor(Sensor.TYPE_GAME_ROTATION_VECTOR)
            ?.let { it to SensorSource.Kind.GAME_ROTATION_VECTOR }
            ?: sm.getDefaultSensor(Sensor.TYPE_GRAVITY)
                ?.let { it to SensorSource.Kind.GRAVITY }
            ?: sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
                ?.let { it to SensorSource.Kind.ACCELEROMETER }
    }

    override val isAvailable: Boolean = selected != null

    override val kind: SensorSource.Kind = selected?.second ?: SensorSource.Kind.NONE

    override val gravity: Flow<GravitySample> = if (selected == null || sensorManager == null) {
        emptyFlow()
    } else {
        callbackFlow {
            val (sensor, sensorKind) = selected
            val rotationMatrix = FloatArray(9)
            // Raw-accelerometer fallback only: strip linear acceleration before the
            // measurement-layer EMA sees the sample. TODO(tune) against recorded traces.
            var lp: Triple<Double, Double, Double>? = null

            val listener = object : SensorEventListener {
                override fun onSensorChanged(event: SensorEvent) {
                    val sample = when (sensorKind) {
                        SensorSource.Kind.GAME_ROTATION_VECTOR -> {
                            SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values)
                            // Converted to the GravitySample contract (device-frame
                            // world-up); see RotationVectorMath and SensorContractTest.
                            val (x, y, z) = RotationVectorMath.worldUpDeviceFrame(rotationMatrix)
                            GravitySample(
                                x = x * STANDARD_GRAVITY,
                                y = y * STANDARD_GRAVITY,
                                z = z * STANDARD_GRAVITY,
                                timestampNanos = event.timestamp,
                            )
                        }
                        SensorSource.Kind.GRAVITY -> GravitySample(
                            x = event.values[0].toDouble(),
                            y = event.values[1].toDouble(),
                            z = event.values[2].toDouble(),
                            timestampNanos = event.timestamp,
                        )
                        else -> {
                            val prev = lp
                            val next = if (prev == null) {
                                Triple(
                                    event.values[0].toDouble(),
                                    event.values[1].toDouble(),
                                    event.values[2].toDouble(),
                                )
                            } else {
                                Triple(
                                    prev.first + ACCEL_LP_ALPHA * (event.values[0] - prev.first),
                                    prev.second + ACCEL_LP_ALPHA * (event.values[1] - prev.second),
                                    prev.third + ACCEL_LP_ALPHA * (event.values[2] - prev.third),
                                )
                            }
                            lp = next
                            GravitySample(next.first, next.second, next.third, event.timestamp)
                        }
                    }
                    trySend(sample)
                }

                override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) = Unit
            }

            sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_GAME)
            awaitClose { sensorManager.unregisterListener(listener) }
        }
    }

    private companion object {
        const val STANDARD_GRAVITY = 9.80665
        const val ACCEL_LP_ALPHA = 0.15
    }
}
