import collections
import copy
import datetime
import hashlib
import hmac
import json
import logging

try:
    import cPickle as pickle
except ImportError:
    import pickle
import sys
import threading
import time
from xml.etree import ElementTree

from authomatic.exceptions import (
    ConfigError,
    CredentialsError,
    ImportStringError,
    RequestElementsError,
    SessionError,
)
from authomatic import six
from authomatic.six.moves import urllib_parse as parse


# =========================================================================
# Global variables !!!
# =========================================================================

_logger = logging.getLogger(__name__)
_logger.addHandler(logging.StreamHandler(sys.stdout))

_counter = None


def normalize_dict(dict_):
    """
    Replaces all values that are single-item iterables with the value of its
    index 0.

    :param dict dict_:
        Dictionary to normalize.

    :returns:
        Normalized dictionary.

    """

    return dict([(k, v[0] if not isinstance(v, str) and len(v) == 1 else v) for k, v in list(dict_.items())])


def items_to_dict(items):
    """
    Converts list of tuples to dictionary with duplicate keys converted to
    lists.

    :param list items:
        List of tuples.

    :returns:
        :class:`dict`

    """

    res = collections.defaultdict(list)

    for k, v in items:
        res[k].append(v)

    return normalize_dict(dict(res))


class Counter(object):
    """
    A simple counter to be used in the config to generate unique `id` values.
    """

    def __init__(self, start=0):
        self._count = start

    def count(self):
        self._count += 1
        return self._count


_counter = Counter()


def provider_id():
    """
    A simple counter to be used in the config to generate unique `IDs`.

    :returns:
        :class:`int`.

    Use it in the :doc:`config` like this:
    ::

        import authomatic

        CONFIG = {
            'facebook': {
                 'class_': authomatic.providers.oauth2.Facebook,
                 'id': authomatic.provider_id(), # returns 1
                 'consumer_key': '##########',
                 'consumer_secret': '##########',
                 'scope': ['user_about_me', 'email']
            },
            'google': {
                 'class_': 'authomatic.providers.oauth2.Google',
                 'id': authomatic.provider_id(), # returns 2
                 'consumer_key': '##########',
                 'consumer_secret': '##########',
                 'scope': ['https://www.googleapis.com/auth/userinfo.profile',
                           'https://www.googleapis.com/auth/userinfo.email']
            },
            'windows_live': {
                 'class_': 'oauth2.WindowsLive',
                 'id': authomatic.provider_id(), # returns 3
                 'consumer_key': '##########',
                 'consumer_secret': '##########',
                 'scope': ['wl.basic', 'wl.emails', 'wl.photos']
            },
        }

    """

    return _counter.count()


def escape(s):
    """
    Escape a URL including any /.
    """
    return parse.quote(s.encode("utf-8"), safe="~")


def json_qs_parser(body):
    """
    Parses response body from JSON, XML or query string.

    :param body:
        string

    :returns:
        :class:`dict`, :class:`list` if input is JSON or query string,
        :class:`xml.etree.ElementTree.Element` if XML.

    """
    try:
        # Try JSON first.
        return json.loads(body)
    except (OverflowError, TypeError, ValueError):
        pass

    try:
        # Then XML.
        return ElementTree.fromstring(body)
    except (ElementTree.ParseError, TypeError, ValueError):
        pass

    # Finally query string.
    return dict(parse.parse_qsl(body))


def import_string(import_name, silent=False):
    """
    Imports an object by string in dotted notation.

    taken `from webapp2.import_string() <http://webapp-
    improved.appspot.com/api/webapp2.html#webapp2.import_string>`_

    """

    try:
        if "." in import_name:
            module, obj = import_name.rsplit(".", 1)
            return getattr(__import__(module, None, None, [obj]), obj)
        else:
            return __import__(import_name)
    except (ImportError, AttributeError) as e:
        if not silent:
            raise ImportStringError("Import from string failed for path {0}".format(import_name), str(e))


def resolve_provider_class(class_):
    """
    Returns a provider class.

    :param class_name: :class:`string` or
    :class:`authomatic.providers.BaseProvider` subclass.

    """

    if isinstance(class_, str):
        # prepare path for authomatic.providers package
        path = ".".join([__package__, "providers", class_])

        # try to import class by string from providers module or by fully
        # qualified path
        return import_string(class_, True) or import_string(path)
    else:
        return class_


def id_to_name(config, short_name):
    """
    Returns the provider :doc:`config` key based on it's ``id`` value.

    :param dict config:
        :doc:`config`.
    :param id:
        Value of the id parameter in the :ref:`config` to search for.

    """

    for k, v in list(config.items()):
        if v.get("id") == short_name:
            return k

    raise Exception("No provider with id={0} found in the config!".format(short_name))


class ReprMixin(object):
    """
    Provides __repr__() method with output *ClassName(arg1=value, arg2=value)*.

    Ignored are attributes

    * which values are considered false.
    * with leading underscore.
    * listed in _repr_ignore.

    Values of attributes listed in _repr_sensitive will be replaced by *###*.
    Values which repr() string is longer than _repr_length_limit will be
    represented as *ClassName(...)*

    """

    #: Iterable of attributes to be ignored.
    _repr_ignore = []
    #: Iterable of attributes which value should not be visible.
    _repr_sensitive = []
    #: `int` Values longer than this will be truncated to *ClassName(...)*.
    _repr_length_limit = 20

    def __repr__(self):
        # get class name
        name = self.__class__.__name__

        # construct keyword arguments
        args = []

        for k, v in list(self.__dict__.items()):
            # ignore attributes with leading underscores and those listed in
            # _repr_ignore
            if v and not k.startswith("_") and k not in self._repr_ignore:
                # replace sensitive values
                if k in self._repr_sensitive:
                    v = "###"

                # if repr is too long
                if len(repr(v)) > self._repr_length_limit:
                    # Truncate to ClassName(...)
                    v = "{0}(...)".format(v.__class__.__name__)
                else:
                    v = repr(v)

                args.append("{0}={1}".format(k, v))

        return "{0}({1})".format(name, ", ".join(args))


