import json
import logging
import random
import string
from types import ModuleType
from typing import Any, Optional

from filip.models.ngsi_v2.context import ContextAttribute, ContextEntity
from filip.models.ngsi_v2.iot import (
    Device,
    DeviceAttribute,
    DeviceCommand,
    StaticDeviceAttribute,
)
from pydantic import Field, ValidationError

from fidere.meta import (
    KEYVALUE_DEVICE_FIELDS,
    KEYVALUE_ENITITY_FIELDS,
    DataType,
    DeviceAttributeType,
)
from fidere.models.base import DataModel
from fidere.util import get_field_type, parse_with_dmcg, resolve_field

_logger = logging.getLogger(__name__)


class NormalizedModel(DataModel):
    """Dataclass to handle to JSON-Schemas and the general functionality
    around it"""

    module: Optional[ModuleType] = Field(None)
    pydantic_class: Any = Field(None)

    def parse(self, **generate_args):
        # once the model is created and imported using importlib, it is
        # in memory and the source file/folder can be deleted!
        name = "".join(random.sample(string.ascii_uppercase, 20))
        self.module = parse_with_dmcg(
            self.schema_location, name, **generate_args
        )
        self.pydantic_class = self.module.__getattribute__(name)

    def instance_entity(self, instance: dict, **kwargs) -> ContextEntity:
        """Function to create an entity instance, based on the parsed schema!

        Args:
            instance (dict): Dict in key value representation.

        Keyword Args:
            omit_none (bool): omits any attributes in the resulting entity
                which value is None

        Returns:
            entity (ContextEntity): A context entity based on the parsed
                JSON-Schema and instance
        """
        # parse kwargs
        omit_none = kwargs["omit_none"] if "omit_none" in kwargs else False

        temp = instance.copy()
        # extract the entity settings (key-value fields,
        # namely 'id' and 'type')
        settings = {
            key: temp.pop(key)
            for key in instance
            if key in KEYVALUE_ENITITY_FIELDS
        }
        entity = ContextEntity(**settings)

        for name, field in self.pydantic_class.model_fields.items():
            # if the field is already in the entity (only id or type), continue
            if hasattr(entity, name):
                continue
            # resolve field recursively (convert to a dict)
            attr_dict = resolve_field(field)
            if name in temp:
                attr_dict["value"] = temp.pop(name)
                type_ = get_field_type(field)
                # validate the field against the parsed JSON-Schema
                type_(**attr_dict)
            # if value is None, and they are to be omitted,
            # do not add the attribute
            if omit_none and attr_dict["value"] is None:
                continue
            # finally add the attribute
            entity.add_attributes({name: ContextAttribute(**attr_dict)})

        # if the copied instance has keys left at this point, they were
        # not defined in the schema raise an error accordingly
        if temp:
            raise ValueError(
                "instance did supply values for unknown "
                f"field {list(temp.keys())}"
            )
        return entity

    def instance_device(self, instance: dict, **kwargs) -> Device:
        """Function to create a device instance, based on the parsed schema!

        Args:
            instance (dict): Dict in key value representation.

        Keyword Args:
            omit_none (bool): omits any static attributes in the resulting
                device which value is None
            attr_types (dict[str, str]): A dictionary containing which
                attribute is what kind of attribute on the IoT-Agent. Keys
                are the names of ALL attributes, values is one of
                [StaticDeviceAttribute, DeviceAttribute, DeviceCommand,
                LazyDeviceAttribute]

        Returns:
            entity (ContextEntity): A Device based on the parsed JSON-Schema
                and instance
        """
        # parse kwargs
        omit_none = kwargs.pop("omit_none") if "omit_none" in kwargs else False
        attr_types = (
            kwargs.pop("attr_types") if "attr_types" in kwargs else None
        )

        temp = instance.copy()
        # extract the entity settings (key-value fields, like 'entity_type',
        # 'device_id' and so on)
        settings = {
            key: temp.pop(key)
            for key in instance
            if key in KEYVALUE_DEVICE_FIELDS
        }
        device = Device(**settings)

        for name, field in self.pydantic_class.model_fields.items():
            # do not add fields as attributes that are unique to ContextEntity
            # or already in the device
            if hasattr(device, name) or name in KEYVALUE_ENITITY_FIELDS:
                continue

            # resolve field recursively (convert to a dict)
            attr_dict = resolve_field(field)
            if name in temp:
                attr_dict["value"] = temp.pop(name)

            # extract the kind of attribute from the data model
            if attr_types is not None:
                dev_attr_type = attr_types[name]
            elif "iotAttributeType" in attr_dict["metadata"]:
                dev_attr_type = attr_dict["metadata"]["iotAttributeType"][
                    "value"
                ]
            else:
                raise AttributeError(
                    f"Attribute {name} has no kind of attribute defined."
                    f"It is neither in attr_types or as 'iotAttributeType' "
                    f"in metadata of the schema"
                )

            # create a common DeviceAttribute
            if dev_attr_type == DeviceAttributeType.ATTRIBUTE:
                attribute = self.__create_device_attribute(attr_dict, name)

            # create a common DeviceAttribute
            elif dev_attr_type == DeviceAttributeType.STATIC:
                # omitting none values is only relevant for static attributes
                if omit_none and attr_dict["value"] is None:
                    continue
                attribute = self.__create_static_device_attribute(
                    attr_dict, name
                )

            elif dev_attr_type == DeviceAttributeType.COMMAND:
                attribute = self.__create_device_command(attr_dict, name)

            elif dev_attr_type == DeviceAttributeType.LAZY:
                raise Exception(
                    "LazyDeviceAttributes are not supported by "
                    f"{self.__repr_name__()}"
                )

            else:
                raise ValueError(
                    f"Attribute type {dev_attr_type} is not a valid attribute "
                    "type. Use one of [Attribute, StaticAttribute, Command, "
                    "LazyAttribute]"
                )

            device.add_attribute(attribute)

        # if the copied instance has keys left at this point, they were
        # not defined in the schema raise an error accordingly
        if temp:
            raise ValueError(
                f"instance did supply values for unknown "
                f"field {list(temp.keys())}"
            )

        return device

    def validate_entity(self, entity: ContextEntity) -> None:
        # Validation is trivial since the data model is basically a
        # ContextEntity
        self.pydantic_class(**entity.model_dump())

    def validate_device(self, device: Device) -> None:
        temp = {"id": device.entity_name, "type": device.entity_type}
        self.validate_entity(ContextEntity.model_validate(temp))
        # since device_id is not equal to entity_name, just raise a warning
        try:
            id_ = get_field_type(self.pydantic_class.model_fields["id"])
            _ = id_(device.device_id)
        except ValidationError:
            _logger.warning(
                "Could not validate device_id " "against id in schema"
            )

        # only static device attributes can be validated since they are
        # the only ones carrying values
        attrs = device.static_attributes
        for attr in attrs:
            type_ = get_field_type(self.pydantic_class.model_fields[attr.name])
            vals = attr.model_dump()
            type_(**{key: vals[key] for key in ["type", "metadata", "value"]})

    @staticmethod
    def __create_device_command(attr_dict, name):
        if attr_dict["value"] is not None:
            _logger.warning(
                "Value for DeviceCommand is ignored! "
                "DeviceCommand does not support assigning values "
                "during provisioning!"
            )
        return DeviceCommand(name=name, type=DataType.COMMAND)

    @staticmethod
    def __create_static_device_attribute(attr_dict, name):
        if attr_dict["value"] is None:
            _logger.warning("A StaticDeviceAttribute value should be filled!")
        # Check if the value is serializable
        try:
            json.dumps(attr_dict["value"])
        except ValueError as err:
            raise ValueError(
                f"Value of {name} is not JSON serializable. This is likely due"
                " to it being a StructuredValue with mandatory fields inside"
                " of this StructuredValue that are not supplied"
            ) from err
        return StaticDeviceAttribute(name=name, **attr_dict)

    @staticmethod
    def __create_device_attribute(attr_dict, name):
        if attr_dict["value"] is not None:
            _logger.warning(
                f"Value for DeviceAttribute {name} is ignored! "
                "DeviceAttribute does not support assigning "
                "values during provisioning!"
            )
            attr_dict.pop("value")
        obj_id = None
        if "object_id" in attr_dict["metadata"]:
            obj_id = attr_dict["metadata"]["object_id"]["value"]
        return DeviceAttribute(name=name, object_id=obj_id, **attr_dict)
