import logging
import json
import datetime
from datetime import timedelta

from flask import request

from data import model
from endpoints.api import (truthy_bool, format_date, nickname, log_action, validate_json_request,
                           require_repo_read, require_repo_write, require_repo_admin,
                           RepositoryParamResource, resource, query_param, parse_args, ApiResource,
                           request_error, require_scope, Unauthorized, NotFound, InvalidRequest,
                           path_param)

from auth.permissions import (ModifyRepositoryPermission, AdministerRepositoryPermission,
                              CreateRepositoryPermission, ReadRepositoryPermission)
from auth.auth_context import get_authenticated_user
from auth import scopes


logger = logging.getLogger(__name__)


@resource('/v1/repository')
class RepositoryList(ApiResource):
  """Operations for creating and listing repositories."""
  schemas = {
    'NewRepo': {
      'id': 'NewRepo',
      'type': 'object',
      'description': 'Description of a new repository',
      'required': [
        'repository',
        'visibility',
        'description',
      ],
      'properties': {
        'repository': {
          'type': 'string',
          'description': 'Repository name',
        },
        'visibility': {
          'type': 'string',
          'description': 'Visibility which the repository will start with',
          'enum': [
            'public',
            'private',
          ],
        },
        'namespace': {
          'type': 'string',
          'description': ('Namespace in which the repository should be created. If omitted, the '
                          'username of the caller is used'),
        },
        'description': {
          'type': 'string',
          'description': 'Markdown encoded description for the repository',
        },
      },
    },
  }

  @require_scope(scopes.CREATE_REPO)
  @nickname('createRepo')
  @validate_json_request('NewRepo')
  def post(self):
    """Create a new repository."""
    owner = get_authenticated_user()
    req = request.get_json()

    if owner is None and 'namespace' not in 'req':
      raise InvalidRequest('Must provide a namespace or must be logged in.')

    namespace_name = req['namespace'] if 'namespace' in req else owner.username

    permission = CreateRepositoryPermission(namespace_name)
    if permission.can():
      repository_name = req['repository']
      visibility = req['visibility']

      existing = model.get_repository(namespace_name, repository_name)
      if existing:
        raise request_error(message='Repository already exists')

      visibility = req['visibility']

      repo = model.create_repository(namespace_name, repository_name, owner, visibility)
      repo.description = req['description']
      repo.save()

      log_action('create_repo', namespace_name, {'repo': repository_name,
                                                 'namespace': namespace_name}, repo=repo)
      return {
        'namespace': namespace_name,
        'name': repository_name
      }, 201

    raise Unauthorized()

  @require_scope(scopes.READ_REPO)
  @nickname('listRepos')
  @parse_args
  @query_param('page', 'Offset page number. (int)', type=int)
  @query_param('limit', 'Limit on the number of results (int)', type=int)
  @query_param('namespace', 'Namespace to use when querying for org repositories.', type=str)
  @query_param('public', 'Whether to include public repositories.', type=truthy_bool, default=True)
  @query_param('private', 'Whether to include private repositories.', type=truthy_bool,
               default=True)
  @query_param('sort', 'Whether to sort the results.', type=truthy_bool, default=False)
  @query_param('count', 'Whether to include a count of the total number of results available.',
               type=truthy_bool, default=False)
  def get(self, args):
    """Fetch the list of repositories under a variety of situations."""
    username = None
    if get_authenticated_user():
      starred_repos = model.get_user_starred_repositories(get_authenticated_user())
      star_lookup = set([repo.id for repo in starred_repos])

      if args['private']:
        username = get_authenticated_user().username

    response = {}

    repo_count = None
    if args['count']:
      repo_count = model.get_visible_repository_count(username, include_public=args['public'],
                                                      namespace=args['namespace'])
      response['count'] = repo_count

    repo_query = model.get_visible_repositories(username, limit=args['limit'], page=args['page'],
                                                include_public=args['public'], sort=args['sort'],
                                                namespace=args['namespace'])
    def repo_view(repo_obj):
      repo = {
        'namespace': repo_obj.namespace_user.username,
        'name': repo_obj.name,
        'description': repo_obj.description,
        'is_public': repo_obj.visibility.name == 'public',
      }
      if get_authenticated_user():
        repo['is_starred'] = repo_obj.id in star_lookup
      return repo

    response['repositories'] = [repo_view(repo) for repo in repo_query
                                if (repo.visibility.name == 'public' or
                                    ReadRepositoryPermission(repo.namespace_user.username,
                                                             repo.name).can())]

    return response


