import os
import typing
from dataclasses import MISSING, Field, fields, is_dataclass
from enum import Enum
from typing import Any, Dict, Optional, Union

from risc_lasagna.typedef import ConfigurationError


def print_dict_like_list(
    map_: Dict[str, Any], indent: int = 4, title: Optional[str] = None
):
    """
    Prints a dict in a list like form (recursively).
    :param map_: The dict
    :param indent: How much intent to add before keys
    :param title: A title which is added at the beginning
    :return: A string representing the dict
    # >>> print_dict_like_list({'A': 'a', 'B': 123, 'C': [1,2,3]})
    # '    A=\n    B=123\n    C=[1, 2, 3]\n'
    # >>> print_dict_like_list({'A': 'a'}, title='test')
    # 'test\n    A=a\n'
    # >>> print_dict_like_list({'a': 'a'})
    # 'a=a'
    """
    if title is not None:
        repr_ = f"{title}:{os.linesep}"
    else:
        repr_ = ""

    for key in map_:
        # recursively print the inner dicts
        if isinstance(map_[key], dict):
            repr_ = f'{repr_}{" " * indent}{key}:{os.linesep}{print_dict_like_list(map_[key], indent=indent * 2)}'
            continue
        # print the the key
        repr_ = f'{repr_}{" " * indent}{key}={map_[key]}{os.linesep}'
    return repr_


def normalize_variable_prefix(prefix: str) -> str:
    """
    Normalizes the prefix to contain an '_' at the end
    :param prefix: The prefix which should be normalized
    :return: The prefix which ends in '_' or an empty prefix
    >>> normalize_variable_prefix('')
    ''
    >>> normalize_variable_prefix('PREFIX')
    'PREFIX_'
    >>> normalize_variable_prefix('PREFIX_')
    'PREFIX_'
    """
    if prefix == "":
        return prefix
    return prefix if prefix.endswith("_") else f"{prefix}_"


def prepend_variable_prefix(prefix: str, variable: str) -> str:
    """
    Appends the prefix to a string variable if it does not already start with it
    :param prefix:
    :param variable:
    :return:
    >>> prepend_variable_prefix('TEST_', 'VAR')
    'TEST_VAR'
    >>> prepend_variable_prefix('TEST_', 'TEST_VAR')
    'TEST_VAR'
    >>> prepend_variable_prefix('', 'VAR')
    'VAR'
    >>> prepend_variable_prefix('', '')
    ''
    """
    if variable.startswith(prefix):
        return variable
    return f"{prefix}{variable}"


class ConfigStyle(Enum):
    DOTENV = "DOTENV"
    YAML = "YAML"


def print_expected_variables_names(
    prefix: str, data_class, style: ConfigStyle = ConfigStyle.DOTENV
):
    if not is_dataclass(data_class):
        raise ConfigurationError(
            f"data_class needs to be a data class! It is a {type(data_class)}"
        )

    if style == ConfigStyle.DOTENV:
        field: Field
        prefix = normalize_variable_prefix(prefix)
        for field in fields(data_class):
            if is_dataclass(field.type):
                # recursively parse the inner dataclass, flattens it out
                print_expected_variables_names(
                    f"{prefix}{field.name.upper()}", field.type
                )
                continue
            if is_list(field.type):
                raise ConfigurationError("Lists are not supported in dot env style")

            # add defaults to output
            if field.default is MISSING:
                default = f"<{field.type.__name__}>"
            else:
                default = field.default

            print(f"{prefix}{field.name.upper()}={default}")
    elif style == ConfigStyle.YAML:
        raise ConfigurationError("Printing yaml style is currently not supported!")


def is_optional(type_hint):
    """
    Checks if a given type is typing.Optional
    >>> from typing import List
    >>> is_optional(Optional[str])
    True
    >>> is_optional(Optional[List[str]])
    True
    >>> is_optional(List[str])
    False
    >>> is_optional(str)
    False
    """
    return typing.get_origin(type_hint) is Union and type(None) in typing.get_args(
        type_hint
    )


def is_optional_list(type_hint) -> bool:
    """
    Checks if a given type hint is an optional list.
    >>> from typing import List
    >>> is_optional_list(List[str])
    False
    >>> is_optional_list(Optional[List[int]])
    True
    >>> is_optional_list(Optional[List])
    True
    """
    return is_optional(type_hint) and list in [
        typing.get_origin(arg) for arg in typing.get_args(type_hint)
    ]


def is_list(type_hint) -> bool:
    """
    Checks if a given type hint is a list. Does not matter if it is optional or the builtin list type.
    >>> from typing import List
    >>> is_list(list)
    True
    >>> is_list(List)
    True
    >>> is_list(List[str])
    True
    >>> is_list(Optional[List])
    True
    >>> is_list(Optional[List[str]])
    True
    >>> is_list(Optional[str])
    False
    """
    return (
        type_hint == list
        or is_optional_list(type_hint)
        or typing.get_origin(type_hint) == list
    )
