import argparse
import dataclasses
import inspect
from argparse import _ActionsContainer
from logging import getLogger
from typing import Any, Dict, List, Optional, Set, Tuple, Type, Union

from .. import utils
from . import docstring
from .wrapper import Wrapper

logger = getLogger(__name__)


class FieldWrapper(Wrapper[dataclasses.Field]):
    """
    FieldWrapper represents a single "atomic" field of a dataclass. That is, a field
    that does not have any subfields (that we attempt to parse).
    The FieldWrapper is essentially a reified call to
     the `add_argument(*option_strings, **arg_options)` function of the
    `argparse._ArgumentGroup`. Typically this is called by DataclassWrapper.

    The `option_strings`, `required`, `help`, `default`, etc.
    attributes just autogenerate the argument of the same name of the
    above-mentioned `add_argument` function. The `arg_options` attribute fills
    in the rest and may overwrite these values, depending on the type of field.

    The `field` argument is the actually wrapped `dataclasses.Field` instance.
    """

    @property
    def field(self) -> dataclasses.Field:
        return self._field

    def __init__(
        self,
        field: dataclasses.Field,
        parent: Optional[Wrapper] = None,
        preferred_help: str = docstring.HelpOrder.inline,
    ):
        self._field = field
        self._parent: Optional[Wrapper] = parent
        # Holders used to 'cache' the properties.
        # (could've used cached_property with Python 3.8).
        self._option_strings: Optional[Set[str]] = None
        self._required: Optional[bool] = None
        self._docstring: docstring.AttributeDocString = docstring.AttributeDocString()
        self._help: Optional[str] = None
        self._default: Optional[Union[Any, List[Any]]] = None
        self._dest: Optional[str] = None
        # the argparse-related options:
        self._arg_options: Dict[str, Any] = {}
        self._type: Optional[Type[Any]] = None

        # preferred parse for docstring / help text in < inline | above | below >
        self.preferred_help = preferred_help

        # stores the resulting values for each of the destination attributes.
        self._results: Dict[str, Any] = {}

    @property
    def arg_options(self) -> Dict[str, Any]:
        """Dictionary of values to be passed to the `add_argument` method.

        The main feature of this package is to infer these arguments
        automatically using features of the built-in `dataclasses` package, as
        well as Python's type annotations.

        By passing additional keyword arguments to the `field()`
        function, the autogenerated arguments can be overwritten,
        giving access to all of the usual argparse features know and love.

        NOTE: When passing an `action` keyword argument, we remove all the
        autogenerated options that aren't required by the Action class
        constructor.
        For example, when specifying a custom `action` like "store_true" or
        "store_false", the `type` argument autogenerated here shouldn't be
        passed to the constructor of the `argparse._StoreFalseAction`, so we
        discard it.
        """
        if self._arg_options:
            return self._arg_options
        # get the auto-generated options.
        options = self.get_arg_options()
        # overwrite the auto-generated options with given ones, if any.
        options.update(self.custom_arg_options)
        # only keep the arguments used by the Action constructor.
        action = options.get("action", "store")
        self._arg_options = only_keep_action_args(options, action)
        return self._arg_options

    def get_arg_options(self) -> Dict[str, Any]:
        """Create the `parser.add_arguments` kwargs for this field."""
        if not self.field.init:
            return {}
        # TODO: Refactor this:
        # 1. Create a `get_argparse_options_for_field` function
        # 2. Use `get_argparse_options_for_annotation` below as part of that function
        # 3. Update the dict returned from 1. with values set in the field() function
        # 4. Update the dict from 3. with the values set by the DataclassWrapper, or
        # when this field is reused. (are they ever modified externally?)
        # 5. Return that dictionary.
        _arg_options: Dict[str, Any] = {}

        _arg_options["required"] = False  # Required arguments can also be set from yaml,
        # so do not enforce with argparse
        _arg_options["dest"] = self.dest
        _arg_options["default"] = self.default

        if self.help:
            _arg_options["help"] = self.help
        elif self.default is not None:
            # issue 64: Need to add an empty 'help' string, so that the formatter
            # automatically adds the (default: '123')
            _arg_options["help"] = " "

        tpe = self.type

        tpe = utils.canonicalize_union(tpe)

        _arg_options["type"] = tpe
        try:
            _arg_options["type"].__name__ = self.type.__repr__().replace("typing.", "")
        except Exception:
            # Only to prettify printing, if fails just continue
            pass

        return _arg_options

    @property
    def action(self) -> Union[str, Type[argparse.Action]]:
        """The `action` argument to be passed to `add_argument(...)`."""
        return self.custom_arg_options.get("action", "store")

    @property
    def action_str(self) -> str:
        if isinstance(self.action, str):
            return self.action
        return self.action.__name__

    @property
    def custom_arg_options(self) -> Dict[str, Any]:
        """Custom argparse options that overwrite those in `arg_options`.

        Can be set by using the `field` function, passing in a keyword argument
        that would usually be passed to the parser.add_argument(
        *option_strings, **kwargs) method.
        """
        return self.field.metadata.get("custom_args", {})

    @property
    def option_strings(self) -> List[str]:
        """Generates the `option_strings` argument to the `add_argument` call.

        `parser.add_argument(*name_or_flags, **arg_options)`

        ## Notes:
        - Additional names for the same argument can be added via the `field`
        function.
        - Whenever the name of an attribute includes underscores ("_"), the same
        argument can be passed by using dashes ("-") instead. This also includes
        aliases.
        - If an alias contained leading dashes, either single or double, the
        same number of dashes will be used

        For an illustration of this, see the aliases example.

        """

        dashes: List[str] = []  # contains the leading dashes.
        options: List[str] = []  # contains the name following the dashes.

        # Currently create only a single option name, no support for aliases
        dashes.append("--")
        options.append(self.dest)

        # remove duplicates by creating a set.
        option_strings = set(f"{dash}{option}" for dash, option in zip(dashes, options))
        return list(sorted(option_strings, key=len))

    @property
    def nargs(self):
        return self.custom_arg_options.get("nargs", None)

    @property
    def default(self) -> Any:
        """Either a single default value, when parsing a single argument, or
        the list of default values, when this argument is reused multiple times
        (which only happens with the `ConflictResolution.ALWAYS_MERGE` option).

        In order of increasing priority, this could either be:
        1. The default attribute of the field
        2. the value of the corresponding attribute on the parent,
        if it has a default value
        """
        if self._default is not None:
            return self._default

        default: Any = utils.default_value(self.field)

        if default is dataclasses.MISSING:
            default = None

        self._default = default
        return self._default

    @default.setter
    def default(self, value: Any):
        self._default = value

    @property
    def required(self) -> bool:
        if self._required is not None:
            return self._required

        if self.action_str.startswith("store_"):
            # all the store_* actions do not require a value.
            self._required = False
        elif self.is_optional:
            self._required = False
        elif self.parent.required:
            # if the parent dataclass is required, then this attribute is too.
            # TODO: does that make sense though?
            self._required = True

        elif self.nargs in {"?", "*"}:
            self._required = False
        elif self.nargs == "+":
            self._required = True

        elif self.default is None:
            self._required = True
        else:
            self._required = False
        return self._required

    @required.setter
    def required(self, value: bool):
        self._required = value

    @property
    def type(self) -> Type[Any]:
        """Returns the wrapped field's type annotation."""
        if self._type is None:
            self._type = self.field.type

        return self._type

    def __str__(self):
        return f"""<FieldWrapper for field '{self.dest}'>"""

    @property
    def help(self) -> Optional[str]:
        if self._help:
            return self._help
        try:
            self._docstring = docstring.get_attribute_docstring(self.parent.dataclass, self.field.name)
        except (SystemExit, Exception) as e:
            logger.debug(f"Couldn't find attribute docstring for field {self.name}, {e}")
            self._docstring = docstring.AttributeDocString()

        help_text = docstring.get_preferred_help_text(self._docstring, preferred_help=self.preferred_help)
        if help_text is not None:
            self._help = help_text
        return self._help

    @help.setter
    def help(self, value: str):
        self._help = value

    @property
    def name(self) -> str:
        return self.field.name

    @property
    def is_list(self):
        return utils.is_list(self.type)

    @property
    def is_enum(self) -> bool:
        return utils.is_enum(self.type)

    @property
    def is_tuple(self) -> bool:
        return utils.is_tuple(self.type)

    @property
    def is_bool(self) -> bool:
        return utils.is_bool(self.type)

    @property
    def is_optional(self) -> bool:
        return utils.is_optional(self.field.type)

    @property
    def is_union(self) -> bool:
        return utils.is_union(self.field.type)

    @property
    def type_arguments(self) -> Optional[Tuple[Type, ...]]:
        return utils.get_type_arguments(self.type)

    @property
    def parent(self):
        return self._parent

    def add_action(self, parser: _ActionsContainer) -> None:
        logger.debug(f"Arg options for field '{self.name}': {self.arg_options}")
        parser.add_argument(*self.option_strings, **self.arg_options)