@resource('/v1/repository/<repopath:repository>')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
class Repository(RepositoryParamResource):
  """Operations for managing a specific repository."""
  schemas = {
    'RepoUpdate': {
      'id': 'RepoUpdate',
      'type': 'object',
      'description': 'Fields which can be updated in a repository.',
      'required': [
        'description',
      ],
      'properties': {
        'description': {
          'type': 'string',
          'description': 'Markdown encoded description for the repository',
        },
      }
    }
  }

  @require_repo_read
  @nickname('getRepo')
  def get(self, namespace, repository):
    """Fetch the specified repository."""
    logger.debug('Get repo: %s/%s' % (namespace, repository))

    def tag_view(tag):
      return {
        'name': tag.name,
        'image_id': tag.image.docker_image_id,
        'last_modified': format_date(datetime.datetime.fromtimestamp(tag.lifetime_start_ts)),
        'size': tag.image.storage.aggregate_size
      }

    organization = None
    try:
      organization = model.get_organization(namespace)
    except model.InvalidOrganizationException:
      pass

    is_public = model.repository_is_public(namespace, repository)
    repo = model.get_repository(namespace, repository)
    if repo:
      tags = model.list_repository_tags(namespace, repository)
      tag_dict = {tag.name: tag_view(tag) for tag in tags}
      can_write = ModifyRepositoryPermission(namespace, repository).can()
      can_admin = AdministerRepositoryPermission(namespace, repository).can()
      active_builds = model.list_repository_builds(namespace, repository, 1,
                                                   include_inactive=False)

      is_starred = (model.repository_is_starred(get_authenticated_user(), repo)
                    if get_authenticated_user() else False)

      return {
        'namespace': namespace,
        'name': repository,
        'description': repo.description,
        'tags': tag_dict,
        'can_write': can_write,
        'can_admin': can_admin,
        'is_public': is_public,
        'is_building': len(list(active_builds)) > 0,
        'is_organization': bool(organization),
        'is_starred': is_starred,
        'status_token': repo.badge_token if not is_public else '',
        'stats': {
          'pulls': {
            'today': model.get_repository_pulls(repo, timedelta(days=1)),
            'thirty_day': model.get_repository_pulls(repo, timedelta(days=30))
          },
          'pushes': {
            'today': model.get_repository_pushes(repo, timedelta(days=1)),
            'thirty_day': model.get_repository_pushes(repo, timedelta(days=30))
          }
        }
      }

    raise NotFound()

  @require_repo_write
  @nickname('updateRepo')
  @validate_json_request('RepoUpdate')
  def put(self, namespace, repository):
    """ Update the description in the specified repository. """
    repo = model.get_repository(namespace, repository)
    if repo:
      values = request.get_json()
      repo.description = values['description']
      repo.save()

      log_action('set_repo_description', namespace,
                 {'repo': repository, 'description': values['description']},
                 repo=repo)
      return {
          'success': True
      }
    raise NotFound()

  @require_repo_admin
  @nickname('deleteRepository')
  def delete(self, namespace, repository):
    """ Delete a repository. """
    model.purge_repository(namespace, repository)
    log_action('delete_repo', namespace,
               {'repo': repository, 'namespace': namespace})
    return 'Deleted', 204


@resource('/v1/repository/<repopath:repository>/changevisibility')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
class RepositoryVisibility(RepositoryParamResource):
  """ Custom verb for changing the visibility of the repository. """
  schemas = {
    'ChangeVisibility': {
      'id': 'ChangeVisibility',
      'type': 'object',
      'description': 'Change the visibility for the repository.',
      'required': [
        'visibility',
      ],
      'properties': {
        'visibility': {
          'type': 'string',
          'description': 'Visibility which the repository will start with',
          'enum': [
            'public',
            'private',
          ],
        },
      }
    }
  }

  @require_repo_admin
  @nickname('changeRepoVisibility')
  @validate_json_request('ChangeVisibility')
  def post(self, namespace, repository):
    """ Change the visibility of a repository. """
    repo = model.get_repository(namespace, repository)
    if repo:
      values = request.get_json()
      model.set_repository_visibility(repo, values['visibility'])
      log_action('change_repo_visibility', namespace,
                 {'repo': repository, 'visibility': values['visibility']},
                 repo=repo)
      return {'success': True}