import itertools
import json
import logging
import queue
import sys
import time
from asyncio import sleep
from io import BytesIO
from typing import Union

from bleak import BleakClient
from jsonic.serializable import serialize

from pymammotion.aliyun.tmp_constant import tmp_constant
from pymammotion.bluetooth.const import UUID_WRITE_CHARACTERISTIC
from pymammotion.bluetooth.data.framectrldata import FrameCtrlData
from pymammotion.bluetooth.data.notifydata import BlufiNotifyData
from pymammotion.bluetooth.model.atomic_integer import AtomicInteger
from pymammotion.data.model.execute_boarder import ExecuteBorder
from pymammotion.proto import (
    dev_net_pb2,
)
from pymammotion.proto.luba_msg import LubaMsg, MsgAttr, MsgCmdType, MsgDevice
from pymammotion.utility.constant.device_constant import bleOrderCmd

_LOGGER = logging.getLogger(__name__)

CRC_TB = [
    0x0000,
    0x1021,
    0x2042,
    0x3063,
    0x4084,
    0x50A5,
    0x60C6,
    0x70E7,
    0x8108,
    0x9129,
    0xA14A,
    0xB16B,
    0xC18C,
    0xD1AD,
    0xE1CE,
    0xF1EF,
    0x1231,
    0x0210,
    0x3273,
    0x2252,
    0x52B5,
    0x4294,
    0x72F7,
    0x62D6,
    0x9339,
    0x8318,
    0xB37B,
    0xA35A,
    0xD3BD,
    0xC39C,
    0xF3FF,
    0xE3DE,
    0x2462,
    0x3443,
    0x0420,
    0x1401,
    0x64E6,
    0x74C7,
    0x44A4,
    0x5485,
    0xA56A,
    0xB54B,
    0x8528,
    0x9509,
    0xE5EE,
    0xF5CF,
    0xC5AC,
    0xD58D,
    0x3653,
    0x2672,
    0x1611,
    0x0630,
    0x76D7,
    0x66F6,
    0x5695,
    0x46B4,
    0xB75B,
    0xA77A,
    0x9719,
    0x8738,
    0xF7DF,
    0xE7FE,
    0xD79D,
    0xC7BC,
    0x48C4,
    0x58E5,
    0x6886,
    0x78A7,
    0x0840,
    0x1861,
    0x2802,
    0x3823,
    0xC9CC,
    0xD9ED,
    0xE98E,
    0xF9AF,
    0x8948,
    0x9969,
    0xA90A,
    0xB92B,
    0x5AF5,
    0x4AD4,
    0x7AB7,
    0x6A96,
    0x1A71,
    0x0A50,
    0x3A33,
    0x2A12,
    0xDBFD,
    0xCBDC,
    0xFBBF,
    0xEB9E,
    0x9B79,
    0x8B58,
    0xBB3B,
    0xAB1A,
    0x6CA6,
    0x7C87,
    0x4CE4,
    0x5CC5,
    0x2C22,
    0x3C03,
    0x0C60,
    0x1C41,
    0xEDAE,
    0xFD8F,
    0xCDEC,
    0xDDCD,
    0xAD2A,
    0xBD0B,
    0x8D68,
    0x9D49,
    0x7E97,
    0x6EB6,
    0x5ED5,
    0x4EF4,
    0x3E13,
    0x2E32,
    0x1E51,
    0x0E70,
    0xFF9F,
    0xEFBE,
    0xDFDD,
    0xCFFC,
    0xBF1B,
    0xAF3A,
    0x9F59,
    0x8F78,
    0x9188,
    0x81A9,
    0xB1CA,
    0xA1EB,
    0xD10C,
    0xC12D,
    0xF14E,
    0xE16F,
    0x1080,
    0x00A1,
    0x30C2,
    0x20E3,
    0x5004,
    0x4025,
    0x7046,
    0x6067,
    0x83B9,
    0x9398,
    0xA3FB,
    0xB3DA,
    0xC33D,
    0xD31C,
    0xE37F,
    0xF35E,
    0x02B1,
    0x1290,
    0x22F3,
    0x32D2,
    0x4235,
    0x5214,
    0x6277,
    0x7256,
    0xB5EA,
    0xA5CB,
    0x95A8,
    0x8589,
    0xF56E,
    0xE54F,
    0xD52C,
    0xC50D,
    0x34E2,
    0x24C3,
    0x14A0,
    0x0481,
    0x7466,
    0x6447,
    0x5424,
    0x4405,
    0xA7DB,
    0xB7FA,
    0x8799,
    0x97B8,
    0xE75F,
    0xF77E,
    0xC71D,
    0xD73C,
    0x26D3,
    0x36F2,
    0x0691,
    0x16B0,
    0x6657,
    0x7676,
    0x4615,
    0x5634,
    0xD94C,
    0xC96D,
    0xF90E,
    0xE92F,
    0x99C8,
    0x89E9,
    0xB98A,
    0xA9AB,
    0x5844,
    0x4865,
    0x7806,
    0x6827,
    0x18C0,
    0x08E1,
    0x3882,
    0x28A3,
    0xCB7D,
    0xDB5C,
    0xEB3F,
    0xFB1E,
    0x8BF9,
    0x9BD8,
    0xABBB,
    0xBB9A,
    0x4A75,
    0x5A54,
    0x6A37,
    0x7A16,
    0x0AF1,
    0x1AD0,
    0x2AB3,
    0x3A92,
    0xFD2E,
    0xED0F,
    0xDD6C,
    0xCD4D,
    0xBDAA,
    0xAD8B,
    0x9DE8,
    0x8DC9,
    0x7C26,
    0x6C07,
    0x5C64,
    0x4C45,
    0x3CA2,
    0x2C83,
    0x1CE0,
    0x0CC1,
    0xEF1F,
    0xFF3E,
    0xCF5D,
    0xDF7C,
    0xAF9B,
    0xBFBA,
    0x8FD9,
    0x9FF8,
    0x6E17,
    0x7E36,
    0x4E55,
    0x5E74,
    0x2E93,
    0x3EB2,
    0x0ED1,
    0x1EF0,
]


