package com.ariacockpit

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
import android.media.audiofx.AcousticEchoCanceler
import android.media.audiofx.AutomaticGainControl
import android.media.audiofx.NoiseSuppressor
import android.os.PowerManager
import android.util.Base64
import android.util.Log
import androidx.core.content.ContextCompat
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.atomic.AtomicBoolean

/**
 * PCM-Streaming-Recorder fuer die Streaming-Whisper-Bridge.
 *
 * Oeffnet AudioRecord (16 kHz mono s16le, VOICE_COMMUNICATION-Source mit
 * automatischer AEC + NS) und feuert ~200ms-Chunks als base64-Event
 * "PcmStreamChunk" an die JS-Bridge.
 *
 * audio.ts schickt die Chunks via RVS direkt an die whisper-bridge die
 * dort einen ML-Endpointer laufen laesst — kein dB-VAD-Tuning mehr.
 *
 * Mic-Ownership: dieser Recorder DARF nicht gleichzeitig mit
 * OpenWakeWord laufen — beide wollen AudioRecord vom MIC. Caller
 * muss OpenWakeWord.stop() vor start() hier aufrufen und nach stop()
 * hier wieder OpenWakeWord.start() — genau wie's audio.ts ohnehin
 * macht.
 *
 * Events:
 *   "PcmStreamChunk"   { pcm: base64-s16le, seq: N, ts: epochMs }
 *   "PcmStreamError"   { error: string }
 */
