import logging

from functools import wraps
from uuid import UUID
from datetime import datetime
from flask import request, session
from flask.ext.principal import identity_changed, Identity
from flask.ext.login import current_user
from flask.sessions import SecureCookieSessionInterface, BadSignature
from base64 import b64decode

import scopes

from data import model
from data.model import oauth
from app import app, authentication
from permissions import QuayDeferredPermissionUser
from auth_context import (set_authenticated_user, set_validated_token, set_grant_user_context,
                          set_authenticated_user_deferred, set_validated_oauth_token)
from util.http import abort


logger = logging.getLogger(__name__)

SIGNATURE_PREFIX = 'sigv2='

def _load_user_from_cookie():
  if not current_user.is_anonymous():
    try:
      # Attempt to parse the user uuid to make sure the cookie has the right value type
      UUID(current_user.get_id())
    except ValueError:
      return None

    logger.debug('Loading user from cookie: %s', current_user.get_id())
    set_authenticated_user_deferred(current_user.get_id())
    loaded = QuayDeferredPermissionUser(current_user.get_id(), 'user_uuid', {scopes.DIRECT_LOGIN})
    identity_changed.send(app, identity=loaded)
    return current_user.db_user()
  return None


def _validate_and_apply_oauth_token(token):
  validated = oauth.validate_access_token(token)
  if not validated:
    logger.warning('OAuth access token could not be validated: %s', token)
    authenticate_header = {
      'WWW-Authenticate': ('Bearer error="invalid_token", '
                           'error_description="The access token is invalid"'),
    }
    abort(401, message='OAuth access token could not be validated: %(token)s',
          issue='invalid-oauth-token', token=token, headers=authenticate_header)
  elif validated.expires_at <= datetime.utcnow():
    logger.info('OAuth access with an expired token: %s', token)
    authenticate_header = {
      'WWW-Authenticate': ('Bearer error="invalid_token", '
                           'error_description="The access token expired"'),
    }
    abort(401, message='OAuth access token has expired: %(token)s',
          issue='invalid-oauth-token', token=token, headers=authenticate_header)

  # We have a valid token
  scope_set = scopes.scopes_from_scope_string(validated.scope)
  logger.debug('Successfully validated oauth access token: %s with scope: %s', token,
               scope_set)

  set_authenticated_user(validated.authorized_user)
  set_validated_oauth_token(validated)

  new_identity = QuayDeferredPermissionUser(validated.authorized_user.uuid, 'user_uuid', scope_set)
  identity_changed.send(app, identity=new_identity)


def _process_basic_auth(auth):
  normalized = [part.strip() for part in auth.split(' ') if part]
  if normalized[0].lower() != 'basic' or len(normalized) != 2:
    logger.debug('Invalid basic auth format.')
    return

  credentials = [part.decode('utf-8') for part in b64decode(normalized[1]).split(':', 1)]

  if len(credentials) != 2:
    logger.debug('Invalid basic auth credential format.')

  elif credentials[0] == '$token':
    # Use as token auth
    try:
      token = model.load_token_data(credentials[1])
      logger.debug('Successfully validated token: %s' % credentials[1])
      set_validated_token(token)

      identity_changed.send(app, identity=Identity(token.code, 'token'))
      return

    except model.DataModelException:
      logger.debug('Invalid token: %s' % credentials[1])

  elif credentials[0] == '$oauthtoken':
    oauth_token = credentials[1]
    _validate_and_apply_oauth_token(oauth_token)

  elif '+' in credentials[0]:
    logger.debug('Trying robot auth with credentials %s' % str(credentials))
    # Use as robot auth
    try:
      robot = model.verify_robot(credentials[0], credentials[1])
      logger.debug('Successfully validated robot: %s' % credentials[0])
      set_authenticated_user(robot)

      deferred_robot = QuayDeferredPermissionUser(robot.uuid, 'user_uuid', {scopes.DIRECT_LOGIN})
      identity_changed.send(app, identity=deferred_robot)
      return
    except model.InvalidRobotException:
      logger.debug('Invalid robot or password for robot: %s' % credentials[0])

  else:
    authenticated = authentication.verify_user(credentials[0], credentials[1])

    if authenticated:
      logger.debug('Successfully validated user: %s' % authenticated.username)
      set_authenticated_user(authenticated)

      new_identity = QuayDeferredPermissionUser(authenticated.uuid, 'user_uuid',
                                                {scopes.DIRECT_LOGIN})
      identity_changed.send(app, identity=new_identity)
      return

  # We weren't able to authenticate via basic auth.
  logger.debug('Basic auth present but could not be validated.')


def generate_signed_token(grants, user_context):
  ser = SecureCookieSessionInterface().get_signing_serializer(app)
  data_to_sign = {
    'grants': grants,
    'user_context': user_context,
  }

  encrypted = ser.dumps(data_to_sign)
  return '{0}{1}'.format(SIGNATURE_PREFIX, encrypted)


def _process_signed_grant(auth):
  normalized = [part.strip() for part in auth.split(' ') if part]
  if normalized[0].lower() != 'token' or len(normalized) != 2:
    logger.debug('Not a token: %s', auth)
    return

  if not normalized[1].startswith(SIGNATURE_PREFIX):
    logger.debug('Not a signed grant token: %s', auth)
    return

  encrypted = normalized[1][len(SIGNATURE_PREFIX):]
  ser = SecureCookieSessionInterface().get_signing_serializer(app)

  try:
    token_data = ser.loads(encrypted, max_age=app.config['SIGNED_GRANT_EXPIRATION_SEC'])
  except BadSignature:
    logger.warning('Signed grant could not be validated: %s', encrypted)
    abort(401, message='Signed grant could not be validated: %(auth)s', issue='invalid-auth-token',
          auth=auth)

  logger.debug('Successfully validated signed grant with data: %s', token_data)

  loaded_identity = Identity(None, 'signed_grant')
  set_grant_user_context(token_data['user_context'])
  loaded_identity.provides.update(token_data['grants'])
  identity_changed.send(app, identity=loaded_identity)


def process_oauth(func):
  @wraps(func)
  def wrapper(*args, **kwargs):
    auth = request.headers.get('authorization', '')
    if auth:
      normalized = [part.strip() for part in auth.split(' ') if part]
      if normalized[0].lower() != 'bearer' or len(normalized) != 2:
        logger.debug('Invalid oauth bearer token format.')
        return

      token = normalized[1]
      _validate_and_apply_oauth_token(token)
    elif _load_user_from_cookie() is None:
      logger.debug('No auth header or login cookie.')
    return func(*args, **kwargs)
  return wrapper


def process_auth(func):
  @wraps(func)
  def wrapper(*args, **kwargs):
    auth = request.headers.get('authorization', '')

    if auth:
      logger.debug('Validating auth header: %s' % auth)
      _process_signed_grant(auth)
      _process_basic_auth(auth)
    else:
      logger.debug('No auth header.')

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


def require_session_login(func):
  @wraps(func)
  def wrapper(*args, **kwargs):
    loaded = _load_user_from_cookie()
    if loaded is None or loaded.organization:
      abort(401, message='Method requires login and no valid login could be loaded.')
    return func(*args, **kwargs)
  return wrapper


def extract_namespace_repo_from_session(func):
  @wraps(func)
  def wrapper(*args, **kwargs):
    if 'namespace' not in session or 'repository' not in session:
      logger.error('Unable to load namespace or repository from session: %s' % session)
      abort(400, message='Missing namespace in request')

    return func(session['namespace'], session['repository'], *args, **kwargs)
  return wrapper