import dataclasses
import typing
from dataclasses import MISSING, Field, fields, is_dataclass
from typing import Any, List, Union

from .helper import is_list, is_optional, normalize_variable_prefix
from .layer import AggregationLayer, DictLayer
from .layer.typedef import Layer


def parse_value(type_: type, value: Any):
    """
    Parse values which should be simple types. Handles some edge caes (actually parses bool 'f' as false, ...)
    :param type_: The type to which it is parsed to
    :param value: The value (likely string)
    :return: The parsed value
    """
    # edge cases for boolean parsing
    if type_ == bool and value in ["f", "F", "False", "false", "", 0, "0", "None"]:
        return False

    # trust that complex types are initiated correctly
    if (
        typing.get_origin(type_) is typing.Union
        or typing.get_origin(type_) is typing.Optional
    ):
        return value

    # in case it is not an object but a class (for class methods, grouping) we do not
    # need to instantiate it
    if typing.get_origin(type_) is typing.get_origin(typing.Type):
        return value

    # default
    return type_(value)


def build(data_class, layers: Union[List[Layer], Layer], variable_prefix: str = ""):
    """
    Parse config from a given layer into a data class
    :param data_class: The data class that should be created
    :param layers: A layer (or list of layer) which contains the necessary values
    :param variable_prefix: A prefix which ALL variables have
    :raise ValueError: If parameter data_class is not a data class according to the dataclasses.is_dataclass function
    :raise ValueError: If required field in the data_class is not set in the layer
    :raise ValueError: If the annotated type in the data_class can not be used to parse the provided data from the layer
    :return: The parsed data class from the layer
    """
    if not is_dataclass(data_class):
        raise ValueError("Please provide a dataclass that represents the config")

    # convert list of layer to aggregation layer
    if isinstance(layers, list):
        layers = AggregationLayer(layers)

    # build dict for constructor
    dict_for_constructor = {}
    data_field: Field
    # read data from layer and construct dict for data class constructor
    for data_field in fields(data_class):
        # build the name in the environment (with prefix, ...)
        name_in_environment = (
            f"{normalize_variable_prefix(variable_prefix)}{data_field.name}"
        )

        # extract type from optional wrapper
        suggested_type = data_field.type
        if is_optional(data_field.type):
            suggested_type = typing.get_args(data_field.type)[0]

        # parse nested dataclasses
        if is_dataclass(suggested_type):
            parsed_nested = build(
                suggested_type, layers, variable_prefix=name_in_environment
            )
            dict_for_constructor = {
                **dict_for_constructor,
                **{data_field.name: parsed_nested},
            }
        else:
            # value is missing, which is fine. When the value is required (no default set) the constructor
            # throws an error
            if not layers.contains(name_in_environment):
                if data_field.default is MISSING:
                    raise ValueError(
                        f"The required attribute '{data_field.name}' is not set, "
                        f"expected the variable '{name_in_environment}'"
                    )

            # parse the value using the type information in the data field
            value = layers.get(name_in_environment)
            if value is None:
                value = data_field.default

            if value is not None and value is not MISSING:
                try:
                    if is_list(suggested_type):
                        # get type args of list, can be a primitive, a dataclass or optional
                        args = typing.get_args(suggested_type)

                        # no type specified -> we can only try to guess it later
                        suggested_type = None
                        if len(args) > 0:
                            suggested_type = args[0]

                        values = []
                        for element in layers.get(name_in_environment):
                            # recursively parse nested class
                            if is_dataclass(suggested_type or type(element)):
                                # parse default data classes (if nested into config in some way)
                                if is_dataclass(element):
                                    element = dataclasses.asdict(element)

                                parsed_nested = build(
                                    suggested_type,
                                    DictLayer(element, capitalize_keys=False),
                                )
                                values.append(parsed_nested)
                            else:
                                # try to parse value from inferred type
                                values.append(parse_value(type(element), element))

                        value = values
                    else:
                        value = parse_value(suggested_type, value)
                except ValueError as error:
                    raise ValueError(
                        f"Could not parse data field '{data_field.name}' of data class '{data_class.__name__}' "
                        f"(full name in layer: '{name_in_environment}'). Please check if the value or "
                        f"type of the data field is set correctly."
                    ) from error

            # in case the given variable is not in the environment the value is of type MISSING
            # if the value is not set in other layer (in case of an aggregation layer) the constructor should
            # fail!
            if value is not MISSING:
                dict_for_constructor = {
                    **dict_for_constructor,
                    **{data_field.name: value},
                }

    return data_class(**dict_for_constructor)
