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

import android.annotation.SuppressLint
import android.bluetooth.*
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
import android.content.Context
import android.graphics.Bitmap
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.coldboreballisticsllc.scopedope.BuildConfig
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import com.coldboreballisticsllc.scopedope.domain.printer.ConnectedDeviceInfo
import com.coldboreballisticsllc.scopedope.domain.printer.PrinterSource
import com.coldboreballisticsllc.scopedope.domain.printer.PrinterState
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton

// ── BLE UUIDs ─────────────────────────────────────────────────────────────────
// !! PENDING HARDWARE VERIFICATION !!
// These UUIDs must be confirmed against a physical Niimbot B21 Pro unit using
// nRF Connect (same procedure as the B1). The B21 Pro is expected to use the
// same ISSC UART-over-BLE bridge as the B1, but UUIDs are NOT confirmed.
// Replace the placeholders below after HCI snoop capture (GH #1 / #77).
private val SERVICE_UUID    = UUID.fromString("49535343-fe7d-4ae5-8fa9-9fafd205e455") // UNVERIFIED
private val WRITE_UUID      = UUID.fromString("49535343-6daa-4d02-abf6-19569aca69fe") // UNVERIFIED
private val NOTIFY_SVC_UUID = UUID.fromString("e7810a71-73ae-499d-8c15-faa9aef0c3f2") // UNVERIFIED
private val NOTIFY_UUID     = UUID.fromString("bef8d6c9-9c21-4c9e-b632-bd58c1009f9f") // UNVERIFIED
private val CCCD_UUID       = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")

// ── Niimbot B21 Pro opcodes ───────────────────────────────────────────────────
// Expected to match the B1 ISSC UART protocol. Confirm against physical hardware.
private const val OP_SET_LABEL_TYPE = 0x23.toByte()
private const val OP_SET_DENSITY    = 0x21.toByte()
private const val OP_START_PRINT    = 0x01.toByte()
private const val OP_START_PAGE     = 0x03.toByte()
private const val OP_SET_DIMENSION  = 0x13.toByte()
private const val OP_SEND_LINE      = 0x85.toByte()
private const val OP_END_PAGE       = 0xE3.toByte()
private const val OP_END_PRINT      = 0xF3.toByte()

// ── Print dimensions ──────────────────────────────────────────────────────────
// B21 Pro: same 48 mm printhead at 203 DPI as B1. Confirm if different.
private const val PRINT_PX   = 400
private const val PRINT_COLS = 384