class BleMessage:
    """Class for sending and recieving messages from Luba"""

    AES_TRANSFORMATION = "AES/CFB/NoPadding"
    DEFAULT_PACKAGE_LENGTH = 20
    DH_G = "2"
    DH_P = "cf5cf5c38419a724957ff5dd323b9c45c3cdd261eb740f69aa94b8bb1a5c96409153bd76b24222d03274e4725a5406092e9e82e9135c643cae98132b0d95f7d65347c68afc1e677da90e51bbab5f5cf429c291b4ba39c6b2dc5e8c7231e46aa7728e87664532cdf547be20c9a3fa8342be6e34371a27c06f7dc0edddd2f86373"
    MIN_PACKAGE_LENGTH = 20
    NEG_SECURITY_SET_ALL_DATA = 1
    NEG_SECURITY_SET_TOTAL_LENGTH = 0
    PACKAGE_HEADER_LENGTH = 4
    mPrintDebug = False
    mWriteTimeout = -1
    mPackageLengthLimit = -1
    mBlufiMTU = -1
    mEncrypted = False
    mChecksum = False
    mRequireAck = False
    mConnectState = 0

    def __init__(self, client: BleakClient) -> None:
        self.client = client
        self.mSendSequence = AtomicInteger(-1)
        self.mReadSequence = AtomicInteger(-1)
        self.mAck = queue.Queue()
        self.notification = BlufiNotifyData()

    async def get_task(self) -> None:
        hash_map = {"pver": 1, "subCmd": 2, "result": 0}
        await self.post_custom_data(self.get_json_string(bleOrderCmd.task, hash_map))

    async def send_ble_alive(self) -> None:
        hash_map = {"ctrl": 1}
        await self.post_custom_data(self.get_json_string(bleOrderCmd.bleAlive, hash_map))

    def get_json_string(self, cmd: int, hash_map: dict[str, object]) -> str:
        jSONObject = {}
        try:
            jSONObject["cmd"] = cmd
            jSONObject[tmp_constant.REQUEST_ID] = int(time.time())
            jSONObject2 = {}
            for key, value in hash_map.items():
                jSONObject2[key] = value
            jSONObject["params"] = jSONObject2
            return json.dumps(jSONObject)
        except Exception as e:
            print(e)
            return ""

    def clearNotification(self) -> None:
        self.notification = None
        self.notification = BlufiNotifyData()

    # async def get_device_info(self):
    #     await self.postCustomData(self.getJsonString(bleOrderCmd.getDeviceInfo))

    async def send_device_info(self) -> None:
        """Currently not called"""
        luba_msg = LubaMsg(
            msgtype=MsgCmdType.MSG_CMD_TYPE_ESP,
            sender=MsgDevice.DEV_MOBILEAPP,
            rcver=MsgDevice.DEV_COMM_ESP,
            msgattr=MsgAttr.MSG_ATTR_REQ,
            seqs=1,
            version=1,
            subtype=1,
            net=dev_net_pb2.DevNet(todev_ble_sync=1, todev_devinfo_req=dev_net_pb2.DrvDevInfoReq()),
        )
        byte_arr = luba_msg.SerializeToString()
        await self.post_custom_data_bytes(byte_arr)

    async def requestDeviceStatus(self) -> None:
        request = False
        type = self.getTypeValue(0, 5)
        try:
            request = await self.post(BleMessage.mEncrypted, BleMessage.mChecksum, False, type, None)
            # _LOGGER.debug(request)
        except Exception as err:
            # Log.w(TAG, "post requestDeviceStatus interrupted")
            request = False
            _LOGGER.error(err)

        # if not request:
        #     onStatusResponse(BlufiCallback.CODE_WRITE_DATA_FAILED, null)

    async def requestDeviceVersion(self) -> None:
        request = False
        type = self.getTypeValue(0, 7)
        try:
            request = await self.post(BleMessage.mEncrypted, BleMessage.mChecksum, False, type, None)
            # _LOGGER.debug(request)
        except Exception as err:
            # Log.w(TAG, "post requestDeviceStatus interrupted")
            request = False
            _LOGGER.error(err)

    async def sendBorderPackage(self, executeBorder: ExecuteBorder) -> None:
        await self.post_custom_data(serialize(executeBorder))

    async def gatt_write(self, data: bytes) -> None:
        await self.client.write_gatt_char(UUID_WRITE_CHARACTERISTIC, data, True)

    def parseNotification(self, response: bytearray):
        """Parse notification data from BLE device."""
        if response is None:
            # Log.w(TAG, "parseNotification null data");
            return -1

        # if (this.mPrintDebug):
        #     Log.d(TAG, "parseNotification Notification= " + Arrays.toString(response));
        # }
        if len(response) < 4:
            _LOGGER.debug("parseNotification data length less than 4")
            return -2

        sequence = int(response[2])  # toInt
        current_sequence = self.mReadSequence.get() & 255
        if sequence == current_sequence:
            _LOGGER.debug(f"Received bluetooth data 1: {response.hex()}, object: {self}")
            return 2

        # Compare with the second counter, mod 255
        if sequence != (self.mReadSequence.increment_and_get() & 255):
            _LOGGER.debug(
                "parseNotification read sequence wrong %s %s",
                sequence,
                self.mReadSequence.get(),
            )
            # Set the value for mReadSequence manually
            self.mReadSequence.set(sequence)

        # LogUtil.m7773e(self.mGatt.getDevice().getName() + "打印丢包率", self.mReadSequence_2 + "/" + self.mReadSequence_1);
        pkt_type = int(response[0])  # toInt
        pkgType = self._getPackageType(pkt_type)
        subType = self._getSubType(pkt_type)
        self.notification.setType(pkt_type)
        self.notification.setPkgType(pkgType)
        self.notification.setSubType(subType)
        frameCtrl = int(response[1])  # toInt
        # _LOGGER.debug("frame ctrl")
        # _LOGGER.debug(frameCtrl)
        # _LOGGER.debug(response)
        # _LOGGER.debug(f"pktType {pkt_type} pkgType {pkgType} subType {subType}")
        self.notification.setFrameCtrl(frameCtrl)
        frameCtrlData = FrameCtrlData(frameCtrl)
        dataLen = int(response[3])  # toInt specifies length of data

        try:
            dataBytes = response[4 : 4 + dataLen]
            if frameCtrlData.isEncrypted():
                _LOGGER.debug("is encrypted")
            #     BlufiAES aes = new BlufiAES(self.mAESKey, AES_TRANSFORMATION, generateAESIV(sequence));
            #     dataBytes = aes.decrypt(dataBytes);
            # }
            if frameCtrlData.isChecksum():
                respChecksum1 = int(response[-1])
                respChecksum2 = int(response[-2])
                crc = self.calc_crc(self.calc_crc(0, bytes([sequence, dataLen])), dataBytes)
                calcChecksum1 = (crc >> 8) & 255
                calcChecksum2 = crc & 255

                if respChecksum1 != calcChecksum1 or respChecksum2 != calcChecksum2:
                    _LOGGER.debug(
                        f"expect checksum: {respChecksum1}, {respChecksum2}\n"
                        f"received checksum: {calcChecksum1}, {calcChecksum2}"
                    )
                    return -4

            data_offset = 2 if frameCtrlData.hasFrag() else 0

            self.notification.addData(dataBytes, data_offset)
            return 1 if frameCtrlData.hasFrag() else 0
        except Exception as e:
            _LOGGER.debug(e)
            return -100

    async def parseBlufiNotifyData(self, return_bytes: bool = False):
        pkgType = self.notification.getPkgType()
        subType = self.notification.getSubType()
        dataBytes = self.notification.getDataArray()
        if pkgType == 0:
            # never seem to get these..
            self._parseCtrlData(subType, dataBytes)
        if pkgType == 1:
            if return_bytes:
                return dataBytes
            return await self._parseDataData(subType, dataBytes)

    def _parseCtrlData(self, subType: int, data: bytes) -> None:
        pass
        # self._parseAck(data)

    async def _parseDataData(self, subType: int, data: bytes):
        #     if (subType == 0) {
        #         this.mSecurityCallback.onReceiveDevicePublicKey(data);
        #         return;
        #     }
        _LOGGER.debug(subType)
        match subType:
            #         case 15:
            #             parseWifiState(data);
            #             return;
            #         case 16:
            #             parseVersion(data);
            #             return;
            #         case 17:
            #             parseWifiScanList(data);
            #             return;
            #         case 18:
            #             int errCode = data.length > 0 ? 255 & data[0] : 255;
            #             onError(errCode);
            #             return;
            case 19:
                #             # com/agilexrobotics/utils/EspBleUtil$BlufiCallbackMain.smali
                # luba_msg = parse_custom_data(data)  # parse to protobuf message
                return data

    # private void parseCtrlData(int i, byte[] bArr) {
    #     if (i == 0) {
    #         parseAck(bArr);
    #     }
    # }

    # private void parseAck(byte[] bArr) {
    #     this.mAck.add(Integer.valueOf(bArr.length > 0 ? bArr[0] & 255 : 256));
    # }

    def getJsonString(self, cmd: int) -> str:
        jSONObject = {}
        try:
            jSONObject["cmd"] = cmd
            jSONObject[tmp_constant.REQUEST_ID] = int(time.time())
            return json.dumps(jSONObject)
        except Exception:
            return ""

    def current_milli_time(self):
        return round(time.time() * 1000)

    def _getPackageType(self, typeValue: int):
        return typeValue & 3

    def _getSubType(self, typeValue: int):
        return (typeValue & 252) >> 2

    def getTypeValue(self, type: int, subtype: int):
        return (subtype << 2) | type

    def receiveAck(self, expectAck: int) -> bool:
        try:
            ack = self.mAck.get()
            return ack == expectAck
        except Exception as err:
            _LOGGER.debug(err)
            return False

    def generate_send_sequence(self) -> int:
        return self.mSendSequence.increment_and_get() & 255

    async def post_custom_data_bytes(self, data: bytes) -> None:
        if data is None:
            return
        type_val = self.getTypeValue(1, 19)
        try:
            suc = await self.post(self.mEncrypted, self.mChecksum, self.mRequireAck, type_val, data)
            # int status = suc ? 0 : BlufiCallback.CODE_WRITE_DATA_FAILED
            # onPostCustomDataResult(status, data)
            # _LOGGER.debug(suc)
        except Exception as err:
            await self.client.disconnect()
            _LOGGER.debug(err)

    async def post_custom_data(self, data_str: str) -> None:
        data = data_str.encode()
        if data == None:
            return
        type_val = self.getTypeValue(1, 19)
        try:
            suc = await self.post(self.mEncrypted, self.mChecksum, self.mRequireAck, type_val, data)
            # int status = suc ? 0 : BlufiCallback.CODE_WRITE_DATA_FAILED
            # onPostCustomDataResult(status, data)
        except Exception as err:
            _LOGGER.debug(err)
            # we might be constantly connected and in a bad state
            self.mSendSequence = itertools.count()
            self.mReadSequence = itertools.count()
            await self.client.disconnect()

    async def post(
        self,
        encrypt: bool,
        checksum: bool,
        require_ack: bool,
        type_of: int,
        data: bytes,
    ) -> bool:
        if data is None:
            return await self.post_non_data(encrypt, checksum, require_ack, type_of)

        return await self.post_contains_data(encrypt, checksum, require_ack, type_of, data)

    async def post_non_data(self, encrypt: bool, checksum: bool, require_ack: bool, type_of: int) -> bool:
        sequence = self.generate_send_sequence()
        postBytes = self.getPostBytes(type_of, encrypt, checksum, require_ack, False, sequence, None)
        posted = await self.gatt_write(postBytes)
        return posted and (not require_ack or self.receiveAck(sequence))

    async def post_contains_data(
        self,
        encrypt: bool,
        checksum: bool,
        require_ack: bool,
        type_of: int,
        data: bytes,
    ) -> bool:
        chunk_size = 517  # self.client.mtu_size - 3

        chunks = list()
        for i in range(0, len(data), chunk_size):
            if i + chunk_size > len(data):
                chunks.append(data[i : len(data)])
            else:
                chunks.append(data[i : i + chunk_size])
        for index, chunk in enumerate(chunks):
            frag = index != len(chunks) - 1
            sequence = self.generate_send_sequence()
            postBytes = self.getPostBytes(type_of, encrypt, checksum, require_ack, frag, sequence, chunk)
            # _LOGGER.debug("sequence")
            # _LOGGER.debug(sequence)
            posted = await self.gatt_write(postBytes)
            if posted is not None:
                return False

            if not frag:
                return not require_ack or self.receiveAck(sequence)

            if require_ack and not self.receiveAck(sequence):
                return False

            _LOGGER.debug("sleeping 0.01")
            await sleep(0.01)
            if require_ack and not self.receiveAck(sequence):
                return False
            else:
                return True

    def getPostBytes(
        self,
        type: int,
        encrypt: bool,
        checksum: bool,
        require_ack: bool,
        hasFrag: bool,
        sequence: int,
        data: bytes | None,
    ) -> bytes:
        byteOS = BytesIO()
        dataLength = 0 if data == None else len(data)
        frameCtrl = FrameCtrlData.getFrameCTRLValue(encrypt, checksum, 0, require_ack, hasFrag)
        byteOS.write(type.to_bytes(1, sys.byteorder))
        byteOS.write(frameCtrl.to_bytes(1, sys.byteorder))
        byteOS.write(sequence.to_bytes(1, sys.byteorder))
        byteOS.write(dataLength.to_bytes(1, sys.byteorder))

        if data is not None:
            byteOS.write(data)

        _LOGGER.debug(byteOS.getvalue())
        return byteOS.getvalue()

    @staticmethod
    def calc_crc(initial: int, data: Union[bytes, bytearray]) -> int:
        """Calculate CRC value for given initial value and byte array.

        Args:
            initial: Initial CRC value
            data: Bytes to calculate CRC for

        Returns:
            Calculated CRC value (16-bit)

        Raises:
            TypeError: If data is not bytes or bytearray
            ValueError: If initial value is out of valid range

        """
        if not isinstance(data, (bytes, bytearray)):
            raise TypeError("Data must be bytes or bytearray")

        if not 0 <= initial <= 0xFFFF:
            raise ValueError("Initial value must be between 0 and 65535")

        try:
            crc = (~initial) & 0xFFFF

            for byte in data:
                crc = ((crc << 8) ^ CRC_TB[byte ^ (crc >> 8)]) & 0xFFFF

            return (~crc) & 0xFFFF

        except Exception as e:
            _LOGGER.error("Error calculating CRC: %s", str(e))
            raise
