from dataclasses import asdict, dataclass, field
from enum import Enum
from typing import Any, Mapping, Self, TypeAlias


class Quality(Enum):
    BAD = 0
    GOOD = 192
    STALE = 500


ValueType: TypeAlias = bool | int | float | str | None


class DataType(Enum):
    Unknown = 0
    Int8 = 1
    Int16 = 2
    Int32 = 3
    Int64 = 4
    UInt8 = 5
    UInt16 = 6
    UInt32 = 7
    UInt64 = 8
    Float = 9
    Double = 10
    Boolean = 11
    String = 12
    DateTime = 13
    Text = 14
    UUID = 15
    DataSet = 16
    Bytes = 17
    File = 18
    Template = 19
    PropertySet = 20
    PropertySetList = 21
    Int8Array = 22
    Int16Array = 23
    Int32Array = 24
    Int64Array = 2
    UInt8Array = 26
    UInt16Array = 27
    UInt32Array = 28
    UInt64Array = 29
    FloatArray = 30
    DoubleArray = 31
    BooleanArray = 32
    StringArray = 33
    DateTimeArray = 34

    @classmethod
    def for_(cls, value: ValueType) -> Self:
        v_type = type(value)
        if v_type not in _DEFAULT_DTs:
            raise ValueError(f"There is no default for type {v_type}")
        return _DEFAULT_DTs[v_type]

    def convert_value(self, str_value: str):
        if "Int" in self.name:
            return int(str_value)

        if "Float" in self.name or "Double" in self.name:
            return float(str_value)

        if "Boolean":
            return str_value == True

        if str_value == "None":
            return

        if "Bytes" == self.name:
            return str_value.encode()

        return str_value


_DEFAULT_DTs = {
    int: DataType.Int64,
    float: DataType.Float,
    bool: DataType.Boolean,
    str: DataType.String,
    bytes: DataType.Bytes,
}


@dataclass(init=False)
class PropertyValue:
    value: ValueType
    data_type: DataType

    def __init__(self, value: ValueType, data_type: DataType | None = None):
        if data_type is None:
            data_type = DataType.for_(value)
        self.value = value
        self.data_type = data_type

    def to_dict(self) -> dict[str, ValueType]:
        return {"value": self.value, "dataType": self.data_type.name}

    @classmethod
    def from_dict(cls, value_map: dict[str, Any]) -> Self:
        return cls(value_map["value"], DataType[value_map["dataType"]])

    @classmethod
    def from_value(cls, value: ValueType) -> Self:
        value_type = type(value)
        if value_type not in cls._DEFAULTS:
            raise ValueError(f"Not default for type {type(value)}")

        datatype = cls._DEFAULTS[value_type]

        return cls(value, datatype)


@dataclass(init=False)
class QualityValue(PropertyValue):
    """Special property value which is quality of metric reading"""

    def __init__(self, quality: Quality):
        if type(quality) is not Quality:
            raise TypeError(f"Quality value should be a Quality Enum")
        super().__init__(quality.value, DataType.Int32)


@dataclass(init=False)
class PropertySet:
    """Map or properties"""

    keys: tuple[str, ...] = field(default_factory=tuple)
    values: tuple[PropertyValue, ...] = field(default_factory=tuple)

    def __init__(self, **kwargs):
        self.keys = tuple(sorted(kwargs.keys()))
        self.values = tuple([kwargs[key] for key in self.keys])

    def compare(self, other: Self):
        ...

    def __contains__(self, key: str) -> bool:
        return key in self.keys

    def __getitem__(self, key: str) -> PropertyValue:
        try:
            index = self.keys.index(key)
        except ValueError:
            raise KeyError(f"Key {key} not found in properties")

        return self.values[index]

    def __bool__(self) -> bool:
        return bool(self.keys)

    @classmethod
    def from_dict(cls, mapping: dict[str, dict[str, Any]]) -> Self:
        """Construct object from a dict"""
        return cls(
            **{key: PropertyValue.from_dict(value) for key, value in mapping.items()}
        )

    def to_dict(self) -> dict[str, dict[str, Any]]:
        """Convert object to a dict"""
        return {key: value.to_dict() for key, value in zip(self.keys, self.values)}


