import json
import logging
import urlparse

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

from data import model
from data.queue import webhook_queue
from app import app, mixpanel
from auth.auth import (process_auth, get_authenticated_user,
                       get_validated_token)
from util.names import parse_repository_name
from util.email import send_confirmation_email
from auth.permissions import (ModifyRepositoryPermission, UserPermission,
                              ReadRepositoryPermission,
                              CreateRepositoryPermission)

logger = logging.getLogger(__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

      # 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


@app.route('/v1/users', methods=['POST'])
@app.route('/v1/users/', methods=['POST'])
def create_user():
  user_data = request.get_json()
  username = user_data['username']
  password = user_data['password']

  if username == '$token':
    try:
      model.load_token_data(password)
      return make_response('Verified', 201)
    except model.InvalidTokenException:
      return make_response('Invalid access token.', 400)

  elif '+' in username:
    try:
      model.verify_robot(username, password)
      return make_response('Verified', 201)
    except model.InvalidRobotException:
      return make_response('Invalid robot account or password.', 400)

  existing_user = model.get_user(username)
  if existing_user:
    verified = model.verify_user(username, password)
    if verified:
      return make_response('Verified', 201)
    else:
      return make_response('Invalid password.', 400)
  else:
    # New user case
    new_user = model.create_user(username, password, user_data['email'])
    code = model.create_confirm_email_code(new_user)
    send_confirmation_email(new_user.username, new_user.email, code.code)
    return make_response('Created', 201)


@app.route('/v1/users', methods=['GET'])
@app.route('/v1/users/', methods=['GET'])
@process_auth
def get_user():
  if get_authenticated_user():  
    return jsonify({
      'username': get_authenticated_user().username,
      'email': get_authenticated_user().email,
    })
  abort(404)


@app.route('/v1/users/<username>/', methods=['PUT'])
@process_auth
def update_user(username):
  permission = UserPermission(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)


@app.route('/v1/repositories/<path:repository>', methods=['PUT'])
@process_auth
@parse_repository_name
@generate_headers(role='write')
def create_repository(namespace, repository):
  image_descriptions = json.loads(request.data)

  repo = model.get_repository(namespace, repository)

  if not repo and get_authenticated_user() is None:
    logger.debug('Attempt to create new repository without user auth.')
    abort(401)

  elif repo:
    permission = ModifyRepositoryPermission(namespace, repository)
    if not permission.can():
      abort(403)

  else:
    permission = CreateRepositoryPermission(namespace)
    if not permission.can():
      logger.info('Attempt to create a new repo with insufficient perms.')
      abort(403)

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

  new_repo_images = {desc['id']: desc for desc in image_descriptions}
  added_images = dict(new_repo_images)
  for existing in model.get_repository_images(namespace, repository):
    if existing.docker_image_id in new_repo_images:
      added_images.pop(existing.docker_image_id)

  for image_description in added_images.values():
    model.create_image(image_description['id'], repo)

  response = make_response('Created', 201)

  extra_params = {
    'repository': '%s/%s' % (namespace, repository),
  }

  metadata = {
    'repo': repository,
    'namespace': namespace
  }

  if get_authenticated_user():
    mixpanel.track(get_authenticated_user().username, 'push_repo',
                   extra_params)
    metadata['username'] = get_authenticated_user().username
  else:
    mixpanel.track(get_validated_token().code, 'push_repo', extra_params)
    metadata['token'] = get_validated_token().friendly_name
    metadata['token_code'] = get_validated_token().code

  model.log_action('push_repo', namespace, performer=get_authenticated_user(),
                   ip=request.remote_addr, metadata=metadata, repository=repo)

  return response


@app.route('/v1/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():
    repo = model.get_repository(namespace, repository)
    if not repo:
      # Make sure the repo actually exists.
      abort(404)

    image_with_checksums = json.loads(request.data)

    updated_tags = {}
    for image in image_with_checksums:
      logger.debug('Setting checksum for image id: %s to %s' %
                   (image['id'], image['checksum']))
      updated_tags[image['Tag']] = image['id']
      model.set_image_checksum(image['id'], repo, image['checksum'])

    # Generate a job for each webhook that has been added to this repo
    webhooks = model.list_webhooks(namespace, repository)
    for webhook in webhooks:
      webhook_data = json.loads(webhook.parameters)
      repo_string = '%s/%s' % (namespace, repository)
      logger.debug('Creating webhook for repository \'%s\' for url \'%s\'' %
                   (repo_string, webhook_data['url']))
      webhook_data['payload'] = {
        'repository': repo_string,
        'namespace': namespace,
        'name': repository,
        'docker_url': 'quay.io/%s' % repo_string,
        'homepage': 'https://quay.io/repository/%s' % repo_string,
        'visibility': repo.visibility.name,
        'updated_tags': updated_tags,
        'pushed_image_count': len(image_with_checksums), 
      }
      webhook_queue.put(json.dumps(webhook_data))

    return make_response('Updated', 204)

  abort(403)


@app.route('/v1/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?
  is_public = model.repository_is_public(namespace, repository)
  if permission.can() or is_public:
    # We can't rely on permissions to tell us if a repo exists anymore
    repo = model.get_repository(namespace, repository)
    if not repo:
      abort(404)

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

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

    metadata = {
      'repo': repository,
      'namespace': namespace,
    }
    if get_authenticated_user():
      metadata['username'] = get_authenticated_user().username
    elif get_validated_token():
      metadata['token'] = get_validated_token().friendly_name
      metadata['token_code'] = get_validated_token().code
    else:
      metadata['public'] = True

    pull_username = 'anonymous'
    if get_authenticated_user():
      pull_username = get_authenticated_user().username

    extra_params = {
      'repository': '%s/%s' % (namespace, repository),
    }

    mixpanel.track(pull_username, 'pull_repo', extra_params)
    model.log_action('pull_repo', namespace,
                     performer=get_authenticated_user(),
                     ip=request.remote_addr, metadata=metadata,
                     repository=repo)
    return resp

  abort(403)


@app.route('/v1/repositories/<path:repository>/images', methods=['DELETE'])
@process_auth
@parse_repository_name
@generate_headers(role='write')
def delete_repository_images(namespace, repository):
  return make_response('Not Implemented', 501)


@app.route('/v1/repositories/<path:repository>/auth', methods=['PUT'])
@parse_repository_name
def put_repository_auth(namespace, repository):
  return make_response('Not Implemented', 501)


@app.route('/v1/search', methods=['GET'])
def get_search():
  return make_response('Not Implemented', 501)


@app.route('/_ping')
@app.route('/v1/_ping')
def ping():
  response = make_response('true', 200)
  response.headers['X-Docker-Registry-Version'] = '0.6.0'
  return response