from enum import unique
from typing import Annotated, Union
from uuid import UUID

import channels
from annotated_types import Ge, Le, Lt, MaxLen
from channels.db import database_sync_to_async
from pydantic import PositiveInt
from pydantic.main import BaseModel
from pydantic.types import Discriminator, NonNegativeInt
from pydantic_extra_types.mac_address import MacAddress

from c3nav.mesh.cformats import CDoc, CEmbed, CName, LenBytes, NoDef, VarLen, discriminator_value, CEnum
from c3nav.mesh.schemas import BoardConfig, ChipType, FirmwareAppDescription, RangeResultItem, RawFTMEntry
from c3nav.mesh.utils import MESH_ALL_UPLINKS_GROUP

MESH_ROOT_ADDRESS = '00:00:00:00:00:00'
MESH_NONE_ADDRESS = '00:00:00:00:00:00'
MESH_PARENT_ADDRESS = '00:00:00:ff:ff:ff'
MESH_CHILDREN_ADDRESS = '00:00:00:00:ff:ff'
MESH_BROADCAST_ADDRESS = 'ff:ff:ff:ff:ff:ff'
NO_LAYER = 0xFF

OTA_CHUNK_SIZE = 512


@unique
class MeshMessageType(CEnum):
    NOOP = "NOOP", 0x00

    ECHO_REQUEST = "ECHO_REQUEST", 0x01
    ECHO_RESPONSE = "ECHO_RESPONSE", 0x02

    MESH_SIGNIN = "MESH_SIGNIN", 0x03
    MESH_LAYER_ANNOUNCE = "MESH_LAYER_ANNOUNCE", 0x04
    MESH_ADD_DESTINATION = "MESH_ADD_DESTINATION", 0x05
    MESH_REMOVE_DESTINATIONS = "MESH_REMOVE_DESTINATIONS", 0x06
    MESH_ROUTE_REQUEST = "MESH_ROUTE_REQUEST", 0x07
    MESH_ROUTE_RESPONSE = "MESH_ROUTE_RESPONSE", 0x08
    MESH_ROUTE_TRACE = "MESH_ROUTE_TRACE", 0x09
    MESH_ROUTING_FAILED = "MESH_ROUTING_FAILED", 0x0a
    MESH_SIGNIN_CONFIRM = "MESH_SIGNIN_CONFIRM", 0x0b
    MESH_RESET = "MESH_RESET", 0x0c

    CONFIG_DUMP = "CONFIG_DUMP", 0x10
    CONFIG_HARDWARE = "CONFIG_HARDWARE", 0x11
    CONFIG_BOARD = "CONFIG_BOARD", 0x12
    CONFIG_FIRMWARE = "CONFIG_FIRMWARE", 0x13
    CONFIG_UPLINK = "CONFIG_UPLINK", 0x14
    CONFIG_POSITION = "CONFIG_POSITION", 0x15
    CONFIG_NODE = "CONFIG_NODE", 0x16
    CONFIG_IBEACON = "CONFIG_IBEACON", 0x17

    OTA_STATUS = "OTA_STATUS", 0x20
    OTA_REQUEST_STATUS = "OTA_REQUEST_STATUS", 0x21
    OTA_START = "OTA_START", 0x22
    OTA_URL = "OTA_URL", 0x23
    OTA_FRAGMENT = "OTA_FRAGMENT", 0x24
    OTA_REQUEST_FRAGMENTS = "OTA_REQUEST_FRAGMENTS", 0x25
    OTA_SETTING = "OTA_SETTING", 0x26
    OTA_APPLY = "OTA_APPLY", 0x27
    OTA_ABORT = "OTA_ABORT", 0x28

    LOCATE_REQUEST_RANGE = "LOCATE_REQUEST_RANGE", 0x30
    LOCATE_RANGE_RESULTS = "LOCATE_RANGE_RESULTS", 0x31
    LOCATE_RAW_FTM_RESULTS = "LOCATE_RAW_FTM_RESULTS", 0x32

    REBOOT = "REBOOT", 0x40

    REPORT_ERROR = "REPORT_ERROR", 0x50

    @property
    def pretty_name(self):
        name = self.name.replace('_', ' ').lower()
        if name.startswith('config'):
            name = name.removeprefix('config').strip() + ' config'
        name.replace('ota', 'OTA')
        return name


