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

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.CancellationException
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.withTimeout
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 WeatherFlow Tactical (WeatherFlow WEATHERmeter for Precision Shooting).
 *
 * Proprietary 16-byte BLE notify protocol, reverse-engineered and confirmed via HCI snoop
 * from physical hardware (2026-06-02). See SPEC_data_engine.md §9.3 for the full frame layout.
 *
 * The device advertises as "WFPSM-...". Data service 961f0001; notify characteristic 961f0005
 * streams a 16-byte little-endian frame ~1 Hz:
 *   off0  u16  wind speed    -> mph = raw / 1024
 *   off2  u8   wind direction (degrees; absolute-vs-relative semantics pending GH #102)
 *   off8  s16  temperature   -> °C = raw / 10
 *   off10 u8   humidity %
 *   off12 u16  pressure      -> hPa = raw / 10
 * The device transmits no density altitude (off14-15 always 0).
 *
 * Wind direction is NOT sourced from the sensor's onboard compass (data[2]); the phone's
 * rotation vector sensor supplies the bearing instead (see SessionViewModel / PhoneCompassManager).
 */
@Singleton
class WeatherFlowTacticalDriver @Inject constructor(
    @ApplicationContext private val context: Context,
) : WeatherSensorSource {

    companion object {
        const val DRIVER_ID = "weatherflow_tactical"

        // Confirmed via HCI snoop (2026-06-02) — see SPEC_data_engine.md §9.3.
        private val SERVICE_UUID = UUID.fromString("961f0001-d2d6-43e3-a417-3bb8217e0e01")
        private val NOTIFY_UUID  = UUID.fromString("961f0005-d2d6-43e3-a417-3bb8217e0e01")
        private val CCCD_UUID    = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")

        private const val WIND_SCALE = 1024.0f  // off0 raw / 1024 = mph

        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        = DRIVER_ID
    override val displayName     = "WeatherFlow Tactical"
    override val supportedFields = setOf(
        WeatherField.WIND_SPEED,
        WeatherField.TEMPERATURE,
        WeatherField.PRESSURE,
        WeatherField.HUMIDITY,
    )

    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 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
        val gatt = activeGatt
        activeGatt = 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.startsWith("WFPSM", ignoreCase = true) ||
                            name.contains("WEATHERmeter", ignoreCase = true) ||
                            name.contains("WeatherFlow", 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("WeatherFlow Tactical service not found — verify UUID via HCI snoop")
        val notifyChar = service.getCharacteristic(NOTIFY_UUID)
            ?: throw Exception("Notify characteristic not found — verify UUID via HCI snoop")

        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
    }

    /**
     * Parse a 16-byte little-endian notify frame from characteristic 961f0005.
     * Layout confirmed via HCI snoop (2026-06-02) — see SPEC_data_engine.md §9.3.
     *   off0  u16  wind speed   -> mph = raw / 1024
     *   off2  u8   wind direction (onboard compass bearing — IGNORED; phone compass used instead)
     *   off8  s16  temperature  -> °C = raw / 10
     *   off10 u8   humidity %
     *   off12 u16  pressure     -> hPa = raw / 10
     * Offsets 4–7 are wind-correlated and unused; 14–15 are always 0 (no density altitude).
     */
    private fun parsePacket(data: ByteArray) {
        if (data.size < 14) return

        runCatching {
            fun u16(i: Int) = (data[i].toInt() and 0xFF) or ((data[i + 1].toInt() and 0xFF) shl 8)

            val windMph  = u16(0) / WIND_SCALE
            val tempC    = u16(8).toShort() / 10.0f          // signed 16-bit
            val humidPct = (data[10].toInt() and 0xFF).toFloat()
            val pressHpa = u16(12) / 10.0f

            if (windMph < 0f || windMph > 200f) return
            if (tempC < -40f || tempC > 60f) return
            if (pressHpa < 500f || pressHpa > 1100f) return
            if (humidPct < 0f || humidPct > 100f) return

            _readings.value = WeatherReading(
                windSpeedMph     = windMph,
                windDirectionDeg = null,
                temperatureF     = tempC * 9f / 5f + 32f,
                pressureHpa      = pressHpa,
                humidityPct      = humidPct,
            )
        }
    }
}