@Singleton
class NiimbotB21ProDriver @Inject constructor(
    @ApplicationContext private val context: Context,
) : PrinterSource {

    override val displayName = "Niimbot B21 Pro"
    override val nativeDpi   = 203

    private val _state = MutableStateFlow<PrinterState>(PrinterState.Disconnected)
    override val state: StateFlow<PrinterState> = _state.asStateFlow()

    private val _connectedDeviceInfo = MutableStateFlow<ConnectedDeviceInfo?>(null)
    override val connectedDeviceInfo: StateFlow<ConnectedDeviceInfo?> = _connectedDeviceInfo.asStateFlow()

    @Volatile private var gatt:      BluetoothGatt?               = null
    @Volatile private var writeChar: BluetoothGattCharacteristic? = null

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

    @SuppressLint("MissingPermission")
    override suspend fun connect(targetMac: String?) {
        _state.value = PrinterState.Scanning
        withContext(Dispatchers.IO) {
            if (targetMac != null) {
                val device = runCatching { bluetoothAdapter.getRemoteDevice(targetMac) }.getOrNull()
                if (device == null) {
                    _state.value = PrinterState.Error("Invalid printer address")
                    return@withContext
                }
                _state.value = PrinterState.Connecting
                connectGatt(device)
                return@withContext
            }

            val scanner = bluetoothAdapter.bluetoothLeScanner ?: run {
                _state.value = PrinterState.Error("Bluetooth not available")
                return@withContext
            }

            val found = CompletableDeferred<BluetoothDevice>()
            val cb = object : ScanCallback() {
                override fun onScanResult(callbackType: Int, result: ScanResult) {
                    val name = result.device.name ?: ""
                    if (name.contains("B21", ignoreCase = true) ||
                        name.contains("Niimbot", ignoreCase = true)) {
                        found.complete(result.device)
                    }
                }
                override fun onScanFailed(errorCode: Int) {
                    found.completeExceptionally(Exception("BLE scan failed: $errorCode"))
                }
            }

            scanner.startScan(cb)
            val device = try {
                withTimeout(10_000) { found.await() }
            } catch (e: Exception) {
                scanner.stopScan(cb)
                _state.value = PrinterState.Error("Niimbot B21 Pro not found")
                return@withContext
            }
            scanner.stopScan(cb)

            _state.value = PrinterState.Connecting
            connectGatt(device)
        }
    }

    @SuppressLint("MissingPermission")
    private suspend fun connectGatt(device: BluetoothDevice) {
        val connected = CompletableDeferred<Unit>()
        val cb = object : BluetoothGattCallback() {
            override fun onConnectionStateChange(g: BluetoothGatt, status: Int, newState: Int) {
                when {
                    status != BluetoothGatt.GATT_SUCCESS -> {
                        connected.completeExceptionally(Exception("Connection failed (status $status)"))
                    }
                    newState == BluetoothProfile.STATE_CONNECTED -> g.requestMtu(512)
                    newState == BluetoothProfile.STATE_DISCONNECTED -> {
                        _state.value = PrinterState.Disconnected
                        _connectedDeviceInfo.value = null
                        gatt = null; writeChar = null
                    }
                }
            }

            override fun onMtuChanged(g: BluetoothGatt, mtu: Int, status: Int) {
                if (BuildConfig.DEBUG) Log.d("NiimbotB21Pro", "MTU negotiated: $mtu (status=$status)")
                try { g.javaClass.getMethod("refresh").invoke(g) } catch (it: Exception) {
                    if (BuildConfig.DEBUG) Log.w("NiimbotB21Pro", "GATT cache refresh failed", it)
                }
                Handler(Looper.getMainLooper()).postDelayed({ g.discoverServices() }, 300)
            }

            override fun onServicesDiscovered(g: BluetoothGatt, status: Int) {
                if (status != BluetoothGatt.GATT_SUCCESS) {
                    connected.completeExceptionally(Exception("Service discovery failed ($status)"))
                    return
                }
                writeChar = g.getService(SERVICE_UUID)?.getCharacteristic(WRITE_UUID)
                val notifyChar = g.getService(NOTIFY_SVC_UUID)?.getCharacteristic(NOTIFY_UUID)
                if (notifyChar != null) {
                    g.setCharacteristicNotification(notifyChar, true)
                    notifyChar.getDescriptor(CCCD_UUID)?.let { cccd ->
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                            g.writeDescriptor(cccd, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)
                        } else {
                            @Suppress("DEPRECATION")
                            cccd.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
                            @Suppress("DEPRECATION")
                            g.writeDescriptor(cccd)
                        }
                    }
                }
                if (writeChar != null) {
                    _connectedDeviceInfo.value = ConnectedDeviceInfo(
                        mac    = device.address,
                        btName = device.name?.takeIf { it.isNotBlank() } ?: device.address,
                    )
                    _state.value = PrinterState.Ready
                    connected.complete(Unit)
                } else {
                    val found = g.services.joinToString { it.uuid.toString() }.ifEmpty { "none" }
                    connected.completeExceptionally(Exception("Write char not found. Services: $found"))
                }
            }
        }
        gatt = device.connectGatt(context, false, cb, BluetoothDevice.TRANSPORT_LE)
        try { withTimeout(15_000) { connected.await() } }
        catch (e: Exception) {
            gatt?.disconnect(); gatt?.close(); gatt = null; writeChar = null
            _state.value = PrinterState.Error(e.message ?: "Connect timeout")
        }
    }

    @SuppressLint("MissingPermission")
    override suspend fun disconnect() {
        gatt?.disconnect(); gatt?.close(); gatt = null; writeChar = null
        _connectedDeviceInfo.value = null
        _state.value = PrinterState.Disconnected
    }

    override suspend fun print(bitmap: Bitmap) {
        val g  = gatt      ?: run { _state.value = PrinterState.Error("Not connected"); return }
        val wc = writeChar ?: run { _state.value = PrinterState.Error("Not connected"); return }

        require(bitmap.width <= PRINT_PX) { "Bitmap width ${bitmap.width} exceeds max $PRINT_PX" }

        val printRows = bitmap.height
        val bw = toBitmap1bit(bitmap)

        withContext(Dispatchers.IO) {
            var timedOut = false
            try {
                withTimeout(30_000L) {
                    send(g, wc, OP_SET_LABEL_TYPE, byteArrayOf(0x01))
                    send(g, wc, OP_SET_DENSITY,    byteArrayOf(0x03))
                    send(g, wc, OP_START_PRINT,
                        byteArrayOf(0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00))
                    send(g, wc, OP_START_PAGE, byteArrayOf(0x01))
                    send(g, wc, OP_SET_DIMENSION,
                        byteArrayOf(
                            (printRows shr 8).toByte(), printRows.toByte(),
                            0x01, 0x80.toByte(),
                            0x00, 0x01,
                        ))
                    bw.forEachIndexed { row, rowBytes ->
                        _state.value = PrinterState.Printing(row.toFloat() / printRows)
                        val data = byteArrayOf(
                            (row shr 8).toByte(), row.toByte(),
                            0x00, 0x00, 0x00, 0x01,
                        ) + rowBytes
                        send(g, wc, OP_SEND_LINE, data)
                    }
                }
            } catch (e: TimeoutCancellationException) {
                timedOut = true
                _state.value = PrinterState.Error("Print timeout — printer unresponsive")
            } finally {
                runCatching {
                    send(g, wc, OP_END_PAGE, byteArrayOf(0x01))
                    delay(if (timedOut) 200L else 2_000L)
                    send(g, wc, OP_END_PRINT, byteArrayOf(0x01))
                }
                if (!timedOut) { _state.value = PrinterState.Done; delay(1_000) }
                _state.value = PrinterState.Ready
            }
        }
    }

    private fun buildPacket(opcode: Byte, data: ByteArray): ByteArray {
        val len = data.size.toByte()
        var xor = (opcode.toInt() xor len.toInt()).toByte()
        for (b in data) xor = (xor.toInt() xor b.toInt()).toByte()
        return byteArrayOf(0x55, 0x55, opcode, len) + data + byteArrayOf(xor, 0xAA.toByte(), 0xAA.toByte())
    }

    @SuppressLint("MissingPermission")
    private suspend fun send(g: BluetoothGatt, c: BluetoothGattCharacteristic, op: Byte, data: ByteArray) {
        val packet = buildPacket(op, data)
        c.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
        @Suppress("DEPRECATION") c.value = packet
        @Suppress("DEPRECATION") val ok = g.writeCharacteristic(c)
        if (!ok && BuildConfig.DEBUG) {
            Log.w("NiimbotB21Pro", "writeCharacteristic returned false (op=0x${op.toInt().and(0xFF).toString(16)})")
        }
        delay(20)
    }

    private fun toBitmap1bit(src: Bitmap): Array<ByteArray> {
        val srcW = src.width; val srcH = src.height
        val pixels = IntArray(srcW * srcH)
        src.getPixels(pixels, 0, srcW, 0, 0, srcW, srcH)
        val padLeft = (PRINT_COLS - srcW) / 2
        return Array(srcH) { row ->
            val rowBytes = ByteArray(PRINT_COLS / 8)
            for (printCol in 0 until PRINT_COLS) {
                val srcCol = printCol - padLeft
                if (srcCol < 0 || srcCol >= srcW) continue
                val px  = pixels[row * srcW + srcCol]
                val lum = (0.299 * ((px shr 16) and 0xFF) + 0.587 * ((px shr 8) and 0xFF) + 0.114 * (px and 0xFF)).toInt()
                if (lum < 128) rowBytes[printCol / 8] = (rowBytes[printCol / 8].toInt() or (0x80 shr (printCol % 8))).toByte()
            }
            rowBytes
        }
    }
}