class NoopMessage(discriminator_value(msg_type=MeshMessageType.NOOP), BaseModel):
    """ noop """
    pass


class EchoRequestMessage(discriminator_value(msg_type=MeshMessageType.ECHO_REQUEST), BaseModel):
    """ repeat back string """
    content: Annotated[str, MaxLen(255), VarLen()] = ""


class EchoResponseMessage(discriminator_value(msg_type=MeshMessageType.ECHO_RESPONSE), BaseModel):
    """ repeat back string """
    content: Annotated[str, MaxLen(255), VarLen()] = ""


class MeshSigninMessage(discriminator_value(msg_type=MeshMessageType.MESH_SIGNIN), BaseModel):
    """ node says hello to upstream node """
    pass


class MeshLayerAnnounceMessage(discriminator_value(msg_type=MeshMessageType.MESH_LAYER_ANNOUNCE), BaseModel):
    """ upstream node announces layer number """
    layer: Annotated[PositiveInt, Lt(2 ** 8), CDoc("mesh layer that the sending node is on")]


class MeshAddDestinationMessage(discriminator_value(msg_type=MeshMessageType.MESH_ADD_DESTINATION), BaseModel):
    """ downstream node announces served destination """
    destination: Annotated[MacAddress, CDoc("address of the added destination",)]
    via: Annotated[MacAddress, CDoc("address of the added destination's upstream",)]


class MeshRemoveDestinationsMessage(discriminator_value(msg_type=MeshMessageType.MESH_REMOVE_DESTINATIONS), BaseModel):
    """ downstream node announces no longer served destination """
    addresses: Annotated[list[MacAddress], MaxLen(16), VarLen(), CDoc("adresses of the removed destinations",)]


class MeshRouteRequestMessage(discriminator_value(msg_type=MeshMessageType.MESH_ROUTE_REQUEST), BaseModel):
    """ request routing information for node """
    request_id: Annotated[PositiveInt, Lt(2**32)]
    address: Annotated[MacAddress, CDoc("target address for the route")]


class MeshRouteResponseMessage(discriminator_value(msg_type=MeshMessageType.MESH_ROUTE_RESPONSE), BaseModel):
    """ reporting the routing table entry to the given address """
    request_id: Annotated[PositiveInt, Lt(2**32)]
    route: Annotated[MacAddress, CDoc("routing table entry or 00:00:00:00:00:00")]


class MeshRouteTraceMessage(discriminator_value(msg_type=MeshMessageType.MESH_ROUTE_TRACE), BaseModel):
    """ special message, collects all hop adresses on its way """
    request_id: Annotated[PositiveInt, Lt(2**32)]
    trace: Annotated[list[MacAddress], MaxLen(16), VarLen(), CDoc("addresses encountered by this message")]


class MeshRoutingFailedMessage(discriminator_value(msg_type=MeshMessageType.MESH_ROUTING_FAILED), BaseModel):
    """ indicates that a message to the given address couldn't be routed """
    address: MacAddress


class MeshSigninConfirmMessage(discriminator_value(msg_type=MeshMessageType.MESH_SIGNIN_CONFIRM), BaseModel):
    """ Confirm signin from root node """
    address: MacAddress


class MeshResetMessage(discriminator_value(msg_type=MeshMessageType.MESH_RESET), BaseModel):
    """ tell nodes with a matching address to leave and re-join the mesh """
    address: MacAddress


class ConfigDumpMessage(discriminator_value(msg_type=MeshMessageType.CONFIG_DUMP), BaseModel):
    """ request for the node to dump its config """
    pass


class ConfigHardwareMessage(discriminator_value(msg_type=MeshMessageType.CONFIG_HARDWARE), BaseModel):
    """ respond hardware/chip info """
    chip: Annotated[ChipType, NoDef(), LenBytes(2), CName("chip_id")]
    revision_major: Annotated[NonNegativeInt, Lt(2**8)]
    revision_minor: Annotated[NonNegativeInt, Lt(2**8)]

    def get_chip_display(self):
        return ChipType(self.chip).name.replace('_', '-')


class ConfigBoardMessage(discriminator_value(msg_type=MeshMessageType.CONFIG_BOARD), BaseModel):
    """ set/respond board config """
    board_config: Annotated[BoardConfig, CEmbed]


