// 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 ─────────────────────────────────────────────────────────────────
// TX (data host→printer): write-without-response char in ISSC UART service
private val SERVICE_UUID     = UUID.fromString("49535343-fe7d-4ae5-8fa9-9fafd205e455")
private val WRITE_UUID       = UUID.fromString("49535343-6daa-4d02-abf6-19569aca69fe")
// RX (printer→host): notify char lives in a separate non-ISSC service
private val NOTIFY_SVC_UUID  = UUID.fromString("e7810a71-73ae-499d-8c15-faa9aef0c3f2")
private val NOTIFY_UUID      = UUID.fromString("bef8d6c9-9c21-4c9e-b632-bd58c1009f9f")
private val CCCD_UUID        = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")

// ── Niimbot B1 opcodes ────────────────────────────────────────────────────────
// WARNING: B1 END_PAGE/END_PRINT opcodes differ from D11/B21.
// 0x04 and 0x02 are ACKs the printer sends, NOT commands to transmit.
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()   // B1-specific — not 0x04
private const val OP_END_PRINT      = 0xF3.toByte()   // B1-specific — not 0x02

// ── Print dimensions ──────────────────────────────────────────────────────────
// PRINT_PX: max source bitmap width — 50 mm at 203 DPI = 400 dots
// PRINT_COLS: printhead width in dots — 48 mm × (203 DPI / 25.4) = 384
// Source bitmaps wider than PRINT_COLS are center-cropped (8 px per side for 400-wide).
// Source bitmaps narrower than PRINT_COLS are center-padded with white.
// PRINT_ROWS is computed at runtime from bitmap.height to support any label length.
private const val PRINT_PX   = 400
private const val PRINT_COLS = 384

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

    override val displayName = "Niimbot B1"
    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

    // ── Connection ────────────────────────────────────────────────────────────

    @SuppressLint("MissingPermission")
    override suspend fun connect(targetMac: String?) {
        _state.value = PrinterState.Scanning
        withContext(Dispatchers.IO) {
            // When a stored MAC is known, skip scanning and connect directly by address.
            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
            }

            // Fallback: scan for any Niimbot B1 nearby.
            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("B1", 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 B1 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 -> {
                        // Negotiate larger MTU first — SEND_LINE packets are ~60 bytes.
                        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("NiimbotB1", "MTU negotiated: $mtu (status=$status)")
                // BluetoothGatt.refresh() is a hidden API — reflection may break on future Android releases.
                // It clears the GATT attribute cache so stale descriptors from a prior connection are not used.
                try { g.javaClass.getMethod("refresh").invoke(g) } catch (it: Exception) {
                    if (BuildConfig.DEBUG) Log.w("NiimbotB1", "GATT cache refresh failed", it)
                }
                // Post with delay to let the cache refresh settle before service discovery.
                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
                }
                if (BuildConfig.DEBUG) {
                    g.services.forEach { svc ->
                        Log.d("NiimbotB1", "Service: ${svc.uuid}")
                        svc.characteristics.forEach { chr ->
                            Log.d("NiimbotB1", "  Char: ${chr.uuid}  props=0x${chr.properties.toString(16)}")
                        }
                    }
                }

                // TX: write-without-response char in ISSC UART service
                writeChar = g.getService(SERVICE_UUID)?.getCharacteristic(WRITE_UUID)

                // RX: notify char in the second (non-ISSC) service
                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
    }

    // ── Print ─────────────────────────────────────────────────────────────────

    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)   // printRows rows × PRINT_COLS/8 bytes each

        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))

                    // B1 START_PRINT: 7-byte format — [pages_hi, pages_lo, 0,0,0,0, color]
                    send(g, wc, OP_START_PRINT,
                        byteArrayOf(0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00))

                    // B1 START_PAGE: data = 0x01
                    send(g, wc, OP_START_PAGE, byteArrayOf(0x01))

                    // B1 SET_DIMENSION: 6-byte [rows_hi, rows_lo, cols_hi, cols_lo, copies_hi, copies_lo]
                    // COLS=384 (0x0180) is constant (printhead width). ROWS varies by label length.
                    send(g, wc, OP_SET_DIMENSION,
                        byteArrayOf(
                            (printRows shr 8).toByte(), printRows.toByte(),
                            0x01, 0x80.toByte(),  // cols = 384
                            0x00, 0x01,           // copies = 1
                        ))

                    bw.forEachIndexed { row, rowBytes ->
                        _state.value = PrinterState.Printing(row.toFloat() / printRows)
                        // B1 SEND_LINE: [row_hi, row_lo, count1, count2, count3, repeats=1, ...bitmap]
                        val data = byteArrayOf(
                            (row shr 8).toByte(), row.toByte(),
                            0x00, 0x00, 0x00,   // count1, count2, count3
                            0x01,               // repeats
                        ) + 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)   // allow the printer motor to finish feeding the label
                    send(g, wc, OP_END_PRINT, byteArrayOf(0x01))
                }
                if (!timedOut) {
                    _state.value = PrinterState.Done
                    delay(1_000)
                }
                _state.value = PrinterState.Ready
            }
        }
    }

    // ── Packet builder ────────────────────────────────────────────────────────

    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("NiimbotB1", "writeCharacteristic returned false (op=0x${op.toInt().and(0xFF).toString(16)})")
        }
        // write-without-response has no callback; one BLE connection interval is sufficient.
        delay(20)
    }

    // ── Canvas → 1-bit ────────────────────────────────────────────────────────

    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)

        // Center-align source in the 384-dot printhead.
        // padLeft > 0: source is narrower than printhead — pad with white on each side.
        // padLeft < 0: source is wider than printhead — crop (skip |padLeft| px each side).
        val padLeft = (PRINT_COLS - srcW) / 2

        return Array(srcH) { row ->
            val rowBytes = ByteArray(PRINT_COLS / 8)   // 48 bytes per row
            for (printCol in 0 until PRINT_COLS) {
                val srcCol = printCol - padLeft
                if (srcCol < 0 || srcCol >= srcW) continue   // white padding — bit stays 0
                val px  = pixels[row * srcW + srcCol]
                val r   = (px shr 16) and 0xFF
                val gr  = (px shr  8) and 0xFF
                val b   = px          and 0xFF
                val lum = (0.299 * r + 0.587 * gr + 0.114 * b).toInt()
                if (lum < 128) {
                    rowBytes[printCol / 8] = (rowBytes[printCol / 8].toInt() or (0x80 shr (printCol % 8))).toByte()
                }
            }
            rowBytes
        }
    }
}