def only_keep_action_args(options: Dict[str, Any], action: Union[str, Any]) -> Dict[str, Any]:
    """Remove all the arguments in `options` that aren't required by the Action.

    Parameters
    ----------
    options : Dict[str, Any]
        A dictionary of options that would usually be passed to
        `add_arguments(*option_strings, **options)`.
    action : Union[str, Any]
        The action class or name.

    Returns
    -------
    Dict[str, Any]
        [description]
    """
    # TODO: explicitly tests these custom actions?
    argparse_action_classes: Dict[str, Type[argparse.Action]] = {
        "store": argparse._StoreAction,
        "store_const": argparse._StoreConstAction,
        "store_true": argparse._StoreTrueAction,
        "store_false": argparse._StoreFalseAction,
        "append": argparse._AppendAction,
        "append_const": argparse._AppendConstAction,
        "count": argparse._CountAction,
        "help": argparse._HelpAction,
        "version": argparse._VersionAction,
        "parsers": argparse._SubParsersAction,
    }
    if action not in argparse_action_classes:
        # the provided `action` is not a standard argparse-action.
        # We don't remove any of the provided options.
        return options

    # Remove all the keys that aren't needed by the action constructor:
    action_class = argparse_action_classes[action]
    argspec = inspect.getfullargspec(action_class)

    if argspec.varargs is not None or argspec.varkw is not None:
        # if the constructor takes variable arguments, pass all the options.
        logger.debug("Constructor takes var args. returning all options.")
        return options

    args_to_keep = [*argspec.args, "action"]

    kept_options, deleted_options = utils.keep_keys(options, args_to_keep)
    if deleted_options:
        logger.debug(
            "Some auto-generated options were deleted, as they were "
            f"not required by the Action constructor: {deleted_options}."
        )
        logger.debug(f"Kept options: \t{kept_options.keys()}")
        logger.debug(f"Removed options: \t{deleted_options.keys()}")
    return kept_options
