// Copyright (C) 2026 Jason M. Schwefel. All Rights Reserved.
package com.coldboreballisticsllc.scopedope.data.sensor.btmeter

import android.annotation.SuppressLint
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattDescriptor
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.content.Context
import android.os.Build
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.withTimeout
import com.coldboreballisticsllc.scopedope.ballistics.UnitConversion
import com.coldboreballisticsllc.scopedope.domain.sensor.SensorState
import com.coldboreballisticsllc.scopedope.domain.sensor.WeatherField
import com.coldboreballisticsllc.scopedope.domain.sensor.WeatherReading
import com.coldboreballisticsllc.scopedope.domain.sensor.WeatherSensorSource
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

/**
 * BLE driver for the BTMETER BT-100APP anemometer.
 *
 * Protocol reverse-engineered from HCI snoop logs (1,667 packets validated).
 * Spec: https://github.com/jschwefel/btmeter-bluetooth
 *
 * Packet format: 14 bytes, header 0xA5, footer 0xAA.
 *   Type A (byte[1]=0x41): startup ~25 s — temperature from Ch-A ADC.
 *   Type B (byte[1]=0x42): operational — device-computed wind speed.
 * Checksum: sum(bytes[0..11]) & 0xFF == byte[12].
 *
 * Service 0xFFB0:
 *   0xFFB1 NOTIFY  — sensor notifications
 *   0xFFB3 WRITE   — command channel (unused here)
 */
