/*
 * Hermes Node Android
 * Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net>
 * SPDX-License-Identifier: GPL-3.0-or-later
 */
package net.nexlab.hermesnodeandroid.capabilities

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.media.AudioDeviceInfo
import android.media.AudioFormat
import android.media.AudioManager
import android.media.AudioRecord
import android.media.MediaPlayer
import android.media.MediaRecorder
import android.os.Build
import androidx.core.content.ContextCompat
import net.nexlab.hermesnodeandroid.CapabilityResult
import org.json.JSONArray
import org.json.JSONObject
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.Base64
import kotlin.math.min

class AndroidAudioControl(private val context: Context) {
    private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager

    fun capabilityInfo(): JSONObject = JSONObject()
        .put("available", true)
        .put("can_get_status", true)
        .put("can_list_devices", Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
        .put("can_play_audio", true)
        .put("can_capture_input", hasRecordAudioPermission())
        .put("capture_input_requires_microphone_permission", !hasRecordAudioPermission())
        .put("capture_output_ready", false)

    fun handle(action: String?, params: JSONObject): CapabilityResult = when (action) {
        "list_audio_devices" -> listAudioDevices()
        "get_audio_status" -> getAudioStatus()
        "play_audio" -> playAudio(params)
        "capture_input" -> captureInput(params)
        "capture_output" -> CapabilityResult.unsupported("capture_output is not generally available to normal Android apps")
        else -> CapabilityResult.unsupported("Unknown audio_control action: $action")
    }

    private fun getAudioStatus(): CapabilityResult = CapabilityResult.ok(
        "music_volume" to audioManager.getStreamVolume(AudioManager.STREAM_MUSIC),
        "music_volume_max" to audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC),
        "ringer_mode" to audioManager.ringerMode,
        "speakerphone_on" to audioManager.isSpeakerphoneOn,
        "microphone_mute" to audioManager.isMicrophoneMute,
    )

    private fun listAudioDevices(): CapabilityResult {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return CapabilityResult.unsupported("AudioDeviceInfo requires Android M+")
        val arr = JSONArray()
        audioManager.getDevices(AudioManager.GET_DEVICES_ALL).forEach { device ->
            arr.put(JSONObject()
                .put("id", device.id)
                .put("type", audioDeviceTypeName(device.type))
                .put("is_source", device.isSource)
                .put("is_sink", device.isSink)
                .put("product_name", device.productName?.toString() ?: "")
                .put("sample_rates", JSONArray(device.sampleRates.toList()))
                .put("channel_counts", JSONArray(device.channelCounts.toList()))
            )
        }
        return CapabilityResult.ok("devices" to arr, "count" to arr.length())
    }

    private fun captureInput(params: JSONObject): CapabilityResult {
        if (!hasRecordAudioPermission()) return CapabilityResult.unsupported("capture_input requires RECORD_AUDIO permission; grant Microphone permission in Android settings/app prompt")
        val durationMs = min(params.optLong("duration_ms", params.optLong("duration", 1000L)), 5000L)
        val sampleRate = params.optInt("sample_rate", 16000)
        val minBuffer = AudioRecord.getMinBufferSize(
            sampleRate,
            AudioFormat.CHANNEL_IN_MONO,
            AudioFormat.ENCODING_PCM_16BIT,
        )
        if (minBuffer <= 0) return CapabilityResult.unsupported("AudioRecord does not support ${sampleRate}Hz mono PCM16 on this device")
        val targetBytes = ((sampleRate * durationMs / 1000L) * 2L).toInt()
        val bufferSize = maxOf(minBuffer * 2, 4096)
        val audioRecord = AudioRecord(
            MediaRecorder.AudioSource.MIC,
            sampleRate,
            AudioFormat.CHANNEL_IN_MONO,
            AudioFormat.ENCODING_PCM_16BIT,
            bufferSize,
        )
        return try {
            audioRecord.startRecording()
            val out = ByteArrayOutputStream(targetBytes)
            val buf = ByteArray(bufferSize)
            while (out.size() < targetBytes) {
                val n = audioRecord.read(buf, 0, min(buf.size, targetBytes - out.size()))
                if (n <= 0) break
                out.write(buf, 0, n)
            }
            audioRecord.stop()
            val pcm = out.toByteArray()
            val wav = wavWrap(pcm, sampleRate)
            CapabilityResult.ok(
                "format" to "wav",
                "sample_rate" to sampleRate,
                "channels" to 1,
                "duration_ms" to durationMs,
                "encoding" to "base64",
                "data" to Base64.getEncoder().encodeToString(wav),
                "bytes" to wav.size,
            )
        } catch (e: SecurityException) {
            CapabilityResult.unsupported("capture_input permission denied: ${e.message}")
        } catch (e: Exception) {
            CapabilityResult.unsupported("capture_input failed: ${e.message}")
        } finally {
            runCatching { audioRecord.release() }
        }
    }

