from __future__ import (
    absolute_import,
    unicode_literals,
)

from typing import (
    Any,
    Dict,
    Iterable,
    List,
    Mapping,
    Optional,
    TypeVar,
    Union,
)

import attr
import six

from pysoa.common.errors import Error


__all__ = (
    'ActionRequest',
    'ActionResponse',
    'Body',
    'Context',
    'Control',
    'JobRequest',
    'JobResponse',
    'UnicodeKeysDict',
)


_VT = TypeVar('_VT')
_AT = TypeVar('_AT')


class UnicodeKeysDict(dict):
    """
    A special dictionary factory used to ensure that Attrs object-to-dict dictionaries contain keys that are only
    unicode strings instead of byte strings. This factory will become unnecessary and be removed in PySOA 2.0.0 when
    all Python 2 support is removed.
    """

    def __setitem__(self, key, value):  # type: (six.text_type, Any) -> None
        super(UnicodeKeysDict, self).__setitem__(six.text_type(key), value)

    def setdefault(self, key, default=None):  # type: (six.text_type, Optional[_VT]) -> _VT
        return super(UnicodeKeysDict, self).setdefault(six.text_type(key), default)


Body = Dict[six.text_type, Any]
"""A type used for annotating attributes and arguments that represent action request and response bodies."""

Context = Dict[six.text_type, Any]
"""A type used for annotating attributes and arguments that represent job request and response context headers."""

Control = Dict[six.text_type, Any]
"""A type used for annotating attributes and arguments that represent job request control headers."""


def _convert_errors(errors):
    # type: (Union[Iterable[Mapping[six.text_type, Any]], Iterable[Error]]) -> List[Error]
    value = []  # type: List[Error]
    for a in errors:
        value.append(a if isinstance(a, Error) else Error(**a))
    return value


@attr.s
class ActionRequest(object):
    """
    A request that the server execute a single action.

    :param action: The name of the action to execute.
    :param body: The request body input.
    """

    action = attr.ib()  # type: six.text_type
    body = attr.ib(default=attr.Factory(dict))  # type: Body


def _convert_action_requests(actions):
    # type: (Union[Iterable[Mapping[six.text_type, Any]], Iterable[ActionRequest]]) -> List[ActionRequest]
    value = []  # type: List[ActionRequest]
    for a in actions:
        value.append(a if isinstance(a, ActionRequest) else ActionRequest(**a))
    return value


@attr.s
class ActionResponse(object):
    """
    A response generated by a single action on the server.

    :param action: The name of the action that was executed.
    :param body: The response body output.
    :param errors: A list of any action :class:`Error` or errors that occurred, or an empty list if no errors occurred.
    """

    action = attr.ib()  # type: six.text_type
    errors = attr.ib(default=attr.Factory(list), converter=_convert_errors)  # type: List[Error]
    body = attr.ib(default=attr.Factory(dict))  # type: Body


def _convert_action_responses(actions):
    # type: (Union[Iterable[Mapping[six.text_type, Any]], Iterable[ActionResponse]]) -> List[ActionResponse]
    value = []  # type: List[ActionResponse]
    for a in actions:
        value.append(a if isinstance(a, ActionResponse) else ActionResponse(**a))
    return value


@attr.s
class JobRequest(object):
    """
    A request that the server execute a job.

    A job consists of one or more actions and context and control headers. Each action is an :class:`ActionRequest`,
    while the context and control headers are dictionaries.

    :param control: The control header dictionary.
    :param context: The context header dictionary.
    :param actions: The list of `ActionRequest` objects to be executed. This list must contain at least one request.
    """

    control = attr.ib(default=attr.Factory(dict))  # type: Control
    context = attr.ib(default=attr.Factory(dict))  # type: Context
    actions = attr.ib(default=attr.Factory(list), converter=_convert_action_requests)  # type: List[ActionRequest]


@attr.s
class JobResponse(object):
    """
    A response generated by a server job.

    Contains the result or error generated by each action in the job.

    :param context: The context header dictionary.
    :param actions: The list of :class:`ActionResponse` objects that were executed. This list *may* be shorter than the
                    number of actions in the :class:`JobRequest` (even empty) if the `errors` attribute contains
                    one or more errors.
    :param errors: A list of any job :class:`Error` or errors that occurred, or an empty list of no errors occurred.
    """

    errors = attr.ib(default=attr.Factory(list), converter=_convert_errors)  # type: List[Error]
    context = attr.ib(default=attr.Factory(dict))  # type: Context
    actions = attr.ib(default=attr.Factory(list), converter=_convert_action_responses)  # type: List[ActionResponse]
