import asyncio
import json
import logging
import time
from datetime import datetime
from enum import Enum
from typing import Any, Callable, Optional

from bleak import BleakClient
from bleak.backends.characteristic import BleakGATTCharacteristic

from .csrmeshcrypto import decrypt_packet, generate_key, make_packet, random_seq

logger = logging.getLogger(__name__)

CHARACTERISTIC_LOW = "c4edc000-9daf-11e3-8003-00025b000b00"
CHARACTERISTIC_HIGH = "c4edc000-9daf-11e3-8004-00025b000b00"

CAPABILITIES = {"dimming": {0, 90, 93, 94, 97, 134, 137, 162}, "color_temp": {0, 93, 134, 137, 162}}
PRODUCT_NAMES = {
    0: "Group",
    90: "Lamp Dimmer",
    93: "Recessed Downlight (RL)",
    94: "Light Adapter",
    97: "Smart Dimmer",
    134: "Smart Bulb (A19)",
    137: "Surface Downlight (BLD)",
    162: "MicroEdge (HLB)",
    167: "Smart Switch",
}


class Verb(Enum):
    WRITE = 0
    READ = 1
    INSERT = 2
    TRUNCATE = 3
    COUNT = 4
    DELETE = 5
    PING = 6
    SYNC = 7
    OTA = 8
    PUSH = 11
    SCAN_WIFI = 12
    CANCEL_DATASTREAM = 13
    UPDATE = 16
    TRIM = 17
    DISCONNECT_OTA = 18
    UNREGISTER = 20
    MARK = 21
    REBOOT = 22
    RESTART = 23
    OPEN_SSH = 32
    NONE = 255


class Noun(Enum):
    DIMMING = 10
    FADE_TIME = 25
    COUNTDOWN = 9
    DATE = 21
    TIME = 22
    SCHEDULE = 7
    GROUPS = 3
    SUNRISE_SUNSET = 6
    ASSOCIATION = 27
    WAKE_STATUS = 28
    COLOR = 29
    CONFIG = 30
    WIFI_NETWORKS = 31
    DIMMING_TABLE = 17
    ASSOCIATED_WIFI_NETWORK = 32
    ASSOCIATED_WIFI_NETWORK_STATUS = 33
    SCENES = 34
    SCHEDULE_2 = 35
    RAB_IP = 36
    RAB_ENV = 37
    RAB_CONFIG = 38
    THERMOMETER = 39
    FIRMWARE_VERSION = 40
    LUX_VALUE = 41
    TEST_MODE = 42
    HARCODED_STRING = 43
    RAB_MARKS = 44
    MOTION_SENSOR = 45
    ALS_DIMMING = 46
    ASSOCIATION_2 = 48
    RTC_SUN_RISE_SET_TABLE = 71
    RTC_DATE = 72
    RTC_TIME = 73
    RTC_DAYLIGHT_SAVING_TIME_TABLE = 74
    AVION_SENSOR = 91
    NONE = 255


def _parse_data(target_id: int, data: bytes) -> Optional[dict]:
    logger.info(f"mesh: parsing data {data!r} from {target_id}")

    if data[0] == 0 and data[1] == 0:
        logger.warning("empty data")
        return None

    try:
        verb = Verb(data[0])
        noun = Noun(data[1])

        if verb == Verb.WRITE:
            target_id = (
                target_id
                if target_id
                else int.from_bytes(bytes([data[2], data[3]]), byteorder="big")
            )
            value_bytes = data[4:]
        else:
            value_bytes = data[2:]

        logger.info(
            f"mesh: target_id({target_id}), verb({verb}), noun({noun}), value:{value_bytes!r})"
        )

        if noun == Noun.DIMMING:
            brightness = int.from_bytes(value_bytes[1:2], byteorder="big")
            return {"avid": target_id, "brightness": brightness}
        elif noun == Noun.COLOR:
            kelvin = int.from_bytes(value_bytes[2:4], byteorder="big")
            return {"avid": target_id, "color_temp": kelvin}
        else:
            logger.warning(f"unknown noun {noun}")
    except Exception as e:
        logger.exception(f"mesh: Exception parsing {data!r} from {target_id}")
        raise e
    return None


# BLEBridge.decryptMessage
def _parse_command(source: int, data: bytes):
    hex = "-".join(map(lambda b: format(b, "01x"), data))
    logger.info(f"mesh: parsing notification {hex}")
    if data[2] == 0x73:
        if data[0] == 0x0 and data[1] == 0x80:
            return _parse_data(source, data[3:])
        else:
            return _parse_data(int.from_bytes(bytes([data[1], data[0]]), byteorder="big"), data[3:])
    else:
        logger.warning(f"Unable to handle {data[2]}")