class ConfigFirmwareMessage(discriminator_value(msg_type=MeshMessageType.CONFIG_FIRMWARE), BaseModel):
    """ respond firmware info """
    app_desc: FirmwareAppDescription


class ConfigPositionMessage(discriminator_value(msg_type=MeshMessageType.CONFIG_POSITION), BaseModel):
    """ set/respond position config """
    x_pos: Annotated[int, Ge(-2**31), Lt(2**31)]
    y_pos: Annotated[int, Ge(-2**31), Lt(2**31)]
    z_pos: Annotated[int, Ge(-2**15), Lt(2**15)]


class ConfigNodeMessage(discriminator_value(msg_type=MeshMessageType.CONFIG_NODE), BaseModel):
    """ set/respond node config """
    number: Annotated[NonNegativeInt, Lt(2**16)]
    name: Annotated[str, MaxLen(16)]


class ConfigIBeaconMessage(discriminator_value(msg_type=MeshMessageType.CONFIG_IBEACON), BaseModel):
    """ set/respond ibeacon config """
    uuid: UUID
    major: Annotated[NonNegativeInt, Lt(2**16)]
    minor: Annotated[NonNegativeInt, Lt(2**16)]


class ConfigUplinkMessage(discriminator_value(msg_type=MeshMessageType.CONFIG_UPLINK), BaseModel):
    """ set/respond uplink config """
    enabled: bool
    ssid: Annotated[str, MaxLen(32)]
    password: Annotated[str, MaxLen(64)]
    channel: Annotated[NonNegativeInt, Le(15)]
    udp: bool
    ssl: bool
    host: Annotated[str, MaxLen(64)]
    port: Annotated[PositiveInt, Lt(2**16)]


@unique
class OTADeviceStatus(CEnum):
    """ ota status, the ones >= 0x10 denote a permanent failure """
    NONE = "NONE", 0x00

    STARTED = "STARTED", 0x01
    APPLIED = "APPLIED", 0x02

    START_FAILED = "START_FAILED", 0x10
    WRITE_FAILED = "WRITE_FAILED", 0x12
    APPLY_FAILED = "APPLY_FAILED", 0x13
    ROLLED_BACK = "ROLLED_BACK", 0x14

    @property
    def pretty_name(self):
        return self.name.replace('_', ' ').lower()

    @property
    def is_failed(self):
        return self >= self.START_FAILED


class OTAStatusMessage(discriminator_value(msg_type=MeshMessageType.OTA_STATUS), BaseModel):
    """ report OTA status """
    update_id: Annotated[NonNegativeInt, Lt(2**32)]
    received_bytes: Annotated[NonNegativeInt, Lt(2**32)]
    next_expected_chunk: Annotated[NonNegativeInt, Lt(2**16)]
    auto_apply: bool
    auto_reboot: bool
    status: OTADeviceStatus


class OTARequestStatusMessage(discriminator_value(msg_type=MeshMessageType.OTA_REQUEST_STATUS), BaseModel):
    """ request OTA status """
    pass


class OTAStartMessage(discriminator_value(msg_type=MeshMessageType.OTA_START), BaseModel):
    """ instruct node to start OTA """
    update_id: Annotated[PositiveInt, Lt(2**32)]
    total_bytes: Annotated[PositiveInt, Lt(2**32)]
    auto_apply: bool
    auto_reboot: bool


class OTAURLMessage(discriminator_value(msg_type=MeshMessageType.OTA_URL), BaseModel):
    """ supply download URL for OTA update and who to distribute it to """
    update_id: Annotated[PositiveInt, Lt(2**32)]
    distribute_to: MacAddress
    url: Annotated[str, MaxLen(255), VarLen()]


class OTAFragmentMessage(discriminator_value(msg_type=MeshMessageType.OTA_FRAGMENT), BaseModel):
    """ supply OTA fragment """
    update_id: Annotated[PositiveInt, Lt(2**32)]
    chunk: Annotated[PositiveInt, Lt(2**16)]
    data: Annotated[bytes, MaxLen(OTA_CHUNK_SIZE), VarLen()]