class Future(threading.Thread):
    """
    Represents an activity run in a separate thread. Subclasses the standard
    library :class:`threading.Thread` and adds :attr:`.get_result` method.

    .. warning::

        |async|

    """

    def __init__(self, func, *args, **kwargs):
        """
        :param callable func:
            The function to be run in separate thread.

        Calls :data:`func` in separate thread and returns immediately.
        Accepts arbitrary positional and keyword arguments which will be
        passed to :data:`func`.
        """

        super(Future, self).__init__()
        self._func = func
        self._args = args
        self._kwargs = kwargs
        self._result = None

        self.start()

    def run(self):
        self._result = self._func(*self._args, **self._kwargs)

    def get_result(self, timeout=None):
        """
        Waits for the wrapped :data:`func` to finish and returns its result.

        .. note::

            This will block the **calling thread** until the :data:`func`
            returns.

        :param timeout:
            :class:`float` or ``None`` A timeout for the :data:`func` to
            return in seconds.

        :returns:
            The result of the wrapped :data:`func`.

        """

        self.join(timeout)
        return self._result


class Session(object):
    """
    A dictionary-like secure cookie session implementation.
    """

    def __init__(self, adapter, secret, name="authomatic", max_age=600, secure=False):
        """
        :param str secret:
            Session secret used to sign the session cookie.
        :param str name:
            Session cookie name.
        :param int max_age:
            Maximum allowed age of session cookie nonce in seconds.
        :param bool secure:
            If ``True`` the session cookie will be saved with ``Secure``
            attribute.
        """

        self.adapter = adapter
        self.name = name
        self.secret = secret
        self.max_age = max_age
        self.secure = secure
        self._data = {}

    def create_cookie(self, delete=None):
        """
        Creates the value for ``Set-Cookie`` HTTP header.

        :param bool delete:
            If ``True`` the cookie value will be ``deleted`` and the
            Expires value will be ``Thu, 01-Jan-1970 00:00:01 GMT``.

        """
        value = "deleted" if delete else self._serialize(self.data)
        split_url = parse.urlsplit(self.adapter.url)
        domain = split_url.netloc.split(":")[0]

        # Work-around for issue #11, failure of WebKit-based browsers to accept
        # cookies set as part of a redirect response in some circumstances.
        if "." not in domain:
            template = "{name}={value}; Path={path}; HttpOnly{secure}{expires}"
        else:
            template = "{name}={value}; Domain={domain}; Path={path}; HttpOnly{secure}{expires}"

        return template.format(
            name=self.name,
            value=value,
            domain=domain,
            path=split_url.path,
            secure="; Secure" if self.secure else "",
            expires="; Expires=Thu, 01-Jan-1970 00:00:01 GMT" if delete else "",
        )

    def save(self):
        """
        Adds the session cookie to headers.
        """
        if self.data:
            cookie = self.create_cookie()
            cookie_len = len(cookie)

            if cookie_len > 4093:
                raise SessionError("Cookie too long! The cookie size {0} is more than 4093 bytes.".format(cookie_len))

            self.adapter.set_header("Set-Cookie", cookie)

            # Reset data
            self._data = {}

    def delete(self):
        self.adapter.set_header("Set-Cookie", self.create_cookie(delete=True))

    def _get_data(self):
        """
        Extracts the session data from cookie.
        """
        cookie = self.adapter.cookies.get(self.name)
        return self._deserialize(cookie) if cookie else {}

    @property
    def data(self):
        """
        Gets session data lazily.
        """
        if not self._data:
            self._data = self._get_data()
        # Always return a dict, even if deserialization returned nothing
        if self._data is None:
            self._data = {}
        return self._data

    def _signature(self, *parts):
        """
        Creates signature for the session.
        """
        signature = hmac.new(six.b(self.secret), digestmod=hashlib.sha1)
        signature.update(six.b("|".join(parts)))
        return signature.hexdigest()

    def _serialize(self, value):
        """
        Converts the value to a signed string with timestamp.

        :param value:
            Object to be serialized.

        :returns:
            Serialized value.

        """

        # data = copy.deepcopy(value)
        data = value

        # 1. Serialize
        serialized = pickle.dumps(data).decode("latin-1")

        # 2. Encode
        # Percent encoding produces smaller result then urlsafe base64.
        encoded = parse.quote(serialized, "")

        # 3. Concatenate
        timestamp = str(int(time.time()))
        signature = self._signature(self.name, encoded, timestamp)
        concatenated = "|".join([encoded, timestamp, signature])

        return concatenated

    def _deserialize(self, value):
        """
        Deserializes and verifies the value created by :meth:`._serialize`.

        :param str value:
            The serialized value.

        :returns:
            Deserialized object.

        """

        # 3. Split
        encoded, timestamp, signature = value.split("|")

        # Verify signature
        if not signature == self._signature(self.name, encoded, timestamp):
            raise SessionError('Invalid signature "{0}"!'.format(signature))

        # Verify timestamp
        if int(timestamp) < int(time.time()) - self.max_age:
            return None

        # 2. Decode
        decoded = parse.unquote(encoded)

        # 1. Deserialize
        deserialized = pickle.loads(decoded.encode("latin-1"))

        return deserialized

    def __setitem__(self, key, value):
        self._data[key] = value

    def __getitem__(self, key):
        return self.data.__getitem__(key)

    def __delitem__(self, key):
        return self._data.__delitem__(key)

    def get(self, key, default=None):
        return self.data.get(key, default)


