import json
import logging
import urlparse

from flask import request, make_response, jsonify, session, Blueprint
from functools import wraps
from collections import OrderedDict

from data import model
from data.model import oauth
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 util.useremails import send_confirmation_email
from auth.permissions import (ModifyRepositoryPermission, UserAdminPermission,
                              ReadRepositoryPermission, CreateRepositoryPermission,
                              AlwaysFailPermission, repository_read_grant, repository_write_grant)

from util.http import abort
from endpoints.trackhelper import track_and_log
from endpoints.notificationhelper import spawn_notification

import features

logger = logging.getLogger(__name__)

index = Blueprint('index', __name__)


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


def generate_headers(scope=GrantType.READ_REPOSITORY):
  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', '')

      if has_token_request:
        permission = AlwaysFailPermission()
        grants = []
        if scope == GrantType.READ_REPOSITORY:
          permission = ReadRepositoryPermission(namespace, repository)
          grants.append(repository_read_grant(namespace, repository))
        elif scope == GrantType.WRITE_REPOSITORY:
          permission = ModifyRepositoryPermission(namespace, repository)
          grants.append(repository_write_grant(namespace, repository))

        if permission.can():
          # Generate a signed grant which expires here
          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
        else:
          logger.warning('Registry request with invalid credentials on repository: %s/%s',
                         namespace, repository)
      return response
    return wrapper
  return decorator_method


@index.route('/users', methods=['POST'])
@index.route('/users/', methods=['POST'])
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.load_token_data(password)
      return success
    except model.InvalidTokenException:
      abort(400, 'Invalid access token.', issue='invalid-access-token')

  elif username == '$oauthtoken':
    validated = 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.verify_robot(username, password)
      return success
    except model.InvalidRobotException:
      abort(400, 'Invalid robot account or password.',
            issue='robot-login-failure')

  if authentication.user_exists(username):
    (verified, error_message) = authentication.verify_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')

  elif not features.USER_CREATION:
    abort(400, 'User creation is disabled. Please speak to your administrator.')

  else:
    # New user case
    logger.debug('Creating user')
    new_user = None

    try:
      new_user = model.create_user(username, password, user_data['email'])
    except model.TooManyUsersException as ex:
      abort(402, 'Seat limit has been reached for this license', issue='seat-limit')

    logger.debug('Creating email code for user')
    code = model.create_confirm_email_code(new_user)

    logger.debug('Sending email code to user')
    send_confirmation_email(new_user.username, new_user.email, code.code)

    return make_response('Created', 201)


@index.route('/users', methods=['GET'])
@index.route('/users/', methods=['GET'])
@process_auth
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)


@index.route('/users/<username>/', methods=['PUT'])
@process_auth
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.change_password(get_authenticated_user(),
                            update_request['password'])

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

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

  abort(403)


@index.route('/repositories/<path:repository>', methods=['PUT'])
@process_auth
@parse_repository_name
@generate_headers(scope=GrantType.WRITE_REPOSITORY)
def create_repository(namespace, repository):
  logger.debug('Parsing image descriptions')
  image_descriptions = json.loads(request.data.decode('utf8'))

  logger.debug('Looking up repository')
  repo = model.get_repository(namespace, repository)

  logger.debug('Repository looked up')
  if not repo and get_authenticated_user() is None:
    logger.debug('Attempt to create new repository without user auth.')
    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 with insufficient perms.')
      abort(403,
            message='You do not have permission to create repositories in namespace "%(namespace)s"',
            issue='no-create-permission',
            namespace=namespace)

    logger.debug('Creaing repository with owner: %s', get_authenticated_user().username)
    repo = model.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)


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

  if permission.can():
    logger.debug('Looking up repository')
    repo = model.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.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)


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

  # TODO invalidate token?
  if permission.can() or model.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.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)


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


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


@index.route('/search', methods=['GET'])
@process_auth
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.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

# Note: This is *not* part of the Docker index spec. This is here for our own health check,
# since we have nginx handle the _ping below.
@index.route('/_internal_ping')
def internal_ping():
  return make_response('true', 200)

@index.route('/_ping')
@index.route('/_ping')
def ping():
  # NOTE: any changes made here must also be reflected in the nginx config
  response = make_response('true', 200)
  response.headers['X-Docker-Registry-Version'] = '0.6.0'
  response.headers['X-Docker-Registry-Standalone'] = '0'
  return response