    private fun playAudio(params: JSONObject): CapabilityResult {
        val base64Data = params.optString("data")
        val path = params.optString("path")
        var tempFile: File? = null
        val mediaPlayer = MediaPlayer()
        return try {
            val file = when {
                base64Data.isNotBlank() -> writeAudioTempFile(base64Data, params.optString("format", "mp3")).also { tempFile = it }
                path.isNotBlank() -> File(path)
                else -> return CapabilityResult.unsupported("play_audio requires params.data (base64 audio) or params.path")
            }
            if (!file.exists()) return CapabilityResult.unsupported("play_audio source file does not exist: ${file.absolutePath}")
            mediaPlayer.setDataSource(file.absolutePath)
            mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC)
            mediaPlayer.prepare()
            val durationMs = mediaPlayer.duration
            mediaPlayer.start()
            CapabilityResult.ok(
                "started" to true,
                "path" to file.absolutePath,
                "duration_ms" to durationMs,
            )
        } catch (e: Exception) {
            CapabilityResult.unsupported("play_audio failed: ${e.message}")
        } finally {
            mediaPlayer.setOnCompletionListener {
                runCatching { it.release() }
                tempFile?.let { file -> runCatching { file.delete() } }
            }
            mediaPlayer.setOnErrorListener { mp, _, _ ->
                runCatching { mp.release() }
                tempFile?.let { file -> runCatching { file.delete() } }
                true
            }
        }
    }

    private fun writeAudioTempFile(base64Data: String, format: String): File {
        val ext = when (format.lowercase()) {
            "wav" -> ".wav"
            "ogg" -> ".ogg"
            "m4a" -> ".m4a"
            else -> ".mp3"
        }
        val file = File.createTempFile("hermes-audio-", ext, context.cacheDir)
        val bytes = Base64.getDecoder().decode(base64Data)
        FileOutputStream(file).use { it.write(bytes) }
        return file
    }

    private fun hasRecordAudioPermission(): Boolean =
        ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED

    private fun wavWrap(pcm: ByteArray, sampleRate: Int): ByteArray {
        val header = ByteArrayOutputStream(44 + pcm.size)
        fun ascii(s: String) = header.write(s.toByteArray(Charsets.US_ASCII))
        fun le32(v: Int) = header.write(ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(v).array())
        fun le16(v: Int) = header.write(ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort(v.toShort()).array())
        ascii("RIFF"); le32(36 + pcm.size); ascii("WAVE")
        ascii("fmt "); le32(16); le16(1); le16(1); le32(sampleRate); le32(sampleRate * 2); le16(2); le16(16)
        ascii("data"); le32(pcm.size); header.write(pcm)
        return header.toByteArray()
    }

    private fun audioDeviceTypeName(type: Int): String = when (type) {
        AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> "builtin_earpiece"
        AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> "builtin_speaker"
        AudioDeviceInfo.TYPE_WIRED_HEADSET -> "wired_headset"
        AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> "wired_headphones"
        AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> "bluetooth_sco"
        AudioDeviceInfo.TYPE_BLUETOOTH_A2DP -> "bluetooth_a2dp"
        AudioDeviceInfo.TYPE_BUILTIN_MIC -> "builtin_mic"
        AudioDeviceInfo.TYPE_USB_DEVICE -> "usb_device"
        AudioDeviceInfo.TYPE_USB_HEADSET -> "usb_headset"
        AudioDeviceInfo.TYPE_HDMI -> "hdmi"
        else -> "type_$type"
    }
}