class User(ReprMixin):
    """
    Provides unified interface to selected **user** info returned by different
    **providers**.

    .. note:: The value format may vary across providers.

    """

    def __init__(self, provider, **kwargs):
        #: A :doc:`provider <providers>` instance.
        self.provider = provider

        #: An :class:`.Credentials` instance.
        self.credentials = kwargs.get("credentials")

        #: A :class:`dict` containing all the **user** information returned
        #: by the **provider**.
        #: The structure differs across **providers**.
        self.data = kwargs.get("data")

        #: The :attr:`.Response.content` of the request made to update
        #: the user.
        self.content = kwargs.get("content")

        #: :class:`str` ID assigned to the **user** by the **provider**.
        self.id = kwargs.get("id")
        #: :class:`str` User name e.g. *andrewpipkin*.
        self.username = kwargs.get("username")
        #: :class:`str` Name e.g. *Andrew Pipkin*.
        self.name = kwargs.get("name")
        #: :class:`str` First name e.g. *Andrew*.
        self.first_name = kwargs.get("first_name")
        #: :class:`str` Last name e.g. *Pipkin*.
        self.last_name = kwargs.get("last_name")
        #: :class:`str` Nickname e.g. *Andy*.
        self.nickname = kwargs.get("nickname")
        #: :class:`str` Link URL.
        self.link = kwargs.get("link")
        #: :class:`str` Gender.
        self.gender = kwargs.get("gender")
        #: :class:`str` Timezone.
        self.timezone = kwargs.get("timezone")
        #: :class:`str` Locale.
        self.locale = kwargs.get("locale")
        #: :class:`str` E-mail.
        self.email = kwargs.get("email")
        #: :class:`str` phone.
        self.phone = kwargs.get("phone")
        #: :class:`str` Picture URL.
        self.picture = kwargs.get("picture")
        #: Birth date as :class:`datetime.datetime()` or :class:`str`
        #  if parsing failed or ``None``.
        self.birth_date = kwargs.get("birth_date")
        #: :class:`str` Country.
        self.country = kwargs.get("country")
        #: :class:`str` City.
        self.city = kwargs.get("city")
        #: :class:`str` Geographical location.
        self.location = kwargs.get("location")
        #: :class:`str` Postal code.
        self.postal_code = kwargs.get("postal_code")
        #: Instance of the Google App Engine Users API
        #: `User <https://developers.google.com/appengine/docs/python/users/userclass>`_ class.
        #: Only present when using the :class:`authomatic.providers.gaeopenid.GAEOpenID` provider.
        self.gae_user = kwargs.get("gae_user")

    def update(self):
        """
        Updates the user info by fetching the **provider's** user info URL.

        :returns:
            Updated instance of this class.

        """

        return self.provider.update_user()

    def async_update(self):
        """
        Same as :meth:`.update` but runs asynchronously in a separate thread.

        .. warning::

            |async|

        :returns:
            :class:`.Future` instance representing the separate thread.

        """

        return Future(self.update)

    def to_dict(self):
        """
        Converts the :class:`.User` instance to a :class:`dict`.

        :returns:
            :class:`dict`

        """

        # copy the dictionary
        d = copy.copy(self.__dict__)

        # Keep only the provider name to avoid circular reference
        d["provider"] = self.provider.name
        d["credentials"] = self.credentials.serialize() if self.credentials else None
        d["birth_date"] = str(d["birth_date"])

        # Remove content
        d.pop("content")

        if isinstance(self.data, ElementTree.Element):
            d["data"] = None

        return d


SupportedUserAttributesNT = collections.namedtuple(
    typename="SupportedUserAttributesNT",
    field_names=[
        "birth_date",
        "city",
        "country",
        "email",
        "first_name",
        "gender",
        "id",
        "last_name",
        "link",
        "locale",
        "location",
        "name",
        "nickname",
        "phone",
        "picture",
        "postal_code",
        "timezone",
        "username",
    ],
)


class SupportedUserAttributes(SupportedUserAttributesNT):
    def __new__(cls, **kwargs):
        defaults = dict((i, False) for i in SupportedUserAttributes._fields)  # pylint:disable=no-member
        defaults.update(**kwargs)
        return super(SupportedUserAttributes, cls).__new__(cls, **defaults)


