import re

import requests
from pyramid import authentication as base_auth
from pyramid.interfaces import IAuthenticationPolicy
from zope.interface import implementer

from kinto.core import logger
from kinto.core import utils as core_utils
from kinto.core.openapi import OpenAPI

from .utils import fetch_openid_config


@implementer(IAuthenticationPolicy)
class OpenIDConnectPolicy(base_auth.CallbackAuthenticationPolicy):
    def __init__(self, issuer, client_id, realm='Realm', **kwargs):
        self.realm = realm
        self.issuer = issuer
        self.client_id = client_id
        self.client_secret = kwargs.get('client_secret', '')
        self.header_type = kwargs.get('header_type', 'Bearer')
        self.userid_field = kwargs.get('userid_field', 'sub')
        self.verification_ttl = int(kwargs.get('verification_ttl_seconds', 86400))

        # Fetch OpenID config (at instantiation, ie. startup)
        self.oid_config = fetch_openid_config(issuer)

        self._jwt_keys = None

    def unauthenticated_userid(self, request):
        """Return the userid or ``None`` if token could not be verified.
        """
        settings = request.registry.settings
        hmac_secret = settings['userid_hmac_secret']

        authorization = request.headers.get('Authorization', '')
        try:
            authmeth, access_token = authorization.split(' ', 1)
        except ValueError:
            return None

        if authmeth.lower() != self.header_type.lower():
            return None

        # XXX JWT Access token
        # https://auth0.com/docs/tokens/access-token#access-token-format

        # Check cache if these tokens were already verified.
        hmac_tokens = core_utils.hmac_digest(hmac_secret, access_token)
        cache_key = 'openid:verify:{}'.format(hmac_tokens)
        payload = request.registry.cache.get(cache_key)
        if payload is None:
            # This can take some time.
            payload = self._verify_token(access_token)
            if payload is None:
                return None
        # Save for next time / refresh ttl.
        request.registry.cache.set(cache_key, payload, ttl=self.verification_ttl)
        # Extract meaningful field from userinfo (eg. email or sub)
        return payload.get(self.userid_field)

    def forget(self, request):
        """A no-op. Credentials are sent on every request.
        Return WWW-Authenticate Realm header for Bearer token.
        """
        return [('WWW-Authenticate', '%s realm="%s"' % (self.header_type, self.realm))]

    def _verify_token(self, access_token):
        uri = self.oid_config['userinfo_endpoint']
        # Opaque access token string. Fetch user info from profile.
        try:
            resp = requests.get(uri, headers={'Authorization': 'Bearer ' + access_token})
            resp.raise_for_status()
            userprofile = resp.json()
            return userprofile

        except (requests.exceptions.HTTPError, ValueError, KeyError) as e:
            logger.debug('Unable to fetch user profile from %s (%s)' % (uri, e))
            return None


def includeme(config):
    # Activate end-points.
    config.scan('kinto.plugins.openid.views')

    settings = config.get_settings()

    openid_policies = []
    for k, v in settings.items():
        m = re.match('multiauth\.policy\.(.*)\.use', k)
        if m:
            if v.endswith('OpenIDConnectPolicy'):
                openid_policies.append(m.group(1))

    if len(openid_policies) == 0:
        # Do not add the capability if no policy is configured.
        return

    providers_infos = []
    for name in openid_policies:
        issuer = settings['multiauth.policy.%s.issuer' % name]
        openid_config = fetch_openid_config(issuer)

        client_id = settings['multiauth.policy.%s.client_id' % name]
        header_type = settings.get('multiauth.policy.%s.header_type', 'Bearer')

        providers_infos.append({
            'name': name,
            'issuer': openid_config['issuer'],
            'auth_path': '/openid/%s/login' % name,
            'client_id': client_id,
            'header_type': header_type,
            'userinfo_endpoint': openid_config['userinfo_endpoint'],
        })

        OpenAPI.expose_authentication_method(name, {
            'type': 'oauth2',
            'authorizationUrl': openid_config['authorization_endpoint'],
        })

    config.add_api_capability(
        'openid',
        description='OpenID connect support.',
        url='http://kinto.readthedocs.io/en/stable/api/1.x/authentication.html',
        providers=providers_infos)
