package app.chanora.chanora_flutter

import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothProfile
import android.Manifest
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.os.Build
import android.util.Log
import androidx.core.content.ContextCompat

/**
 * Manages Android Bluetooth voice routing for the Chanora voice session.
 *
 * Trace: SDD-110 (Android Bluetooth SCO)
 *
 * On Android 13 / API 33 and newer, VoIP apps are expected to select a
 * Bluetooth communication route with `AudioManager.setCommunicationDevice()`
 * so BLE audio headsets are supported. On older Android releases we fall
 * back to legacy SCO start / stop management.
 *
 * ## Lifecycle
 *
 *  1. [start] — called by the Rust audio engine (via JNI) after the Oboe
 *     voice streams are opened. On API 33+ this prefers
 *     `AudioManager.setCommunicationDevice()` for Bluetooth communication
 *     devices. On older releases it calls `AudioManager.startBluetoothSco()`
 *     if a Bluetooth SCO-capable device is connected and registers a
 *     `BroadcastReceiver` for `ACTION_SCO_AUDIO_STATE_UPDATED`.
 *  2. Legacy SCO state changes, or synthetic connected/disconnected state
 *     changes for the API 33+ path, are forwarded to the Rust engine via
 *     `publishScoStateChange(int)`, a JNI function declared in
 *     `crates/chanora_audio/src/android_voice_unit.rs`.
 *  3. [stop] — called by the Rust engine on voice stop. Clears the selected
 *     communication device on API 33+ when it is one we selected, otherwise
 *     falls back to `AudioManager.stopBluetoothSco()` and receiver teardown.
 *
 * ## Thread model
 *
 * `start` / `stop` are called from a tokio worker thread (via JNI).
 * `AudioManager.startBluetoothSco` is asynchronous on legacy devices —
 * the platform responds with `ACTION_SCO_AUDIO_STATE_UPDATED` which arrives
 * on the main thread via the `BroadcastReceiver`.
 */
internal class AndroidBluetoothScoController {

    companion object {
        private const val TAG = "ChanoraBluetoothSco"

        /**
         * JNI entry point implemented in
         * `crates/chanora_audio/src/android_voice_unit.rs`.
         *
         * Kotlin calls this from the SCO state `BroadcastReceiver` to
         * forward the state integer to the Rust engine's BackendEvent
         * channel.
         */
        @JvmStatic
        external fun publishScoStateChange(state: Int)

        /**
         * Start Bluetooth SCO management.
         *
         * Called from Rust via JNI after voice unit start.
         * Idempotent: repeated calls against an already-started instance
         * are silently ignored.
         */
        @JvmStatic
        fun start(context: Context) {
            val appContext = context.applicationContext
            if (scoStarted) {
                Log.d(TAG, "start() called but SCO already active; no-op")
                return
            }
            scoStarted = true
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                if (trySelectBluetoothCommunicationDevice(appContext)) {
                    return
                }
                Log.i(
                    TAG,
                    "No selectable Bluetooth communication device on API 33+; legacy SCO fallback is skipped",
                )
                return
            }
            registerScoReceiver(appContext)
            tryStartSco(appContext)
        }

