import json
import logging
import urlparse

from flask import request, make_response, jsonify, session
from functools import wraps

from data import model
from app import app, authentication, userevents, storage
from auth.auth import process_auth, generate_signed_token
from auth.auth_context import get_authenticated_user, get_validated_token, get_validated_oauth_token
from util.names import parse_repository_name
from auth.permissions import (ModifyRepositoryPermission, UserAdminPermission,
                              ReadRepositoryPermission, CreateRepositoryPermission,
                              repository_read_grant, repository_write_grant)

from util.http import abort
from endpoints.v1 import v1_bp
from endpoints.trackhelper import track_and_log
from endpoints.notificationhelper import spawn_notification
from endpoints.decorators import anon_protect, anon_allowed


logger = logging.getLogger(__name__)


class GrantType(object):
  READ_REPOSITORY = 'read'
  WRITE_REPOSITORY = 'write'


def generate_headers(scope=GrantType.READ_REPOSITORY, add_grant_for_status=None):
  def decorator_method(f):
    @wraps(f)
    def wrapper(namespace, repository, *args, **kwargs):
      response = f(namespace, repository, *args, **kwargs)

      # Setting session namespace and repository
      session['namespace'] = namespace
      session['repository'] = repository

      # We run our index and registry on the same hosts for now
      registry_server = urlparse.urlparse(request.url).netloc
      response.headers['X-Docker-Endpoints'] = registry_server

      has_token_request = request.headers.get('X-Docker-Token', '')
      force_grant = (add_grant_for_status == response.status_code)

      if has_token_request or force_grant:
        grants = []

        if scope == GrantType.READ_REPOSITORY:
          if force_grant or ReadRepositoryPermission(namespace, repository).can():
            grants.append(repository_read_grant(namespace, repository))
        elif scope == GrantType.WRITE_REPOSITORY:
          if force_grant or ModifyRepositoryPermission(namespace, repository).can():
            grants.append(repository_write_grant(namespace, repository))

        # Generate a signed token for the user (if any) and the grants (if any)
        if grants or get_authenticated_user():
          user_context = get_authenticated_user() and get_authenticated_user().username
          signature = generate_signed_token(grants, user_context)
          response.headers['WWW-Authenticate'] = signature
          response.headers['X-Docker-Token'] = signature

      return response
    return wrapper
  return decorator_method


@v1_bp.route('/users', methods=['POST'])
@v1_bp.route('/users/', methods=['POST'])
@anon_allowed
def create_user():
  user_data = request.get_json()
  if not user_data or not 'username' in user_data:
    abort(400, 'Missing username')

  username = user_data['username']
  password = user_data.get('password', '')

  # UGH! we have to use this response when the login actually worked, in order
  # to get the CLI to try again with a get, and then tell us login succeeded.
  success = make_response('"Username or email already exists"', 400)

  if username == '$token':
    try:
      model.token.load_token_data(password)
      return success
    except model.InvalidTokenException:
      abort(400, 'Invalid access token.', issue='invalid-access-token')

  elif username == '$oauthtoken':
    validated = model.oauth.validate_access_token(password)
    if validated is not None:
      return success
    else:
      abort(400, 'Invalid oauth access token.', issue='invalid-oauth-access-token')

  elif '+' in username:
    try:
      model.user.verify_robot(username, password)
      return success
    except model.InvalidRobotException:
      abort(400, 'Invalid robot account or password.',
            issue='robot-login-failure')

  (verified, error_message) = authentication.verify_and_link_user(username, password,
                                                                  basic_auth=True)
  if verified:
    # Mark that the user was logged in.
    event = userevents.get_event(username)
    event.publish_event_data('docker-cli', {'action': 'login'})
    return success
  else:
    # Mark that the login failed.
    event = userevents.get_event(username)
    event.publish_event_data('docker-cli', {'action': 'loginfailure'})
    abort(400, error_message, issue='login-failure')


@v1_bp.route('/users', methods=['GET'])
@v1_bp.route('/users/', methods=['GET'])
@process_auth
@anon_allowed
def get_user():
  if get_validated_oauth_token():
    return jsonify({
      'username': '$oauthtoken',
      'email': None,
    })
  elif get_authenticated_user():
    return jsonify({
      'username': get_authenticated_user().username,
      'email': get_authenticated_user().email,
    })
  elif get_validated_token():
    return jsonify({
      'username': '$token',
      'email': None,
    })
  abort(404)


@v1_bp.route('/users/<username>/', methods=['PUT'])
@process_auth
@anon_allowed
def update_user(username):
  permission = UserAdminPermission(username)

  if permission.can():
    update_request = request.get_json()

    if 'password' in update_request:
      logger.debug('Updating user password')
      model.user.change_password(get_authenticated_user(), update_request['password'])

    if 'email' in update_request:
      logger.debug('Updating user email')
      model.user.update_email(get_authenticated_user(), update_request['email'])

    return jsonify({
      'username': get_authenticated_user().username,
      'email': get_authenticated_user().email,
    })

  abort(403)