class OTARequestFragmentsMessage(discriminator_value(msg_type=MeshMessageType.OTA_REQUEST_FRAGMENTS), BaseModel):
    """ request missing fragments """
    update_id: Annotated[PositiveInt, Lt(2**32)]
    chunks: Annotated[list[Annotated[PositiveInt, Lt(2**16)]], MaxLen(128), VarLen()]


class OTASettingMessage(discriminator_value(msg_type=MeshMessageType.OTA_SETTING), BaseModel):
    """ configure whether to automatically apply and reboot when update is completed """
    update_id: Annotated[PositiveInt, Lt(2**32)]
    auto_apply: bool
    auto_reboot: bool


class OTAApplyMessage(discriminator_value(msg_type=MeshMessageType.OTA_APPLY), BaseModel):
    """ apply OTA and optionally reboot """
    update_id: Annotated[PositiveInt, Lt(2**32)]
    reboot: bool


class OTAAbortMessage(discriminator_value(msg_type=MeshMessageType.OTA_ABORT), BaseModel):
    """ announcing OTA abort """
    update_id: Annotated[NonNegativeInt, Lt(2**32)]


class LocateRequestRangeMessage(discriminator_value(msg_type=MeshMessageType.LOCATE_REQUEST_RANGE), BaseModel):
    """ request to report distance to all nearby nodes """
    pass


class LocateRangeResults(discriminator_value(msg_type=MeshMessageType.LOCATE_RANGE_RESULTS), BaseModel):
    """ reports distance to given nodes """
    ranges: Annotated[list[RangeResultItem], MaxLen(16), VarLen()]


class LocateRawFTMResults(discriminator_value(msg_type=MeshMessageType.LOCATE_RAW_FTM_RESULTS), BaseModel):
    """ reports distance to given nodes """
    peer: MacAddress
    results: Annotated[list[RawFTMEntry], MaxLen(16), VarLen()]


class Reboot(discriminator_value(msg_type=MeshMessageType.REBOOT), BaseModel):
    """ reboot the device """
    pass


class ReportError(discriminator_value(msg_type=MeshMessageType.REPORT_ERROR), BaseModel):
    """ report a critical error to upstream """
    message: Annotated[str, MaxLen(255), VarLen()]


MeshMessageContent = Annotated[
    Union[
        NoopMessage,
        EchoRequestMessage,
        EchoResponseMessage,
        MeshSigninMessage,
        MeshLayerAnnounceMessage,
        MeshAddDestinationMessage,
        MeshRemoveDestinationsMessage,
        MeshRouteRequestMessage,
        MeshRouteResponseMessage,
        MeshRouteTraceMessage,
        MeshRoutingFailedMessage,
        MeshSigninConfirmMessage,
        MeshResetMessage,
        ConfigDumpMessage,
        ConfigHardwareMessage,
        ConfigBoardMessage,
        ConfigFirmwareMessage,
        ConfigPositionMessage,
        ConfigUplinkMessage,
        ConfigNodeMessage,
        ConfigIBeaconMessage,
        OTAStatusMessage,
        OTARequestStatusMessage,
        OTAStartMessage,
        OTAURLMessage,
        OTAFragmentMessage,
        OTARequestFragmentsMessage,
        OTASettingMessage,
        OTAApplyMessage,
        OTAAbortMessage,
        LocateRequestRangeMessage,
        LocateRangeResults,
        LocateRawFTMResults,
        Reboot,
        ReportError,
    ],
    Discriminator("msg_type")
]


class MeshMessage(BaseModel):
    dst: MacAddress
    src: MacAddress
    content: MeshMessageContent

    async def send(self, sender=None, exclude_uplink_address=None) -> bool:
        data = {
            "type": "mesh.send",
            "sender": sender,
            "exclude_uplink_address": exclude_uplink_address,
            "msg": self.model_dump(mode="json"),
        }

        if self.dst in (MESH_CHILDREN_ADDRESS, MESH_BROADCAST_ADDRESS):
            await channels.layers.get_channel_layer().group_send(MESH_ALL_UPLINKS_GROUP, data)
            return True

        from c3nav.mesh.models import MeshNode
        uplink = await database_sync_to_async(MeshNode.get_node_and_uplink)(self.dst)
        if not uplink:
            return False
        if uplink.node_id == exclude_uplink_address:
            return False
        await channels.layers.get_channel_layer().send(uplink.name, data)