        /**
         * Stop Bluetooth SCO management.
         *
         * Called from Rust via JNI on voice stop.
         * Idempotent: safe to call when SCO is not active.
         */
        @JvmStatic
        fun stop(context: Context) {
            val appContext = context.applicationContext
            scoStarted = false
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                clearSelectedBluetoothCommunicationDevice(appContext)
                return
            }
            unregisterScoReceiver(appContext)
            tryStopSco(appContext)
        }

        private var scoStarted: Boolean = false
        private var receiverRegistered: Boolean = false
        private var selectedCommunicationDeviceId: Int? = null

        private val scoReceiver = object : BroadcastReceiver() {
            override fun onReceive(context: Context?, intent: Intent?) {
                if (intent?.action != AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED) return
                val state = intent.getIntExtra(
                    AudioManager.EXTRA_SCO_AUDIO_STATE,
                    AudioManager.SCO_AUDIO_STATE_ERROR,
                )
                val prevState = intent.getIntExtra(
                    AudioManager.EXTRA_SCO_AUDIO_PREVIOUS_STATE,
                    -1,
                )
                Log.i(
                    TAG,
                    "SCO state: ${scoStateName(state)} (prev: ${scoStateName(prevState)})",
                )
                try {
                    publishScoStateChange(state)
                } catch (t: Throwable) {
                    Log.w(TAG, "publishScoStateChange JNI failed: ${t.message}", t)
                }
            }
        }

        private fun registerScoReceiver(context: Context) {
            if (receiverRegistered) return
            try {
                val filter = IntentFilter(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)
                if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
                    context.registerReceiver(scoReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
                } else {
                    @Suppress("UnspecifiedRegisterReceiverFlag")
                    context.registerReceiver(scoReceiver, filter)
                }
                receiverRegistered = true
                Log.d(TAG, "SCO receiver registered")
            } catch (e: Exception) {
                Log.w(TAG, "Failed to register SCO receiver: ${e.message}", e)
            }
        }

        private fun unregisterScoReceiver(context: Context) {
            if (!receiverRegistered) return
            try {
                context.unregisterReceiver(scoReceiver)
                receiverRegistered = false
                Log.d(TAG, "SCO receiver unregistered")
            } catch (e: IllegalArgumentException) {
                // Already unregistered — ignore silently.
                receiverRegistered = false
            }
        }

        private val bluetoothAdapter: BluetoothAdapter?
            get() = try {
                BluetoothAdapter.getDefaultAdapter()
            } catch (e: SecurityException) {
                Log.w(TAG, "BluetoothAdapter unavailable: ${e.message}")
                null
            }

        private fun isBluetoothScoOn(am: AudioManager): Boolean = try {
            am.isBluetoothScoOn
        } catch (e: SecurityException) {
            Log.w(TAG, "isBluetoothScoOn failed: ${e.message}")
            false
        }

        private fun isBluetoothScoAvailableOffCall(am: AudioManager): Boolean = try {
            am.isBluetoothScoAvailableOffCall
        } catch (e: SecurityException) {
            Log.w(TAG, "isBluetoothScoAvailableOffCall failed: ${e.message}")
            false
        }

        private fun tryStartSco(context: Context) {
            val am = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
            if (am == null) {
                Log.e(TAG, "AudioManager unavailable; cannot start SCO")
                return
            }
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S &&
                ContextCompat.checkSelfPermission(
                    context,
                    Manifest.permission.BLUETOOTH_CONNECT,
                ) != PackageManager.PERMISSION_GRANTED
            ) {
                Log.i(TAG, "BLUETOOTH_CONNECT not granted; skipping SCO start")
                return
            }

            if (isBluetoothScoOn(am)) {
                Log.i(TAG, "SCO already on; no-op")
                return
            }

            val adapter = bluetoothAdapter
            if (adapter == null || !adapter.isEnabled) {
                Log.i(TAG, "Bluetooth not enabled; skipping SCO start")
                return
            }

            val scoAvailableOffCall = isBluetoothScoAvailableOffCall(am)
            if (!scoAvailableOffCall) {
                Log.i(TAG, "Bluetooth SCO not available off-call; skipping SCO start")
                return
            }

            val hasScoHeadset = try {
                val state = adapter.getProfileConnectionState(BluetoothProfile.HEADSET)
                state == BluetoothProfile.STATE_CONNECTED
            } catch (e: SecurityException) {
                Log.w(TAG, "getProfileConnectionState(HEADSET) failed: ${e.message}")
                false
            }

            if (!hasScoHeadset) {
                Log.i(TAG, "No SCO-capable Bluetooth headset connected; skipping SCO start")
                return
            }

            try {
                am.startBluetoothSco()
                Log.i(TAG, "startBluetoothSco() dispatched")
            } catch (e: SecurityException) {
                Log.e(TAG, "startBluetoothSco denied: ${e.message}", e)
            }
        }

        private fun trySelectBluetoothCommunicationDevice(context: Context): Boolean {
            val am = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
            if (am == null) {
                Log.e(TAG, "AudioManager unavailable; cannot select communication device")
                return false
            }
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
                return false
            }
            if (ContextCompat.checkSelfPermission(
                    context,
                    Manifest.permission.BLUETOOTH_CONNECT,
                ) != PackageManager.PERMISSION_GRANTED
            ) {
                Log.i(TAG, "BLUETOOTH_CONNECT not granted; skipping bluetooth communication device selection")
                return false
            }
            val target = am.availableCommunicationDevices.firstOrNull { it.isBluetoothCommunicationDevice() }
            if (target == null) {
                Log.i(TAG, "No Bluetooth communication device available on API 33+")
                return false
            }
            val current = am.communicationDevice
            if (current?.id == target.id) {
                selectedCommunicationDeviceId = target.id
                publishSyntheticScoState(AudioManager.SCO_AUDIO_STATE_CONNECTED)
                Log.i(TAG, "Bluetooth communication device already selected: ${target.logLabel()}")
                return true
            }
            val changed = am.setCommunicationDevice(target)
            Log.i(
                TAG,
                "setCommunicationDevice bluetooth target=${target.logLabel()} changed=$changed selected=${am.communicationDevice?.logLabel()}",
            )
            if (changed) {
                selectedCommunicationDeviceId = target.id
                publishSyntheticScoState(AudioManager.SCO_AUDIO_STATE_CONNECTED)
            }
            return changed
        }

        private fun clearSelectedBluetoothCommunicationDevice(context: Context) {
            val am = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
            if (am == null) {
                Log.e(TAG, "AudioManager unavailable; cannot clear communication device")
                return
            }
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
                return
            }
            val selectedId = selectedCommunicationDeviceId
            val current = am.communicationDevice
            if (selectedId == null || current?.id != selectedId) {
                selectedCommunicationDeviceId = null
                Log.d(TAG, "No app-selected Bluetooth communication device to clear")
                return
            }
            am.clearCommunicationDevice()
            selectedCommunicationDeviceId = null
            publishSyntheticScoState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED)
            Log.i(TAG, "clearCommunicationDevice() dispatched for bluetooth route")
        }

        private fun tryStopSco(context: Context) {
            val am = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
            if (am == null) {
                Log.e(TAG, "AudioManager unavailable; cannot stop SCO")
                return
            }
            if (!isBluetoothScoOn(am)) {
                Log.d(TAG, "SCO not on; no-op")
                return
            }
            try {
                am.stopBluetoothSco()
                Log.i(TAG, "stopBluetoothSco() dispatched")
            } catch (e: SecurityException) {
                Log.w(TAG, "stopBluetoothSco denied: ${e.message}", e)
            }
        }

        private fun publishSyntheticScoState(state: Int) {
            try {
                publishScoStateChange(state)
            } catch (t: Throwable) {
                Log.w(TAG, "publishScoStateChange JNI failed: ${t.message}", t)
            }
        }

        private fun AudioDeviceInfo.isBluetoothCommunicationDevice(): Boolean =
            type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO ||
                (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
                    (type == AudioDeviceInfo.TYPE_BLE_HEADSET ||
                        type == AudioDeviceInfo.TYPE_BLE_SPEAKER ||
                        (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
                            type == AudioDeviceInfo.TYPE_BLE_BROADCAST)))

        private fun AudioDeviceInfo.logLabel(): String =
            "id=$id type=$type product=${productName?.toString()?.trim().orEmpty()}"

        private fun scoStateName(state: Int): String = when (state) {
            AudioManager.SCO_AUDIO_STATE_DISCONNECTED -> "DISCONNECTED"
            AudioManager.SCO_AUDIO_STATE_CONNECTED -> "CONNECTED"
            AudioManager.SCO_AUDIO_STATE_CONNECTING -> "CONNECTING"
            AudioManager.SCO_AUDIO_STATE_ERROR -> "ERROR"
            else -> "UNKNOWN($state)"
        }
    }
}
