package com.ariacockpit

import android.media.AudioAttributes
import android.media.AudioFormat
import android.media.AudioManager
import android.media.AudioTrack
import android.os.Build
import android.util.Base64
import android.util.Log
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.modules.core.DeviceEventManagerModule
import java.util.concurrent.LinkedBlockingQueue

/**
 * Streamt PCM-s16le Audio direkt via AudioTrack MODE_STREAM mit Pre-Roll.
 *
 * Pre-Roll: AudioTrack wird zwar direkt gebaut und gefuttert, aber play()
 * wird erst aufgerufen wenn PREROLL_SECONDS Audio im Buffer ist. So hat
 * der Stream Zeit einen Vorrat aufzubauen — wenn XTTS mit RTF>1 rendert
 * (langsamer als Echtzeit), laeuft der Buffer trotzdem nicht leer.
 *
 * Flow:
 *   JS: start(sampleRate, channels) → öffnet AudioTrack (noch nicht play())
 *   JS: writeChunk(base64)           → dekodiert, queued, Writer schreibt
 *   Writer: spielt los sobald PREROLL erreicht ist
 *   JS: end()                         → wartet bis Queue leer, schließt
 *   JS: stop()                        → Hart stoppen (Cancel)
 */
class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
    companion object {
        private const val TAG = "PcmStreamPlayer"
        // Fallback wenn JS keinen Wert uebergibt.
        private const val DEFAULT_PREROLL_SECONDS = 3.5
        // 0.0 = sofortige Wiedergabe — play() direkt beim ersten Chunk.
        // Macht Sinn fuer F5-TTS weil Render so schnell ist dass ein Puffer
        // unnoetig ist und bei kurzen Saetzen sogar stoeren kann.
        private const val MIN_PREROLL_SECONDS = 0.0
        private const val MAX_PREROLL_SECONDS = 10.0
        // Stille am Stream-Anfang, damit AudioTrack sauber anfaehrt und die
        // ersten Samples nicht abgeschnitten werden (XTTS-Warmup + play()-Latenz).
        private const val LEADING_SILENCE_SECONDS = 0.3
        // Stille am Ende — puffert das Hardware-Flushen damit die letzten
        // echten Samples garantiert ausgespielt werden bevor stop() kommt.
        private const val TRAILING_SILENCE_SECONDS = 0.3
    }

    override fun getName() = "PcmStreamPlayer"

    private var track: AudioTrack? = null
    private val queue = LinkedBlockingQueue<ByteArray>()
    private var writerThread: Thread? = null
    @Volatile private var writerShouldStop = false
    @Volatile private var endRequested = false
    @Volatile private var prerollBytes: Int = 0
    @Volatile private var playbackStarted = false
    @Volatile private var bytesBuffered: Long = 0
    @Volatile private var streamBytesPerFrame: Int = 2 // mono s16le default

    // ── Lifecycle ──

    @ReactMethod
    fun start(sampleRate: Int, channels: Int, prerollSeconds: Double, promise: Promise) {
        try {
            // Alte Session beenden falls vorhanden
            stopInternal()

            // Nur NaN/Inf → Default. 0.0 ist gueltig (= sofortige Wiedergabe).
            val prerollSec = if (prerollSeconds.isFinite() && prerollSeconds >= 0.0) {
                prerollSeconds.coerceIn(MIN_PREROLL_SECONDS, MAX_PREROLL_SECONDS)
            } else {
                DEFAULT_PREROLL_SECONDS
            }

            val channelConfig = if (channels == 2) AudioFormat.CHANNEL_OUT_STEREO else AudioFormat.CHANNEL_OUT_MONO
            val encoding = AudioFormat.ENCODING_PCM_16BIT
            val minBuf = AudioTrack.getMinBufferSize(sampleRate, channelConfig, encoding)
            val bytesPerSecond = sampleRate * channels * 2 // 16-bit = 2 bytes
            val prerollTarget = (bytesPerSecond * prerollSec).toInt()
            // Buffer entkoppelt von Preroll — fester ~4s-Buffer. OnePlus A12
            // mit USAGE_ASSISTANT laeuft AudioTrack erst ab ~3s gepufferter
            // Daten an. Wir padden Kurztexte vor play() auf 3s (siehe Block
            // nach mainLoop), Buffer braucht ~1s Headroom weil write() blockt.
            val bufferSize = (bytesPerSecond * 4).coerceAtLeast(minBuf * 8)
            prerollBytes = prerollTarget
            bytesBuffered = 0
            playbackStarted = false
            streamBytesPerFrame = channels * 2 // s16 = 2 bytes per sample

            val newTrack = AudioTrack.Builder()
                .setAudioAttributes(
                    AudioAttributes.Builder()
                        .setUsage(AudioAttributes.USAGE_ASSISTANT)
                        .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
                        .build(),
                )
                .setAudioFormat(
                    AudioFormat.Builder()
                        .setSampleRate(sampleRate)
                        .setChannelMask(channelConfig)
                        .setEncoding(encoding)
                        .build(),
                )
                .setBufferSizeInBytes(bufferSize)
                .setTransferMode(AudioTrack.MODE_STREAM)
                .build()

            // Start-Threshold runterdrehen: Default ist bufferSize/2 (= 2s bei 4s
            // Buffer). AudioTrack startet sonst nicht bevor 2s im Puffer sind —
            // bei kurzen TTS-Antworten (3 Worte ~ 1.4s) bleibt pos auf 0 stehen.
            // 0.1s reicht damit AudioTrack sofort mit dem ersten Chunk anlaeuft.
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                try {
                    val startFrames = (sampleRate / 10).coerceAtLeast(1)  // 100ms
                    newTrack.setStartThresholdInFrames(startFrames)
                    Log.i(TAG, "Start-Threshold gesetzt: ${startFrames} frames (~100ms)")
                } catch (e: Exception) {
                    Log.w(TAG, "setStartThresholdInFrames failed: ${e.message}")
                }
            }

            track = newTrack
            queue.clear()
            writerShouldStop = false
            endRequested = false

            writerThread = Thread({
                val t = track ?: return@Thread
                try {
                    // Leading-Silence in den Buffer — gibt AudioTrack Zeit anzufahren.
                    val leadingBytes = ((sampleRate * channels * 2) * LEADING_SILENCE_SECONDS).toInt() and 0x7FFFFFFE
                    if (leadingBytes > 0) {
                        val silence = ByteArray(leadingBytes)
                        var silOff = 0
                        while (silOff < silence.size && !writerShouldStop) {
                            val w = t.write(silence, silOff, silence.size - silOff)
                            if (w <= 0) break
                            silOff += w
                        }
                        bytesBuffered += silence.size
                    }
                    // Bei preroll=0: play() SOFORT nach Leading-Silence aufrufen,
                    // nicht erst bei Ankunft des ersten echten Chunks. Android's
                    // AudioTrack haelt den Play-State und wartet auf neue Samples.
                    // So verschluckt es keine Worte wenn der erste Chunk erst
                    // nach play()-Startup-Latenz eintrifft.
                    if (prerollBytes == 0 && !playbackStarted) {
                        try {
                            t.play()
                            playbackStarted = true
                            Log.i(TAG, "Playback sofort gestartet (preroll=0, ${bytesBuffered}B silence)")
                        } catch (e: Exception) {
                            Log.w(TAG, "play() sofort failed: ${e.message}")
                        }
                    }
                    // Idle-Cutoff: wenn endRequested NICHT kam aber lange nichts mehr
                    // reinkommt, brechen wir ab (Bridge-Crash, verlorener final).
                    // 120s damit lange F5-TTS-Render-Pausen zwischen Saetzen (z.B. bei
                    // Modell-Wechsel oder kalter GPU) nicht den Stream abreissen.
                    var idleMs = 0L
                    val maxIdleMs = 120_000L
                    // Zielpufferfuellung — unter diesem Wasserstand fuettern wir
                    // Stille rein damit AudioTrack nicht underrunt waehrend die
                    // Bridge den naechsten Satz rendert. Spotify/YouTube reagieren
                    // sonst mit eigenmaechtiger Wiederaufnahme nach ~10s Stille.
                    val underrunGuardFrames = sampleRate / 10  // ~100ms
                    val silenceFillFrames = sampleRate / 20    // ~50ms pro Refill

                    mainLoop@ while (!writerShouldStop) {
                        val data = queue.poll(50, java.util.concurrent.TimeUnit.MILLISECONDS)
                        if (data == null) {
                            if (endRequested) {
                                // Falls play() noch gar nicht lief (Stream ohne data
                                // ueberhaupt — sehr seltene Edge-Case): jetzt anstossen
                                // damit das finally{}-Wait nicht endlos blockt.
                                if (!playbackStarted) {
                                    try { t.play(); playbackStarted = true } catch (_: Exception) {}
                                }
                                break@mainLoop
                            }
                            // Underrun-Schutz: Stille reinfuettern wenn der AudioTrack-
                            // Puffer leerzulaufen droht. Spotify resumed sonst nach
                            // ~10s Pause auf eigene Faust, obwohl wir den Fokus halten.
                            if (playbackStarted) {
                                val framesWritten = bytesBuffered / streamBytesPerFrame
                                val framesPlayed = t.playbackHeadPosition.toLong()
                                val framesInBuffer = framesWritten - framesPlayed
                                if (framesInBuffer < underrunGuardFrames) {
                                    val fillBytes = silenceFillFrames * streamBytesPerFrame
                                    val silence = ByteArray(fillBytes)
                                    var silOff = 0
                                    while (silOff < silence.size && !writerShouldStop) {
                                        val w = t.write(silence, silOff, silence.size - silOff)
                                        if (w <= 0) break
                                        silOff += w
                                    }
                                    bytesBuffered += silence.size
                                }
                            }
                            idleMs += 50L
                            if (idleMs >= maxIdleMs) {
                                Log.w(TAG, "Idle-Cutoff: ${maxIdleMs}ms keine Daten — Stream wird beendet")
                                break@mainLoop
                            }
                            continue@mainLoop
                        }
                        idleMs = 0L

                        // play() beim ALLERERSTEN data-chunk aufrufen — egal wie wenig
                        // Daten da sind. Sonst stallt AudioTrack auf OnePlus A12 wenn
                        // play() erst gerufen wird nachdem der Buffer komplett gefuellt
                        // ist. Pre-Roll als "Vorrat aufbauen" passiert dann waehrend
                        // der Track schon spielt — Underrun-Schutz fuettert ggf. Stille.
                        if (!playbackStarted) {
                            try {
                                t.play()
                                playbackStarted = true
                                Log.i(TAG, "Playback gestartet beim 1. Chunk (${bytesBuffered}B leading + ${data.size}B data)")
                            } catch (e: Exception) {
                                Log.w(TAG, "play() failed: ${e.message}")
                            }
                        }

                        var offset = 0
                        while (offset < data.size && !writerShouldStop) {
                            val written = t.write(data, offset, data.size - offset)
                            if (written <= 0) break
                            offset += written
                        }
                        bytesBuffered += data.size
                    }
                    // Trailing-Silence damit die letzten echten Samples garantiert
                    // durch das Hardware-Buffering kommen bevor stop() sie abschneidet
                    val trailingBytes = ((sampleRate * channels * 2) * TRAILING_SILENCE_SECONDS).toInt() and 0x7FFFFFFE
                    if (trailingBytes > 0 && !writerShouldStop) {
                        val silence = ByteArray(trailingBytes)
                        var silOff = 0
                        while (silOff < silence.size && !writerShouldStop) {
                            val w = t.write(silence, silOff, silence.size - silOff)
                            if (w <= 0) break
                            silOff += w
                        }
                        bytesBuffered += silence.size
                    }
                } catch (e: Exception) {
                    Log.w(TAG, "Writer-Thread Fehler: ${e.message}")
                } finally {
                    // Warten bis alle geschriebenen Samples tatsaechlich abgespielt sind,
                    // sonst cuttet t.release() die letzten Sekunden ab.
                    try {
                        val totalFrames = (bytesBuffered / streamBytesPerFrame).toInt()
                        var lastPos = -1
                        var stalledCount = 0
                        var retried = false
                        while (!writerShouldStop) {
                            val pos = t.playbackHeadPosition
                            if (pos >= totalFrames) break
                            if (pos == lastPos) {
                                stalledCount++
                                // Nach 500ms Stillstand: AudioTrack-Quirk auf manchen
                                // Geraeten (OnePlus A12) — play() nochmal anstossen.
                                if (stalledCount == 10 && pos == 0 && !retried) {
                                    retried = true
                                    Log.w(TAG, "playback nicht angefahren — retry play()")
                                    try { t.play() } catch (e: Exception) {
                                        Log.w(TAG, "retry play() failed: ${e.message}")
                                    }
                                }
                                if (stalledCount > 40) {
                                    Log.w(TAG, "playback stalled at $pos/$totalFrames — give up")
                                    break
                                }
                            } else {
                                stalledCount = 0
                                lastPos = pos
                            }
                            Thread.sleep(50)
                        }
                        Log.i(TAG, "Playback fertig: frames=$totalFrames pos=${t.playbackHeadPosition}")
                    } catch (_: Exception) {}
                    try { t.stop() } catch (_: Exception) {}
                    try { t.release() } catch (_: Exception) {}
                    // RN-Event: AudioTrack ist wirklich durch (alle Samples gespielt).
                    // JS released erst JETZT den AudioFocus — sonst spielt Spotify
                    // beim end()-Cap waehrend ARIA noch redet (15s+ je nach Buffer).
                    try {
                        val params = Arguments.createMap()
                        reactApplicationContext
                            .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
                            .emit("PcmPlaybackFinished", params)
                    } catch (e: Exception) {
                        Log.w(TAG, "PlaybackFinished emit failed: ${e.message}")
                    }
                }
            }, "PcmStreamWriter").apply { start() }

            Log.i(TAG, "Stream gestartet: ${sampleRate}Hz ch=$channels buf=${bufferSize}B preroll=${prerollBytes}B (${prerollSec}s)")
            promise.resolve(true)
        } catch (e: Exception) {
            Log.e(TAG, "start fehlgeschlagen", e)
            promise.reject("START_FAILED", e.message, e)
        }
    }

    @ReactMethod
    fun writeChunk(base64Pcm: String, promise: Promise) {
        try {
            if (base64Pcm.isEmpty()) {
                promise.resolve(true)
                return
            }
            val bytes = Base64.decode(base64Pcm, Base64.DEFAULT)
            queue.put(bytes)
            promise.resolve(true)
        } catch (e: Exception) {
            promise.reject("WRITE_FAILED", e.message, e)
        }
    }

    /** Signalisiert: keine weiteren Chunks. Writer spielt aus, dann stoppt.
     *  Das Promise resolved erst wenn der Writer-Thread fertig ist —
     *  wichtig damit der Aufrufer den AudioFocus erst NACH dem letzten
     *  abgespielten Sample wieder freigibt (sonst dreht Spotify hoch
     *  waehrend das Pre-Roll noch ausspielt).
     */
    @ReactMethod
    fun end(promise: Promise) {
        endRequested = true
        val t = writerThread
        if (t == null || !t.isAlive) {
            promise.resolve(true)
            return
        }
        // Im Hintergrund auf den Writer warten — kein Threading-Block fuer JS-Bridge
        Thread({
            try {
                t.join(15_000) // hartes Cap, falls Writer haengt
            } catch (_: InterruptedException) {}
            promise.resolve(true)
        }, "PcmStreamEndWaiter").start()
    }

    /** Harter Stop (Cancel) — Queue verwerfen. */
    @ReactMethod
    fun stop(promise: Promise) {
        stopInternal()
        promise.resolve(true)
    }

    @ReactMethod fun addListener(eventName: String) {}
    @ReactMethod fun removeListeners(count: Int) {}

    private fun stopInternal() {
        writerShouldStop = true
        endRequested = true
        queue.clear()
        writerThread?.interrupt()
        writerThread = null
        val t = track
        if (t != null) {
            // pause() + flush() vor stop() — sonst spielt der Hardware-Buffer
            // (200-500ms PCM-Samples) noch hörbar weiter, nachdem der User
            // den Mute-Button gedrückt hat. Stefan-Bug-Report: "wenn ich auf
            // den Mund halten Button klicke während ARIA redet stoppt sie nicht".
            try { t.pause() } catch (_: Exception) {}
            try { t.flush() } catch (_: Exception) {}
            try { t.stop() } catch (_: Exception) {}
            try { t.release() } catch (_: Exception) {}
        }
        track = null
    }

    override fun onCatalystInstanceDestroy() {
        stopInternal()
        super.onCatalystInstanceDestroy()
    }
}