class Credentials(ReprMixin):
    """
    Contains all necessary information to fetch **user's protected resources**.
    """

    _repr_sensitive = ("token", "refresh_token", "token_secret", "consumer_key", "consumer_secret")

    def __init__(self, config, **kwargs):
        #: :class:`dict` :doc:`config`.
        self.config = config

        #: :class:`str` User **access token**.
        self.token = kwargs.get("token", "")

        #: :class:`str` Access token type.
        self.token_type = kwargs.get("token_type", "")

        #: :class:`str` Refresh token.
        self.refresh_token = kwargs.get("refresh_token", "")

        #: :class:`str` Access token secret.
        self.token_secret = kwargs.get("token_secret", "")

        #: :class:`int` Expiration date as UNIX timestamp.
        self.expiration_time = int(kwargs.get("expiration_time", 0))

        #: A :doc:`Provider <providers>` instance**.
        provider = kwargs.get("provider")

        self.expire_in = int(kwargs.get("expire_in", 0))

        if provider:
            #: :class:`str` Provider name specified in the :doc:`config`.
            self.provider_name = provider.name

            #: :class:`str` Provider type e.g.
            #  ``"authomatic.providers.oauth2.OAuth2"``.
            self.provider_type = provider.get_type()

            #: :class:`str` Provider type e.g.
            #  ``"authomatic.providers.oauth2.OAuth2"``.
            self.provider_type_id = provider.type_id

            #: :class:`str` Provider short name specified in the :doc:`config`.
            self.provider_id = int(provider.id) if provider.id else None

            #: :class:`class` Provider class.
            self.provider_class = provider.__class__

            #: :class:`str` Consumer key specified in the :doc:`config`.
            self.consumer_key = provider.consumer_key

            #: :class:`str` Consumer secret specified in the :doc:`config`.
            self.consumer_secret = provider.consumer_secret

        else:
            self.provider_name = kwargs.get("provider_name", "")
            self.provider_type = kwargs.get("provider_type", "")
            self.provider_type_id = kwargs.get("provider_type_id")
            self.provider_id = kwargs.get("provider_id")
            self.provider_class = kwargs.get("provider_class")

            self.consumer_key = kwargs.get("consumer_key", "")
            self.consumer_secret = kwargs.get("consumer_secret", "")

    @property
    def expire_in(self):
        """ """

        return self._expire_in

    @expire_in.setter
    def expire_in(self, value):
        """
        Computes :attr:`.expiration_time` when the value is set.
        """

        # pylint:disable=attribute-defined-outside-init
        if value:
            self._expiration_time = int(time.time()) + int(value)
            self._expire_in = value

    @property
    def expiration_time(self):
        return self._expiration_time

    @expiration_time.setter
    def expiration_time(self, value):
        # pylint:disable=attribute-defined-outside-init
        self._expiration_time = int(value)
        self._expire_in = self._expiration_time - int(time.time())

    @property
    def expiration_date(self):
        """
        Expiration date as :class:`datetime.datetime` or ``None`` if
        credentials never expire.
        """

        if self.expire_in < 0:
            return None
        else:
            return datetime.datetime.fromtimestamp(self.expiration_time)

    @property
    def valid(self):
        """
        ``True`` if credentials are valid, ``False`` if expired.
        """

        if self.expiration_time:
            return self.expiration_time > int(time.time())
        else:
            return True

    def expire_soon(self, seconds):
        """
        Returns ``True`` if credentials expire sooner than specified.

        :param int seconds:
            Number of seconds.

        :returns:
            ``True`` if credentials expire sooner than specified,
            else ``False``.

        """

        if self.expiration_time:
            return self.expiration_time < int(time.time()) + int(seconds)
        else:
            return False

    def refresh(self, force=False, soon=86400):
        """
        Refreshes the credentials only if the **provider** supports it and if
        it will expire in less than one day. It does nothing in other cases.

        .. note::

            The credentials will be refreshed only if it gives sense
            i.e. only |oauth2|_ has the notion of credentials
            *refreshment/extension*.
            And there are also differences across providers e.g. Google
            supports refreshment only if there is a ``refresh_token`` in
            the credentials and that in turn is present only if the
            ``access_type`` parameter was set to ``offline`` in the
            **user authorization request**.

        :param bool force:
            If ``True`` the credentials will be refreshed even if they
            won't expire soon.

        :param int soon:
            Number of seconds specifying what means *soon*.

        """

        if hasattr(self.provider_class, "refresh_credentials"):
            if force or self.expire_soon(soon):
                logging.info("PROVIDER NAME: {0}".format(self.provider_name))
                return self.provider_class(self, None, self.provider_name).refresh_credentials(self)

    def async_refresh(self, *args, **kwargs):
        """
        Same as :meth:`.refresh` but runs asynchronously in a separate thread.

        .. warning::

            |async|

        :returns:
            :class:`.Future` instance representing the separate thread.

        """

        return Future(self.refresh, *args, **kwargs)

    def provider_type_class(self):
        """
        Returns the :doc:`provider <providers>` class specified in the
        :doc:`config`.

        :returns:
            :class:`authomatic.providers.BaseProvider` subclass.

        """

        return resolve_provider_class(self.provider_type)

    def serialize(self):
        """
        Converts the credentials to a percent encoded string to be stored for
        later use.

        :returns:
            :class:`string`

        """

        if self.provider_id is None:
            raise ConfigError(
                "To serialize credentials you need to specify a "
                'unique integer under the "id" key in the config '
                "for each provider!"
            )

        # Get the provider type specific items.
        rest = self.provider_type_class().to_tuple(self)

        # Provider ID and provider type ID are always the first two items.
        result = (self.provider_id, self.provider_type_id) + rest

        # Make sure that all items are strings.
        stringified = [str(i) for i in result]

        # Concatenate by newline.
        concatenated = "\n".join(stringified)

        # Percent encode.
        return parse.quote(concatenated, "")

    @classmethod
    def deserialize(cls, config, credentials):
        """
        A *class method* which reconstructs credentials created by
        :meth:`serialize`. You can also pass it a :class:`.Credentials`
        instance.

        :param dict config:
            The same :doc:`config` used in the :func:`.login` to get the
            credentials.
        :param str credentials:
            :class:`string` The serialized credentials or
            :class:`.Credentials` instance.

        :returns:
            :class:`.Credentials`

        """

        # Accept both serialized and normal.
        if isinstance(credentials, Credentials):
            return credentials

        decoded = parse.unquote(credentials)

        split = decoded.split("\n")

        # We need the provider ID to move forward.
        if split[0] is None:
            raise CredentialsError(
                "To deserialize credentials you need to specify a unique "
                'integer under the "id" key in the config for each provider!'
            )

        # Get provider config by short name.
        provider_name = id_to_name(config, int(split[0]))
        cfg = config.get(provider_name)

        # Get the provider class.
        ProviderClass = resolve_provider_class(cfg.get("class_"))

        deserialized = Credentials(config)

        deserialized.provider_id = int(split[0])
        deserialized.provider_type = ProviderClass.get_type()
        deserialized.provider_type_id = split[1]
        deserialized.provider_class = ProviderClass
        deserialized.provider_name = provider_name
        deserialized.provider_class = ProviderClass

        # Add provider type specific properties.
        return ProviderClass.reconstruct(split[2:], deserialized, cfg)