def _create_packet(target_id: int, verb: Verb, noun: Noun, value_bytes: bytes) -> bytes:
    if target_id < 32896:
        group_id = target_id
        target_id = 0
    else:
        group_id = 0

    target_bytes = bytearray(target_id.to_bytes(2, byteorder="big"))
    group_bytes = bytearray(group_id.to_bytes(2, byteorder="big"))
    return bytes(
        [
            target_bytes[1],
            target_bytes[0],
            0x73,
            verb.value,
            noun.value,
            group_bytes[0],
            group_bytes[1],
            0,  # id
            *value_bytes,
            0,
            0,
        ]
    )


def _get_color_temp_packet(target_id: int, color: int) -> bytes:
    return _create_packet(
        target_id,
        Verb.WRITE,
        Noun.COLOR,
        bytes([0x01, *bytearray(color.to_bytes(2, byteorder="big"))]),
    )


def _get_brightness_packet(target_id: int, brightness: int) -> bytes:
    return _create_packet(target_id, Verb.WRITE, Noun.DIMMING, bytes([brightness, 0, 0]))


def _get_date_packet(year: int, month: int, day: int) -> bytes:
    year -= 2000
    return _create_packet(
        0,
        Verb.WRITE,
        Noun.DATE,
        bytearray(
            [
                year,
                month,
                day,
            ]
        ),
    )


def _get_time_packet(hour: int, minute: int, seconds: int) -> bytes:
    return _create_packet(
        0,
        Verb.WRITE,
        Noun.TIME,
        bytearray(
            [
                hour,
                minute,
                seconds,
            ]
        ),
    )


def apply_overrides_from_settings(settings: dict):
    capabilities_overrides = settings.get("capabilities_overrides")
    if capabilities_overrides is not None:
        dimming_overrides = capabilities_overrides.get("dimming")
        if dimming_overrides is not None:
            for product_id in dimming_overrides:
                CAPABILITIES["dimming"].add(product_id)
        color_temp_overrides = capabilities_overrides.get("color_temp")
        if color_temp_overrides is not None:
            for product_id in color_temp_overrides:
                CAPABILITIES["color_temp"].add(product_id)


