package com.ledgrab.android

import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.hardware.usb.UsbManager
import android.os.Build
import android.util.Log
import androidx.core.content.ContextCompat
import com.hoho.android.usbserial.driver.UsbSerialDriver
import com.hoho.android.usbserial.driver.UsbSerialPort
import com.hoho.android.usbserial.driver.UsbSerialProber
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.TimeoutCancellationException

/**
 * USB-serial bridge exposed to the Python server via Chaquopy.
 *
 * Uses the `usb-serial-for-android` library (mik3y) which ships drivers
 * for the common USB-to-TTL chips (CH340, CP2102, FTDI, Prolific, and
 * CDC-ACM) found on Arduino boards and Adalight/AmbiLED controllers.
 *
 * Python callers access the singleton instance via
 *   `UsbSerialBridge.INSTANCE.listDevices()` etc. — see
 *   `server/src/ledgrab/core/devices/android_serial_transport.py`.
 *
 * The bridge holds no Context of its own; [init] must be called once
 * from [LedGrabApp.onCreate] to bind the application context.
 */
object UsbSerialBridge {
    private const val TAG = "UsbSerialBridge"
    private const val ACTION_USB_PERMISSION = "com.ledgrab.android.USB_PERMISSION"

    @Volatile private var appContext: Context? = null

    private val handleSeq = AtomicInteger(1)
    private val openPorts = HashMap<Int, UsbSerialPort>()
    private val initialized = AtomicBoolean(false)
    private val pendingPermissions = ConcurrentHashMap<String, CompletableDeferred<Boolean>>()

    /** Called once from [LedGrabApp.onCreate] so we can resolve services. */
    @JvmStatic
    fun init(context: Context) {
        val app = context.applicationContext
        appContext = app
        // Idempotent: re-entrant init() must not double-register the
        // receiver (which would leak listeners and double-fire callbacks).
        if (!initialized.compareAndSet(false, true)) return

        val filter = IntentFilter(ACTION_USB_PERMISSION)
        val ourPackage = app.packageName
        val receiver = object : BroadcastReceiver() {
            override fun onReceive(ctx: Context, intent: Intent) {
                // Defence-in-depth: the receiver is registered as
                // RECEIVER_NOT_EXPORTED, but on pre-API-33 platforms
                // older Android versions historically defaulted to
                // exported. Also enforce the package check here so an
                // explicit-intent attack from another app on the device
                // is rejected even if the OS treats us as exported.
                if (intent.`package` != null && intent.`package` != ourPackage) {
                    Log.w(
                        TAG,
                        "Ignoring USB permission broadcast from " +
                            "package='${intent.`package`}' (not us)",
                    )
                    return
                }
                val granted = intent.getBooleanExtra(
                    UsbManager.EXTRA_PERMISSION_GRANTED,
                    false,
                )
                val device = intent.getParcelableExtra<android.hardware.usb.UsbDevice>(
                    UsbManager.EXTRA_DEVICE,
                )
                Log.i(TAG, "USB permission broadcast: granted=$granted device=${device?.deviceName}")
                device?.deviceName?.let { name ->
                    pendingPermissions.remove(name)?.complete(granted)
                }
            }
        }
        // ContextCompat handles the RECEIVER_NOT_EXPORTED flag correctly
        // across all supported API levels (it's a no-op on platforms
        // where the flag doesn't exist, and explicit on API ≥33 where
        // Android enforces it).
        ContextCompat.registerReceiver(
            app,
            receiver,
            filter,
            ContextCompat.RECEIVER_NOT_EXPORTED,
        )
    }

    private fun ctx(): Context =
        appContext ?: error("UsbSerialBridge.init() not called — app context unavailable")

    private fun safeSerial(driver: UsbSerialDriver): String =
        try {
            driver.device.serialNumber ?: ""
        } catch (_: SecurityException) {
            // Reading the serial requires USB permission on API 29+.
            ""
        }

    /**
     * Enumerate attached USB-serial devices.
     *
     * Each entry is `"VID|PID|serial|description"` with VID/PID as
     * 4-char lowercase hex. Pipe is used as the separator so device
     * descriptions containing colons (common on FTDI strings) don't
     * confuse the Python parser.
     */
    @JvmStatic
    fun listDevices(): List<String> {
        val manager = ctx().getSystemService(Context.USB_SERVICE) as UsbManager
        val drivers = UsbSerialProber.getDefaultProber().findAllDrivers(manager)
        return drivers.map { driver ->
            val dev = driver.device
            val vid = "%04x".format(dev.vendorId)
            val pid = "%04x".format(dev.productId)
            val serial = safeSerial(driver)
            val description = buildString {
                append(dev.manufacturerName ?: "USB")
                val product = dev.productName
                if (!product.isNullOrBlank()) {
                    append(' ')
                    append(product)
                }
            }.trim().ifEmpty { "USB $vid:$pid" }
            "$vid|$pid|$serial|$description"
        }
    }

