# Copyright 2017 Canonical Ltd.
# Licensed under the LGPLv3, see LICENCE file for details.
import json
from collections import namedtuple

import macaroonbakery.bakery as bakery

ERR_INTERACTION_REQUIRED = 'interaction required'
ERR_DISCHARGE_REQUIRED = 'macaroon discharge required'


class InteractionMethodNotFound(Exception):
    '''This is thrown by client-side interaction methods when
    they find that a given interaction isn't supported by the
    client for a location'''
    pass


class DischargeError(Exception):
    '''This is thrown by Client when a third party has refused a discharge'''
    def __init__(self, msg):
        super(DischargeError, self).__init__(
            'third party refused dischargex: {}'.format(msg))


class InteractionError(Exception):
    '''This is thrown by Client when it fails to deal with an
    interaction-required error
    '''
    def __init__(self, msg):
        super(InteractionError, self).__init__(
            'cannot start interactive session: {}'.format(msg))


def discharge_required_response(macaroon, path, cookie_suffix_name,
                                message=None):
    ''' Get response content and headers from a discharge macaroons error.

    @param macaroon may hold a macaroon that, when discharged, may
    allow access to a service.
    @param path holds the URL path to be associated with the macaroon.
    The macaroon is potentially valid for all URLs under the given path.
    @param cookie_suffix_name holds the desired cookie name suffix to be
    associated with the macaroon. The actual name used will be
    ("macaroon-" + CookieName). Clients may ignore this field -
    older clients will always use ("macaroon-" + macaroon.signature() in hex)
    @return content(bytes) and the headers to set on the response(dict).
    '''
    if message is None:
        message = 'discharge required'
    content = json.dumps(
        {
            'Code': 'macaroon discharge required',
            'Message': message,
            'Info': {
                'Macaroon': macaroon.to_dict(),
                'MacaroonPath': path,
                'CookieNameSuffix': cookie_suffix_name
            },
        }
    ).encode('utf-8')
    return content, {
        'WWW-Authenticate': 'Macaroon',
        'Content-Type': 'application/json'
    }

# BAKERY_PROTOCOL_HEADER is the header that HTTP clients should set
# to determine the bakery protocol version. If it is 0 or missing,
# a discharge-required error response will be returned with HTTP status 407;
# if it is greater than 0, the response will have status 401 with the
# WWW-Authenticate header set to "Macaroon".
BAKERY_PROTOCOL_HEADER = 'Bakery-Protocol-Version'


def request_version(req_headers):
    ''' Determines the bakery protocol version from a client request.
    If the protocol cannot be determined, or is invalid, the original version
    of the protocol is used. If a later version is found, the latest known
    version is used, which is OK because versions are backwardly compatible.

    @param req_headers: the request headers as a dict.
    @return: bakery protocol version (for example macaroonbakery.VERSION_1)
    '''
    vs = req_headers.get(BAKERY_PROTOCOL_HEADER)
    if vs is None:
        # No header - use backward compatibility mode.
        return bakery.VERSION_1
    try:
        x = int(vs)
    except ValueError:
        # Badly formed header - use backward compatibility mode.
        return bakery.VERSION_1
    if x > bakery.LATEST_VERSION:
        # Later version than we know about - use the
        # latest version that we can.
        return bakery.LATEST_VERSION
    return x


class Error(namedtuple('Error', 'code, message, version, info')):
    '''This class defines an error value as returned from
    an httpbakery API.
    '''
    @classmethod
    def from_dict(cls, serialized):
        '''Create an error from a JSON-deserialized object
        @param serialized the object holding the serialized error {dict}
        '''
        # Some servers return lower case field names for message and code.
        # The Go client is tolerant of this, so be similarly tolerant here.
        def field(name):
            return serialized.get(name) or serialized.get(name.lower())
        return Error(
            code=field('Code'),
            message=field('Message'),
            info=ErrorInfo.from_dict(field('Info')),
            version=bakery.LATEST_VERSION,
        )

    def interaction_method(self, kind, x):
        ''' Checks whether the error is an InteractionRequired error
        that implements the method with the given name, and JSON-unmarshals the
        method-specific data into x by calling its from_dict method
        with the deserialized JSON object.
        @param kind The interaction method kind (string).
        @param x A class with a class method from_dict that returns a new
        instance of the interaction info for the given kind.
        @return The result of x.from_dict.
        '''
        if self.info is None or self.code != ERR_INTERACTION_REQUIRED:
            raise InteractionError(
                'not an interaction-required error (code {})'.format(
                    self.code)
            )
        entry = self.info.interaction_methods.get(kind)
        if entry is None:
            raise InteractionMethodNotFound(
                'interaction method {} not found'.format(kind)
            )
        return x.from_dict(entry)


class ErrorInfo(
    namedtuple('ErrorInfo', 'macaroon, macaroon_path, cookie_name_suffix, '
                            'interaction_methods, visit_url, wait_url')):
    '''  Holds additional information provided
    by an error.

    @param macaroon may hold a macaroon that, when
    discharged, may allow access to a service.
    This field is associated with the ERR_DISCHARGE_REQUIRED
    error code.

    @param macaroon_path holds the URL path to be associated
    with the macaroon. The macaroon is potentially
    valid for all URLs under the given path.
    If it is empty, the macaroon will be associated with
    the original URL from which the error was returned.

    @param cookie_name_suffix holds the desired cookie name suffix to be
    associated with the macaroon. The actual name used will be
    ("macaroon-" + cookie_name_suffix). Clients may ignore this field -
    older clients will always use ("macaroon-" +
    macaroon.signature() in hex).

    @param visit_url holds a URL that the client should visit
    in a web browser to authenticate themselves.

    @param wait_url holds a URL that the client should visit
    to acquire the discharge macaroon. A GET on
    this URL will block until the client has authenticated,
    and then it will return the discharge macaroon.
    '''

    __slots__ = ()

    @classmethod
    def from_dict(cls, serialized):
        '''Create a new ErrorInfo object from a JSON deserialized
        dictionary
        @param serialized The JSON object {dict}
        @return ErrorInfo object
        '''
        if serialized is None:
            return None
        macaroon = serialized.get('Macaroon')
        if macaroon is not None:
            macaroon = bakery.Macaroon.from_dict(macaroon)
        path = serialized.get('MacaroonPath')
        cookie_name_suffix = serialized.get('CookieNameSuffix')
        visit_url = serialized.get('VisitURL')
        wait_url = serialized.get('WaitURL')
        interaction_methods = serialized.get('InteractionMethods')
        return ErrorInfo(macaroon=macaroon, macaroon_path=path,
                         cookie_name_suffix=cookie_name_suffix,
                         visit_url=visit_url, wait_url=wait_url,
                         interaction_methods=interaction_methods)

    def __new__(cls, macaroon=None, macaroon_path=None,
                cookie_name_suffix=None, interaction_methods=None,
                visit_url=None, wait_url=None):
        '''Override the __new__ method so that we can
        have optional arguments, which namedtuple doesn't
        allow'''
        return super(ErrorInfo, cls).__new__(
            cls, macaroon, macaroon_path, cookie_name_suffix,
            interaction_methods, visit_url, wait_url)
