package com.ledgrab.android

import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
import android.content.Context
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
import android.util.Log
import java.util.Collections
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.TimeoutCancellationException

/**
 * Android BLE bridge exposed to the Python server via Chaquopy.
 *
 * Wraps the Android BluetoothGatt / BluetoothLeScanner APIs into
 * synchronous, blocking calls that can be safely invoked from
 * a Python thread (Chaquopy proxy threads are real OS threads).
 *
 * All GATT callbacks run on a private [HandlerThread] so they don't
 * block the main looper.  [runBlocking] is used to bridge callback
 * completions back to the calling Python thread.
 *
 * Python callers access the singleton via
 *   `BleBridge.INSTANCE.scan()` etc. — see
 *   `server/src/ledgrab/core/devices/android_ble_transport.py`.
 */
object BleBridge {
    private const val TAG = "BleBridge"
    private const val CONNECT_TIMEOUT_MS = 18_000L // connect + service discovery
    private const val WRITE_TIMEOUT_MS = 5_000L

    @Volatile private var appContext: Context? = null

    private val handleSeq = AtomicInteger(1)

    // Dedicated looper thread so BLE callbacks don't land on the main thread.
    private val bleHandlerThread = HandlerThread("LedGrab-BLE").also { it.start() }
    private val bleHandler = Handler(bleHandlerThread.looper)

    private data class GattHandle(
        val gatt: BluetoothGatt,
        val writeChar: BluetoothGattCharacteristic,
    )

    private val handles = ConcurrentHashMap<Int, GattHandle>()

    // Write completion futures, keyed by handle.  Only populated for
    // WRITE_TYPE_DEFAULT (with-response) writes.
    private val pendingWrites = ConcurrentHashMap<Int, CompletableDeferred<Boolean>>()

    /** Called once from [LedGrabApp.onCreate] to bind the application context. */
    @JvmStatic
    fun init(context: Context) {
        appContext = context.applicationContext
    }

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

    private fun adapter() =
        ctx().getSystemService(BluetoothManager::class.java)?.adapter

    // ─── Public API ──────────────────────────────────────────────────────────

    /**
     * Scan for BLE peripherals for [timeoutMs] milliseconds.
     *
     * Returns a list of `"address|name|rssi"` strings.  Addresses are
     * deduplicated — only the last-seen RSSI for each address is kept.
     * Returns an empty list if Bluetooth is off or the permission is denied.
     */
    @JvmStatic
    @JvmOverloads
    fun scan(timeoutMs: Long = 4_000L): List<String> {
        val adapter = adapter() ?: return emptyList()
        if (!adapter.isEnabled) return emptyList()
        val scanner = try { adapter.bluetoothLeScanner } catch (_: SecurityException) { null }
            ?: return emptyList()

        val seen = Collections.synchronizedMap(LinkedHashMap<String, String>())
        val callback = object : ScanCallback() {
            override fun onScanResult(callbackType: Int, result: ScanResult) {
                val address = result.device.address ?: return
                val name = result.scanRecord?.deviceName ?: result.device.name ?: ""
                seen[address] = "$address|$name|${result.rssi}"
            }

            override fun onScanFailed(errorCode: Int) {
                Log.w(TAG, "BLE scan failed with error $errorCode")
            }
        }

        try {
            bleHandler.post { scanner.startScan(callback) }
            Thread.sleep(timeoutMs)
        } catch (_: InterruptedException) {
            Thread.currentThread().interrupt()
        } finally {
            try { bleHandler.post { scanner.stopScan(callback) } } catch (_: SecurityException) {}
        }
        return seen.values.toList()
    }

    /**
     * Connect to the BLE peripheral at [address] and locate the GATT
     * characteristic identified by [writeCharUuid] across all services.
     *
     * Blocks until connected + services discovered, or returns -1 on failure.
     * The returned integer is an opaque handle passed to [write]/[disconnect].
     */
    @JvmStatic
    fun connect(address: String, writeCharUuid: String): Int {
        val adapter = adapter() ?: return -1
        val device = try { adapter.getRemoteDevice(address) } catch (e: Exception) {
            Log.e(TAG, "Invalid BLE address '$address': ${e.message}")
            return -1
        }

        val readyDeferred = CompletableDeferred<Boolean>()

        val callback = object : BluetoothGattCallback() {
            override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
                when {
                    newState == BluetoothProfile.STATE_CONNECTED
                            && status == BluetoothGatt.GATT_SUCCESS -> {
                        Log.d(TAG, "GATT connected to $address, discovering services")
                        gatt.discoverServices()
                    }
                    newState == BluetoothProfile.STATE_DISCONNECTED -> {
                        Log.w(TAG, "GATT disconnected from $address (status=$status)")
                        readyDeferred.complete(false)
                    }
                }
            }

            override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
                readyDeferred.complete(status == BluetoothGatt.GATT_SUCCESS)
            }