class LoginResult(ReprMixin):
    """
    Result of the :func:`authomatic.login` function.
    """

    def __init__(self, provider):
        #: A :doc:`provider <providers>` instance.
        self.provider = provider

        #: An instance of the :exc:`authomatic.exceptions.BaseError` subclass.
        self.error = None

    def popup_js(self, callback_name=None, indent=None, custom=None, stay_open=False):
        """
        Returns JavaScript that:

        #.  Triggers the ``options.onLoginComplete(result, closer)``
            handler set with the :ref:`authomatic.setup() <js_setup>`
            function of :ref:`javascript.js <js>`.
        #.  Calls the JavasScript callback specified by :data:`callback_name`
            on the opener of the *login handler popup* and passes it the
            *login result* JSON object as first argument and the `closer`
            function which you should call in your callback to close the popup.

        :param str callback_name:
            The name of the javascript callback e.g ``foo.bar.loginCallback``
            will result in ``window.opener.foo.bar.loginCallback(result);``
            in the HTML.

        :param int indent:
            The number of spaces to indent the JSON result object.
            If ``0`` or negative, only newlines are added.
            If ``None``, no newlines are added.

        :param custom:
            Any JSON serializable object that will be passed to the
            ``result.custom`` attribute.

        :param str stay_open:
            If ``True``, the popup will stay open.

        :returns:
            :class:`str` with JavaScript.

        """

        custom_callback = (
            """
        try {{ window.opener.{cb}(result, closer); }} catch(e) {{}}
        """.format(cb=callback_name)
            if callback_name
            else ""
        )

        # TODO: Move the window.close() to the opener
        return """
        (function(){{

            closer = function(){{
                window.close();
            }};

            var result = {result};
            result.custom = {custom};

            {custom_callback}

            try {{
                window.opener.authomatic.loginComplete(result, closer);
            }} catch(e) {{}}

        }})();

        """.format(
            result=self.to_json(indent),
            custom=json.dumps(custom),
            custom_callback=custom_callback,
            stay_open="// " if stay_open else "",
        )

    def popup_html(self, callback_name=None, indent=None, title="Login | {0}", custom=None, stay_open=False):
        """
        Returns a HTML with JavaScript that:

        #.  Triggers the ``options.onLoginComplete(result, closer)`` handler
            set with the :ref:`authomatic.setup() <js_setup>` function of
            :ref:`javascript.js <js>`.
        #.  Calls the JavasScript callback specified by :data:`callback_name`
            on the opener of the *login handler popup* and passes it the
            *login result* JSON object as first argument and the `closer`
            function which you should call in your callback to close the popup.

        :param str callback_name:
            The name of the javascript callback e.g ``foo.bar.loginCallback``
            will result in ``window.opener.foo.bar.loginCallback(result);``
            in the HTML.

        :param int indent:
            The number of spaces to indent the JSON result object.
            If ``0`` or negative, only newlines are added.
            If ``None``, no newlines are added.

        :param str title:
            The text of the HTML title. You can use ``{0}`` tag inside,
            which will be replaced by the provider name.

        :param custom:
            Any JSON serializable object that will be passed to the
            ``result.custom`` attribute.

        :param str stay_open:
            If ``True``, the popup will stay open.

        :returns:
            :class:`str` with HTML.

        """

        return """
        <!DOCTYPE html>
        <html>
            <head><title>{title}</title></head>
            <body>
            <script type="text/javascript">
                {js}
            </script>
            </body>
        </html>
        """.format(
            title=title.format(self.provider.name if self.provider else ""),
            js=self.popup_js(callback_name, indent, custom, stay_open),
        )

    @property
    def user(self):
        """
        A :class:`.User` instance.
        """

        return self.provider.user if self.provider else None

    def to_dict(self):
        return dict(provider=self.provider, user=self.user, error=self.error)

    def to_json(self, indent=4):
        return json.dumps(self, default=lambda obj: obj.to_dict() if hasattr(obj, "to_dict") else "", indent=indent)