@v1_bp.route('/repositories/<path:repository>', methods=['PUT'])
@process_auth
@parse_repository_name
@generate_headers(scope=GrantType.WRITE_REPOSITORY, add_grant_for_status=201)
@anon_allowed
def create_repository(namespace, repository):
  logger.debug('Looking up repository %s/%s', namespace, repository)
  repo = model.repository.get_repository(namespace, repository)

  logger.debug('Found repository %s/%s', namespace, repository)
  if not repo and get_authenticated_user() is None:
    logger.debug('Attempt to create repository %s/%s without user auth', namespace, repository)
    abort(401,
          message='Cannot create a repository as a guest. Please login via "docker login" first.',
          issue='no-login')

  elif repo:
    permission = ModifyRepositoryPermission(namespace, repository)
    if not permission.can():
      abort(403,
            message='You do not have permission to modify repository %(namespace)s/%(repository)s',
            issue='no-repo-write-permission',
            namespace=namespace, repository=repository)
  else:
    permission = CreateRepositoryPermission(namespace)
    if not permission.can():
      logger.info('Attempt to create a new repo %s/%s with insufficient perms', namespace,
                  repository)
      msg = 'You do not have permission to create repositories in namespace "%(namespace)s"'
      abort(403, message=msg, issue='no-create-permission', namespace=namespace)

    # Attempt to create the new repository.
    logger.debug('Creating repository %s/%s with owner: %s', namespace, repository,
                 get_authenticated_user().username)

    repo = model.repository.create_repository(namespace, repository, get_authenticated_user())

  if get_authenticated_user():
    user_event_data = {
      'action': 'push_start',
      'repository': repository,
      'namespace': namespace
    }

    event = userevents.get_event(get_authenticated_user().username)
    event.publish_event_data('docker-cli', user_event_data)

  return make_response('Created', 201)


@v1_bp.route('/repositories/<path:repository>/images', methods=['PUT'])
@process_auth
@parse_repository_name
@generate_headers(scope=GrantType.WRITE_REPOSITORY)
@anon_allowed
def update_images(namespace, repository):
  permission = ModifyRepositoryPermission(namespace, repository)

  if permission.can():
    logger.debug('Looking up repository')
    repo = model.repository.get_repository(namespace, repository)
    if not repo:
      # Make sure the repo actually exists.
      abort(404, message='Unknown repository', issue='unknown-repo')

    logger.debug('GCing repository')
    model.repository.garbage_collect_repository(namespace, repository)

    # Generate a job for each notification that has been added to this repo
    logger.debug('Adding notifications for repository')

    updated_tags = session.get('pushed_tags', {})
    event_data = {
      'updated_tags': updated_tags,
    }

    track_and_log('push_repo', repo)
    spawn_notification(repo, 'repo_push', event_data)
    return make_response('Updated', 204)

  abort(403)


@v1_bp.route('/repositories/<path:repository>/images', methods=['GET'])
@process_auth
@parse_repository_name
@generate_headers(scope=GrantType.READ_REPOSITORY)
@anon_protect
def get_repository_images(namespace, repository):
  permission = ReadRepositoryPermission(namespace, repository)

  # TODO invalidate token?
  if permission.can() or model.repository.repository_is_public(namespace, repository):
    # We can't rely on permissions to tell us if a repo exists anymore
    logger.debug('Looking up repository')
    repo = model.repository.get_repository(namespace, repository)
    if not repo:
      abort(404, message='Unknown repository', issue='unknown-repo')

    logger.debug('Building repository image response')
    resp = make_response(json.dumps([]), 200)
    resp.mimetype = 'application/json'

    track_and_log('pull_repo', repo)
    return resp

  abort(403)


@v1_bp.route('/repositories/<path:repository>/images', methods=['DELETE'])
@process_auth
@parse_repository_name
@generate_headers(scope=GrantType.WRITE_REPOSITORY)
@anon_allowed
def delete_repository_images(namespace, repository):
  abort(501, 'Not Implemented', issue='not-implemented')


@v1_bp.route('/repositories/<path:repository>/auth', methods=['PUT'])
@parse_repository_name
@anon_allowed
def put_repository_auth(namespace, repository):
  abort(501, 'Not Implemented', issue='not-implemented')


@v1_bp.route('/search', methods=['GET'])
@process_auth
@anon_protect
def get_search():
  def result_view(repo):
    return {
      "name": repo.namespace_user.username + '/' + repo.name,
      "description": repo.description
    }

  query = request.args.get('q')

  username = None
  user = get_authenticated_user()
  if user is not None:
    username = user.username

  if query:
    matching = model.repository.get_matching_repositories(query, username)
  else:
    matching = []

  results = [result_view(repo) for repo in matching
             if (repo.visibility.name == 'public' or
                 ReadRepositoryPermission(repo.namespace_user.username, repo.name).can())]

  data = {
    "query": query,
    "num_results": len(results),
    "results" : results
  }

  resp = make_response(json.dumps(data), 200)
  resp.mimetype = 'application/json'
  return resp