@Singleton
class BtmeterBt100Driver @Inject constructor(
    @ApplicationContext private val context: Context,
) : WeatherSensorSource {

    companion object {
        private val SERVICE_UUID = UUID.fromString("0000ffb0-0000-1000-8000-00805f9b34fb")
        private val NOTIFY_UUID  = UUID.fromString("0000ffb1-0000-1000-8000-00805f9b34fb")
        private val CCCD_UUID    = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")

        // Type B wind formula constants (validated against 1,667 packets)
        private const val WIND_BASELINE = 14333.0
        private const val WIND_SCALE    = 2477.3

        // Type A temperature formula (valid when byte[11] = 0x00)
        private const val TEMP_BASELINE = 16381.0
        private const val TEMP_SCALE    = 197.7
        private const val TEMP_OFFSET   = 27.1

        private const val SCAN_TIMEOUT_MS    = 30_000L
        private const val CONNECT_TIMEOUT_MS = 15_000L
        private const val DISCOVER_TIMEOUT_MS = 10_000L
        private const val CCCD_TIMEOUT_MS    =  5_000L
    }

    override val driverId        = "btmeter_bt100"
    override val displayName     = "BTMETER BT-100APP"
    override val supportedFields = setOf(WeatherField.WIND_SPEED, WeatherField.TEMPERATURE)

    private val _state    = MutableStateFlow<SensorState>(SensorState.Disconnected)
    private val _readings = MutableStateFlow(WeatherReading())
    override val state:    Flow<SensorState>    = _state.asStateFlow()
    override val readings: Flow<WeatherReading> = _readings.asStateFlow()

    private val bluetoothAdapter =
        (context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter

    @Volatile private var activeGatt:    BluetoothGatt? = null
    @Volatile private var latestTempC:   Float?         = null
    @Volatile private var connectionJob: kotlinx.coroutines.Job? = null
    private val driverScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

    override suspend fun connect() {
        val s = _state.value
        if (s is SensorState.Scanning || s is SensorState.Connecting || s is SensorState.Connected) return
        val adapter = bluetoothAdapter ?: run {
            _state.value = SensorState.Error("Bluetooth not available")
            return
        }
        if (!adapter.isEnabled) {
            _state.value = SensorState.Error("Bluetooth is off")
            return
        }
        connectionJob = driverScope.launch { runConnectionSequence() }
    }

    @SuppressLint("MissingPermission")
    override suspend fun disconnect() {
        _state.value = SensorState.Disconnected
        connectionJob?.cancel()
        connectionJob = null
        // Null activeGatt before calling disconnect() so the GATT STATE_DISCONNECTED callback
        // identity check won't attempt a redundant close() if it fires concurrently.
        val gatt = activeGatt
        activeGatt  = null
        latestTempC = null
        gatt?.disconnect()
        gatt?.close()
        _readings.value = WeatherReading()
    }

    override suspend fun snapshot(): WeatherReading? =
        if (_state.value is SensorState.Connected) _readings.value else null

    private suspend fun runConnectionSequence() {
        val adapter = bluetoothAdapter ?: return
        _state.value = SensorState.Scanning
        val device = try {
            scanForDevice(adapter)
        } catch (e: Exception) {
            _state.value = SensorState.Error("Scan failed: ${e.message}")
            return
        }
        _state.value = SensorState.Connecting
        try {
            openGatt(device)
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            _state.value = SensorState.Error("Connect failed: ${e.message}")
            val staleGatt = activeGatt
            activeGatt = null
            staleGatt?.close()
        }
    }

    private suspend fun reconnectWithBackoff() {
        val delays = listOf(2_000L, 5_000L, 10_000L)
        for (delayMs in delays) {
            kotlinx.coroutines.delay(delayMs)
            if (_state.value !is SensorState.Disconnected) return
            runConnectionSequence()
            if (_state.value is SensorState.Connected) return
        }
    }

    @SuppressLint("MissingPermission")
    private suspend fun scanForDevice(adapter: android.bluetooth.BluetoothAdapter): BluetoothDevice =
        withTimeout(SCAN_TIMEOUT_MS) {
            suspendCancellableCoroutine { cont ->
                val cb = object : ScanCallback() {
                    override fun onScanResult(callbackType: Int, result: ScanResult) {
                        val name = result.device.name ?: return
                        if (name.contains("Anemometer", ignoreCase = true) ||
                            name.contains("BT-100", ignoreCase = true)) {
                            adapter.bluetoothLeScanner?.stopScan(this)
                            if (cont.isActive) cont.resume(result.device)
                        }
                    }
                    override fun onScanFailed(errorCode: Int) {
                        if (cont.isActive) cont.resumeWithException(Exception("scan error $errorCode"))
                    }
                }
                val settings = ScanSettings.Builder()
                    .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
                    .build()
                adapter.bluetoothLeScanner?.startScan(null, settings, cb)
                cont.invokeOnCancellation { adapter.bluetoothLeScanner?.stopScan(cb) }
            }
        }

    @SuppressLint("MissingPermission")
    private suspend fun openGatt(device: BluetoothDevice) {
        val connCh = Channel<Boolean>(Channel.BUFFERED)
        val svcCh  = Channel<Boolean>(Channel.BUFFERED)
        val cccdCh = Channel<Boolean>(Channel.BUFFERED)

        val cb = object : BluetoothGattCallback() {
            override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
                when (newState) {
                    BluetoothProfile.STATE_CONNECTED -> {
                        gatt.discoverServices()
                        connCh.trySend(true)
                    }
                    BluetoothProfile.STATE_DISCONNECTED -> {
                        connCh.trySend(false)
                        val wasConnected = _state.value is SensorState.Connected
                        if (wasConnected || _state.value is SensorState.Connecting) {
                            _state.value = SensorState.Disconnected
                        }
                        gatt.close()
                        if (activeGatt === gatt) activeGatt = null
                        if (wasConnected) {
                            connectionJob = driverScope.launch { reconnectWithBackoff() }
                        }
                    }
                }
            }

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

            override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
                cccdCh.trySend(status == BluetoothGatt.GATT_SUCCESS)
            }

            @Deprecated("Deprecated in API 33; retained for API 21–32 compatibility")
            override fun onCharacteristicChanged(
                gatt: BluetoothGatt,
                characteristic: BluetoothGattCharacteristic,
            ) {
                @Suppress("DEPRECATION")
                parsePacket(characteristic.value)
            }

            override fun onCharacteristicChanged(
                gatt: BluetoothGatt,
                characteristic: BluetoothGattCharacteristic,
                value: ByteArray,
            ) {
                parsePacket(value)
            }
        }

        val gatt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            device.connectGatt(context, false, cb, BluetoothDevice.TRANSPORT_LE)
        } else {
            @Suppress("DEPRECATION")
            device.connectGatt(context, false, cb)
        }
        activeGatt = gatt

        withTimeout(CONNECT_TIMEOUT_MS) {
            if (!connCh.receive()) throw Exception("GATT disconnected during handshake")
        }
        withTimeout(DISCOVER_TIMEOUT_MS) {
            if (!svcCh.receive()) throw Exception("Service discovery failed")
        }

        val service    = gatt.getService(SERVICE_UUID)
            ?: throw Exception("Service 0xFFB0 not found — verify device is BT-100APP")
        val notifyChar = service.getCharacteristic(NOTIFY_UUID)
            ?: throw Exception("Notify characteristic 0xFFB1 not found")

        gatt.setCharacteristicNotification(notifyChar, true)

        val cccd = notifyChar.getDescriptor(CCCD_UUID)
            ?: throw Exception("CCCD descriptor not found")
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            gatt.writeDescriptor(cccd, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)
        } else {
            @Suppress("DEPRECATION")
            cccd.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
            @Suppress("DEPRECATION")
            gatt.writeDescriptor(cccd)
        }

        withTimeout(CCCD_TIMEOUT_MS) {
            if (!cccdCh.receive()) throw Exception("Failed to enable notifications")
        }

        _state.value = SensorState.Connected
    }

    private fun parsePacket(data: ByteArray) {
        if (data.size != 14) return
        if (data[0].toInt() and 0xFF != 0xA5) return
        if (data[13].toInt() and 0xFF != 0xAA) return
        val checksum = data.slice(0..11).sumOf { it.toInt() and 0xFF } and 0xFF
        if (checksum != data[12].toInt() and 0xFF) return

        when (data[1].toInt() and 0xFF) {
            0x42 -> {
                // Type B: operational wind speed (device-computed, temp-compensated)
                val raw     = ((data[2].toInt() and 0x7F) shl 8) or (data[3].toInt() and 0xFF)
                val windMs  = maxOf(0.0, (raw - WIND_BASELINE) / WIND_SCALE)
                val windMph = windMs.toFloat() * UnitConversion.MPS_TO_MPH_F
                _readings.value = _readings.value.copy(
                    windSpeedMph = windMph,
                    temperatureF = latestTempC?.let { it * 9f / 5f + 32f },
                )
            }
            0x41 -> {
                // Type A: startup-phase temperature (valid only when byte[11] = 0x00 = calm)
                if (data[11].toInt() and 0xFF == 0x00) {
                    val raw   = ((data[2].toInt() and 0x7F) shl 8) or (data[3].toInt() and 0xFF)
                    val tempC = TEMP_OFFSET + (TEMP_BASELINE - raw) / TEMP_SCALE
                    if (tempC in -40.0..80.0) {
                        latestTempC = tempC.toFloat()
                        _readings.value = _readings.value.copy(
                            temperatureF = tempC.toFloat() * 9f / 5f + 32f,
                        )
                    }
                }
            }
        }
    }
}
