import logging

from functools import wraps
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 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_authenticated_user_deferred, set_validated_oauth_token)
from util.http import abort


logger = logging.getLogger(__name__)


def _load_user_from_cookie():
  if not current_user.is_anonymous():
    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_db_id', {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.id, 'user_db_id', 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.id, 'user_db_id', {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.id, 'user_db_id',
                                                {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 process_token(auth):
  normalized = [part.strip() for part in auth.split(' ') if part]
  if normalized[0].lower() != 'token' or len(normalized) != 2:
    logger.debug('Not an auth token: %s' % auth)
    return

  token_details = normalized[1].split(',')

  if len(token_details) != 1:
    logger.warning('Invalid token format: %s' % auth)
    abort(401, message='Invalid token format: %(auth)s', issue='invalid-auth-token', auth=auth)

  def safe_get(lst, index, default_value):
    try:
      return lst[index]
    except IndexError:
      return default_value

  token_vals = {val[0]: safe_get(val, 1, '') for val in
                (detail.split('=') for detail in token_details)}

  if 'signature' not in token_vals:
    logger.warning('Token does not contain signature: %s' % auth)
    abort(401, message='Token does not contain a valid signature: %(auth)s',
          issue='invalid-auth-token', auth=auth)

  try:
    token_data = model.load_token_data(token_vals['signature'])

  except model.InvalidTokenException:
    logger.warning('Token could not be validated: %s', token_vals['signature'])
    abort(401, message='Token could not be validated: %(auth)s', issue='invalid-auth-token',
          auth=auth)

  logger.debug('Successfully validated token: %s', token_data.code)
  set_validated_token(token_data)

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


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_token(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
