package com.onthelevel.feature.level

import android.content.Context
import android.media.AudioAttributes
import android.media.SoundPool
import android.os.SystemClock
import android.speech.tts.TextToSpeech
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.onthelevel.R
import com.onthelevel.core.audio.SurfaceTickPolicy
import com.onthelevel.core.audio.VoiceGuidancePolicy
import com.onthelevel.core.settings.AudioAssistMode
import java.util.Locale
import kotlin.math.hypot

/**
 * Hands-free Surface audio assist (SOUND_DESIGN_HANDOFF.md).
 *
 * TONES — proximity ticks that speed up as you near level; the moment you're on the
 * bullseye (aligned zone, spatial hysteresis) the ticks give way to the looping `level.wav`
 * bullseye sound — immediately, even on a fast pass. One vocabulary: faster ticks = closer,
 * bullseye sound = centered now. The persistent lime/label/haptic "held level" is separate
 * (velocity-aware LockDetector); the sound does not wait on it.
 *
 * VOICE — deliberately separate: spoken corrections + "that's level", settled-change only.
 *
 * Everything plays on the media stream (so the volume slider works) and never takes audio
 * focus. Players/speakers exist only while enabled and foregrounded; released on dispose.
 */
@Composable
fun AudioLevelAssist(reading: LevelReading?, mode: AudioAssistMode) {
    val isForeground = rememberIsResumed()
    val tickPolicy = remember { SurfaceTickPolicy() }
    val context = LocalContext.current.applicationContext

    val player = remember(mode, isForeground, context) {
        if (mode != AudioAssistMode.OFF && isForeground) SonarSoundPool(context) else null
    }
    val speaker = remember(mode, isForeground, context) {
        if (mode == AudioAssistMode.VOICE && isForeground) VoiceSpeaker(context) else null
    }
    // Fresh per speaker so each VOICE session re-evaluates from the current reading — and
    // the policy is only consumed once the speaker is ready, so a phrase can't be queued
    // and then go stale during TTS init (Codex).
    val voicePolicy = remember(speaker) { VoiceGuidancePolicy() }
    DisposableEffect(player) { onDispose { player?.release() } }
    DisposableEffect(speaker) { onDispose { speaker?.shutdown() } }

    LaunchedEffect(reading, mode, isForeground, player, speaker) {
        val current = reading ?: return@LaunchedEffect

        // TONES: proximity ticks outside the center zone; the bullseye sound the instant
        // you're aligned. Alignment uses the SAME axes as the bubble (hypot of stable
        // pitch/roll), so the sound can't lag the visual.
        val alignError = hypot(
            current.stableSurfacePitchDegrees ?: 0.0,
            current.stableSurfaceRollDegrees ?: 0.0,
        )
        when (
            tickPolicy.update(
                SurfaceTickPolicy.Input(
                    isEnabled = mode == AudioAssistMode.TONES,
                    isAppForeground = isForeground,
                    placementOk = current.placementOk,
                    surfacePresentation = current.surfacePresentation,
                    errorDegrees = alignError,
                    nowMillis = SystemClock.elapsedRealtime(),
                ),
            )
        ) {
            // Aligned: hold the looping bullseye sound (idempotent — starts once).
            SurfaceTickPolicy.Cue.ALIGNED -> player?.ensureLevelLooping()
            // A tick means we're outside the zone: stop the bullseye first (order matters),
            // then tick.
            SurfaceTickPolicy.Cue.TICK -> {
                player?.stopLevel()
                player?.playTick()
            }
            // Outside the zone, no tick due: make sure the bullseye is stopped (covers exit).
            null -> player?.stopLevel()
        }

        // Only run the voice policy once the speaker is ready — never consume its state
        // for a phrase we can't speak yet. When ready, it evaluates the current reading,
        // so nothing stale gets spoken after init finishes.
        if (speaker?.isReady == true) {
            val phrase = voicePolicy.update(
                VoiceGuidancePolicy.Input(
                    isEnabled = true,
                    isAppForeground = isForeground,
                    placementOk = current.placementOk,
                    presentation = current.surfacePresentation,
                    isSettling = current.isSettling,
                    pitchDegrees = current.stableSurfacePitchDegrees ?: 0.0,
                    rollDegrees = current.stableSurfaceRollDegrees ?: 0.0,
                    isLocked = current.isLocked,
                    nowMillis = System.currentTimeMillis(),
                ),
            )
            if (phrase != null) speaker.speak(phrase)
        }
    }
}

