import importlib
import json
import os
import sys
import tempfile
from pathlib import Path
from types import ModuleType
from typing import Any, Dict, Literal, Union, get_args, get_origin
from urllib.error import HTTPError
from urllib.parse import ParseResult

import httpx
import typing_extensions
from datamodel_code_generator import DataModelType, InputFileType, generate
from pydantic.fields import FieldInfo


def parse_with_dmcg(
    input_: Union[Path, ParseResult], name: str, **kwargs
) -> ModuleType:
    """Parses a JSON-Schema with the `datamodel_code_generator` library.
    A temporary folder is created to allow imporing the generated pydantic
    model. The temporary folder is deleted before returning the module,
    since the module is now in memory!

    Args:
        input_ (Path, ParseResult): The location of the JSON-Schema. Either
            locally on the drive (Path) or as a parsed URL (ParseResult)
        name (str): Name the parsed pydantic class should use. The string
            is cast to UPPER letters

    Keyword Args:
        *: Anything that should be handed to the generate function. See the
            `documentation <https://github.com/koxudaxi/datamodel-code-generator>`
            of `datamodel_code_generator`

    Returns:
         module (ModuleType): Module containing the parsed pydantic data model
    """

    name = name.upper()
    with tempfile.TemporaryDirectory() as temp_dir:
        # make the temporary folder a module, by adding an __init__.py file
        with open(Path(temp_dir, "__init__.py"), "w"):
            pass

        sys.path.append(temp_dir)
        # TODO (sa.johnen): filter kwargs to only pass on allowed
        #  keywords (it"s not safe to assume the function gets passed
        #  only viable arguments for generate().

        # generate pydantic model
        generate(
            input_,
            class_name=f"{name}",
            output=Path(temp_dir, f"{name}.py"),
            output_model_type=DataModelType.PydanticV2BaseModel,
            use_subclass_enum=True,
            input_file_type=InputFileType.JsonSchema,
            **kwargs,
        )
        # import the whole module
        module = importlib.import_module(name)

        # remove the temp folder before leaving the context manager
        sys.path.remove(temp_dir)
    return module


def load_json_from_file_or_url(
    location: Union[Path, ParseResult], **kwargs
) -> dict:
    """Since json files (either instances or schemas) need to be
    accessed frequently, it is useful to have a dedicated function for
    loading these from url or local path!

    Args:
        location (Path, ParseResult): Either a "pathlib.Path" or
            "urllib.parse.ParseResult" containing the location of the json.

    Keyword Args:
        http_headers (dict[str, Any]): Headers that should be used for
            the possible HTTP(S) request

    Returns:
        dict: The parsed JSON as dict
    """
    if isinstance(location, Path):
        if os.path.exists(location):
            with open(location, "r") as file:
                content = json.load(file)
        else:
            raise ValueError(f"{location} does not exist")
    elif isinstance(location, ParseResult):
        headers = kwargs["http_headers"] if "http_headers" in kwargs else None
        res = httpx.get(location.geturl(), headers=headers)
        try:
            res.raise_for_status()
        except HTTPError as e:
            # Check if request was successful
            if not 200 <= res.status_code < 300:
                raise ValueError(
                    "Could not retrieve schema (the URL is likely "
                    "not  pointing to a JSON-Schema) or was redirected"
                ) from e
            else:
                raise
        content = res.json()
    else:
        raise TypeError(
            "location of content is neither a Path (pathlib.Path) "
            "nor a url (urllib.parse.ParseResult)"
        )
    return content


def entity_instance_to_device_instance(
    instance: Dict[str, Any], add_fields: Dict[str, Any] = None
) -> dict:
    """A very rudimentary implementation of a function to convert from
    an entity instance to a device instance. The field "id" is used as
    "entity_name" and "device_id' and 'type' is used for 'entity_type'.
    Additional information like the apikey or service info is supplied
    via the add_fields argument

    Args:
        instance (dict): The entity instance to be used as basis
        add_fields (dict): A dictionary of additional fields
    Returns:
        dict: instance that should be a viable
    """
    # convert entity id and type fields to their according
    # TODO (sa.johnen): This function might need to be updated at some point
    #  again so make sure the add_fields only contain valid things
    temp = instance.copy()
    temp["device_id"] = temp.pop("id")
    temp["entity_name"] = temp["device_id"]
    temp["entity_type"] = temp.pop("type")

    if add_fields:
        temp.update(add_fields)
    return temp


def get_field_type(field: FieldInfo) -> Any:
    """Get the type of a specific field and return the class object of it"""
    origin = get_origin(field.annotation)
    # field is annotated with just one type
    if origin is None:
        return field.annotation
    # field is Optional, which is just Union[Any, NoneType]. In this
    # specific case
    if origin is Union:
        return get_args(field.annotation)[0]
    return origin


def resolve_field(field: FieldInfo):
    """Check if field is another pydantic model. If yes recursively
    call `resolve_model`"""
    type_ = get_field_type(field)

    # check if type of field is not a whole other model
    if not hasattr(type_, "model_fields"):
        return field.default

    # if a separate class was created for things, use the entry of the root field
    if "root" in type_.model_fields and len(type_.model_fields) == 1:
        field = type_.model_fields["root"]

    # if the field is a Literal return the according value
    if get_origin(field.annotation) in (Literal, typing_extensions.Literal):
        return get_args(field.annotation)[0]

    return resolve_model(type_)


def resolve_model(model):
    """Get all fields of a pydantic model and call `resolve_field` for each."""
    return {
        fname: resolve_field(field)
        for fname, field in model.model_fields.items()
    }