            override fun onCharacteristicWrite(
                gatt: BluetoothGatt,
                characteristic: BluetoothGattCharacteristic,
                status: Int,
            ) {
                val h = handles.entries.firstOrNull { it.value.gatt === gatt }?.key ?: return
                pendingWrites.remove(h)?.complete(status == BluetoothGatt.GATT_SUCCESS)
            }
        }

        val gatt: BluetoothGatt = try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                device.connectGatt(
                    ctx(), false, callback,
                    android.bluetooth.BluetoothDevice.TRANSPORT_LE,
                    android.bluetooth.BluetoothDevice.PHY_LE_1M_MASK,
                    bleHandler,
                )
            } else {
                @Suppress("DEPRECATION")
                device.connectGatt(
                    ctx(), false, callback,
                    android.bluetooth.BluetoothDevice.TRANSPORT_LE,
                )
            }
        } catch (e: SecurityException) {
            Log.e(TAG, "BLUETOOTH_CONNECT permission denied for $address", e)
            return -1
        } catch (e: Exception) {
            Log.e(TAG, "connectGatt failed for $address", e)
            return -1
        }

        val ready = try {
            runBlocking { withTimeout(CONNECT_TIMEOUT_MS) { readyDeferred.await() } }
        } catch (_: TimeoutCancellationException) {
            Log.e(TAG, "BLE connect+discovery timed out for $address")
            runCatching { gatt.close() }
            return -1
        }

        if (!ready) {
            runCatching { gatt.close() }
            return -1
        }

        val charUuid = try { UUID.fromString(writeCharUuid) } catch (e: Exception) {
            Log.e(TAG, "Invalid characteristic UUID '$writeCharUuid'")
            gatt.disconnect(); gatt.close()
            return -1
        }
        val writeChar = gatt.services.flatMap { it.characteristics }
            .firstOrNull { it.uuid == charUuid }

        if (writeChar == null) {
            Log.e(TAG, "Characteristic $writeCharUuid not found on $address")
            gatt.disconnect(); gatt.close()
            return -1
        }

        val handle = handleSeq.getAndIncrement()
        handles[handle] = GattHandle(gatt, writeChar)
        Log.i(TAG, "BLE connected: address=$address char=$writeCharUuid handle=$handle")
        return handle
    }

    /**
     * Write [data] to the characteristic associated with [handle].
     *
     * [withResponse] controls the GATT write type:
     *  - `true`  → Write Request  (waits for device ACK, slower but reliable)
     *  - `false` → Write Command  (fire-and-forget, faster, used by SP110E/Triones/Zengge)
     *
     * Returns `true` on success, `false` on any error.
     */
    @JvmStatic
    fun write(handle: Int, data: ByteArray, withResponse: Boolean): Boolean {
        val entry = handles[handle] ?: return false
        val gatt = entry.gatt
        val char = entry.writeChar
        val writeType = if (withResponse)
            BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
        else
            BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE

        return if (withResponse) {
            val deferred = CompletableDeferred<Boolean>()
            pendingWrites[handle] = deferred

            val initiated = gattWrite(gatt, char, data, writeType)
            if (!initiated) {
                pendingWrites.remove(handle)
                return false
            }
            try {
                runBlocking { withTimeout(WRITE_TIMEOUT_MS) { deferred.await() } }
            } catch (_: TimeoutCancellationException) {
                pendingWrites.remove(handle)
                Log.w(TAG, "BLE write-with-response timed out on handle $handle")
                false
            }
        } else {
            gattWrite(gatt, char, data, writeType)
        }
    }

    /** Disconnect and close the GATT connection for [handle]. */
    @JvmStatic
    fun disconnect(handle: Int) {
        val entry = handles.remove(handle) ?: return
        pendingWrites.remove(handle)?.complete(false)
        runCatching {
            entry.gatt.disconnect()
            entry.gatt.close()
        }.onFailure { Log.w(TAG, "BLE disconnect error for handle $handle: ${it.message}") }
        Log.i(TAG, "BLE disconnected handle=$handle")
    }

    // ─── Internal helpers ─────────────────────────────────────────────────────

    private fun gattWrite(
        gatt: BluetoothGatt,
        char: BluetoothGattCharacteristic,
        data: ByteArray,
        writeType: Int,
    ): Boolean = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        gatt.writeCharacteristic(char, data, writeType) ==
                android.bluetooth.BluetoothStatusCodes.SUCCESS
    } else {
        @Suppress("DEPRECATION")
        char.writeType = writeType
        @Suppress("DEPRECATION")
        char.value = data
        @Suppress("DEPRECATION")
        gatt.writeCharacteristic(char)
    }
}