class PcmStreamRecorderModule(reactContext: ReactApplicationContext) :
    ReactContextBaseJavaModule(reactContext) {

    override fun getName() = "PcmStreamRecorder"

    companion object {
        private const val TAG = "PcmStreamRecorder"
        private const val SAMPLE_RATE = 16000
        // 200ms-Chunks: gross genug fuer wenig RVS-Overhead, klein genug damit
        // der Endpointer im Whisper-Bridge granular sieht. 200ms ist auch das
        // Whisper-VAD-Frame-Hop — passt also zu downstream.
        private const val CHUNK_SAMPLES = 3200       // 200ms @ 16 kHz
        private const val BYTES_PER_SAMPLE = 2       // s16
        private const val CHUNK_BYTES = CHUNK_SAMPLES * BYTES_PER_SAMPLE
    }

    private var audioRecord: AudioRecord? = null
    private val running = AtomicBoolean(false)
    private var captureThread: Thread? = null

    private var aec: AcousticEchoCanceler? = null
    private var ns: NoiseSuppressor? = null
    private var agc: AutomaticGainControl? = null

    // PARTIAL_WAKE_LOCK damit der JS-Bridge-Loop weiterlaeuft auch wenn das
    // Display aus ist — sonst sammeln sich zwar Chunks in der nativen Queue
    // an, aber emit() landet nicht zeitnah in JS und der Whisper-Bridge
    // bekommt die Audio-Chunks erst beim App-Foreground-Resume.
    private var wakeLock: PowerManager.WakeLock? = null

    private var seq: Long = 0L

    @ReactMethod
    fun start(promise: Promise) {
        if (running.get()) {
            promise.resolve(true)
            return
        }
        val perm = ContextCompat.checkSelfPermission(
            reactApplicationContext, Manifest.permission.RECORD_AUDIO
        )
        if (perm != PackageManager.PERMISSION_GRANTED) {
            promise.reject("NO_MIC_PERMISSION", "RECORD_AUDIO Permission fehlt")
            return
        }

        try {
            val minBuf = AudioRecord.getMinBufferSize(
                SAMPLE_RATE,
                AudioFormat.CHANNEL_IN_MONO,
                AudioFormat.ENCODING_PCM_16BIT,
            ).coerceAtLeast(CHUNK_BYTES * 4)  // 4x Chunk-Size als Sicherheit

            val record = AudioRecord(
                MediaRecorder.AudioSource.VOICE_COMMUNICATION,
                SAMPLE_RATE,
                AudioFormat.CHANNEL_IN_MONO,
                AudioFormat.ENCODING_PCM_16BIT,
                minBuf,
            )
            if (record.state != AudioRecord.STATE_INITIALIZED) {
                record.release()
                promise.reject("AUDIO_INIT", "AudioRecord nicht initialisiert (Mikro belegt? OpenWakeWord noch aktiv?)")
                return
            }
            audioRecord = record

            // AEC/NS/AGC explizit anschalten — manche Geraete liefern's via
            // VOICE_COMMUNICATION zwar mit, aber Belt-and-Suspenders.
            try {
                if (AcousticEchoCanceler.isAvailable()) {
                    aec = AcousticEchoCanceler.create(record.audioSessionId)?.apply { enabled = true }
                }
            } catch (e: Exception) { Log.w(TAG, "AEC failed: ${e.message}") }
            try {
                if (NoiseSuppressor.isAvailable()) {
                    ns = NoiseSuppressor.create(record.audioSessionId)?.apply { enabled = true }
                }
            } catch (e: Exception) { Log.w(TAG, "NS failed: ${e.message}") }
            try {
                if (AutomaticGainControl.isAvailable()) {
                    agc = AutomaticGainControl.create(record.audioSessionId)?.apply { enabled = true }
                }
            } catch (e: Exception) { Log.w(TAG, "AGC failed: ${e.message}") }

            seq = 0L
            running.set(true)
            record.startRecording()

            try {
                val pm = reactApplicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager
                wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
                                          "AriaCockpit:PcmStreamRecord").apply {
                    setReferenceCounted(false)
                    acquire(8 * 60 * 60 * 1000L)  // 8h Cap
                }
            } catch (e: Exception) {
                Log.w(TAG, "WakeLock acquire fehlgeschlagen: ${e.message}")
            }

            captureThread = Thread({ captureLoop() }, "PcmStreamRecorderCapture").apply {
                isDaemon = true
                start()
            }

            Log.i(TAG, "Recording gestartet (16kHz mono s16le, ${CHUNK_SAMPLES} samples/chunk)")
            promise.resolve(true)
        } catch (e: Exception) {
            Log.e(TAG, "start fehlgeschlagen", e)
            running.set(false)
            audioRecord?.release()
            audioRecord = null
            releaseAudioEffects()
            releaseWakeLock()
            promise.reject("START_FAILED", e.message ?: "Unbekannter Fehler", e)
        }
    }

    @ReactMethod
    fun stop(promise: Promise) {
        running.set(false)
        try {
            captureThread?.join(1500)
        } catch (_: InterruptedException) {}
        captureThread = null
        try { audioRecord?.stop() } catch (_: Exception) {}
        try { audioRecord?.release() } catch (_: Exception) {}
        audioRecord = null
        releaseAudioEffects()
        releaseWakeLock()
        Log.i(TAG, "Recording gestoppt (seq=$seq Chunks gesendet)")
        promise.resolve(true)
    }

    @ReactMethod
    fun isRecording(promise: Promise) {
        promise.resolve(running.get())
    }

    private fun captureLoop() {
        val buffer = ByteArray(CHUNK_BYTES)
        val rec = audioRecord ?: return
        try {
            while (running.get()) {
                var offset = 0
                // Solange lesen bis ein voller 200ms-Chunk zusammen ist.
                // AudioRecord.read kann weniger als angefordert liefern.
                while (offset < CHUNK_BYTES && running.get()) {
                    val n = rec.read(buffer, offset, CHUNK_BYTES - offset)
                    if (n <= 0) {
                        if (!running.get()) break
                        // Fehlerzustand — kurze Pause, dann weiter probieren
                        Thread.sleep(5)
                        continue
                    }
                    offset += n
                }
                if (offset < CHUNK_BYTES) break

                val b64 = Base64.encodeToString(buffer, Base64.NO_WRAP)
                val ts = System.currentTimeMillis()
                val params = Arguments.createMap().apply {
                    putString("pcm", b64)
                    // putLong existiert nicht in WritableMap — putDouble fuer ts/seq.
                    putDouble("seq", seq.toDouble())
                    putDouble("ts", ts.toDouble())
                }
                reactApplicationContext
                    .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
                    .emit("PcmStreamChunk", params)
                seq++
            }
        } catch (e: Exception) {
            Log.e(TAG, "captureLoop crashed", e)
            try {
                val err = Arguments.createMap().apply {
                    putString("error", e.message ?: "unknown")
                }
                reactApplicationContext
                    .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
                    .emit("PcmStreamError", err)
            } catch (_: Exception) {}
        }
    }

    private fun releaseAudioEffects() {
        try { aec?.release() } catch (_: Exception) {}
        try { ns?.release() } catch (_: Exception) {}
        try { agc?.release() } catch (_: Exception) {}
        aec = null; ns = null; agc = null
    }

    private fun releaseWakeLock() {
        try {
            if (wakeLock?.isHeld == true) wakeLock?.release()
        } catch (_: Exception) {}
        wakeLock = null
    }

    // Damit RCTEventEmitter den Listener-Lifecycle nicht crasht
    @ReactMethod fun addListener(eventName: String) {}
    @ReactMethod fun removeListeners(count: Int) {}
}
