package app.chanora.chanora_flutter

import android.content.Context
import android.Manifest
import android.content.pm.PackageManager
import android.media.AudioDeviceCallback
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.content.ContextCompat
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel

internal class AndroidAudioOutputController(context: Context) :
    MethodChannel.MethodCallHandler,
    EventChannel.StreamHandler {
    private val appContext = context.applicationContext
    private val audioManager: AudioManager = appContext.getSystemService(AudioManager::class.java)
    private val mainHandler = Handler(Looper.getMainLooper())
    private var eventSink: EventChannel.EventSink? = null
    private var callbackRegistered = false

    private val deviceCallback = object : AudioDeviceCallback() {
        override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) {
            emitDeviceChanged()
        }

        override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>) {
            emitDeviceChanged()
        }
    }

    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
        try {
            when (call.method) {
                MethodChannels.METHOD_GET_OUTPUT_DEVICES -> result.success(getOutputDevices())
                MethodChannels.METHOD_GET_COMMUNICATION_DEVICES -> result.success(getCommunicationDevices())
                MethodChannels.METHOD_SET_COMMUNICATION_DEVICE -> {
                    val deviceId = call.argument<String>("deviceId")
                    if (deviceId.isNullOrBlank()) {
                        result.error("missing_device_id", "deviceId is required", null)
                    } else {
                        result.success(setCommunicationDevice(deviceId))
                    }
                }
                MethodChannels.METHOD_CLEAR_COMMUNICATION_DEVICE -> {
                    clearCommunicationDevice()
                    result.success(null)
                }
                else -> result.notImplemented()
            }
        } catch (t: Throwable) {
            result.error("audio_output_failed", t.message, null)
        }
    }

    override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
        eventSink = events
        startObserveAudioDevices()
    }

    override fun onCancel(arguments: Any?) {
        stopObserveAudioDevices()
        eventSink = null
    }

    fun detach() {
        stopObserveAudioDevices()
        eventSink = null
    }

    private fun getOutputDevices(): List<Map<String, Any>> {
        val communicationDevices = communicationDevices()
        val communicationIds = CommunicationDeviceIds(
            selectedId = selectedCommunicationDeviceId(),
            availableIds = communicationDevices.map { it.id.toString() }.toSet(),
            availableTypes = communicationDevices.map { it.type }.toSet(),
        )
        return audioManager
            .getDevices(AudioManager.GET_DEVICES_OUTPUTS)
            .map { device ->
                val dto = device.toDto(communicationIds.selectedId, communicationIds.availableIds)
                dto + ("isAvailableForCommunication" to (
                    dto["isAvailableForCommunication"] == true ||
                        communicationIds.availableTypes.contains(device.type)
                    ))
            }
    }

    private fun getCommunicationDevices(): List<Map<String, Any>> {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            val selectedId = selectedCommunicationDeviceId()
            val devices = communicationDevices()
            val availableIds = devices.map { it.id.toString() }.toSet()
            return devices.map { it.toDto(selectedId, availableIds) }
        }

        return audioManager
            .getDevices(AudioManager.GET_DEVICES_OUTPUTS)
            .map { it.toDto(selectedId = null, communicationIds = emptySet()) }
    }

    private fun setCommunicationDevice(deviceId: String): Boolean {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
            Log.w(TAG, "setCommunicationDevice unsupported below API 31 deviceId=$deviceId")
            return false
        }
        val communicationDevices = communicationDevices()
        val outputDevice = audioManager
            .getDevices(AudioManager.GET_DEVICES_OUTPUTS)
            .firstOrNull { it.id.toString() == deviceId }
        val target = communicationDevices.firstOrNull { it.id.toString() == deviceId }
            ?: outputDevice?.let { output ->
                communicationDevices.firstOrNull { it.type == output.type }
            }
        if (target == null) {
            Log.w(
                TAG,
                "setCommunicationDevice target not available deviceId=$deviceId outputs=${audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS).joinToString { it.logLabel() }} communication=${communicationDevices.joinToString { it.logLabel() }}",
            )
            return false
        }
        if (target.requiresBluetoothConnectPermission() &&
            ContextCompat.checkSelfPermission(
                appContext,
                Manifest.permission.BLUETOOTH_CONNECT,
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            Log.w(
                TAG,
                "setCommunicationDevice blocked for bluetooth target=${target.logLabel()}: BLUETOOTH_CONNECT not granted",
            )
            return false
        }
        val changed = audioManager.setCommunicationDevice(target)
        Log.i(TAG, "setCommunicationDevice device=${target.logLabel()} changed=$changed selected=${audioManager.communicationDevice?.logLabel()}")
        return changed
    }

    private fun clearCommunicationDevice() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            audioManager.clearCommunicationDevice()
            Log.i(TAG, "clearCommunicationDevice selected=${audioManager.communicationDevice?.logLabel()}")
        }
    }

    private fun startObserveAudioDevices() {
        if (callbackRegistered) return
        audioManager.registerAudioDeviceCallback(deviceCallback, mainHandler)
        callbackRegistered = true
    }

    private fun stopObserveAudioDevices() {
        if (!callbackRegistered) return
        audioManager.unregisterAudioDeviceCallback(deviceCallback)
        callbackRegistered = false
    }

    private fun emitDeviceChanged() {
        mainHandler.post {
            eventSink?.success(mapOf("type" to "audioDeviceChanged"))
        }
    }

    private fun communicationDeviceIds(): CommunicationDeviceIds {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
            return CommunicationDeviceIds(
                selectedId = null,
                availableIds = emptySet(),
                availableTypes = emptySet(),
            )
        }
        val devices = communicationDevices()
        return CommunicationDeviceIds(
            selectedId = selectedCommunicationDeviceId(),
            availableIds = devices.map { it.id.toString() }.toSet(),
            availableTypes = devices.map { it.type }.toSet(),
        )
    }

    private fun selectedCommunicationDeviceId(): String? =
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            audioManager.communicationDevice?.id?.toString()
        } else {
            null
        }

    private fun communicationDevices(): List<AudioDeviceInfo> =
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            audioManager.availableCommunicationDevices
        } else {
            emptyList()
        }

    private fun AudioDeviceInfo.toDto(
        selectedId: String?,
        communicationIds: Set<String>,
    ): Map<String, Any> {
        val id = this.id.toString()
        val normalizedType = normalizedType()
        return mapOf(
            "id" to id,
            "name" to displayName(normalizedType),
            "type" to normalizedType,
            "isSelected" to (selectedId == id),
            "isAvailableForCommunication" to communicationIds.contains(id),
        )
    }

    private fun AudioDeviceInfo.displayName(normalizedType: String): String {
        val product = productName?.toString()?.trim().orEmpty()
        if (product.isNotEmpty()) return product
        return when (normalizedType) {
            "speaker" -> "Speaker"
            "earpiece" -> "Earpiece"
            "wiredHeadset", "wiredHeadphones" -> "Wired Headset"
            "bluetoothA2dp", "bluetoothSco", "bluetoothLe" -> "Bluetooth Headset"
            "usbHeadset" -> "USB Headset"
            "hdmi" -> "HDMI"
            else -> "Other Device"
        }
    }

    private fun AudioDeviceInfo.normalizedType(): String = when (type) {
        AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> "speaker"
        AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> "earpiece"
        AudioDeviceInfo.TYPE_WIRED_HEADSET -> "wiredHeadset"
        AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> "wiredHeadphones"
        AudioDeviceInfo.TYPE_BLUETOOTH_A2DP -> "bluetoothA2dp"
        AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> "bluetoothSco"
        AudioDeviceInfo.TYPE_USB_HEADSET -> "usbHeadset"
        AudioDeviceInfo.TYPE_HDMI -> "hdmi"
        else -> if (isBluetoothLe()) "bluetoothLe" else "unknown"
    }

    private fun AudioDeviceInfo.isBluetoothLe(): Boolean {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return false
        return 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.requiresBluetoothConnectPermission(): Boolean =
        normalizedType() == "bluetoothA2dp" ||
            normalizedType() == "bluetoothSco" ||
            normalizedType() == "bluetoothLe"

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

    private data class CommunicationDeviceIds(
        val selectedId: String?,
        val availableIds: Set<String>,
        val availableTypes: Set<Int>,
    )

    private companion object {
        const val TAG = "ChanoraAudioOutput"
    }
}
