import abc
from dataclasses import asdict, dataclass, field
from typing import TYPE_CHECKING, Any, Self

if TYPE_CHECKING:
    from aiospb.mqtt import MqttClient

from .data import (
    DataType,
    MetricBirth,
    MetricChange,
    PropertySet,
    PropertyValue,
    ValueType,
)

_TOPIC = "spBv1.0/{group}/{ctype}/{node_name}"


@dataclass
class MetricDataTransfer:
    """Data Transfer Object (dto) for sending MetricBirths and MetricChanges"""

    timestamp: int
    value: ValueType
    datatype: DataType
    alias: int = 0
    name: str = ""
    properties: PropertySet = field(default_factory=PropertySet)
    is_transient: bool = False
    is_null: bool = False
    is_historical: bool = False

    @classmethod
    def from_metric_birth(cls, birth: MetricBirth) -> Self:
        return cls(
            birth.timestamp,
            birth.value,
            birth.datatype,
            birth.alias,
            birth.name,
            birth.properties,
            birth.is_transient,
            birth.value is None,
        )

    @classmethod
    def from_metric_change(cls, change: MetricChange) -> Self:
        properties = (
            PropertySet(quality=PropertyValue(change.quality.value, DataType.Int32))
            if change.has_changed_quality
            else PropertySet()
        )
        return cls(
            change.timestamp,
            change.value,
            change.datatype,
            change.alias,
            change.metric_name,
            properties,
        )

    @classmethod
    def from_dict(cls, mapping: dict[str, Any]) -> Self:
        """Construct dto from a dict"""
        return cls(
            mapping["timestamp"],
            mapping["value"],
            DataType[mapping["dataType"]],
            mapping.get("alias", 0),
            mapping.get("name", ""),
            PropertySet.from_dict(mapping.get("properties", {})),
            mapping.get("is_transient", False),
            mapping.get("is_null", False),
            mapping.get("is_historical", False),
        )

    def to_dict(self) -> dict[str, Any]:
        """Convert dto to dict for parsing"""
        mapping = {
            "timestamp": self.timestamp,
            "value": self.value,
            "dataType": self.datatype.name,
        }
        if self.alias:
            mapping["alias"] = self.alias

        if self.name:
            mapping["name"] = self.name

        if self.is_transient:
            mapping["is_transient"] = True

        if self.value is None:
            mapping["is_null"] = True

        if self.properties:
            mapping["properties"] = self.properties.to_dict()

        if self.is_historical:
            mapping["is_historical"] = True
        return mapping


@dataclass
class Payload(abc.ABC):
    timestamp: int

    @classmethod
    @abc.abstractmethod
    def from_dict(cls, payload_map: dict[str, Any]) -> Self:
        """Create a payload from a plain dict"""

    @abc.abstractmethod
    def to_dict(self) -> dict[str, Any]:
        """Convert to dict for parsing"""


@dataclass
class Topic:
    value: str

    @property
    def component_name(self) -> str:
        items = self.value.split("/")
        if items[1] == "STATE":
            return "/".join(items[2:])
        else:
            return "/".join([items[1]] + items[3:])

    @property
    def message_type(self) -> str:
        items = self.value.split("/")
        if items[1] == "STATE":
            return "STATE"
        return items[2]

    @classmethod
    def from_component(cls, name: str, message_type: str) -> Self:
        if message_type not in (
            "STATE",
            "NDATA",
            "NBIRTH",
            "NDEATH",
            "DDATA",
            "DBIRTH",
            "DDEATH",
        ):
            raise ValueError(f'Message type "{message_type}" is not a standard')
        if message_type == "STATE":
            return cls(f"spBv1.0/STATE/{name}")
        items = name.split("/")
        return cls(f"spBv1.0/{items[0]}/{message_type}/{'/'.join(items[1:])}")


@dataclass
class SpbMessage:
    topic: Topic
    payload: Payload

    def is_a(self, message_type: str) -> bool:
        return message_type == self.topic.message_type

    def is_from_node(self) -> bool:
        return "STATE" != self.topic.message_type


@dataclass
class Will:
    """Will message to sent to MQTT broker"""

    message: SpbMessage
    qos: int
    retain: bool


@dataclass
class NodePayload(Payload):
    """Payload to send by MQTT for/to edge node messages"""

    metrics: list[MetricDataTransfer]
    seq: int | None = None

    @classmethod
    def from_dict(cls, payload_map: dict[str, Any]) -> Self:
        """Create a payload from a plain dict"""
        return cls(
            payload_map["timestamp"],
            [MetricDataTransfer.from_dict(value) for value in payload_map["metrics"]],
            payload_map.get("seq"),
        )

    def to_dict(self) -> dict[str, Any]:
        """Convert to dict for parsing"""
        outcome = {
            "timestamp": self.timestamp,
            "metrics": [dto.to_dict() for dto in self.metrics],
        }
        if self.seq is not None:
            outcome["seq"] = self.seq

        return outcome


@dataclass
class HostPayload(Payload):
    """Payload to send by MQTT for State messages"""

    online: bool

    @classmethod
    def from_dict(cls, payload_map: dict[str, Any]) -> Self:
        """Create a payload from a plain dict"""
        return cls(payload_map["timestamp"], payload_map["online"])

    def to_dict(self) -> dict[str, Any]:
        """Convert to dict for parsing"""
        return {"timestamp": self.timestamp, "online": self.online}


@dataclass
class MessageContent(abc.ABC):
    """Interface of a message content"""

    @abc.abstractmethod
    async def send(
        self,
        mqtt_client: "MqttClient",
        timestamp: int,
        component_name: str,
        seq: int | None = None,
    ):
        """Publish to MQTT Broker the content of the message"""