    /**
     * Open the first matching USB-serial device. Returns a non-negative
     * opaque handle on success, -1 on failure (device not found, user
     * denied permission, or driver error). Failures also trigger an
     * async permission-request dialog when applicable — subsequent
     * open() calls will succeed once the user grants.
     */
    @JvmStatic
    fun open(vendorId: Int, productId: Int, serial: String, baud: Int): Int {
        val context = ctx()
        val manager = context.getSystemService(Context.USB_SERVICE) as UsbManager
        val drivers = UsbSerialProber.getDefaultProber().findAllDrivers(manager)
        val driver = drivers.firstOrNull { d ->
            val dev = d.device
            dev.vendorId == vendorId &&
                dev.productId == productId &&
                (serial.isEmpty() || safeSerial(d) == serial)
        }
        if (driver == null) {
            Log.w(TAG, "No matching device for $vendorId:$productId:$serial")
            return -1
        }

        if (!manager.hasPermission(driver.device)) {
            Log.w(TAG, "USB permission not yet granted for ${driver.device.deviceName}")
            requestPermission(context, manager, driver)
            return -1
        }

        val connection = manager.openDevice(driver.device)
        if (connection == null) {
            Log.w(TAG, "openDevice returned null for ${driver.device.deviceName}")
            return -1
        }
        val port = driver.ports.firstOrNull()
        if (port == null) {
            connection.close()
            Log.w(TAG, "Driver reports no ports for ${driver.device.deviceName}")
            return -1
        }

        try {
            port.open(connection)
            port.setParameters(
                baud,
                8,
                UsbSerialPort.STOPBITS_1,
                UsbSerialPort.PARITY_NONE,
            )
        } catch (e: Exception) {
            Log.e(TAG, "Failed to configure serial port", e)
            runCatching { port.close() }
            return -1
        }

        val handle = handleSeq.getAndIncrement()
        synchronized(openPorts) { openPorts[handle] = port }
        Log.i(
            TAG,
            "Opened USB serial ${driver.device.deviceName} baud=$baud handle=$handle",
        )
        return handle
    }

    /** Write bytes to the previously-opened handle. Throws if invalid. */
    @JvmStatic
    fun write(handle: Int, data: ByteArray) {
        val port = synchronized(openPorts) { openPorts[handle] }
            ?: throw IllegalStateException("Invalid handle $handle")
        // 1s write timeout matches the old pyserial `timeout=1` behavior.
        port.write(data, 1_000)
    }

    /** Close a previously-opened handle. Silently ignores unknown handles. */
    @JvmStatic
    fun close(handle: Int) {
        val port = synchronized(openPorts) { openPorts.remove(handle) } ?: return
        runCatching { port.close() }
            .onFailure { Log.w(TAG, "close($handle): ${it.message}") }
    }

    /**
     * Block until the user grants (or denies) USB permission for the
     * device with [deviceName] (e.g. "/dev/bus/usb/001/004"). Returns
     * true if granted within [timeoutMs], false otherwise. Safe to call
     * from a Python thread via Chaquopy.
     */
    @JvmStatic
    @JvmOverloads
    fun requestPermissionBlocking(deviceName: String, timeoutMs: Long = 15_000L): Boolean {
        val context = ctx()
        val manager = context.getSystemService(Context.USB_SERVICE) as UsbManager
        val driver = UsbSerialProber.getDefaultProber().findAllDrivers(manager)
            .firstOrNull { it.device.deviceName == deviceName }
            ?: return false
        if (manager.hasPermission(driver.device)) return true

        // Coalesce concurrent requests for the same device — only the
        // first caller actually fires the system dialog.
        val deferred = pendingPermissions.computeIfAbsent(deviceName) {
            CompletableDeferred<Boolean>().also {
                requestPermission(context, manager, driver)
            }
        }

        return try {
            runBlocking {
                withTimeout(timeoutMs) { deferred.await() }
            }
        } catch (_: TimeoutCancellationException) {
            pendingPermissions.remove(deviceName)
            Log.w(TAG, "Permission request timed out for $deviceName")
            false
        } catch (e: Exception) {
            pendingPermissions.remove(deviceName)
            Log.w(TAG, "Permission request failed for $deviceName: ${e.message}")
            false
        }
    }

    /**
     * Like [open] but blocks for permission first. Use this from Python
     * instead of relying on the open()/retry pattern.
     */
    @JvmStatic
    @JvmOverloads
    fun openWithPermission(
        vendorId: Int,
        productId: Int,
        serial: String,
        baud: Int,
        timeoutMs: Long = 15_000L,
    ): Int {
        val context = ctx()
        val manager = context.getSystemService(Context.USB_SERVICE) as UsbManager
        val driver = UsbSerialProber.getDefaultProber().findAllDrivers(manager)
            .firstOrNull { d ->
                val dev = d.device
                dev.vendorId == vendorId &&
                    dev.productId == productId &&
                    (serial.isEmpty() || safeSerial(d) == serial)
            } ?: return -1

        if (!manager.hasPermission(driver.device)) {
            val granted = requestPermissionBlocking(driver.device.deviceName, timeoutMs)
            if (!granted) return -1
        }
        return open(vendorId, productId, serial, baud)
    }

    private fun requestPermission(
        context: Context,
        manager: UsbManager,
        driver: UsbSerialDriver,
    ) {
        val flags =
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                PendingIntent.FLAG_IMMUTABLE
            } else {
                0
            }
        val intent = Intent(ACTION_USB_PERMISSION).apply {
            setPackage(context.packageName)
        }
        val pending = PendingIntent.getBroadcast(context, 0, intent, flags)
        manager.requestPermission(driver.device, pending)
    }
}