class Response(ReprMixin):
    """
    Wraps :class:`httplib.HTTPResponse` and adds.

    :attr:`.content` and :attr:`.data` attributes.

    """

    def __init__(self, httplib_response, content_parser=None):
        """
        :param httplib_response:
            The wrapped :class:`httplib.HTTPResponse` instance.

        :param function content_parser:
            Callable which accepts :attr:`.content` as argument,
            parses it and returns the parsed data as :class:`dict`.
        """

        self.httplib_response = httplib_response
        self.content_parser = content_parser or json_qs_parser
        self._data = None
        self._content = None

        #: Same as :attr:`httplib.HTTPResponse.msg`.
        self.msg = httplib_response.msg
        #: Same as :attr:`httplib.HTTPResponse.version`.
        self.version = httplib_response.version
        #: Same as :attr:`httplib.HTTPResponse.status`.
        self.status = httplib_response.status
        #: Same as :attr:`httplib.HTTPResponse.reason`.
        self.reason = httplib_response.reason

    def read(self, amt=None):
        """
        Same as :meth:`httplib.HTTPResponse.read`.

        :param amt:

        """

        return self.httplib_response.read(amt)

    def getheader(self, name, default=None):
        """
        Same as :meth:`httplib.HTTPResponse.getheader`.

        :param name:
        :param default:

        """

        return self.httplib_response.getheader(name, default)

    def fileno(self):
        """
        Same as :meth:`httplib.HTTPResponse.fileno`.
        """
        return self.httplib_response.fileno()

    def getheaders(self):
        """
        Same as :meth:`httplib.HTTPResponse.getheaders`.
        """
        return self.httplib_response.getheaders()

    @staticmethod
    def is_binary_string(content):
        """
        Return true if string is binary data.
        """

        textchars = bytearray([7, 8, 9, 10, 12, 13, 27]) + bytearray(range(0x20, 0x100))
        return bool(content.translate(None, textchars))

    @property
    def content(self):
        """
        The whole response content.
        """

        if not self._content:
            content = self.httplib_response.read()
            if self.is_binary_string(content):
                self._content = content
            else:
                self._content = content.decode("utf-8")
        return self._content

    @property
    def data(self):
        """
        A :class:`dict` of data parsed from :attr:`.content`.
        """

        if not self._data:
            self._data = self.content_parser(self.content)
        return self._data


class UserInfoResponse(Response):
    """
    Inherits from :class:`.Response`, adds  :attr:`~UserInfoResponse.user`
    attribute.
    """

    def __init__(self, user, *args, **kwargs):
        super(UserInfoResponse, self).__init__(*args, **kwargs)

        #: :class:`.User` instance.
        self.user = user


class RequestElements(tuple):
    """
    A tuple of ``(url, method, params, headers, body)`` request elements.

    With some additional properties.

    """

    def __new__(cls, url, method, params, headers, body):
        return tuple.__new__(cls, (url, method, params, headers, body))

    @property
    def url(self):
        """
        Request URL.
        """

        return self[0]

    @property
    def method(self):
        """
        HTTP method of the request.
        """

        return self[1]

    @property
    def params(self):
        """
        Dictionary of request parameters.
        """

        return self[2]

    @property
    def headers(self):
        """
        Dictionary of request headers.
        """

        return self[3]

    @property
    def body(self):
        """
        :class:`str` Body of ``POST``, ``PUT`` and ``PATCH`` requests.
        """

        return self[4]

    @property
    def query_string(self):
        """
        Query string of the request.
        """

        return parse.urlencode(self.params)

    @property
    def full_url(self):
        """
        URL with query string.
        """

        return self.url + "?" + self.query_string

    def to_json(self):
        return json.dumps(
            dict(url=self.url, method=self.method, params=self.params, headers=self.headers, body=self.body)
        )