@dataclass
class NconnectContent(MessageContent):
    """Content for initializing MQTT connection"""

    primary_host: str = ""

    async def send(
        self,
        mqtt_client: "MqttClient",
        timestamp: int,
        component_name: str,
        seq: int | None = None,
    ):
        group, name = component_name.split("/")
        topic = _TOPIC.format(group=group, ctype="NDEATH", node_name=name)
        timestamp = timestamp
        await mqtt_client.connect(
            component_name,
            will=Will(
                SpbMessage(
                    topic,
                    NodePayload(
                        timestamp=timestamp,
                        metrics=[
                            MetricDataTransfer(
                                timestamp, seq, DataType.Int64, name="bdSeq"
                            )
                        ],
                    ),
                ),
                qos=1,
                retain=False,
            ),
        )

        topic = _TOPIC.format(group=group, ctype="NCMD", node_name=name)
        await mqtt_client.subscribe(topic, qos=1)

        if not self.primary_host:
            topic = "spBv1.0/STATE/*"
        else:
            topic = f"spBv1.0/STATE/{self.primary_host}"
        await mqtt_client.subscribe(topic, qos=1)


@dataclass
class HconnectContent(MessageContent):
    """Connect to MQTT broker a Host application"""

    groups: list[str] | None = None

    async def send(
        self,
        mqtt_client: "MqttClient",
        timestamp: int,
        component_name: str,
        _: int | None = None,
    ):
        await mqtt_client.connect(
            component_name,
            will=Will(
                SpbMessage(
                    f"spBv1.0/STATE/{component_name}", HostPayload(timestamp, False)
                ),
                qos=1,
                retain=True,
            ),
        )
        await mqtt_client.subscribe(f"spBv1.0/STATE/{component_name}", qos=1)
        groups = self.groups or ["+"]
        for group in groups:
            for mtype in ("NBIRTH", "NDATA", "NDEATH", "DBIRTH", "DDATA", "DDEATH"):
                await mqtt_client.subscribe(f"spBv1.0/{group}/{mtype}/+", qos=1)


@dataclass
class DisconnectContent(MessageContent):
    """Disconnect from the MQTT broker"""

    async def send(
        self,
        mqtt_client: "MqttClient",
        _timestamp: int,
        _component_name: str,
        _seq: int | None = None,
    ):
        await mqtt_client.disconnect()


@dataclass
class NbirthContent(MessageContent):
    """Content for Nbirth message"""

    metrics: list[MetricDataTransfer]

    async def send(
        self,
        mqtt_client: "MqttClient",
        timestamp: int,
        component_name: str,
        seq: int | None = None,
    ):
        if seq is None:
            raise ValueError("Seq variable can not be None")

        group, name = component_name.split("/")
        topic = _TOPIC.format(group=group, ctype="NDEATH", node_name=name)
        bd_seq = None
        for metric in self.metrics:
            if metric.name == "bdSeq":
                bd_seq = metric
                break

        if not bd_seq:
            raise ValueError("Not found bdSeq metric in list of metrics")

        topic = _TOPIC.format(group=group, ctype="NBIRTH", node_name=name)
        payload = NodePayload(timestamp, self.metrics, seq)
        if seq != 0:
            raise ValueError(f"Seq value has been used before!!!")
        await mqtt_client.publish(SpbMessage(topic, payload), qos=0, retain=False)


@dataclass
class NdeathContent(MessageContent):
    """Content of NDEATH message"""

    bd_seq: MetricDataTransfer
    reason: str = ""

    def __post_init__(self):
        if self.bd_seq.name != "bdSeq":
            raise ValueError(f'Name of metric should be "bdSeq"')

    async def send(
        self,
        mqtt_client: "MqttClient",
        timestamp: int,
        component_name: str,
        _: int | None = None,
    ):
        group, name = component_name.split("/")
        topic = _TOPIC.format(group=group, ctype="NDEATH", node_name=name)
        payload = NodePayload(timestamp, [self.bd_seq])
        await mqtt_client.publish(SpbMessage(topic, payload), qos=1, retain=False)


@dataclass
class NdataContent(MessageContent):
    """Content of NDATA message"""

    metrics: list[MetricDataTransfer]

    async def send(
        self,
        mqtt_client: "MqttClient",
        timestamp: int,
        component_name: str,
        seq: int | None = None,
    ):
        if seq is None:
            raise ValueError("Seq object shall be added")

        group, name = component_name.split("/")
        topic = _TOPIC.format(group=group, ctype="NDATA", node_name=name)
        payload = NodePayload(timestamp, self.metrics, seq)
        await mqtt_client.publish(SpbMessage(topic, payload), qos=0, retain=False)


@dataclass
class NcmdContent(MessageContent):
    """Content of NCMD message"""

    metrics: list[MetricDataTransfer]

    async def send(
        self,
        mqtt_client: "MqttClient",
        timestamp: int,
        component_name: str,
        _: int | None = None,
    ):
        group, name = component_name.split("/")
        topic = _TOPIC.format(group=group, ctype="NCMD", node_name=name)
        payload = NodePayload(
            timestamp,
            self.metrics,
        )
        await mqtt_client.publish(SpbMessage(topic, payload), qos=0, retain=False)


@dataclass
class StateContent(MessageContent):
    """Content of STATE message"""

    online: bool

    async def send(
        self,
        mqtt_client: "MqttClient",
        timestamp: int,
        component_name: str,
        _: int | None = None,
    ):
        topic = f"spBv1.0/STATE/{component_name}"
        await mqtt_client.publish(
            SpbMessage(topic, HostPayload(timestamp, self.online)), qos=1, retain=True
        )
