import logging
import os
from pathlib import Path
from typing import Dict, List, Type, Union
from urllib.parse import ParseResult, urlparse

import requests
from filip.models.ngsi_v2.context import ContextEntity
from filip.models.ngsi_v2.iot import Device

from fidere.meta import MANDATORY_DEVICE_FIELDS, MANDATORY_ENTITY_FIELDS
from fidere.models import DataModel, Identifier

_logger = logging.getLogger(__name__)


class JsonSchemaParser:
    """A class for managing the different JSONSchemaModels and
    instancing them"""

    def __init__(self):
        """Create the Parser using either the context manager option or
        the classic approach (see examples)"""

        # dict containing every datamodel that the parser manages
        self.__data_models: Dict[str, DataModel] = {}

    def __enter__(self) -> "JsonSchemaParser":
        """Entry point for context manager."""
        return self

    def __exit__(self, exc_type, exc_val, exc_tb) -> None:
        """Exit point for the context manager. Calls the __del__ method
        of every data model to run necessary possibly clean up jobs"""
        for identifier, model in self.__data_models.items():
            del model

    def __getitem__(self, item: Union[str, Identifier]):
        """Function to allow retrieval of JsonSchemaModels using ["..."] syntax"""
        try:
            return self.__data_models[item]
        # if not available raise error
        except KeyError as err:
            raise KeyError(f"No data model is available under {item}") from err

    def __delitem__(self, key):
        """Delete a previously stored/parsed Schema"""
        try:
            del self.__data_models[key]
        # if not available raise error
        except KeyError as err:
            raise KeyError(f"No data model is available under {key}") from err

    def get_identifiers(self) -> List[str]:
        """Function to return a list of all currently available identifiers"""
        return list(self.__data_models.keys())

    def parse_schema(
        self,
        schema: Union[str, Path, ParseResult],
        model_class: Type[DataModel],
        identifier: str = None,
        update: bool = False,
        **kwargs,
    ) -> Identifier:
        """Get the JSON schema (from url or file) and parse them to pydantic
        models/class objects.

        Args:
            schema (str, Path, ParseResult): Either an url or path where
                to schema is located model_class (Type[DataModel]):
            identifier (str, opional): a unique identifier used for
                accessing the parsed data model at a later point in time.
                If not defined the create_with_id_schema() classmethod
                of the model_class is called
            model_class (Type[DataModel]): A class object (not instanced)
                that will be used to instance
            update (bool, optional): if True, overwrite an existing parsed
                model with the given identifier
            **kwargs: Any kwargs will be passed to the underlying
                `DataModel.parse()`

        Returns:
            identifier (str, Identifier): the identifier used for later
                accessing the parsed data model
        """
        # when path is just a string check if it is a path or url
        if isinstance(schema, str):
            # check if url
            if schema.startswith("https://") or schema.startswith("http://"):
                schema = urlparse(schema)
                headers = (
                    kwargs["http_headers"]
                    if "http_headers" in kwargs
                    else None
                )
                try:
                    r = requests.get(schema.geturl(), headers=headers)
                except requests.ConnectionError as err:
                    raise ValueError("URL invalid") from err
                if not 200 <= r.status_code < 300:
                    raise ValueError(
                        f"Could not find URL. Status code: {r.status_code}"
                    )
            # check if it is an existing path
            elif os.path.exists(schema):
                schema = Path(schema)
            else:
                raise ValueError(
                    f"{schema} is neither a valid url "
                    "(starts with http:// or https://) "
                    "or an existing path"
                )
        _logger.debug(f"Creating object based on {model_class.__name__}")

        # create Instance DataModel or one of its derived classes
        if not issubclass(model_class, DataModel):
            raise ValueError(
                f"{model_class.__name__} does is not a valid data "
                "model class. It should inherit from "
                "fidere.models.base.DataModel"
            )

        # call the underlying API for extracting the identifier if not provided
        if not identifier:
            _logger.debug(
                "Identifier not supplied, trying to extract it from data model"
            )
            model = model_class.with_id_from_schema(
                schema_location=schema, **kwargs
            )
        else:
            model = model_class(
                schema_location=schema, identifier=identifier, **kwargs
            )

        if identifier in self.__data_models and not update:
            raise ValueError(
                f"Identifier {identifier} already used. Define update = True"
                "to overwrite the existing one or supply a unique identifier"
            )

        # call the API endpoints of the model class
        _logger.debug(f"Parsing data model at {schema}")
        model.parse(**kwargs)

        # save the parsed model into the dict and
        self.__data_models[model.identifier] = model

        return model.identifier

    def create_device(
        self, instance: dict, identifier: Union[Identifier, str], **kwargs
    ) -> Device:
        """Function to create a device instance based on a dictionary
        containing the model instance in key value format.

        Args:
            instance (dict): Dict in key value representation. Must
                contain valid values for fields 'device_id', 'entity_name', and
                'entity_type'!
            identifier (Identifier, str): identifier of a previously parsed model

        Returns:
            Device: A Device object to be used with `FiLiP`
        """
        # check for mandatory fields
        missing_fields = [
            attr for attr in MANDATORY_DEVICE_FIELDS if attr not in instance
        ]
        if missing_fields:
            raise KeyError(
                f"Fields {missing_fields} are missing from device instance"
            )

        # try getting the parsed model for the according type
        try:
            model = self.__data_models[identifier]
        # if not available raise error
        except KeyError as err:
            raise KeyError(
                f"No parsed schema is available for id {err}"
            ) from err

        device = model.instance_device(instance, **kwargs)

        self.validate_device(device, identifier)

        return device

    def create_context_entity(
        self, instance: dict, identifier: Union[Identifier, str], **kwargs
    ) -> ContextEntity:
        """Function to create a device instance based on a dictionary containing the model instance in key value format.

        Args:
            instance (dict): Dict in key value representation. Must contain valid values for fields 'id' and 'type'
            identifier (Identifier, str): identifier of a previously parsed model

        Returns:
            ContextEntity: A Entity object to be used with `FiLiP`
        """
        # check for mandatory fields
        missing_fields = [
            attr for attr in MANDATORY_ENTITY_FIELDS if attr not in instance
        ]
        if missing_fields:
            raise KeyError(
                f"Fields {missing_fields} are missing from entity instance."
            )

        # try getting the parsed model for the according type
        try:
            model = self[identifier]
        # if not available raise error
        except KeyError as err:
            raise KeyError(
                f"No datamodel is available for type {err}. Make sure its type (when parsed) is the same as "
                "the type defined in the instance."
            ) from err

        entity = model.instance_entity(instance)

        # make final check against the parsed data model for additional validation
        # like min, max and so on which are lost when parsing the pydantic class to a dict
        self.validate_entity(entity, identifier)

        # make instance a ContextEntity
        return entity

    def validate_entity(
        self, entity: ContextEntity, identifier: Union[Identifier, str]
    ) -> None:
        """Validate a ContextEntity from FiLiP with a certain schema.

        Args:
            entity (ContextEntity): entity to be verified
            identifier (Identifier, str): the id associated with the according data model

        Raises:
            Exception: raised when entity is no validated successfully
        """
        model = self[identifier]
        model.validate_entity(entity)

    def validate_device(
        self, device: Device, identifier: Union[Identifier, str]
    ) -> None:
        """Validate a Device from FiLiP with a certain schema.

        Args:
            device (Device): entity to be verified
            identifier (Identifier, str): the id associated with the according data model

        Raises:
            Exception: raised when entity is no validated successfully
        """
        model = self[identifier]
        model.validate_device(device)
