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
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)

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

import features

logger = logging.getLogger(__name__)
profile = logging.getLogger('application.profiler')

index = Blueprint('index', __name__)

def generate_headers(role='read'):
  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

      if get_authenticated_user():
        session['username'] = get_authenticated_user().username
      else:
        session.pop('username', None)

      # 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:
        repo = model.get_repository(namespace, repository)
        if repo:
          token = model.create_access_token(repo, role)
          token_str = 'signature=%s' % token.code
          response.headers['WWW-Authenticate'] = token_str
          response.headers['X-Docker-Token'] = token_str
        else:
          logger.info('Token request in non-existing repo: %s/%s' %
                      (namespace, repository))

      return response
    return wrapper
  return decorator_method


@index.route('/users', methods=['POST'])
@index.route('/users/', methods=['POST'])
def create_user():
  if not features.USER_CREATION:
    abort(400, 'User creation is disabled. Please speak to your administrator.')

  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 = authentication.verify_user(username, password)
    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, 'Invalid password.', issue='login-failure')

  else:
    # New user case
    profile.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')

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

    profile.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:
      profile.debug('Updating user password')
      model.change_password(get_authenticated_user(),
                            update_request['password'])

    if 'email' in update_request:
      profile.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(role='write')
def create_repository(namespace, repository):
  profile.debug('Parsing image descriptions')
  image_descriptions = json.loads(request.data.decode('utf8'))

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

  profile.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)

    profile.debug('Creaing repository with owner: %s', get_authenticated_user().username)
    repo = model.create_repository(namespace, repository,
                                   get_authenticated_user())

  profile.debug('Determining already added images')
  added_images = OrderedDict([(desc['id'], desc) for desc in image_descriptions])
  new_repo_images = dict(added_images)

  # Optimization: Lookup any existing images in the repository with matching docker IDs and
  # remove them from the added dict, so we don't need to look them up one-by-one.
  def chunks(l, n):
    for i in xrange(0, len(l), n):
      yield l[i:i+n]

  # Note: We do this in chunks in an effort to not hit the SQL query size limit.
  for chunk in chunks(new_repo_images.keys(), 50):
    existing_images = model.lookup_repository_images(namespace, repository, chunk)
    for existing in existing_images:
      added_images.pop(existing.docker_image_id)

  profile.debug('Creating/Linking necessary images')
  username = get_authenticated_user() and get_authenticated_user().username
  translations = {}
  for image_description in added_images.values():
    model.find_create_or_link_image(image_description['id'], repo, username,
                                    translations, storage.preferred_locations[0])


  profile.debug('Created images')
  track_and_log('push_repo', repo)
  return make_response('Created', 201)


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

  if permission.can():
    profile.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')

    if get_authenticated_user():
      profile.debug('Publishing push event')
      username = get_authenticated_user().username

      # Mark that the user has pushed the repo.
      user_data = {
        'action': 'pushed_repo',
        'repository': repository,
        'namespace': namespace
      }

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

    profile.debug('GCing repository')
    num_removed = model.garbage_collect_repository(namespace, repository)

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

    updated_tags = session.get('pushed_tags', {})
    event_data = {
      'updated_tags': updated_tags,
      'pruned_image_count': num_removed
    }
    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(role='read')
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
    profile.debug('Looking up repository')
    repo = model.get_repository(namespace, repository)
    if not repo:
      abort(404, message='Unknown repository', issue='unknown-repo')

    all_images = []
    profile.debug('Retrieving repository images')
    for image in model.get_repository_images(namespace, repository):
      new_image_view = {
        'id': image.docker_image_id,
        'checksum': image.storage.checksum,
      }
      all_images.append(new_image_view)

    profile.debug('Building repository image response')
    resp = make_response(json.dumps(all_images), 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(role='write')
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


@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
