# Copyright 2017 Canonical Ltd.
# Licensed under the LGPLv3, see LICENCE file for details.
import base64
import binascii
import ipaddress
import json
import webbrowser
from datetime import datetime

import six
from pymacaroons import Macaroon
from pymacaroons.serializers import json_serializer

import six.moves.http_cookiejar as http_cookiejar
from six.moves.urllib.parse import urlparse


def to_bytes(s):
    '''Return s as a bytes type, using utf-8 encoding if necessary.
    @param s string or bytes
    @return bytes
    '''
    if isinstance(s, six.binary_type):
        return s
    if isinstance(s, six.string_types):
        return s.encode('utf-8')
    raise TypeError('want string or bytes, got {}', type(s))


def macaroon_from_dict(json_macaroon):
    '''Return a pymacaroons.Macaroon object from the given
    JSON-deserialized dict.

    @param JSON-encoded macaroon as dict
    @return the deserialized macaroon object.
    '''
    return Macaroon.deserialize(json.dumps(json_macaroon),
                                json_serializer.JsonSerializer())


def macaroon_to_dict(macaroon):
    '''Turn macaroon into JSON-serializable dict object
    @param pymacaroons.Macaroon.
    '''
    return json.loads(macaroon.serialize(json_serializer.JsonSerializer()))


def macaroon_to_json_string(macaroon):
    '''Serialize macaroon object to a JSON-encoded string.

    @param macaroon object to be serialized.
    @return a string serialization form of the macaroon.
    '''
    return macaroon.serialize(json_serializer.JsonSerializer())


def _add_base64_padding(b):
    '''Add padding to base64 encoded bytes.

    pymacaroons does not give padded base64 bytes from serialization.

    @param bytes b to be padded.
    @return a padded bytes.
    '''
    return b + b'=' * (-len(b) % 4)


def _remove_base64_padding(b):
    '''Remove padding from base64 encoded bytes.

    pymacaroons does not give padded base64 bytes from serialization.

    @param bytes b to be padded.
    @return a padded bytes.
    '''
    return b.rstrip(b'=')


def b64decode(s):
    '''Base64 decodes a base64-encoded string in URL-safe
    or normal format, with or without padding.
    The argument may be string or bytes.

    @param s bytes decode
    @return bytes decoded
    @raises ValueError on failure
    '''
    # add padding if necessary.
    s = to_bytes(s)
    if not s.endswith(b'='):
        s = s + b'=' * (-len(s) % 4)
    try:
        if '_' or '-' in s:
            return base64.urlsafe_b64decode(s)
        else:
            return base64.b64decode(s)
    except (TypeError, binascii.Error) as e:
        raise ValueError(str(e))


def raw_urlsafe_b64encode(b):
    '''Base64 encode using URL-safe encoding with padding removed.

    @param b bytes to decode
    @return bytes decoded
    '''
    b = to_bytes(b)
    b = base64.urlsafe_b64encode(b)
    b = b.rstrip(b'=')  # strip padding
    return b


def visit_page_with_browser(visit_url):
    '''Open a browser so the user can validate its identity.

    @param visit_url: where to prove your identity.
    '''
    webbrowser.open(visit_url, new=1)
    print('Opening an authorization web page in your browser.')
    print('If it does not open, please open this URL:\n', visit_url, '\n')


def cookie(
        url,
        name,
        value,
        expires=None):
    '''Return a new Cookie using a slightly more
    friendly API than that provided by six.moves.http_cookiejar

    @param name The cookie name {str}
    @param value The cookie value {str}
    @param url The URL path of the cookie {str}
    @param expires The expiry time of the cookie {datetime}. If provided,
        it must be a naive timestamp in UTC.
    '''
    u = urlparse(url)
    domain = u.hostname
    if '.' not in domain and not _is_ip_addr(domain):
        domain += ".local"
    port = str(u.port) if u.port is not None else None
    secure = u.scheme == 'https'
    if expires is not None:
        if expires.tzinfo is not None:
            raise ValueError('Cookie expiration must be a naive datetime')
        expires = (expires - datetime(1970, 1, 1)).total_seconds()
    return http_cookiejar.Cookie(
        version=0,
        name=name,
        value=value,
        port=port,
        port_specified=port is not None,
        domain=domain,
        domain_specified=True,
        domain_initial_dot=False,
        path=u.path,
        path_specified=True,
        secure=secure,
        expires=expires,
        discard=False,
        comment=None,
        comment_url=None,
        rest=None,
        rfc2109=False,
    )


def _is_ip_addr(h):
    if six.PY2:
        # the python2.7 backport of ipaddr needs a bytestring passed in
        try:
            h = h.decode('ascii')
        except UnicodeDecodeError:
            # If there are non-ascii chars it's not an address anyway
            return False
    try:
        ipaddress.ip_address(h)
    except ValueError:
        return False
    return True
