import logging

from functools import wraps

from jsonschema import validate, ValidationError
from flask import request, url_for
from flask_principal import identity_changed, Identity

from app import app, get_app_url, instance_keys
from .auth_context import set_grant_context, get_grant_context
from .permissions import repository_read_grant, repository_write_grant
from util.names import parse_namespace_repository
from util.http import abort
from util.security.registry_jwt import (ANONYMOUS_SUB, decode_bearer_header,
                                        InvalidBearerTokenException)
from data import model


logger = logging.getLogger(__name__)

CONTEXT_KINDS = ['user', 'token', 'oauth']

ACCESS_SCHEMA = {
  'type': 'array',
  'description': 'List of access granted to the subject',
  'items': {
    'type': 'object',
    'required': [
      'type',
      'name',
      'actions',
    ],
    'properties': {
      'type': {
        'type': 'string',
        'description': 'We only allow repository permissions',
        'enum': [
          'repository',
        ],
      },
      'name': {
        'type': 'string',
        'description': 'The name of the repository for which we are receiving access'
      },
      'actions': {
        'type': 'array',
        'description': 'List of specific verbs which can be performed against repository',
        'items': {
          'type': 'string',
          'enum': [
            'push',
            'pull',
          ],
        },
      },
    },
  },
}


class InvalidJWTException(Exception):
  pass


class GrantedEntity(object):
  def __init__(self, user=None, token=None, oauth=None):
    self.user = user
    self.token = token
    self.oauth = oauth


def get_granted_entity():
  """ Returns the entity granted in the current context, if any. Returns the GrantedEntity or None
      if none.
  """
  context = get_grant_context()
  if not context:
    return None

  kind = context.get('kind', 'anonymous')

  if not kind in CONTEXT_KINDS:
    return None

  if kind == 'user':
    user = model.user.get_user(context.get('user', ''))
    if not user:
      return None

    return GrantedEntity(user=user)

  if kind == 'token':
    token = model.token.load_token_data(context.get('token'))
    if not token:
      return None

    return GrantedEntity(token=token)

  if kind == 'oauth':
    user = model.user.get_user(context.get('user', ''))
    if not user:
      return None

    oauthtoken = model.oauth.lookup_access_token_for_user(user, context.get('oauth', ''))
    if not oauthtoken:
      return None

    return GrantedEntity(oauth=oauthtoken, user=user)

  return None


def get_granted_username():
  """ Returns the username inside the grant, if any. """
  granted = get_granted_entity()
  if not granted or not granted.user:
    return None

  return granted.user.username


def get_auth_headers(repository=None, scopes=None):
  """ Returns a dictionary of headers for auth responses. """
  headers = {}
  realm_auth_path = url_for('v2.generate_registry_jwt')
  authenticate = 'Bearer realm="{0}{1}",service="{2}"'.format(get_app_url(),
                                                              realm_auth_path,
                                                              app.config['SERVER_HOSTNAME'])
  if repository:
    scopes_string = "repository:{0}".format(repository)
    if scopes:
      scopes_string += ':' + ','.join(scopes)

    authenticate += ',scope="{0}"'.format(scopes_string)

  headers['WWW-Authenticate'] = authenticate
  headers['Docker-Distribution-API-Version'] = 'registry/2.0'
  return headers


def identity_from_bearer_token(bearer_header):
  """ Process a bearer header and return the loaded identity, or raise InvalidJWTException if an
      identity could not be loaded. Expects tokens and grants in the format of the Docker registry
      v2 auth spec: https://docs.docker.com/registry/spec/auth/token/
  """
  logger.debug('Validating auth header: %s', bearer_header)

  try:
    payload = decode_bearer_header(bearer_header, instance_keys, app.config)
  except InvalidBearerTokenException as bte:
    logger.exception('Invalid bearer token: %s', bte)
    raise InvalidJWTException(bte)

  loaded_identity = Identity(payload['sub'], 'signed_jwt')

  # Process the grants from the payload
  if 'access' in payload:
    try:
      validate(payload['access'], ACCESS_SCHEMA)
    except ValidationError:
      logger.exception('We should not be minting invalid credentials')
      raise InvalidJWTException('Token contained invalid or malformed access grants')

    lib_namespace = app.config['LIBRARY_NAMESPACE']
    for grant in payload['access']:
      namespace, repo_name = parse_namespace_repository(grant['name'], lib_namespace)

      if 'push' in grant['actions']:
        loaded_identity.provides.add(repository_write_grant(namespace, repo_name))
      elif 'pull' in grant['actions']:
        loaded_identity.provides.add(repository_read_grant(namespace, repo_name))

  default_context = {
    'kind': 'anonymous'
  }

  if payload['sub'] != ANONYMOUS_SUB:
    default_context = {
      'kind': 'user',
      'user': payload['sub'],
    }

  return loaded_identity, payload.get('context', default_context)


def process_registry_jwt_auth(scopes=None):
  def inner(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
      logger.debug('Called with params: %s, %s', args, kwargs)
      auth = request.headers.get('authorization', '').strip()
      if auth:
        try:
          extracted_identity, context = identity_from_bearer_token(auth)
          identity_changed.send(app, identity=extracted_identity)
          set_grant_context(context)
          logger.debug('Identity changed to %s', extracted_identity.id)
        except InvalidJWTException as ije:
          repository = None
          if 'namespace_name' in kwargs and 'repo_name' in kwargs:
            repository = kwargs['namespace_name'] + '/' + kwargs['repo_name']

          abort(401, message=ije.message, headers=get_auth_headers(repository=repository,
                                                                   scopes=scopes))
      else:
        logger.debug('No auth header.')

      return func(*args, **kwargs)
    return wrapper
  return inner