class Authomatic(object):
    def __init__(
        self,
        config,
        secret,
        session_max_age=600,
        secure_cookie=False,
        session=None,
        session_save_method=None,
        report_errors=True,
        debug=False,
        logging_level=logging.INFO,
        prefix="authomatic",
        logger=None,
    ):
        """
        Encapsulates all the functionality of this package.

        :param dict config:
            :doc:`config`

        :param str secret:
            A secret string that will be used as the key for signing
            :class:`.Session` cookie and as a salt by *CSRF* token generation.

        :param session_max_age:
            Maximum allowed age of :class:`.Session` cookie nonce in seconds.

        :param bool secure_cookie:
            If ``True`` the :class:`.Session` cookie will be saved wit
            ``Secure`` attribute.

        :param session:
            Custom dictionary-like session implementation.

        :param callable session_save_method:
            A method of the supplied session or any mechanism that saves the
            session data and cookie.

        :param bool report_errors:
            If ``True`` exceptions encountered during the **login procedure**
            will be caught and reported in the :attr:`.LoginResult.error`
            attribute.
            Default is ``True``.

        :param bool debug:
            If ``True`` traceback of exceptions will be written to response.
            Default is ``False``.

        :param int logging_level:
            The logging level threshold for the default logger as specified in
            the standard Python
            `logging library <http://docs.python.org/2/library/logging.html>`_.
            This setting is ignored when :data:`logger` is set.
            Default is ``logging.INFO``.

        :param str prefix:
            Prefix used as the :class:`.Session` cookie name.

        :param logger:
            A :class:`logging.logger` instance.

        """

        self.config = config
        self.secret = secret
        self.session_max_age = session_max_age
        self.secure_cookie = secure_cookie
        self.session = session
        self.session_save_method = session_save_method
        self.report_errors = report_errors
        self.debug = debug
        self.logging_level = logging_level
        self.prefix = prefix
        self._logger = logger or logging.getLogger(str(id(self)))

        # Set logging level.
        if logger is None:
            self._logger.setLevel(logging_level)

    def login(self, adapter, provider_name, callback=None, session=None, session_saver=None, **kwargs):
        """
        If :data:`provider_name` specified, launches the login procedure for
        corresponding :doc:`provider </reference/providers>` and returns
        :class:`.LoginResult`.

        If :data:`provider_name` is empty, acts like
        :meth:`.Authomatic.backend`.

        .. warning::

            The method redirects the **user** to the **provider** which in
            turn redirects **him/her** back to the *request handler* where
            it has been called.

        :param str provider_name:
            Name of the provider as specified in the keys of the :doc:`config`.

        :param callable callback:
            If specified the method will call the callback with
            :class:`.LoginResult` passed as argument and will return nothing.

        :param bool report_errors:

        .. note::

            Accepts additional keyword arguments that will be passed to
            :doc:`provider <providers>` constructor.

        :returns:
            :class:`.LoginResult`

        """

        if provider_name:
            # retrieve required settings for current provider and raise
            # exceptions if missing
            provider_settings = self.config.get(provider_name)
            if not provider_settings:
                raise ConfigError('Provider name "{0}" not specified!'.format(provider_name))

            if not (session is None or session_saver is None):
                session = session
                session_saver = session_saver
            else:
                session = Session(
                    adapter=adapter,
                    secret=self.secret,
                    max_age=self.session_max_age,
                    name=self.prefix,
                    secure=self.secure_cookie,
                )

                session_saver = session.save

            # Resolve provider class.
            class_ = provider_settings.get("class_")
            if not class_:
                raise ConfigError(
                    'The "class_" key not specified in the config for provider {0}!'.format(provider_name)
                )
            ProviderClass = resolve_provider_class(class_)

            # FIXME: Find a nicer solution
            ProviderClass._logger = self._logger

            # instantiate provider class
            provider = ProviderClass(
                self,
                adapter=adapter,
                provider_name=provider_name,
                callback=callback,
                session=session,
                session_saver=session_saver,
                **kwargs,
            )

            # return login result
            return provider.login()

        else:
            # Act like backend.
            self.backend(adapter)

    def credentials(self, credentials):
        """
        Deserializes credentials.

        :param credentials:
            Credentials serialized with :meth:`.Credentials.serialize` or
            :class:`.Credentials` instance.

        :returns:
            :class:`.Credentials`

        """

        return Credentials.deserialize(self.config, credentials)

    def access(
        self, credentials, url, params=None, method="GET", headers=None, body="", max_redirects=5, content_parser=None
    ):
        """
        Accesses **protected resource** on behalf of the **user**.

        :param credentials:
            The **user's** :class:`.Credentials` (serialized or normal).

        :param str url:
            The **protected resource** URL.

        :param str method:
            HTTP method of the request.

        :param dict headers:
            HTTP headers of the request.

        :param str body:
            Body of ``POST``, ``PUT`` and ``PATCH`` requests.

        :param int max_redirects:
            Maximum number of HTTP redirects to follow.

        :param function content_parser:
            A function to be used to parse the :attr:`.Response.data`
            from :attr:`.Response.content`.

        :returns:
            :class:`.Response`

        """

        # Deserialize credentials.
        credentials = Credentials.deserialize(self.config, credentials)

        # Resolve provider class.
        ProviderClass = credentials.provider_class
        logging.info("ACCESS HEADERS: {0}".format(headers))
        # Access resource and return response.

        provider = ProviderClass(self, adapter=None, provider_name=credentials.provider_name)
        provider.credentials = credentials

        return provider.access(
            url=url,
            params=params,
            method=method,
            headers=headers,
            body=body,
            max_redirects=max_redirects,
            content_parser=content_parser,
        )

    def async_access(self, *args, **kwargs):
        """
        Same as :meth:`.Authomatic.access` but runs asynchronously in a
        separate thread.

        .. warning::

            |async|

        :returns:
            :class:`.Future` instance representing the separate thread.

        """

        return Future(self.access, *args, **kwargs)

    def request_elements(
        self,
        credentials=None,
        url=None,
        method="GET",
        params=None,
        headers=None,
        body="",
        json_input=None,
        return_json=False,
    ):
        """
        Creates request elements for accessing **protected resource of a
        user**. Required arguments are :data:`credentials` and :data:`url`. You
        can pass :data:`credentials`, :data:`url`, :data:`method`, and
        :data:`params` as a JSON object.

        :param credentials:
            The **user's** credentials (can be serialized).

        :param str url:
            The url of the protected resource.

        :param str method:
            The HTTP method of the request.

        :param dict params:
            Dictionary of request parameters.

        :param dict headers:
            Dictionary of request headers.

        :param str body:
            Body of ``POST``, ``PUT`` and ``PATCH`` requests.

        :param str json_input:
            you can pass :data:`credentials`, :data:`url`, :data:`method`,
            :data:`params` and :data:`headers` in a JSON object.
            Values from arguments will be used for missing properties.

            ::

                {
                    "credentials": "###",
                    "url": "https://example.com/api",
                    "method": "POST",
                    "params": {
                        "foo": "bar"
                    },
                    "headers": {
                        "baz": "bing",
                        "Authorization": "Bearer ###"
                    },
                    "body": "Foo bar baz bing."
                }

        :param bool return_json:
            if ``True`` the function returns a json object.

            ::

                {
                    "url": "https://example.com/api",
                    "method": "POST",
                    "params": {
                        "access_token": "###",
                        "foo": "bar"
                    },
                    "headers": {
                        "baz": "bing",
                        "Authorization": "Bearer ###"
                    },
                    "body": "Foo bar baz bing."
                }

        :returns:
            :class:`.RequestElements` or JSON string.

        """

        # Parse values from JSON
        if json_input:
            parsed_input = json.loads(json_input)

            credentials = parsed_input.get("credentials", credentials)
            url = parsed_input.get("url", url)
            method = parsed_input.get("method", method)
            params = parsed_input.get("params", params)
            headers = parsed_input.get("headers", headers)
            body = parsed_input.get("body", body)

        if not credentials and url:
            raise RequestElementsError(
                "To create request elements, you must provide credentials "
                "and URL either as keyword arguments or in the JSON object!"
            )

        # Get the provider class
        credentials = Credentials.deserialize(self.config, credentials)
        ProviderClass = credentials.provider_class

        # Create request elements
        request_elements = ProviderClass.create_request_elements(
            ProviderClass.PROTECTED_RESOURCE_REQUEST_TYPE,
            credentials=credentials,
            url=url,
            method=method,
            params=params,
            headers=headers,
            body=body,
        )

        if return_json:
            return request_elements.to_json()

        else:
            return request_elements

    def backend(self, adapter):
        """
        Converts a *request handler* to a JSON backend which you can use with
        :ref:`authomatic.js <js>`.

        Just call it inside a *request handler* like this:

        ::

            class JSONHandler(webapp2.RequestHandler):
                def get(self):
                    authomatic.backend(Webapp2Adapter(self))

        :param adapter:
            The only argument is an :doc:`adapter <adapters>`.

        The *request handler* will now accept these request parameters:

        :param str type:
            Type of the request. Either ``auto``, ``fetch`` or ``elements``.
            Default is ``auto``.

        :param str credentials:
            Serialized :class:`.Credentials`.

        :param str url:
            URL of the **protected resource** request.

        :param str method:
            HTTP method of the **protected resource** request.

        :param str body:
            HTTP body of the **protected resource** request.

        :param JSON params:
            HTTP params of the **protected resource** request as a JSON object.

        :param JSON headers:
            HTTP headers of the **protected resource** request as a
            JSON object.

        :param JSON json:
            You can pass all of the aforementioned params except ``type``
            in a JSON object.

            .. code-block:: javascript

                {
                    "credentials": "######",
                    "url": "https://example.com",
                    "method": "POST",
                    "params": {"foo": "bar"},
                    "headers": {"baz": "bing"},
                    "body": "the body of the request"
                }

        Depending on the ``type`` param, the handler will either write
        a JSON object with *request elements* to the response,
        and add an ``Authomatic-Response-To: elements`` response header, ...

        .. code-block:: javascript

            {
                "url": "https://example.com/api",
                "method": "POST",
                "params": {
                    "access_token": "###",
                    "foo": "bar"
                },
                "headers": {
                    "baz": "bing",
                    "Authorization": "Bearer ###"
                }
            }

        ... or make a fetch to the **protected resource** and forward
        it's response content, status and headers with an additional
        ``Authomatic-Response-To: fetch`` header to the response.

        .. warning::

            The backend will not work if you write anything to the
            response in the handler!

        """

        AUTHOMATIC_HEADER = "Authomatic-Response-To"

        # Collect request params
        request_type = adapter.params.get("type", "auto")
        json_input = adapter.params.get("json")
        credentials = adapter.params.get("credentials")
        url = adapter.params.get("url")
        method = adapter.params.get("method", "GET")
        body = adapter.params.get("body", "")

        params = adapter.params.get("params")
        params = json.loads(params) if params else {}

        headers = adapter.params.get("headers")
        headers = json.loads(headers) if headers else {}

        ProviderClass = Credentials.deserialize(self.config, credentials).provider_class

        if request_type == "auto":
            # If there is a "callback" param, it's a JSONP request.
            jsonp = params.get("callback")

            # JSONP is possible only with GET method.
            if ProviderClass.supports_jsonp and method == "GET":
                request_type = "elements"
            else:
                # Remove the JSONP callback
                if jsonp:
                    params.pop("callback")
                request_type = "fetch"

        if request_type == "fetch":
            # Access protected resource
            response = self.access(credentials, url, params, method, headers, body)
            result = response.content

            # Forward status
            adapter.status = str(response.status) + " " + str(response.reason)

            # Forward headers
            for k, v in response.getheaders():
                logging.info("    {0}: {1}".format(k, v))
                adapter.set_header(k, v)

        elif request_type == "elements":
            # Create request elements
            if json_input:
                result = self.request_elements(json_input=json_input, return_json=True)
            else:
                result = self.request_elements(
                    credentials=credentials,
                    url=url,
                    method=method,
                    params=params,
                    headers=headers,
                    body=body,
                    return_json=True,
                )

            adapter.set_header("Content-Type", "application/json")
        else:
            result = '{"error": "Bad Request!"}'

        # Add the authomatic header
        adapter.set_header(AUTHOMATIC_HEADER, request_type)

        # Write result to response
        adapter.write(result)