@dataclass
class Metric:
    """Metric owned by a edge node or device"""

    name: str
    datatype: DataType
    properties: PropertySet = field(default_factory=PropertySet)
    alias: int = 0
    is_transient: bool = False

    # @classmethod
    # def from_dict(cls, metric_map: dict[str, Any]):
    #     properties = {
    #         key: PropertyValue(value)
    #         for key, value in metric_map.get("properties", {}).items()
    #     }

    #     metric_value = None if value.get("is_null") else value.get("value")
    #     return cls(
    #         value.get("name", ""),
    #         value["timestamp"],
    #         metric_value,
    #         DataType[value["dataType"]],
    #         properties,
    #         value.get("alias", None),
    #         value.get("is_transient", False),
    #     )

    # def to_dict(self):
    #     result = {
    #         "name": self.name,
    #         "datatype": self.data_type.name,
    #     }

    #     if self.is_transient:
    #         result["is_transient"] = True

    #     if self.alias:
    #         result["alias"] = self.alias

    #     if properties:
    #         result["properties"] = {key: value.to_dict() for self.properties.to_dict().items()}

    #     return result

    # def compare(self, metric: Self) -> MetricChange | None:
    #     if metric.name != self.name:
    #         raise ValueError("Metrics has diferent names!!")

    #     if set(metric.properties.keys()) != set(self.properties.keys()):
    #         raise ValueError("Properties definitions has changed")

    #     if metric.timestamp <= self.timestamp:
    #         return

    #     if metric.value == self.value and metric.properties == self.properties:
    #         return

    #     properties = {}
    #     for key, value in metric.properties.items():
    #         if self.properties[key] != value:
    #             properties[key] = value

    #     alias = self.alias
    #     name = "" if alias else self.name
    #     return MetricChange(
    #         metric.timestamp, metric.value, self.data_type, alias, name, properties
    #     )


@dataclass
class Sample:
    timestamp: int
    value: ValueType
    quality: Quality

    def has_changed(self, other: Self) -> bool:
        """Compare two samples by value and quality"""
        return self.value != other.value or self.quality != other.quality


@dataclass
class MetricChange:
    """Data transfer object to publish with data messages"""

    timestamp: int
    value: ValueType
    datatype: DataType
    quality: Quality
    metric_name: str
    alias: int = 0
    has_changed_quality: bool = False
    is_historical: bool = False

    def update_from_sample(self, sample: Sample) -> Self | None:
        """Update current metric change from the sample"""
        if self.timestamp > sample.timestamp:
            raise ValueError("Sample time is before metric change")

        if sample.quality == self.quality and sample.value == self.value:
            return

        has_changed_quality = sample.quality != self.quality

        return MetricChange(
            sample.timestamp,
            sample.value,
            self.datatype,
            sample.quality,
            self.metric_name,
            self.alias,
            has_changed_quality,
        )


@dataclass
class MetricBirth:
    """Data transfer to send with birth certificates"""

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

    @classmethod
    def from_metric(cls, metric: Metric, sample: Sample) -> Self:
        properties = metric.properties.to_dict()

        if sample.quality and "Properties/" not in metric.name:
            properties["quality"] = {"value": sample.quality.value, "dataType": "Int32"}

        return cls(
            metric.name,
            sample.value,
            metric.datatype,
            sample.timestamp,
            PropertySet.from_dict(properties),
            metric.alias,
            metric.is_transient,
        )

    def create_metric(self) -> Metric:
        """Create metric from its birth"""
        return Metric(
            self.name, self.datatype, self.properties, self.alias, self.is_transient
        )

    def create_sample(self) -> Sample:
        """Create sample from its metric birth"""
        quality_value = self.properties.to_dict().get(
            "quality", QualityValue(Quality.GOOD)
        )
        return Sample(self.timestamp, self.value, quality_value.value)

    def extract_change(self) -> MetricChange:
        quality = (
            Quality(self.properties["quality"].value)
            if "quality" in self.properties
            else Quality.GOOD
        )
        return MetricChange(
            self.timestamp,
            self.value,
            self.datatype,
            quality,
            self.name,
            self.alias,
        )


@dataclass(frozen=True)
class MetricChangeRequest:
    timestamp: int
    value: ValueType
    data_type: DataType
    metric_name: str = ""
    alias: int = 0

    def to_dict(self):
        result = {
            "timestamp": self.timestamp,
            "value": self.value,
            "dataType": self.data_type.name,
        }
        if self.alias is not None:
            result["alias"] = self.alias
        else:
            result["name"] = self.metric_name
        return result

    @classmethod
    def from_dict(cls, payload: dict[str, Any]) -> Self:
        return cls(
            payload["timestamp"],
            payload["value"],
            DataType[payload["dataType"]],
            payload.get("name", ""),
            payload.get("alias", 0),
        )