class Mesh:
    def __init__(self, mesh: BleakClient, passphrase: str) -> None:
        super().__init__()
        self._mesh = mesh
        self._key = generate_key(passphrase.encode("ascii") + b"\x00\x4d\x43\x50")
        self._notification_callback: Optional[Callable] = None
        # Track dimming commands for rapid dimming detection
        self._dimming_commands: dict[
            int, tuple[int, float]
        ] = {}  # target_id -> (brightness, timestamp)

    async def _write_gatt(self, packet: bytes) -> bool:
        hex = "-".join(map(lambda b: format(b, "02x"), packet))
        logger.debug(f"Writing to gatt: {hex}")

        csrpacket = make_packet(self._key, random_seq(), packet)
        low = csrpacket[:20]
        high = csrpacket[20:]
        await self._mesh.write_gatt_char(CHARACTERISTIC_LOW, low)
        await self._mesh.write_gatt_char(CHARACTERISTIC_HIGH, high)
        return True

    async def read_all(self):
        packet = _create_packet(0, Verb.READ, Noun.DIMMING, bytearray(3))
        await self._write_gatt(packet)

    async def set_network_time(self):
        now = datetime.now()
        await self._write_gatt(_get_date_packet(now.year, now.month, now.day))
        await asyncio.sleep(3)
        now = datetime.now()
        await self._write_gatt(_get_time_packet(now.hour, now.minute, now.second))

    async def subscribe(self, callback: Callable[[dict], Any]):
        """
        Subscribe to mesh status updates.

        This method now accepts an async callback that will be called
        for each status update from the mesh. The callback should accept
        a dict and can be async.

        Args:
            callback: Async function to call with status updates.
                     Signature: async def callback(status_data: dict)
        """
        self._notification_callback = callback

        try:
            # Start notifications on the RX characteristic
            await self._mesh.start_notify(CHARACTERISTIC_LOW, self._handle_notification)
            await self._mesh.start_notify(CHARACTERISTIC_HIGH, self._handle_notification)

            logger.info("Subscribed to mesh notifications")

            # Keep the subscription alive
            while self._mesh.is_connected:
                await asyncio.sleep(1)

        except Exception as e:
            logger.error(f"Error in mesh subscription: {e}")
            raise
        finally:
            # Stop notifications on cleanup
            if self._mesh.is_connected:
                try:
                    await self._mesh.stop_notify(CHARACTERISTIC_LOW)
                    await self._mesh.stop_notify(CHARACTERISTIC_HIGH)
                except Exception as e:
                    logger.warning(f"Error stopping notifications: {e}")

    def _check_rapid_dimming(self, target_id: int, brightness: int) -> Optional[int]:
        """
        Check if this is a rapid dimming command (within 750ms of the previous command).
        If it is, determine if commands are incrementing or decrementing and return
        the appropriate brightness value (5 for decrementing, 255 for incrementing).

        Args:
            target_id: Target device ID
            brightness: New brightness value

        Returns:
            Brightness value to use (0, 255, or None if not a rapid dimming scenario)
        """
        current_time = time.time()

        if target_id in self._dimming_commands:
            prev_brightness, prev_time = self._dimming_commands[target_id]
            time_diff = (current_time - prev_time) * 1000  # Convert to milliseconds

            if time_diff < 750:
                # This is a rapid dimming command
                if brightness == prev_brightness:
                    # Same value - not a valid rapid dimming sequence
                    logger.debug(
                        f"Rapid commands but same brightness ({brightness}) for target_id {target_id}, ignoring"
                    )
                    return None

                is_incrementing = brightness > prev_brightness
                logger.info(
                    f"Rapid dimming detected for target_id {target_id}: "
                    f"{prev_brightness} -> {brightness} ({time_diff:.0f}ms), "
                    f"direction: {'incrementing' if is_incrementing else 'decrementing'}"
                )
                # Return 255 for incrementing, 5 for decrementing
                final_brightness = 255 if is_incrementing else 5
                # Clear the command history since we're processing it
                del self._dimming_commands[target_id]
                return final_brightness

        # Store this command for next check
        self._dimming_commands[target_id] = (brightness, current_time)
        return None

    def _handle_notification(self, charactheristic: BleakGATTCharacteristic, data: bytearray):
        """
        Handle incoming BLE notifications from the mesh.
        This is called synchronously by Bleak, so we need to schedule
        the async callback properly.

        Args:
            sender: Characteristic handle
            data: Raw notification data
        """
        try:
            if charactheristic.uuid == CHARACTERISTIC_LOW:
                self._low_bytes = data
            elif charactheristic.uuid == CHARACTERISTIC_HIGH:
                encrypted = bytes([*self._low_bytes, *data])
                decoded = decrypt_packet(self._key, encrypted)
                parsed = _parse_command(decoded["source"], decoded["decpayload"])
                if parsed:
                    # Check for rapid dimming if this is a brightness command
                    if "brightness" in parsed:
                        rapid_dimming_result = self._check_rapid_dimming(
                            parsed["avid"], parsed["brightness"]
                        )

                        if rapid_dimming_result is not None:
                            # Rapid dimming detected, send the extreme brightness value
                            logger.info(
                                f"mesh: Sending rapid dimming brightness {rapid_dimming_result}"
                            )
                            asyncio.create_task(
                                self._send_brightness_async(parsed["avid"], rapid_dimming_result)
                            )
                            return

                    # No rapid dimming, proceed with normal notification
                    if self._notification_callback:
                        # Schedule the async callback
                        asyncio.create_task(self._notification_callback(parsed))

        except Exception as e:
            logger.error(f"Error handling notification: {e}")

    async def _send_brightness_async(self, target_id: int, brightness: int):
        """
        Send a brightness command to the mesh and notify via callback.

        Args:
            target_id: Target device ID
            brightness: Brightness value (0-255)
        """
        try:
            packet = _get_brightness_packet(target_id, brightness)
            if await self._write_gatt(packet):
                logger.info(f"mesh: Sent brightness {brightness} to {target_id}")
                # Notify via callback
                if self._notification_callback:
                    await self._notification_callback({"avid": target_id, "brightness": brightness})
        except Exception as e:
            logger.error(f"Error sending brightness command: {e}")

    async def send_command(self, command_data: dict):
        """
        Send a command to the mesh network.

        Args:
            command_data: Command dict containing:
                - device_id: Target device ID
                - state: "ON" or "OFF"
                - brightness: Optional brightness (0-255)
                - color_temp: Optional color temperature
        """
        try:
            avid: int = command_data.get("avid")  # type: ignore
            command = command_data.get("command")
            if command == "read_all":
                await self.read_all()

            elif command == "update":
                payload = json.loads(command_data.get("json"))  # type: ignore
                if "brightness" in payload:
                    packet = _get_brightness_packet(avid, payload["brightness"])
                elif "color_temp" in payload:
                    packet = _get_color_temp_packet(avid, payload["color_temp"])
                elif "state" in payload:
                    packet = _get_brightness_packet(avid, 255 if payload["state"] == "ON" else 0)
                else:
                    logger.warning("mesh: Unknown payload")
                    return False

                if await self._write_gatt(packet):
                    logger.info("mesh: Acknowedging directly")
                    parsed = _parse_command(avid, packet)
                    if self._notification_callback and parsed:
                        await self._notification_callback(parsed)

            logger.debug(f"Sent command to device {avid}: {command_data}")

        except Exception as e:
            logger.error(f"Error sending command to mesh: {e}")
            raise