@Composable
private fun rememberIsResumed(): Boolean {
    val lifecycleOwner = LocalLifecycleOwner.current
    var isResumed by remember(lifecycleOwner) {
        mutableStateOf(lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED))
    }
    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, _ ->
            isResumed = lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)
        }
        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
    }
    return isResumed
}

private class SonarSoundPool(context: Context) {
    private val loaded = mutableSetOf<Int>()
    private val soundPool = SoundPool.Builder()
        .setMaxStreams(4)
        .setAudioAttributes(
            AudioAttributes.Builder()
                .setUsage(AudioAttributes.USAGE_MEDIA)
                .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                .build(),
        )
        .build()
    private val tickSound: Int
    private val levelSound: Int
    private var levelStreamId: Int = 0
    private var levelPending = false // Level asked for before the sample finished loading

    init {
        soundPool.setOnLoadCompleteListener { _, sampleId, status ->
            if (status == 0) {
                loaded += sampleId
                // If ALIGNED arrived before the Level sample finished loading, start the loop
                // the moment it lands (ensureLevelLooping set levelPending instead of dropping it).
                if (sampleId == levelSound && levelPending) {
                    levelPending = false
                    startLevelLoop()
                }
            }
        }
        tickSound = soundPool.load(context, R.raw.tick, 1)
        levelSound = soundPool.load(context, R.raw.level, 1)
    }

    /** One proximity tick — constant volume (the rate is the signal, not loudness). */
    fun playTick() {
        if (tickSound in loaded) soundPool.play(tickSound, TICK_VOLUME, TICK_VOLUME, 1, 0, 1f)
    }

    /**
     * The bullseye sound, looped while you're aligned. Idempotent — safe to call every frame;
     * starts once and keeps going. Queues if the sample hasn't loaded yet.
     */
    fun ensureLevelLooping() {
        if (levelStreamId != 0) return // already looping
        if (levelSound in loaded) startLevelLoop() else levelPending = true
    }

    private fun startLevelLoop() {
        levelStreamId = soundPool.play(levelSound, LEVEL_VOLUME, LEVEL_VOLUME, 2, -1, 1f)
    }

    fun stopLevel() {
        levelPending = false // cancel a queued start (we left the zone before it loaded)
        if (levelStreamId != 0) {
            soundPool.stop(levelStreamId)
            levelStreamId = 0
        }
    }

    fun release() = soundPool.release()

    private companion object {
        const val TICK_VOLUME = 0.55f
        const val LEVEL_VOLUME = 0.85f
    }
}

/**
 * On-device text-to-speech, structured so premium pre-recorded clips can be swapped in
 * later without touching callers. Mixes as sonification and does not take audio focus.
 * TODO(voice): prefer bundled clips (res/raw) when present, fall back to TTS.
 */
private class VoiceSpeaker(context: Context) {
    /** The caller waits on this before consuming the voice policy, so nothing goes stale. */
    var isReady = false
        private set
    private lateinit var tts: TextToSpeech

    init {
        tts = TextToSpeech(context.applicationContext) { status ->
            if (status == TextToSpeech.SUCCESS) {
                tts.language = Locale.US
                tts.setAudioAttributes(
                    AudioAttributes.Builder()
                        .setUsage(AudioAttributes.USAGE_MEDIA)
                        .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
                        .build(),
                )
                isReady = true
            }
        }
    }

    fun speak(phrase: VoiceGuidancePolicy.Phrase) {
        if (isReady) tts.speak(textFor(phrase), TextToSpeech.QUEUE_FLUSH, null, "level-voice")
    }

    private fun textFor(phrase: VoiceGuidancePolicy.Phrase): String = when (phrase) {
        VoiceGuidancePolicy.Phrase.Level -> "that's level"
        is VoiceGuidancePolicy.Phrase.Raise -> {
            val side = when (phrase.direction) {
                VoiceGuidancePolicy.Direction.RAISE_LEFT -> "left"
                VoiceGuidancePolicy.Direction.RAISE_RIGHT -> "right"
                VoiceGuidancePolicy.Direction.RAISE_TOP -> "top"
                VoiceGuidancePolicy.Direction.RAISE_BOTTOM -> "bottom"
            }
            if (phrase.fine) "raise the $side, just a little" else "raise the $side"
        }
    }

    fun shutdown() {
        tts.stop()
        tts.shutdown()
    }
}
