import bcrypt
import logging
import dateutil.parser
import json
import time

from datetime import datetime, timedelta, date
from uuid import uuid4

from data.database import (User, Repository, Image, AccessToken, Role, RepositoryPermission,
                           Visibility, RepositoryTag, EmailConfirmation, FederatedLogin,
                           LoginService, RepositoryBuild, Team, TeamMember, TeamRole,
                           LogEntryKind, LogEntry, PermissionPrototype, ImageStorage,
                           BuildTriggerService, RepositoryBuildTrigger, NotificationKind,
                           Notification, ImageStorageLocation, ImageStoragePlacement,
                           ExternalNotificationEvent, ExternalNotificationMethod,
                           RepositoryNotification, RepositoryAuthorizedEmail, TeamMemberInvite,
                           DerivedImageStorage, ImageStorageTransformation, random_string_generator,
                           db, BUILD_PHASE, QuayUserField, ImageStorageSignature, QueueItem,
                           ImageStorageSignatureKind, validate_database_url, db_for_update,
                           AccessTokenKind, Star, get_epoch_timestamp, RepositoryActionCount)
from peewee import JOIN_LEFT_OUTER, fn
from util.validation import (validate_username, validate_email, validate_password,
                             INVALID_PASSWORD_MESSAGE)
from util.names import format_robot_username, parse_robot_username
from util.backoff import exponential_backoff


EXPONENTIAL_BACKOFF_SCALE = timedelta(seconds=1)
PRESUMED_DEAD_BUILD_AGE = timedelta(days=15)


Namespace = User.alias()


logger = logging.getLogger(__name__)


class Config(object):
  def __init__(self):
    self.app_config = None
    self.store = None

config = Config()


class DataModelException(Exception):
  pass


class InvalidEmailAddressException(DataModelException):
  pass


class InvalidUsernameException(DataModelException):
  pass


class InvalidOrganizationException(DataModelException):
  pass


class InvalidRobotException(DataModelException):
  pass


class InvalidTeamException(DataModelException):
  pass

class InvalidTeamMemberException(DataModelException):
  pass


class InvalidPasswordException(DataModelException):
  pass


class InvalidTokenException(DataModelException):
  pass


class InvalidRepositoryBuildException(DataModelException):
  pass


class InvalidNotificationException(DataModelException):
  pass


class InvalidBuildTriggerException(DataModelException):
  pass


class InvalidImageException(DataModelException):
  pass


class TooManyUsersException(DataModelException):
  pass


class UserAlreadyInTeam(DataModelException):
  pass


class TooManyLoginAttemptsException(Exception):
  def __init__(self, message, retry_after):
    super(TooManyLoginAttemptsException, self).__init__(message)
    self.retry_after = retry_after


def _get_repository(namespace_name, repository_name, for_update=False):
  query = (Repository
           .select(Repository, Namespace)
           .join(Namespace, on=(Repository.namespace_user == Namespace.id))
           .where(Namespace.username == namespace_name, Repository.name == repository_name))
  if for_update:
    query = db_for_update(query)

  return query.get()


def hash_password(password, salt=None):
  salt = salt or bcrypt.gensalt()
  return bcrypt.hashpw(password.encode('utf-8'), salt)


def is_create_user_allowed():
  return True


def create_user(username, password, email, auto_verify=False):
  """ Creates a regular user, if allowed. """
  if not validate_password(password):
    raise InvalidPasswordException(INVALID_PASSWORD_MESSAGE)

  if not is_create_user_allowed():
    raise TooManyUsersException()

  created = _create_user(username, email)
  created.password_hash = hash_password(password)
  created.verified = auto_verify
  created.save()

  return created


def _create_user(username, email):
  if not validate_email(email):
    raise InvalidEmailAddressException('Invalid email address: %s' % email)

  (username_valid, username_issue) = validate_username(username)
  if not username_valid:
    raise InvalidUsernameException('Invalid username %s: %s' % (username, username_issue))

  try:
    existing = User.get((User.username == username) | (User.email == email))

    logger.info('Existing user with same username or email.')

    # A user already exists with either the same username or email
    if existing.username == username:
      raise InvalidUsernameException('Username has already been taken: %s' %
                                     username)
    raise InvalidEmailAddressException('Email has already been used: %s' %
                                       email)

  except User.DoesNotExist:
    # This is actually the happy path
    logger.debug('Email and username are unique!')
    pass

  try:
    return User.create(username=username, email=email)
  except Exception as ex:
    raise DataModelException(ex.message)


def is_username_unique(test_username):
  try:
    User.get((User.username == test_username))
    return False
  except User.DoesNotExist:
    return True


def create_organization(name, email, creating_user):
  try:
    # Create the org
    new_org = _create_user(name, email)
    new_org.organization = True
    new_org.save()

    # Create a team for the owners
    owners_team = create_team('owners', new_org, 'admin')

    # Add the user who created the org to the owners team
    add_user_to_team(creating_user, owners_team)

    return new_org
  except InvalidUsernameException:
    msg = ('Invalid organization name: %s Organization names must consist ' +
           'solely of lower case letters, numbers, and underscores. ' +
           '[a-z0-9_]') % name
    raise InvalidOrganizationException(msg)


def create_robot(robot_shortname, parent):
  (username_valid, username_issue) = validate_username(robot_shortname)
  if not username_valid:
    raise InvalidRobotException('The name for the robot \'%s\' is invalid: %s' %
                                (robot_shortname, username_issue))

  username = format_robot_username(parent.username, robot_shortname)

  try:
    User.get(User.username == username)

    msg = 'Existing robot with name: %s' % username
    logger.info(msg)
    raise InvalidRobotException(msg)

  except User.DoesNotExist:
    pass

  try:
    created = User.create(username=username, robot=True)

    service = LoginService.get(name='quayrobot')
    password = created.email
    FederatedLogin.create(user=created, service=service,
                          service_ident=password)

    return created, password
  except Exception as ex:
    raise DataModelException(ex.message)

def get_robot(robot_shortname, parent):
  robot_username = format_robot_username(parent.username, robot_shortname)
  robot = lookup_robot(robot_username)

  if not robot:
    msg = ('Could not find robot with username: %s' %
           robot_username)
    raise InvalidRobotException(msg)

  service = LoginService.get(name='quayrobot')
  login = FederatedLogin.get(FederatedLogin.user == robot, FederatedLogin.service == service)

  return robot, login.service_ident

def lookup_robot(robot_username):
  joined = User.select().join(FederatedLogin).join(LoginService)
  found = list(joined.where(LoginService.name == 'quayrobot',
                            User.username == robot_username))
  if not found or len(found) < 1 or not found[0].robot:
    return None

  return found[0]

def verify_robot(robot_username, password):
  joined = User.select().join(FederatedLogin).join(LoginService)
  found = list(joined.where(FederatedLogin.service_ident == password,
                            LoginService.name == 'quayrobot',
                            User.username == robot_username))
  if not found:
    msg = ('Could not find robot with username: %s and supplied password.' %
           robot_username)
    raise InvalidRobotException(msg)

  return found[0]

def regenerate_robot_token(robot_shortname, parent):
  robot_username = format_robot_username(parent.username, robot_shortname)

  robot = lookup_robot(robot_username)
  if not robot:
    raise InvalidRobotException('Could not find robot with username: %s' %
                                robot_username)

  password = random_string_generator(length=64)()
  robot.email = password

  service = LoginService.get(name='quayrobot')
  login = FederatedLogin.get(FederatedLogin.user == robot, FederatedLogin.service == service)
  login.service_ident = password

  login.save()
  robot.save()

  return robot, password

def delete_robot(robot_username):
  try:
    robot = User.get(username=robot_username, robot=True)
    robot.delete_instance(recursive=True, delete_nullable=True)

  except User.DoesNotExist:
    raise InvalidRobotException('Could not find robot with username: %s' %
                                robot_username)


def _list_entity_robots(entity_name):
  """ Return the list of robots for the specified entity. This MUST return a query, not a
      materialized list so that callers can use db_for_update.
      """
  return (User
          .select()
          .join(FederatedLogin)
          .where(User.robot == True, User.username ** (entity_name + '+%')))


class _TupleWrapper(object):
  def __init__(self, data, fields):
    self._data = data
    self._fields = fields

  def get(self, field):
    return self._data[self._fields.index(field.name + ':' + field.model_class.__name__)]


class TupleSelector(object):
  """ Helper class for selecting tuples from a peewee query and easily accessing
      them as if they were objects.
  """
  def __init__(self, query, fields):
    self._query = query.select(*fields).tuples()
    self._fields = [field.name + ':' + field.model_class.__name__ for field in fields]

  def __iter__(self):
    return self._build_iterator()

  def _build_iterator(self):
    for tuple_data in self._query:
      yield _TupleWrapper(tuple_data, self._fields)



def list_entity_robot_permission_teams(entity_name):
  query = (_list_entity_robots(entity_name)
          .join(RepositoryPermission, JOIN_LEFT_OUTER,
                on=(RepositoryPermission.user == FederatedLogin.user))
          .join(Repository, JOIN_LEFT_OUTER)
          .switch(User)
          .join(TeamMember, JOIN_LEFT_OUTER)
          .join(Team, JOIN_LEFT_OUTER))

  fields = [User.username, FederatedLogin.service_ident, Repository.name, Team.name]
  return TupleSelector(query, fields)


def list_robot_permissions(robot_name):
  return (RepositoryPermission.select(RepositoryPermission, User, Repository)
                              .join(Repository)
                              .join(Visibility)
                              .switch(RepositoryPermission)
                              .join(Role)
                              .switch(RepositoryPermission)
                              .join(User)
                              .where(User.username == robot_name, User.robot == True))

def convert_user_to_organization(user, admin_user):
  # Change the user to an organization.
  user.organization = True

  # disable this account for login.
  user.password_hash = None
  user.save()

  # Clear any federated auth pointing to this user
  FederatedLogin.delete().where(FederatedLogin.user == user).execute()

  # Create a team for the owners
  owners_team = create_team('owners', user, 'admin')

  # Add the user who will admin the org to the owners team
  add_user_to_team(admin_user, owners_team)

  return user


def create_team(name, org, team_role_name, description=''):
  (username_valid, username_issue) = validate_username(name)
  if not username_valid:
    raise InvalidTeamException('Invalid team name %s: %s' % (name, username_issue))

  if not org.organization:
    raise InvalidOrganizationException('User with name %s is not an org.' %
                                       org.username)

  team_role = TeamRole.get(TeamRole.name == team_role_name)
  return Team.create(name=name, organization=org, role=team_role,
                     description=description)


def __get_user_admin_teams(org_name, teamname, username):
  Org = User.alias()
  user_teams = Team.select().join(TeamMember).join(User)
  with_org = user_teams.switch(Team).join(Org,
                                          on=(Org.id == Team.organization))
  with_role = with_org.switch(Team).join(TeamRole)
  admin_teams = with_role.where(User.username == username,
                                Org.username == org_name,
                                TeamRole.name == 'admin')
  return admin_teams


def remove_team(org_name, team_name, removed_by_username):
  joined = Team.select(Team, TeamRole).join(User).switch(Team).join(TeamRole)

  found = list(joined.where(User.organization == True,
                            User.username == org_name,
                            Team.name == team_name))
  if not found:
    raise InvalidTeamException('Team \'%s\' is not a team in org \'%s\'' %
                               (team_name, org_name))

  team = found[0]
  if team.role.name == 'admin':
    admin_teams = list(__get_user_admin_teams(org_name, team_name,
                                              removed_by_username))

    if len(admin_teams) <= 1:
      # The team we are trying to remove is the only admin team for this user
      msg = ('Deleting team \'%s\' would remove all admin from user \'%s\'' %
             (team_name, removed_by_username))
      raise DataModelException(msg)

  team.delete_instance(recursive=True, delete_nullable=True)


def add_or_invite_to_team(inviter, team, user=None, email=None, requires_invite=True):
  # If the user is a member of the organization, then we simply add the
  # user directly to the team. Otherwise, an invite is created for the user/email.
  # We return None if the user was directly added and the invite object if the user was invited.
  if user and requires_invite:
    orgname = team.organization.username

    # If the user is part of the organization (or a robot), then no invite is required.
    if user.robot:
      requires_invite = False
      if not user.username.startswith(orgname + '+'):
        raise InvalidTeamMemberException('Cannot add the specified robot to this team, ' +
                                         'as it is not a member of the organization')
    else:
      Org = User.alias()
      found = User.select(User.username)
      found = found.where(User.username == user.username).join(TeamMember).join(Team)
      found = found.join(Org, on=(Org.username == orgname)).limit(1)
      requires_invite = not any(found)

  # If we have a valid user and no invite is required, simply add the user to the team.
  if user and not requires_invite:
    add_user_to_team(user, team)
    return None

  email_address = email if not user else None
  return TeamMemberInvite.create(user=user, email=email_address, team=team, inviter=inviter)


def add_user_to_team(user, team):
  try:
    return TeamMember.create(user=user, team=team)
  except Exception:
    raise UserAlreadyInTeam('User \'%s\' is already a member of team \'%s\'' %
                            (user.username, team.name))


def remove_user_from_team(org_name, team_name, username, removed_by_username):
  Org = User.alias()
  joined = TeamMember.select().join(User).switch(TeamMember).join(Team)
  with_role = joined.join(TeamRole)
  with_org = with_role.switch(Team).join(Org,
                                         on=(Org.id == Team.organization))
  found = list(with_org.where(User.username == username,
                              Org.username == org_name,
                              Team.name == team_name))

  if not found:
    raise DataModelException('User %s does not belong to team %s' %
                             (username, team_name))

  if username == removed_by_username:
    admin_team_query = __get_user_admin_teams(org_name, team_name, username)
    admin_team_names = {team.name for team in admin_team_query}
    if team_name in admin_team_names and len(admin_team_names) <= 1:
      msg = 'User cannot remove themselves from their only admin team.'
      raise DataModelException(msg)

  user_in_team = found[0]
  user_in_team.delete_instance()


def get_team_org_role(team):
  return TeamRole.get(TeamRole.id == team.role.id)


def set_team_org_permission(team, team_role_name, set_by_username):
  if team.role.name == 'admin' and team_role_name != 'admin':
    # We need to make sure we're not removing the users only admin role
    user_admin_teams = __get_user_admin_teams(team.organization.username,
                                              team.name, set_by_username)
    admin_team_set = {admin_team.name for admin_team in user_admin_teams}
    if team.name in admin_team_set and len(admin_team_set) <= 1:
      msg = (('Cannot remove admin from team \'%s\' because calling user ' +
              'would no longer have admin on org \'%s\'') %
             (team.name, team.organization.username))
      raise DataModelException(msg)

  new_role = TeamRole.get(TeamRole.name == team_role_name)
  team.role = new_role
  team.save()
  return team


def create_federated_user(username, email, service_name, service_id,
                          set_password_notification, metadata={}):
  if not is_create_user_allowed():
    raise TooManyUsersException()

  new_user = _create_user(username, email)
  new_user.verified = True
  new_user.save()

  service = LoginService.get(LoginService.name == service_name)
  FederatedLogin.create(user=new_user, service=service,
                        service_ident=service_id,
                        metadata_json=json.dumps(metadata))

  if set_password_notification:
    create_notification('password_required', new_user)

  return new_user


def attach_federated_login(user, service_name, service_id, metadata={}):
  service = LoginService.get(LoginService.name == service_name)
  FederatedLogin.create(user=user, service=service, service_ident=service_id,
                        metadata_json=json.dumps(metadata))
  return user


def verify_federated_login(service_name, service_id):
  try:
    found = (FederatedLogin
      .select(FederatedLogin, User)
      .join(LoginService)
      .switch(FederatedLogin).join(User)
      .where(FederatedLogin.service_ident == service_id, LoginService.name == service_name)
      .get())
    return found.user
  except FederatedLogin.DoesNotExist:
    return None


def list_federated_logins(user):
  selected = FederatedLogin.select(FederatedLogin.service_ident,
                                   LoginService.name, FederatedLogin.metadata_json)
  joined = selected.join(LoginService)
  return joined.where(LoginService.name != 'quayrobot',
                      FederatedLogin.user == user)


def create_confirm_email_code(user, new_email=None):
  if new_email:
    if not validate_email(new_email):
      raise InvalidEmailAddressException('Invalid email address: %s' %
                                         new_email)

  code = EmailConfirmation.create(user=user, email_confirm=True,
                                  new_email=new_email)
  return code


def confirm_user_email(code):
  try:
    code = EmailConfirmation.get(EmailConfirmation.code == code,
                                 EmailConfirmation.email_confirm == True)
  except EmailConfirmation.DoesNotExist:
    raise DataModelException('Invalid email confirmation code.')

  user = code.user
  user.verified = True

  old_email = None
  new_email = code.new_email
  if new_email and new_email != old_email:
    if find_user_by_email(new_email):
      raise DataModelException('E-mail address already used.')

    old_email = user.email
    user.email = new_email

  user.save()

  code.delete_instance()

  return user, new_email, old_email


def create_reset_password_email_code(email):
  try:
    user = User.get(User.email == email)
  except User.DoesNotExist:
    raise InvalidEmailAddressException('Email address was not found.');

  if user.organization:
    raise InvalidEmailAddressException('Organizations can not have passwords.')

  code = EmailConfirmation.create(user=user, pw_reset=True)
  return code


def validate_reset_code(code):
  try:
    code = EmailConfirmation.get(EmailConfirmation.code == code,
                                 EmailConfirmation.pw_reset == True)
  except EmailConfirmation.DoesNotExist:
    return None

  user = code.user
  code.delete_instance()

  return user


def find_user_by_email(email):
  try:
    return User.get(User.email == email)
  except User.DoesNotExist:
    return None


def get_user(username):
  try:
    return User.get(User.username == username, User.organization == False)
  except User.DoesNotExist:
    return None


def get_namespace_user(username):
  try:
    return User.get(User.username == username)
  except User.DoesNotExist:
    return None


def get_user_or_org(username):
  try:
    return User.get(User.username == username, User.robot == False)
  except User.DoesNotExist:
    return None


def get_user_by_id(user_db_id):
  try:
    return User.get(User.id == user_db_id, User.organization == False)
  except User.DoesNotExist:
    return None


def get_namespace_by_user_id(namespace_user_db_id):
  try:
    return User.get(User.id == namespace_user_db_id, User.robot == False).username
  except User.DoesNotExist:
    raise InvalidUsernameException('User with id does not exist: %s' % namespace_user_db_id)


def get_user_by_uuid(user_uuid):
  try:
    return User.get(User.uuid == user_uuid, User.organization == False)
  except User.DoesNotExist:
    return None


def get_user_or_org_by_customer_id(customer_id):
  try:
    return User.get(User.stripe_id == customer_id)
  except User.DoesNotExist:
    return None

def get_matching_user_namespaces(namespace_prefix, username, limit=10):
  query = (Repository
           .select()
           .join(Namespace, on=(Repository.namespace_user == Namespace.id))
           .switch(Repository)
           .join(Visibility)
           .switch(Repository)
           .join(RepositoryPermission, JOIN_LEFT_OUTER)
           .where(Namespace.username ** (namespace_prefix + '%'))
           .group_by(Repository.namespace_user, Repository))

  count = 0
  namespaces = {}
  for repo in _filter_to_repos_for_user(query, username):
    if not repo.namespace_user.username in namespaces:
      namespaces[repo.namespace_user.username] = repo.namespace_user
      count = count + 1
      if count >= limit:
        break

  return namespaces.values()

def get_matching_user_teams(team_prefix, user, limit=10):
  query = (Team.select()
               .join(User)
               .switch(Team)
               .join(TeamMember)
               .where(TeamMember.user == user, Team.name ** (team_prefix + '%'))
               .distinct(Team.id)
               .limit(limit))

  return query


def get_matching_robots(name_prefix, username, limit=10):
  admined_orgs = (get_user_organizations(username)
                     .switch(Team)
                     .join(TeamRole)
                     .where(TeamRole.name == 'admin'))

  prefix_checks = False

  for org in admined_orgs:
    prefix_checks = prefix_checks | (User.username ** (org.username + '+' + name_prefix + '%'))

  prefix_checks = prefix_checks | (User.username ** (username + '+' + name_prefix + '%'))

  return User.select().where(prefix_checks).limit(limit)


def get_matching_admined_teams(team_prefix, user, limit=10):
  admined_orgs = (get_user_organizations(user.username)
                     .switch(Team)
                     .join(TeamRole)
                     .where(TeamRole.name == 'admin'))

  query = (Team.select()
               .join(User)
               .switch(Team)
               .join(TeamMember)
               .where(Team.name ** (team_prefix + '%'), Team.organization << (admined_orgs))
               .distinct(Team.id)
               .limit(limit))

  return query


def get_matching_teams(team_prefix, organization):
  query = Team.select().where(Team.name ** (team_prefix + '%'),
                              Team.organization == organization)
  return query.limit(10)


def get_matching_users(username_prefix, robot_namespace=None,
                       organization=None):
  direct_user_query = (User.username ** (username_prefix + '%') &
                       (User.organization == False) & (User.robot == False))

  if robot_namespace:
    robot_prefix = format_robot_username(robot_namespace, username_prefix)
    direct_user_query = (direct_user_query |
                         (User.username ** (robot_prefix + '%') &
                          (User.robot == True)))

  query = (User
    .select(User.username, User.email, User.robot)
    .group_by(User.username, User.email, User.robot)
    .where(direct_user_query))

  if organization:
    query = (query
      .select(User.username, User.email, User.robot, fn.Sum(Team.id))
      .join(TeamMember, JOIN_LEFT_OUTER)
      .join(Team, JOIN_LEFT_OUTER, on=((Team.id == TeamMember.team) &
                                       (Team.organization == organization))))


  class MatchingUserResult(object):
    def __init__(self, *args):
      self.username = args[0]
      self.email = args[1]
      self.robot = args[2]

      if organization:
        self.is_org_member = (args[3] != None)
      else:
        self.is_org_member = None


  return (MatchingUserResult(*args) for args in query.tuples().limit(10))


def verify_user(username_or_email, password):
  try:
    fetched = User.get((User.username == username_or_email) |
                       (User.email == username_or_email))
  except User.DoesNotExist:
    return None

  now = datetime.utcnow()

  if fetched.invalid_login_attempts > 0:
    can_retry_at = exponential_backoff(fetched.invalid_login_attempts, EXPONENTIAL_BACKOFF_SCALE,
                                       fetched.last_invalid_login)

    if can_retry_at > now:
      retry_after = can_retry_at - now
      raise TooManyLoginAttemptsException('Too many login attempts.', retry_after.total_seconds())

  if (fetched.password_hash and
      hash_password(password, fetched.password_hash) == fetched.password_hash):
    if fetched.invalid_login_attempts > 0:
      fetched.invalid_login_attempts = 0
      fetched.save()

    return fetched

  fetched.invalid_login_attempts += 1
  fetched.last_invalid_login = now
  fetched.save()

  # We weren't able to authorize the user
  return None


def get_user_organizations(username):
  UserAlias = User.alias()
  all_teams = User.select().distinct().join(Team).join(TeamMember)
  with_user = all_teams.join(UserAlias, on=(UserAlias.id == TeamMember.user))
  return with_user.where(User.organization == True,
                         UserAlias.username == username)


def get_organization(name):
  try:
    return User.get(username=name, organization=True)
  except User.DoesNotExist:
    raise InvalidOrganizationException('Organization does not exist: %s' %
                                       name)


def get_organization_team(orgname, teamname):
  joined = Team.select().join(User)
  query = joined.where(Team.name == teamname, User.organization == True,
                       User.username == orgname).limit(1)
  result = list(query)
  if not result:
    raise InvalidTeamException('Team does not exist: %s/%s', orgname,
                               teamname)

  return result[0]


def get_organization_members_with_teams(organization, membername = None):
  joined = TeamMember.select().annotate(Team).annotate(User)
  query = joined.where(Team.organization == organization)
  if membername:
    query = query.where(User.username == membername)
  return query

def get_organization_team_members(teamid):
  joined = User.select().join(TeamMember).join(Team)
  query = joined.where(Team.id == teamid)
  return query

def get_organization_team_member_invites(teamid):
  joined = TeamMemberInvite.select().join(Team).join(User)
  query = joined.where(Team.id == teamid)
  return query

def get_organization_member_set(orgname):
  Org = User.alias()
  org_users = (User.select(User.username)
               .join(TeamMember)
               .join(Team)
               .join(Org, on=(Org.id == Team.organization))
               .where(Org.username == orgname)
               .distinct())
  return {user.username for user in org_users}


def get_teams_within_org(organization):
  return Team.select().where(Team.organization == organization)


def get_user_teams_within_org(username, organization):
  joined = Team.select().join(TeamMember).join(User)
  return joined.where(Team.organization == organization,
                      User.username == username)


def get_visible_repository_count(username=None, include_public=True,
                                 namespace=None):
  query = _visible_repository_query(username=username,
                                    include_public=include_public,
                                    namespace=namespace)
  return query.count()


def get_visible_repositories(username=None, include_public=True, page=None,
                             limit=None, sort=False, namespace=None, namespace_only=False):
  query = _visible_repository_query(username=username, include_public=include_public, page=page,
                                    limit=limit, namespace=namespace,
                                    select_models=[Repository, Namespace, Visibility])

  if sort:
    query = query.order_by(Repository.description.desc())

  if limit:
    query = query.limit(limit)

  if namespace and namespace_only:
    query = query.where(Namespace.username == namespace)

  return query


def _visible_repository_query(username=None, include_public=True, limit=None,
                              page=None, namespace=None, select_models=[]):
  query = (Repository
           .select(*select_models)  # MySQL/RDS complains is there are selected models for counts.
           .distinct()
           .join(Visibility)
           .switch(Repository)
           .join(Namespace, on=(Repository.namespace_user == Namespace.id))
           .switch(Repository)
           .join(RepositoryPermission, JOIN_LEFT_OUTER))

  query = _filter_to_repos_for_user(query, username, namespace, include_public)
  if page:
    query = query.paginate(page, limit)
  elif limit:
    query = query.limit(limit)

  return query


def _filter_to_repos_for_user(query, username=None, namespace=None, include_public=True):
  if not include_public and not username:
    return Repository.select().where(Repository.id == '-1')

  where_clause = None
  if username:
    UserThroughTeam = User.alias()
    Org = User.alias()
    AdminTeam = Team.alias()
    AdminTeamMember = TeamMember.alias()
    AdminUser = User.alias()

    query = (query
             .switch(RepositoryPermission)
             .join(User, JOIN_LEFT_OUTER)
             .switch(RepositoryPermission)
             .join(Team, JOIN_LEFT_OUTER)
             .join(TeamMember, JOIN_LEFT_OUTER)
             .join(UserThroughTeam, JOIN_LEFT_OUTER, on=(UserThroughTeam.id == TeamMember.user))
             .switch(Repository)
             .join(Org, JOIN_LEFT_OUTER, on=(Repository.namespace_user == Org.id))
             .join(AdminTeam, JOIN_LEFT_OUTER, on=(Org.id == AdminTeam.organization))
             .join(TeamRole, JOIN_LEFT_OUTER, on=(AdminTeam.role == TeamRole.id))
             .switch(AdminTeam)
             .join(AdminTeamMember, JOIN_LEFT_OUTER, on=(AdminTeam.id == AdminTeamMember.team))
             .join(AdminUser, JOIN_LEFT_OUTER, on=(AdminTeamMember.user == AdminUser.id)))

    where_clause = ((User.username == username) | (UserThroughTeam.username == username) |
                    ((AdminUser.username == username) & (TeamRole.name == 'admin')))

    if namespace:
      where_clause = where_clause & (Namespace.username == namespace)

  # TODO(jschorr, jake): Figure out why the old join on Visibility was so darn slow and
  # remove this hack.
  if include_public:
    new_clause = (Repository.visibility == _get_public_repo_visibility())
    if where_clause:
      where_clause = where_clause | new_clause
    else:
      where_clause = new_clause

  return query.where(where_clause)


_public_repo_visibility_cache = None
def _get_public_repo_visibility():
  global _public_repo_visibility_cache

  if not _public_repo_visibility_cache:
    _public_repo_visibility_cache = Visibility.get(name='public')

  return _public_repo_visibility_cache


def get_sorted_matching_repositories(prefix, only_public, checker, limit=10):
  """ Returns repositories matching the given prefix string and passing the given checker
      function.
  """
  last_week = datetime.now() - timedelta(weeks=1)
  results = []
  existing_ids = []

  def get_search_results(search_clause, with_count=False):
    if len(results) >= limit:
      return

    select_items = [Repository, Namespace]
    if with_count:
      select_items.append(fn.Sum(RepositoryActionCount.count).alias('count'))

    query = (Repository.select(*select_items)
                .join(Namespace, JOIN_LEFT_OUTER, on=(Namespace.id == Repository.namespace_user))
                .switch(Repository)
                .where(search_clause)
                .group_by(Repository, Namespace))

    if only_public:
      query = query.where(Repository.visibility == _get_public_repo_visibility())

    if existing_ids:
      query = query.where(~(Repository.id << existing_ids))

    if with_count:
      query = (query.switch(Repository)
                    .join(RepositoryActionCount)
                    .where(RepositoryActionCount.date >= last_week)
                    .order_by(fn.Sum(RepositoryActionCount.count).desc()))

    for result in query:
      if len(results) >= limit:
        return results

      # Note: We compare IDs here, instead of objects, because calling .visibility on the
      # Repository will kick off a new SQL query to retrieve that visibility enum value. We don't
      # join the visibility table in SQL, as well, because it is ungodly slow in MySQL :-/
      result.is_public = result.visibility_id == _get_public_repo_visibility().id
      result.count = result.count if with_count else 0

      if not checker(result):
        continue

      results.append(result)
      existing_ids.append(result.id)

  # For performance reasons, we conduct the repo name and repo namespace searches on their
  # own. This also affords us the ability to give higher precedence to repository names matching
  # over namespaces, which is semantically correct.
  get_search_results(Repository.name ** (prefix + '%'), with_count=True)
  get_search_results(Repository.name ** (prefix + '%'), with_count=False)

  get_search_results(Namespace.username ** (prefix + '%'), with_count=True)
  get_search_results(Namespace.username ** (prefix + '%'), with_count=False)

  return results


def get_matching_repositories(repo_term, username=None, limit=10, include_public=True):
  namespace_term = repo_term
  name_term = repo_term

  visible = get_visible_repositories(username, include_public=include_public)

  search_clauses = (Repository.name ** ('%' + name_term + '%') |
                    Namespace.username ** ('%' + namespace_term + '%'))

  # Handle the case where the user has already entered a namespace path.
  if repo_term.find('/') > 0:
    parts = repo_term.split('/', 1)
    namespace_term = '/'.join(parts[:-1])
    name_term = parts[-1]

    search_clauses = (Repository.name ** ('%' + name_term + '%') &
                      Namespace.username ** ('%' + namespace_term + '%'))

  return visible.where(search_clauses).limit(limit)


def change_password(user, new_password):
  if not validate_password(new_password):
    raise InvalidPasswordException(INVALID_PASSWORD_MESSAGE)

  pw_hash = hash_password(new_password)
  user.invalid_login_attempts = 0
  user.password_hash = pw_hash
  user.uuid = str(uuid4())
  user.save()

  # Remove any password required notifications for the user.
  delete_notifications_by_kind(user, 'password_required')


def change_username(user_id, new_username):
  (username_valid, username_issue) = validate_username(new_username)
  if not username_valid:
    raise InvalidUsernameException('Invalid username %s: %s' % (new_username, username_issue))

  with config.app_config['DB_TRANSACTION_FACTORY'](db):
    # Reload the user for update
    user = db_for_update(User.select().where(User.id == user_id)).get()

    # Rename the robots
    for robot in db_for_update(_list_entity_robots(user.username)):
      _, robot_shortname = parse_robot_username(robot.username)
      new_robot_name = format_robot_username(new_username, robot_shortname)
      robot.username = new_robot_name
      robot.save()

    # Rename the user
    user.username = new_username
    user.save()


def change_invoice_email(user, invoice_email):
  user.invoice_email = invoice_email
  user.save()


def change_user_tag_expiration(user, tag_expiration_s):
  user.removed_tag_expiration_s = tag_expiration_s
  user.save()


def update_email(user, new_email, auto_verify=False):
  user.email = new_email
  user.verified = auto_verify
  user.save()


def get_all_user_permissions(user):
  UserThroughTeam = User.alias()

  return (RepositoryPermission
          .select(RepositoryPermission, Role, Repository, Namespace)
          .join(Role)
          .switch(RepositoryPermission)
          .join(Repository)
          .join(Namespace, on=(Repository.namespace_user == Namespace.id))
          .switch(RepositoryPermission)
          .join(User, JOIN_LEFT_OUTER)
          .switch(RepositoryPermission)
          .join(Team, JOIN_LEFT_OUTER).join(TeamMember, JOIN_LEFT_OUTER)
          .join(UserThroughTeam, JOIN_LEFT_OUTER, on=(UserThroughTeam.id == TeamMember.user))
          .where((User.id == user) | (UserThroughTeam.id == user)))


def delete_prototype_permission(org, uid):
  found = get_prototype_permission(org, uid)
  if not found:
    return None

  found.delete_instance()
  return found


def get_prototype_permission(org, uid):
  try:
    return PermissionPrototype.get(PermissionPrototype.org == org,
                                   PermissionPrototype.uuid == uid)
  except PermissionPrototype.DoesNotExist:
    return None


def get_prototype_permissions(org):
  ActivatingUser = User.alias()
  DelegateUser = User.alias()
  query = (PermissionPrototype
    .select()
    .where(PermissionPrototype.org == org)
    .join(ActivatingUser, JOIN_LEFT_OUTER,
          on=(ActivatingUser.id == PermissionPrototype.activating_user))
    .join(DelegateUser, JOIN_LEFT_OUTER,
          on=(DelegateUser.id == PermissionPrototype.delegate_user))
    .join(Team, JOIN_LEFT_OUTER,
          on=(Team.id == PermissionPrototype.delegate_team))
    .join(Role, JOIN_LEFT_OUTER, on=(Role.id == PermissionPrototype.role)))
  return query


def update_prototype_permission(org, uid, role_name):
  found = get_prototype_permission(org, uid)
  if not found:
    return None

  new_role = Role.get(Role.name == role_name)
  found.role = new_role
  found.save()
  return found


def add_prototype_permission(org, role_name, activating_user,
                             delegate_user=None, delegate_team=None):
  new_role = Role.get(Role.name == role_name)
  return PermissionPrototype.create(org=org, role=new_role,
                                    activating_user=activating_user,
      delegate_user=delegate_user, delegate_team=delegate_team)


def get_org_wide_permissions(user):
  Org = User.alias()
  team_with_role = Team.select(Team, Org, TeamRole).join(TeamRole)
  with_org = team_with_role.switch(Team).join(Org, on=(Team.organization ==
                                                       Org.id))
  with_user = with_org.switch(Team).join(TeamMember).join(User)
  return with_user.where(User.id == user, Org.organization == True)


def get_all_repo_teams(namespace_name, repository_name):
  return (RepositoryPermission.select(Team.name.alias('team_name'), Role.name, RepositoryPermission)
          .join(Team)
          .switch(RepositoryPermission)
          .join(Role)
          .switch(RepositoryPermission)
          .join(Repository)
          .join(Namespace, on=(Repository.namespace_user == Namespace.id))
          .where(Namespace.username == namespace_name, Repository.name == repository_name))


def get_all_repo_users(namespace_name, repository_name):
  return (RepositoryPermission.select(User.username, User.email, User.robot, Role.name,
                                      RepositoryPermission)
          .join(User)
          .switch(RepositoryPermission)
          .join(Role)
          .switch(RepositoryPermission)
          .join(Repository)
          .join(Namespace, on=(Repository.namespace_user == Namespace.id))
          .where(Namespace.username == namespace_name, Repository.name == repository_name))


def get_all_repo_users_transitive_via_teams(namespace_name, repository_name):
  return (User
          .select()
          .distinct()
          .join(TeamMember)
          .join(Team)
          .join(RepositoryPermission)
          .join(Repository)
          .join(Namespace, on=(Repository.namespace_user == Namespace.id))
          .where(Namespace.username == namespace_name, Repository.name == repository_name))


def get_all_repo_users_transitive(namespace_name, repository_name):
  # Load the users found via teams and directly via permissions.
  via_teams = get_all_repo_users_transitive_via_teams(namespace_name, repository_name)
  directly = [perm.user for perm in get_all_repo_users(namespace_name, repository_name)]

  # Filter duplicates.
  user_set = set()

  def check_add(u):
    if u.username in user_set:
      return False

    user_set.add(u.username)
    return True

  return [user for user in list(directly) + list(via_teams) if check_add(user)]


def get_repository_for_resource(resource_key):
  try:
    return (Repository
            .select(Repository, Namespace)
            .join(Namespace, on=(Repository.namespace_user == Namespace.id))
            .switch(Repository)
            .join(RepositoryBuild)
            .where(RepositoryBuild.resource_key == resource_key)
            .get())
  except Repository.DoesNotExist:
    return None


def lookup_repository(repo_id):
  try:
    return Repository.get(Repository.id == repo_id)
  except Repository.DoesNotExist:
    return None


def get_repository(namespace_name, repository_name):
  try:
    return _get_repository(namespace_name, repository_name)
  except Repository.DoesNotExist:
    return None


def get_image(repo, dockerfile_id):
  try:
    return Image.get(Image.docker_image_id == dockerfile_id, Image.repository == repo)
  except Image.DoesNotExist:
    return None


def find_child_image(repo, parent_image, command):
  try:
    return (Image.select()
                .join(ImageStorage)
                .switch(Image)
                .where(Image.ancestors % '%/' + parent_image.id + '/%',
                       ImageStorage.command == command)
                .order_by(ImageStorage.created.desc())
                .get())
  except Image.DoesNotExist:
    return None


def get_repo_image(namespace_name, repository_name, docker_image_id):
  def limit_to_image_id(query):
    return query.where(Image.docker_image_id == docker_image_id).limit(1)

  query = _get_repository_images(namespace_name, repository_name, limit_to_image_id)
  try:
    return query.get()
  except Image.DoesNotExist:
    return None


def get_repo_image_extended(namespace_name, repository_name, docker_image_id):
  def limit_to_image_id(query):
    return query.where(Image.docker_image_id == docker_image_id).limit(1)

  images = _get_repository_images_base(namespace_name, repository_name, limit_to_image_id)
  if not images:
    return None

  return images[0]

def is_repository_public(repository):
  return repository.visibility == _get_public_repo_visibility()

def repository_is_public(namespace_name, repository_name):
  try:
    (Repository
     .select()
     .join(Namespace, on=(Repository.namespace_user == Namespace.id))
     .switch(Repository)
     .join(Visibility)
     .where(Namespace.username == namespace_name, Repository.name == repository_name,
            Visibility.name == 'public')
     .get())
    return True
  except Repository.DoesNotExist:
    return False


def set_repository_visibility(repo, visibility):
  visibility_obj = Visibility.get(name=visibility)
  if not visibility_obj:
    return

  repo.visibility = visibility_obj
  repo.save()


def __apply_default_permissions(repo, proto_query, name_property,
                                create_permission_func):
  final_protos = {}
  for proto in proto_query:
    applies_to = proto.delegate_team or proto.delegate_user
    name = getattr(applies_to, name_property)
    # We will skip the proto if it is pre-empted by a more important proto
    if name in final_protos and proto.activating_user is None:
      continue

    # By this point, it is either a user specific proto, or there is no
    # proto yet, so we can safely assume it applies
    final_protos[name] = (applies_to, proto.role)

  for delegate, role in final_protos.values():
    create_permission_func(delegate, repo, role)


def create_repository(namespace, name, creating_user, visibility='private'):
  private = Visibility.get(name=visibility)
  namespace_user = User.get(username=namespace)
  repo = Repository.create(name=name, visibility=private, namespace_user=namespace_user)
  admin = Role.get(name='admin')

  if creating_user and not creating_user.organization:
    RepositoryPermission.create(user=creating_user, repository=repo,
                                role=admin)

    if creating_user.username != namespace:
      # Permission prototypes only work for orgs
      org = get_organization(namespace)
      user_clause = ((PermissionPrototype.activating_user == creating_user) |
                     (PermissionPrototype.activating_user >> None))

      team_protos = (PermissionPrototype
                      .select()
                      .where(PermissionPrototype.org == org, user_clause,
                             PermissionPrototype.delegate_user >> None))

      def create_team_permission(team, repo, role):
        RepositoryPermission.create(team=team, repository=repo, role=role)

      __apply_default_permissions(repo, team_protos, 'name',
                                  create_team_permission)

      user_protos = (PermissionPrototype
                      .select()
                      .where(PermissionPrototype.org == org, user_clause,
                             PermissionPrototype.delegate_team >> None))

      def create_user_permission(user, repo, role):
        # The creating user always gets admin anyway
        if user.username == creating_user.username:
          return

        RepositoryPermission.create(user=user, repository=repo, role=role)

      __apply_default_permissions(repo, user_protos, 'username',
                                  create_user_permission)

  return repo


def __translate_ancestry(old_ancestry, translations, repository, username, preferred_location):
  if old_ancestry == '/':
    return '/'

  def translate_id(old_id, docker_image_id):
    logger.debug('Translating id: %s', old_id)
    if old_id not in translations:
      image_in_repo = find_create_or_link_image(docker_image_id, repository, username,
                                                translations, preferred_location)
      translations[old_id] = image_in_repo.id
    return translations[old_id]

  # Select all the ancestor Docker IDs in a single query.
  old_ids = [int(id_str) for id_str in old_ancestry.split('/')[1:-1]]
  query = Image.select(Image.id, Image.docker_image_id).where(Image.id << old_ids)
  old_images = {i.id: i.docker_image_id for i in  query}

  # Translate the old images into new ones.
  new_ids = [str(translate_id(old_id, old_images[old_id])) for old_id in old_ids]
  return '/%s/' % '/'.join(new_ids)


def _create_storage(location_name):
  storage = ImageStorage.create()
  location = ImageStorageLocation.get(name=location_name)
  ImageStoragePlacement.create(location=location, storage=storage)
  storage.locations = {location_name}
  return storage


def _find_or_link_image(existing_image, repository, username, translations, preferred_location):
  # TODO(jake): This call is currently recursively done under a single transaction. Can we make
  # it instead be done under a set of transactions?
  with config.app_config['DB_TRANSACTION_FACTORY'](db):
    # Check for an existing image, under the transaction, to make sure it doesn't already exist.
    repo_image = get_repo_image(repository.namespace_user.username, repository.name,
                                existing_image.docker_image_id)
    if repo_image:
      return repo_image

    # Make sure the existing base image still exists.
    try:
      to_copy = Image.select().join(ImageStorage).where(Image.id == existing_image.id).get()

      msg = 'Linking image to existing storage with docker id: %s and uuid: %s'
      logger.debug(msg, existing_image.docker_image_id, to_copy.storage.uuid)

      new_image_ancestry = __translate_ancestry(to_copy.ancestors, translations, repository,
                                                username, preferred_location)

      storage = to_copy.storage
      storage.locations = {placement.location.name
                           for placement in storage.imagestorageplacement_set}

      new_image = Image.create(docker_image_id=existing_image.docker_image_id,
                               repository=repository, storage=storage,
                               ancestors=new_image_ancestry)

      logger.debug('Storing translation %s -> %s', existing_image.id, new_image.id)
      translations[existing_image.id] = new_image.id
      return new_image
    except Image.DoesNotExist:
      return None


def find_create_or_link_image(docker_image_id, repository, username, translations,
                              preferred_location):

  # First check for the image existing in the repository. If found, we simply return it.
  repo_image = get_repo_image(repository.namespace_user.username, repository.name,
                              docker_image_id)
  if repo_image:
    return repo_image

  # We next check to see if there is an existing storage the new image can link to.
  existing_image_query = (Image
                           .select(Image, ImageStorage)
                           .distinct()
                           .join(ImageStorage)
                           .switch(Image)
                           .join(Repository)
                           .join(RepositoryPermission, JOIN_LEFT_OUTER)
                           .switch(Repository)
                           .join(Namespace, on=(Repository.namespace_user == Namespace.id))
                           .where(ImageStorage.uploading == False))

  existing_image_query = (_filter_to_repos_for_user(existing_image_query, username)
                          .where(Image.docker_image_id == docker_image_id))

  # If there is an existing image, we try to translate its ancestry and copy its storage.
  new_image = None
  try:
    logger.debug('Looking up existing image for ID: %s', docker_image_id)
    existing_image = existing_image_query.get()

    logger.debug('Existing image %s found for ID: %s', existing_image.id, docker_image_id)
    new_image = _find_or_link_image(existing_image, repository, username, translations,
                                    preferred_location)
    if new_image:
      return new_image
  except Image.DoesNotExist:
    logger.debug('No existing image found for ID: %s', docker_image_id)
    pass

  # Otherwise, create a new storage directly.
  with config.app_config['DB_TRANSACTION_FACTORY'](db):
    # Final check for an existing image, under the transaction.
    repo_image = get_repo_image(repository.namespace_user.username, repository.name,
                                docker_image_id)
    if repo_image:
      return repo_image

    logger.debug('Creating new storage for docker id: %s', docker_image_id)
    storage = _create_storage(preferred_location)

    return Image.create(docker_image_id=docker_image_id,
                        repository=repository, storage=storage,
                        ancestors='/')


def find_or_create_storage_signature(storage, signature_kind):
  found = lookup_storage_signature(storage, signature_kind)
  if found is None:
    kind = ImageStorageSignatureKind.get(name=signature_kind)
    found = ImageStorageSignature.create(storage=storage, kind=kind)

  return found


def lookup_storage_signature(storage, signature_kind):
  kind = ImageStorageSignatureKind.get(name=signature_kind)
  try:
    return (ImageStorageSignature
                .select()
                .where(ImageStorageSignature.storage == storage,
                       ImageStorageSignature.kind == kind)
                .get())
  except ImageStorageSignature.DoesNotExist:
    return None


def find_derived_storage(source, transformation_name):
  try:
    found = (ImageStorage
      .select(ImageStorage, DerivedImageStorage)
      .join(DerivedImageStorage, on=(ImageStorage.id == DerivedImageStorage.derivative))
      .join(ImageStorageTransformation)
      .where(DerivedImageStorage.source == source,
             ImageStorageTransformation.name == transformation_name)
      .get())

    found.locations = {placement.location.name for placement in found.imagestorageplacement_set}
    return found
  except ImageStorage.DoesNotExist:
    return None


def find_or_create_derived_storage(source, transformation_name, preferred_location):
  existing = find_derived_storage(source, transformation_name)
  if existing is not None:
    return existing

  logger.debug('Creating storage dervied from source: %s', source.uuid)
  trans = ImageStorageTransformation.get(name=transformation_name)
  new_storage = _create_storage(preferred_location)
  DerivedImageStorage.create(source=source, derivative=new_storage, transformation=trans)
  return new_storage


def delete_derived_storage_by_uuid(storage_uuid):
  try:
    image_storage = get_storage_by_uuid(storage_uuid)
  except InvalidImageException:
    return

  try:
    DerivedImageStorage.get(derivative=image_storage)
  except DerivedImageStorage.DoesNotExist:
    return

  image_storage.delete_instance(recursive=True)


def get_storage_by_uuid(storage_uuid):
  placements = list(ImageStoragePlacement
                    .select(ImageStoragePlacement, ImageStorage, ImageStorageLocation)
                    .join(ImageStorageLocation)
                    .switch(ImageStoragePlacement)
                    .join(ImageStorage)
                    .where(ImageStorage.uuid == storage_uuid))

  if not placements:
    raise InvalidImageException('No storage found with uuid: %s', storage_uuid)

  found = placements[0].storage
  found.locations = {placement.location.name for placement in placements}

  return found


def set_image_size(docker_image_id, namespace_name, repository_name, image_size, uncompressed_size):
  try:
    image = (Image
             .select(Image, ImageStorage)
             .join(Repository)
             .join(Namespace, on=(Repository.namespace_user == Namespace.id))
             .switch(Image)
             .join(ImageStorage, JOIN_LEFT_OUTER)
             .where(Repository.name == repository_name, Namespace.username == namespace_name,
                    Image.docker_image_id == docker_image_id)
             .get())

  except Image.DoesNotExist:
    raise DataModelException('No image with specified id and repository')

  image.storage.image_size = image_size
  image.storage.uncompressed_size = uncompressed_size

  ancestors = image.ancestors.split('/')[1:-1]
  if ancestors:
    try:
      # TODO(jschorr): Switch to this faster route once we have full ancestor aggregate_size
      # parent_image = Image.get(Image.id == ancestors[-1])
      # total_size = image_size + parent_image.storage.aggregate_size
      total_size = (ImageStorage.select(fn.Sum(ImageStorage.image_size))
                                .join(Image)
                                .where(Image.id << ancestors)
                                .scalar()) + image_size

      image.storage.aggregate_size = total_size
    except Image.DoesNotExist:
      pass
  else:
    image.storage.aggregate_size = image_size

  image.storage.save()

  return image


def set_image_metadata(docker_image_id, namespace_name, repository_name, created_date_str, comment,
                       command, parent=None):
  with config.app_config['DB_TRANSACTION_FACTORY'](db):
    query = (Image
             .select(Image, ImageStorage)
             .join(Repository)
             .join(Namespace, on=(Repository.namespace_user == Namespace.id))
             .switch(Image)
             .join(ImageStorage)
             .where(Repository.name == repository_name, Namespace.username == namespace_name,
                    Image.docker_image_id == docker_image_id))

    try:
      fetched = db_for_update(query).get()
    except Image.DoesNotExist:
      raise DataModelException('No image with specified id and repository')

    # We cleanup any old checksum in case it's a retry after a fail
    fetched.storage.checksum = None
    fetched.storage.created = datetime.now()

    if created_date_str is not None:
      try:
        fetched.storage.created = dateutil.parser.parse(created_date_str).replace(tzinfo=None)
      except:
        # parse raises different exceptions, so we cannot use a specific kind of handler here.
        pass

    fetched.storage.comment = comment
    fetched.storage.command = command

    if parent:
      fetched.ancestors = '%s%s/' % (parent.ancestors, parent.id)

    fetched.save()
    fetched.storage.save()
    return fetched

def _get_repository_images(namespace_name, repository_name, query_modifier):
  query = (Image
           .select()
           .join(Repository)
           .join(Namespace, on=(Repository.namespace_user == Namespace.id))
           .where(Repository.name == repository_name, Namespace.username == namespace_name))

  query = query_modifier(query)
  return query

def _get_repository_images_base(namespace_name, repository_name, query_modifier):
  query = (ImageStoragePlacement
           .select(ImageStoragePlacement, Image, ImageStorage, ImageStorageLocation)
           .join(ImageStorageLocation)
           .switch(ImageStoragePlacement)
           .join(ImageStorage, JOIN_LEFT_OUTER)
           .join(Image)
           .join(Repository)
           .join(Namespace, on=(Repository.namespace_user == Namespace.id))
           .where(Repository.name == repository_name, Namespace.username == namespace_name))

  query = query_modifier(query)

  location_list = list(query)

  images = {}
  for location in location_list:
    # Make sure we're always retrieving the same image object.
    image = location.storage.image

    # Set the storage to the one we got from the location, to prevent another query
    image.storage = location.storage

    if not image.id in images:
      images[image.id] = image
      image.storage.locations = set()
    else:
      image = images[image.id]

    # Add the location to the image's locations set.
    image.storage.locations.add(location.location.name)

  return images.values()


def lookup_repository_images(namespace_name, repository_name, docker_image_ids):
  return (Image
          .select()
          .join(Repository)
          .join(Namespace, on=(Repository.namespace_user == Namespace.id))
          .where(Repository.name == repository_name, Namespace.username == namespace_name,
                 Image.docker_image_id << docker_image_ids))

def get_matching_repository_images(namespace_name, repository_name, docker_image_ids):
  def modify_query(q):
    return q.where(Image.docker_image_id << docker_image_ids)

  return _get_repository_images_base(namespace_name, repository_name, modify_query)

def get_repository_images(namespace_name, repository_name):
  return _get_repository_images_base(namespace_name, repository_name, lambda q: q)


def _tag_alive(query, now_ts=None):
  if now_ts is None:
    now_ts = get_epoch_timestamp()
  return query.where((RepositoryTag.lifetime_end_ts >> None) |
                     (RepositoryTag.lifetime_end_ts > now_ts))


def list_repository_tag_history(repository, limit=100, specific_tag=None):
  query = (RepositoryTag
           .select(RepositoryTag, Image)
           .join(Image)
           .where(RepositoryTag.repository == repository)
           .where(RepositoryTag.hidden == False)
           .order_by(RepositoryTag.lifetime_start_ts.desc())
           .limit(limit))

  if specific_tag:
    query = query.where(RepositoryTag.name == specific_tag)

  return query


def list_repository_tags(namespace_name, repository_name, include_hidden=False,
                         include_storage=False):

  toSelect = (RepositoryTag, Image)
  if include_storage:
    toSelect = (RepositoryTag, Image, ImageStorage)

  query = _tag_alive(RepositoryTag
                     .select(*toSelect)
                     .join(Repository)
                     .join(Namespace, on=(Repository.namespace_user == Namespace.id))
                     .switch(RepositoryTag)
                     .join(Image)
                     .where(Repository.name == repository_name,
                            Namespace.username == namespace_name))

  if not include_hidden:
    query = query.where(RepositoryTag.hidden == False)

  if include_storage:
    query = query.switch(Image).join(ImageStorage)

  return query


def _garbage_collect_tags(namespace_name, repository_name):
  # We do this without using a join to prevent holding read locks on the repository table
  repo = _get_repository(namespace_name, repository_name)
  expired_time = get_epoch_timestamp() - repo.namespace_user.removed_tag_expiration_s

  tags_to_delete = list(RepositoryTag
                        .select(RepositoryTag.id)
                        .where(RepositoryTag.repository == repo,
                               ~(RepositoryTag.lifetime_end_ts >> None),
                               (RepositoryTag.lifetime_end_ts <= expired_time))
                        .order_by(RepositoryTag.id))
  if len(tags_to_delete) > 0:
    (RepositoryTag
     .delete()
     .where(RepositoryTag.id << tags_to_delete)
     .execute())


def garbage_collect_repository(namespace_name, repository_name):
  storage_id_whitelist = {}

  _garbage_collect_tags(namespace_name, repository_name)

  with config.app_config['DB_TRANSACTION_FACTORY'](db):
    # TODO (jake): We could probably select this and all the images in a single query using
    # a different kind of join.

    # Get a list of all images used by tags in the repository
    tag_query = (RepositoryTag
                 .select(RepositoryTag, Image, ImageStorage)
                 .join(Image)
                 .join(ImageStorage, JOIN_LEFT_OUTER)
                 .switch(RepositoryTag)
                 .join(Repository)
                 .join(Namespace, on=(Repository.namespace_user == Namespace.id))
                 .where(Repository.name == repository_name, Namespace.username == namespace_name))

    referenced_anscestors = set()
    for tag in tag_query:
      # The anscestor list is in the format '/1/2/3/', extract just the ids
      anscestor_id_strings = tag.image.ancestors.split('/')[1:-1]
      ancestor_list = [int(img_id_str) for img_id_str in anscestor_id_strings]
      referenced_anscestors = referenced_anscestors.union(set(ancestor_list))
      referenced_anscestors.add(tag.image.id)

    all_repo_images = _get_repository_images(namespace_name, repository_name, lambda q: q)
    all_images = {int(img.id): img for img in all_repo_images}
    to_remove = set(all_images.keys()).difference(referenced_anscestors)

    if len(to_remove) > 0:
      logger.info('Cleaning up unreferenced images: %s', to_remove)
      storage_id_whitelist = {all_images[to_remove_id].storage.id for to_remove_id in to_remove}
      Image.delete().where(Image.id << list(to_remove)).execute()

  if len(to_remove) > 0:
    logger.info('Garbage collecting storage for images: %s', to_remove)
    _garbage_collect_storage(storage_id_whitelist)


def _garbage_collect_storage(storage_id_whitelist):
  if len(storage_id_whitelist) == 0:
    return

  def placements_query_to_paths_set(placements_query):
    return {(placement.location.name, config.store.image_path(placement.storage.uuid))
            for placement in placements_query}

  def orphaned_storage_query(select_base_query, candidates, group_by):
    return (select_base_query
            .switch(ImageStorage)
            .join(Image, JOIN_LEFT_OUTER)
            .switch(ImageStorage)
            .join(DerivedImageStorage, JOIN_LEFT_OUTER,
                  on=(ImageStorage.id == DerivedImageStorage.derivative))
            .where(ImageStorage.id << list(candidates))
            .group_by(*group_by)
            .having((fn.Count(Image.id) == 0) & (fn.Count(DerivedImageStorage.id) == 0)))

  # Note: We remove the derived image storage in its own transaction as a way to reduce the
  # time that the transaction holds on the database indicies. This could result in a derived
  # image storage being deleted for an image storage which is later reused during this time,
  # but since these are caches anyway, it isn't terrible and worth the tradeoff (for now).
  logger.debug('Garbage collecting derived storage from candidates: %s', storage_id_whitelist)
  with config.app_config['DB_TRANSACTION_FACTORY'](db):
    # Find out which derived storages will be removed, and add them to the whitelist
    # The comma after ImageStorage.id is VERY important, it makes it a tuple, which is a sequence
    orphaned_from_candidates = list(orphaned_storage_query(ImageStorage.select(ImageStorage.id),
                                                           storage_id_whitelist,
                                                           (ImageStorage.id,)))

    if len(orphaned_from_candidates) > 0:
      derived_to_remove = (ImageStorage
                           .select(ImageStorage.id)
                           .join(DerivedImageStorage,
                                 on=(ImageStorage.id == DerivedImageStorage.derivative))
                           .where(DerivedImageStorage.source << orphaned_from_candidates))
      storage_id_whitelist.update({derived.id for derived in derived_to_remove})

      # Remove the dervived image storages with sources of orphaned storages
      (DerivedImageStorage
       .delete()
       .where(DerivedImageStorage.source << orphaned_from_candidates)
       .execute())

  # Note: Both of these deletes must occur in the same transaction (unfortunately) because a
  # storage without any placement is invalid, and a placement cannot exist without a storage.
  # TODO(jake): We might want to allow for null storages on placements, which would allow us to
  # delete the storages, then delete the placements in a non-transaction.
  logger.debug('Garbage collecting storages from candidates: %s', storage_id_whitelist)
  with config.app_config['DB_TRANSACTION_FACTORY'](db):
    # Track all of the data that should be removed from blob storage
    placements_to_remove = list(orphaned_storage_query(ImageStoragePlacement
                                                       .select(ImageStoragePlacement,
                                                               ImageStorage,
                                                               ImageStorageLocation)
                                                       .join(ImageStorageLocation)
                                                       .switch(ImageStoragePlacement)
                                                       .join(ImageStorage),
                                                       storage_id_whitelist,
                                                       (ImageStorage, ImageStoragePlacement,
                                                        ImageStorageLocation)))

    paths_to_remove = placements_query_to_paths_set(placements_to_remove)

    # Remove the placements for orphaned storages
    if len(placements_to_remove) > 0:
      placement_ids_to_remove = [placement.id for placement in placements_to_remove]
      placements_removed = (ImageStoragePlacement
                            .delete()
                            .where(ImageStoragePlacement.id << placement_ids_to_remove)
                            .execute())
      logger.debug('Removed %s image storage placements', placements_removed)

    # Remove all orphaned storages
    # The comma after ImageStorage.id is VERY important, it makes it a tuple, which is a sequence
    orphaned_storages = list(orphaned_storage_query(ImageStorage.select(ImageStorage.id),
                                                    storage_id_whitelist,
                                                    (ImageStorage.id,)).alias('osq'))
    if len(orphaned_storages) > 0:
      storages_removed = (ImageStorage
                          .delete()
                          .where(ImageStorage.id << orphaned_storages)
                          .execute())
      logger.debug('Removed %s image storage records', storages_removed)

  # We are going to make the conscious decision to not delete image storage blobs inside
  # transactions.
  # This may end up producing garbage in s3, trading off for higher availability in the database.
  for location_name, image_path in paths_to_remove:
    logger.debug('Removing %s from %s', image_path, location_name)
    config.store.remove({location_name}, image_path)


def get_tag_image(namespace_name, repository_name, tag_name):
  def limit_to_tag(query):
    return _tag_alive(query
                      .switch(Image)
                      .join(RepositoryTag)
                      .where(RepositoryTag.name == tag_name))

  images = _get_repository_images_base(namespace_name, repository_name, limit_to_tag)
  if not images:
    raise DataModelException('Unable to find image for tag.')
  else:
    return images[0]

def get_image_by_id(namespace_name, repository_name, docker_image_id):
  image = get_repo_image_extended(namespace_name, repository_name, docker_image_id)
  if not image:
    raise DataModelException('Unable to find image \'%s\' for repo \'%s/%s\'' %
                             (docker_image_id, namespace_name, repository_name))
  return image


def get_parent_images(namespace_name, repository_name, image_obj):
  """ Returns a list of parent Image objects in chronilogical order. """
  parents = image_obj.ancestors
  parent_db_ids = parents.strip('/').split('/')

  if parent_db_ids == ['']:
    return []

  def filter_to_parents(query):
    return query.where(Image.id << parent_db_ids)

  parents = _get_repository_images_base(namespace_name, repository_name, filter_to_parents)

  id_to_image = {unicode(image.id): image for image in parents}

  return [id_to_image[parent_id] for parent_id in parent_db_ids]


def create_or_update_tag(namespace_name, repository_name, tag_name,
                         tag_docker_image_id, reversion=False):
  try:
    repo = _get_repository(namespace_name, repository_name)
  except Repository.DoesNotExist:
    raise DataModelException('Invalid repository %s/%s' % (namespace_name, repository_name))

  now_ts = get_epoch_timestamp()

  with config.app_config['DB_TRANSACTION_FACTORY'](db):
    try:
      tag = db_for_update(_tag_alive(RepositoryTag
                                     .select()
                                     .where(RepositoryTag.repository == repo,
                                            RepositoryTag.name == tag_name), now_ts)).get()
      tag.lifetime_end_ts = now_ts
      tag.save()
    except RepositoryTag.DoesNotExist:
      pass

    try:
      image = Image.get(Image.docker_image_id == tag_docker_image_id, Image.repository == repo)
    except Image.DoesNotExist:
      raise DataModelException('Invalid image with id: %s' % tag_docker_image_id)

    return RepositoryTag.create(repository=repo, image=image, name=tag_name,
                                lifetime_start_ts=now_ts, reversion=reversion)

def delete_tag(namespace_name, repository_name, tag_name):
  now_ts = get_epoch_timestamp()
  with config.app_config['DB_TRANSACTION_FACTORY'](db):
    try:
      query = _tag_alive(RepositoryTag
                         .select(RepositoryTag, Repository)
                         .join(Repository)
                         .join(Namespace, on=(Repository.namespace_user == Namespace.id))
                         .where(Repository.name == repository_name,
                                Namespace.username == namespace_name,
                                RepositoryTag.name == tag_name), now_ts)
      found = db_for_update(query).get()
    except RepositoryTag.DoesNotExist:
      msg = ('Invalid repository tag \'%s\' on repository \'%s/%s\'' %
             (tag_name, namespace_name, repository_name))
      raise DataModelException(msg)

    found.lifetime_end_ts = now_ts
    found.save()


def create_temporary_hidden_tag(repo, image, expiration_s):
  """ Create a tag with a defined timeline, that will not appear in the UI or CLI. Returns the name
      of the temporary tag. """
  now_ts = get_epoch_timestamp()
  expire_ts = now_ts + expiration_s
  tag_name = str(uuid4())
  RepositoryTag.create(repository=repo, image=image, name=tag_name, lifetime_start_ts=now_ts,
                       lifetime_end_ts=expire_ts, hidden=True)
  return tag_name


def purge_all_repository_tags(namespace_name, repository_name):
  """ Immediately purge all repository tags without respecting the lifeline procedure """
  try:
    repo = _get_repository(namespace_name, repository_name)
  except Repository.DoesNotExist:
    raise DataModelException('Invalid repository \'%s/%s\'' %
                             (namespace_name, repository_name))
  RepositoryTag.delete().where(RepositoryTag.repository == repo.id).execute()


def __entity_permission_repo_query(entity_id, entity_table, entity_id_property, namespace_name,
                                   repository_name):
  """ This method works for both users and teams. """

  return (RepositoryPermission
          .select(entity_table, Repository, Namespace, Role, RepositoryPermission)
          .join(entity_table)
          .switch(RepositoryPermission)
          .join(Role)
          .switch(RepositoryPermission)
          .join(Repository)
          .join(Namespace, on=(Repository.namespace_user == Namespace.id))
          .where(Repository.name == repository_name, Namespace.username == namespace_name,
                 entity_id_property == entity_id))


def get_user_reponame_permission(username, namespace_name, repository_name):
  fetched = list(__entity_permission_repo_query(username, User, User.username,
                                                namespace_name,
                                                repository_name))
  if not fetched:
    raise DataModelException('User does not have permission for repo.')

  return fetched[0]


def get_team_reponame_permission(team_name, namespace_name, repository_name):
  fetched = list(__entity_permission_repo_query(team_name, Team, Team.name,
                                                namespace_name,
                                                repository_name))
  if not fetched:
    raise DataModelException('Team does not have permission for repo.')

  return fetched[0]


def delete_user_permission(username, namespace_name, repository_name):
  if username == namespace_name:
    raise DataModelException('Namespace owner must always be admin.')

  fetched = list(__entity_permission_repo_query(username, User, User.username,
                                                namespace_name,
                                                repository_name))
  if not fetched:
    raise DataModelException('User does not have permission for repo.')

  fetched[0].delete_instance()


def delete_team_permission(team_name, namespace_name, repository_name):
  fetched = list(__entity_permission_repo_query(team_name, Team, Team.name,
                                                namespace_name,
                                                repository_name))
  if not fetched:
    raise DataModelException('Team does not have permission for repo.')

  fetched[0].delete_instance()


def __set_entity_repo_permission(entity, permission_entity_property,
                                 namespace_name, repository_name, role_name):
  repo = _get_repository(namespace_name, repository_name)
  new_role = Role.get(Role.name == role_name)

  # Fetch any existing permission for this entity on the repo
  try:
    entity_attr = getattr(RepositoryPermission, permission_entity_property)
    perm = RepositoryPermission.get(entity_attr == entity,
                                    RepositoryPermission.repository == repo)
    perm.role = new_role
    perm.save()
    return perm
  except RepositoryPermission.DoesNotExist:
    set_entity_kwargs = {permission_entity_property: entity}
    new_perm = RepositoryPermission.create(repository=repo, role=new_role,
                                           **set_entity_kwargs)
    return new_perm


def set_user_repo_permission(username, namespace_name, repository_name,
                             role_name):
  if username == namespace_name:
    raise DataModelException('Namespace owner must always be admin.')

  try:
    user = User.get(User.username == username)
  except User.DoesNotExist:
    raise InvalidUsernameException('Invalid username: %s' % username)
  return __set_entity_repo_permission(user, 'user', namespace_name,
                                      repository_name, role_name)


def set_team_repo_permission(team_name, namespace_name, repository_name,
                             role_name):
  team = list(Team.select().join(User).where(Team.name == team_name,
                                             User.username == namespace_name))
  if not team:
    raise DataModelException('No team \'%s\' in organization \'%s\'.' %
                             (team_name, namespace_name))

  return __set_entity_repo_permission(team[0], 'team', namespace_name,
                                      repository_name, role_name)


def purge_repository(namespace_name, repository_name):
  # Delete all tags to allow gc to reclaim storage
  purge_all_repository_tags(namespace_name, repository_name)

  # Gc to remove the images and storage
  garbage_collect_repository(namespace_name, repository_name)

  # Delete the rest of the repository metadata
  fetched = _get_repository(namespace_name, repository_name)
  fetched.delete_instance(recursive=True, delete_nullable=True)


def get_private_repo_count(username):
  return (Repository
          .select()
          .join(Visibility)
          .switch(Repository)
          .join(Namespace, on=(Repository.namespace_user == Namespace.id))
          .where(Namespace.username == username, Visibility.name == 'private')
          .count())


def create_access_token(repository, role, kind=None, friendly_name=None):
  role = Role.get(Role.name == role)
  kind_ref = None
  if kind is not None:
    kind_ref = AccessTokenKind.get(AccessTokenKind.name == kind)

  new_token = AccessToken.create(repository=repository, temporary=True,
                                 role=role, kind=kind_ref, friendly_name=friendly_name)
  return new_token


def create_delegate_token(namespace_name, repository_name, friendly_name,
                          role='read'):
  read_only = Role.get(name=role)
  repo = _get_repository(namespace_name, repository_name)
  new_token = AccessToken.create(repository=repo, role=read_only,
                                 friendly_name=friendly_name, temporary=False)
  return new_token


def get_repository_delegate_tokens(namespace_name, repository_name):
  return (AccessToken
          .select(AccessToken, Role)
          .join(Repository)
          .join(Namespace, on=(Repository.namespace_user == Namespace.id))
          .switch(AccessToken)
          .join(Role)
          .switch(AccessToken)
          .join(RepositoryBuildTrigger, JOIN_LEFT_OUTER)
          .where(Repository.name == repository_name, Namespace.username == namespace_name,
                 AccessToken.temporary == False, RepositoryBuildTrigger.uuid >> None))


def get_repo_delegate_token(namespace_name, repository_name, code):
  repo_query = get_repository_delegate_tokens(namespace_name, repository_name)

  try:
    return repo_query.where(AccessToken.code == code).get()
  except AccessToken.DoesNotExist:
    raise InvalidTokenException('Unable to find token with code: %s' % code)


def set_repo_delegate_token_role(namespace_name, repository_name, code, role):
  token = get_repo_delegate_token(namespace_name, repository_name, code)

  if role != 'read' and role != 'write':
    raise DataModelException('Invalid role for delegate token: %s' % role)

  new_role = Role.get(Role.name == role)
  token.role = new_role
  token.save()

  return token


def delete_delegate_token(namespace_name, repository_name, code):
  token = get_repo_delegate_token(namespace_name, repository_name, code)
  token.delete_instance(recursive=True)
  return token


def load_token_data(code):
  """ Load the permissions for any token by code. """
  try:
    return (AccessToken
            .select(AccessToken, Repository, Namespace, Role)
            .join(Role)
            .switch(AccessToken)
            .join(Repository)
            .join(Namespace, on=(Repository.namespace_user == Namespace.id))
            .where(AccessToken.code == code)
            .get())

  except AccessToken.DoesNotExist:
    raise InvalidTokenException('Invalid delegate token code: %s' % code)


def _get_build_base_query():
  return (RepositoryBuild
          .select(RepositoryBuild, RepositoryBuildTrigger, BuildTriggerService, Repository,
                  Namespace)
          .join(Repository)
          .join(Namespace, on=(Repository.namespace_user == Namespace.id))
          .switch(RepositoryBuild)
          .join(RepositoryBuildTrigger, JOIN_LEFT_OUTER)
          .join(BuildTriggerService, JOIN_LEFT_OUTER)
          .order_by(RepositoryBuild.started.desc()))


def get_repository_build(build_uuid):
  try:
    return _get_build_base_query().where(RepositoryBuild.uuid == build_uuid).get()

  except RepositoryBuild.DoesNotExist:
    msg = 'Unable to locate a build by id: %s' % build_uuid
    raise InvalidRepositoryBuildException(msg)


def list_repository_builds(namespace_name, repository_name, limit,
                           include_inactive=True, since=None):
  query = (_get_build_base_query()
           .where(Repository.name == repository_name, Namespace.username == namespace_name)
           .limit(limit))

  if since is not None:
    query = query.where(RepositoryBuild.started >= since)

  if not include_inactive:
    query = query.where(RepositoryBuild.phase != 'error',
                        RepositoryBuild.phase != 'complete')

  return query


def get_recent_repository_build(namespace_name, repository_name):
  query = list_repository_builds(namespace_name, repository_name, 1)
  try:
    return query.get()
  except RepositoryBuild.DoesNotExist:
    return None


def create_repository_build(repo, access_token, job_config_obj, dockerfile_id,
                            display_name, trigger=None, pull_robot_name=None):
  pull_robot = None
  if pull_robot_name:
    pull_robot = lookup_robot(pull_robot_name)

  return RepositoryBuild.create(repository=repo, access_token=access_token,
                                 job_config=json.dumps(job_config_obj),
                                 display_name=display_name, trigger=trigger,
                                 resource_key=dockerfile_id,
                                 pull_robot=pull_robot)


def get_pull_robot_name(trigger):
  if not trigger.pull_robot:
    return None

  return trigger.pull_robot.username


def get_pull_credentials(robotname):
  robot = lookup_robot(robotname)
  if not robot:
    return None

  try:
    login_info = FederatedLogin.get(user=robot)
  except FederatedLogin.DoesNotExist:
    return None

  return {
    'username': robot.username,
    'password': login_info.service_ident,
    'registry': '%s://%s/v1/' % (config.app_config['PREFERRED_URL_SCHEME'],
                                 config.app_config['SERVER_HOSTNAME']),
  }


def create_repo_notification(repo, event_name, method_name, config):
  event = ExternalNotificationEvent.get(ExternalNotificationEvent.name == event_name)
  method = ExternalNotificationMethod.get(ExternalNotificationMethod.name == method_name)

  return RepositoryNotification.create(repository=repo, event=event, method=method,
                                       config_json=json.dumps(config))


def get_repo_notification(uuid):
  try:
    return (RepositoryNotification
            .select(RepositoryNotification, Repository, Namespace)
            .join(Repository)
            .join(Namespace, on=(Repository.namespace_user == Namespace.id))
            .where(RepositoryNotification.uuid == uuid)
            .get())
  except RepositoryNotification.DoesNotExist:
    raise InvalidNotificationException('No repository notification found with id: %s' % uuid)


def delete_repo_notification(namespace_name, repository_name, uuid):
  found = get_repo_notification(uuid)
  if (found.repository.namespace_user.username != namespace_name or
      found.repository.name != repository_name):
    raise InvalidNotificationException('No repository notifiation found with id: %s' % uuid)
  found.delete_instance()
  return found


def list_repo_notifications(namespace_name, repository_name, event_name=None):
  query = (RepositoryNotification
           .select(RepositoryNotification, Repository, Namespace)
           .join(Repository)
           .join(Namespace, on=(Repository.namespace_user == Namespace.id))
           .where(Namespace.username == namespace_name, Repository.name == repository_name))

  if event_name:
    query = (query
             .switch(RepositoryNotification)
             .join(ExternalNotificationEvent)
             .where(ExternalNotificationEvent.name == event_name))

  return query


def list_logs(start_time, end_time, performer=None, repository=None, namespace=None):
  Performer = User.alias()
  joined = (LogEntry.select(LogEntry, LogEntryKind, User, Performer)
                    .join(User)
                    .switch(LogEntry)
                    .join(Performer, JOIN_LEFT_OUTER,
                        on=(LogEntry.performer == Performer.id).alias('performer'))
                    .switch(LogEntry)
                    .join(LogEntryKind)
                    .switch(LogEntry))

  if repository:
    joined = joined.where(LogEntry.repository == repository)

  if performer:
    joined = joined.where(LogEntry.performer == performer)

  if namespace:
    joined = joined.where(User.username == namespace)

  return list(joined.where(
    LogEntry.datetime >= start_time,
    LogEntry.datetime < end_time).order_by(LogEntry.datetime.desc()))


def log_action(kind_name, user_or_organization_name, performer=None,
               repository=None, access_token=None, ip=None, metadata={},
               timestamp=None):
  if not timestamp:
    timestamp = datetime.today()

  kind = LogEntryKind.get(LogEntryKind.name == kind_name)
  account = User.get(User.username == user_or_organization_name)
  LogEntry.create(kind=kind, account=account, performer=performer,
                  repository=repository, ip=ip, metadata_json=json.dumps(metadata),
                  datetime=timestamp)


def create_build_trigger(repo, service_name, auth_token, user, pull_robot=None):
  service = BuildTriggerService.get(name=service_name)
  trigger = RepositoryBuildTrigger.create(repository=repo, service=service,
                                          auth_token=auth_token,
                                          connected_user=user,
                                          pull_robot=pull_robot)
  return trigger


def get_build_trigger(trigger_uuid):
  try:
    return (RepositoryBuildTrigger
            .select(RepositoryBuildTrigger, BuildTriggerService, Repository, Namespace)
            .join(BuildTriggerService)
            .switch(RepositoryBuildTrigger)
            .join(Repository)
            .join(Namespace, on=(Repository.namespace_user == Namespace.id))
            .switch(RepositoryBuildTrigger)
            .join(User)
            .where(RepositoryBuildTrigger.uuid == trigger_uuid)
            .get())
  except RepositoryBuildTrigger.DoesNotExist:
    msg = 'No build trigger with uuid: %s' % trigger_uuid
    raise InvalidBuildTriggerException(msg)


def list_build_triggers(namespace_name, repository_name):
  return (RepositoryBuildTrigger
          .select(RepositoryBuildTrigger, BuildTriggerService, Repository)
          .join(BuildTriggerService)
          .switch(RepositoryBuildTrigger)
          .join(Repository)
          .join(Namespace, on=(Repository.namespace_user == Namespace.id))
          .where(Namespace.username == namespace_name, Repository.name == repository_name))


def list_trigger_builds(namespace_name, repository_name, trigger_uuid,
                        limit):
  return (list_repository_builds(namespace_name, repository_name, limit)
    .where(RepositoryBuildTrigger.uuid == trigger_uuid))


def create_notification(kind_name, target, metadata={}):
  kind_ref = NotificationKind.get(name=kind_name)
  notification = Notification.create(kind=kind_ref, target=target,
                                     metadata_json=json.dumps(metadata))
  return notification


def create_unique_notification(kind_name, target, metadata={}):
  with config.app_config['DB_TRANSACTION_FACTORY'](db):
    if list_notifications(target, kind_name, limit=1).count() == 0:
      create_notification(kind_name, target, metadata)


def lookup_notification(user, uuid):
  results = list(list_notifications(user, id_filter=uuid, include_dismissed=True, limit=1))
  if not results:
    return None

  return results[0]


def list_notifications(user, kind_name=None, id_filter=None, include_dismissed=False,
                       page=None, limit=None):
  Org = User.alias()
  AdminTeam = Team.alias()
  AdminTeamMember = TeamMember.alias()
  AdminUser = User.alias()

  query = (Notification.select()
           .join(User)

           .switch(Notification)
           .join(Org, JOIN_LEFT_OUTER, on=(Org.id == Notification.target))
           .join(AdminTeam, JOIN_LEFT_OUTER, on=(Org.id ==
                                                 AdminTeam.organization))
           .join(TeamRole, JOIN_LEFT_OUTER, on=(AdminTeam.role == TeamRole.id))
           .switch(AdminTeam)
           .join(AdminTeamMember, JOIN_LEFT_OUTER, on=(AdminTeam.id ==
                                                       AdminTeamMember.team))
           .join(AdminUser, JOIN_LEFT_OUTER, on=(AdminTeamMember.user ==
                                                 AdminUser.id))
           .where((Notification.target == user) |
                  ((AdminUser.id == user) & (TeamRole.name == 'admin')))
           .order_by(Notification.created)
           .desc())

  if not include_dismissed:
    query = query.switch(Notification).where(Notification.dismissed == False)

  if kind_name:
    query = (query
      .switch(Notification)
      .join(NotificationKind)
      .where(NotificationKind.name == kind_name))

  if id_filter:
    query = (query
      .switch(Notification)
      .where(Notification.uuid == id_filter))

  if page:
    query = query.paginate(page, limit)
  elif limit:
    query = query.limit(limit)

  return query


def delete_all_notifications_by_kind(kind_name):
  kind_ref = NotificationKind.get(name=kind_name)
  (Notification.delete()
    .where(Notification.kind == kind_ref)
    .execute())


def delete_notifications_by_kind(target, kind_name):
  kind_ref = NotificationKind.get(name=kind_name)
  Notification.delete().where(Notification.target == target,
                              Notification.kind == kind_ref).execute()


def delete_matching_notifications(target, kind_name, **kwargs):
  kind_ref = NotificationKind.get(name=kind_name)

  # Load all notifications for the user with the given kind.
  notifications = Notification.select().where(
      Notification.target == target,
      Notification.kind == kind_ref)

  # For each, match the metadata to the specified values.
  for notification in notifications:
    matches = True
    try:
      metadata = json.loads(notification.metadata_json)
    except:
      continue

    for (key, value) in kwargs.iteritems():
      if not key in metadata or metadata[key] != value:
        matches = False
        break

    if not matches:
      continue

    notification.delete_instance()


def get_active_users():
  return User.select().where(User.organization == False, User.robot == False)


def get_active_user_count():
  return get_active_users().count()


def detach_external_login(user, service_name):
  try:
    service = LoginService.get(name = service_name)
  except LoginService.DoesNotExist:
    return

  FederatedLogin.delete().where(FederatedLogin.user == user,
                                FederatedLogin.service == service).execute()


def delete_user(user):
  user.delete_instance(recursive=True, delete_nullable=True)

  # TODO: also delete any repository data associated


def check_health(app_config):
  # Attempt to connect to the database first. If the DB is not responding,
  # using the validate_database_url will timeout quickly, as opposed to
  # making a normal connect which will just hang (thus breaking the health
  # check).
  try:
    validate_database_url(app_config['DB_URI'], connect_timeout=3)
  except Exception:
    logger.exception('Could not connect to the database')
    return False

  # We will connect to the db, check that it contains some log entry kinds
  try:
    return bool(list(LogEntryKind.select().limit(1)))
  except:
    return False


def get_email_authorized_for_repo(namespace, repository, email):
  try:
    return (RepositoryAuthorizedEmail
            .select(RepositoryAuthorizedEmail, Repository, Namespace)
            .join(Repository)
            .join(Namespace, on=(Repository.namespace_user == Namespace.id))
            .where(Namespace.username == namespace, Repository.name == repository,
                   RepositoryAuthorizedEmail.email == email)
            .get())
  except RepositoryAuthorizedEmail.DoesNotExist:
    return None


def create_email_authorization_for_repo(namespace_name, repository_name, email):
  try:
    repo = _get_repository(namespace_name, repository_name)
  except Repository.DoesNotExist:
    raise DataModelException('Invalid repository %s/%s' %
                             (namespace_name, repository_name))

  return RepositoryAuthorizedEmail.create(repository=repo, email=email, confirmed=False)


def confirm_email_authorization_for_repo(code):
  try:
    found = (RepositoryAuthorizedEmail
             .select(RepositoryAuthorizedEmail, Repository, Namespace)
             .join(Repository)
             .join(Namespace, on=(Repository.namespace_user == Namespace.id))
             .where(RepositoryAuthorizedEmail.code == code)
             .get())
  except RepositoryAuthorizedEmail.DoesNotExist:
    raise DataModelException('Invalid confirmation code.')

  found.confirmed = True
  found.save()

  return found


def delete_team_email_invite(team, email):
  found = TeamMemberInvite.get(TeamMemberInvite.email == email, TeamMemberInvite.team == team)
  found.delete_instance()

def delete_team_user_invite(team, user):
  try:
    found = TeamMemberInvite.get(TeamMemberInvite.user == user, TeamMemberInvite.team == team)
  except TeamMemberInvite.DoesNotExist:
    return False

  found.delete_instance()
  return True

def lookup_team_invites(user):
  return TeamMemberInvite.select().where(TeamMemberInvite.user == user)

def lookup_team_invite(code, user=None):
 # Lookup the invite code.
  try:
    found = TeamMemberInvite.get(TeamMemberInvite.invite_token == code)
  except TeamMemberInvite.DoesNotExist:
    raise DataModelException('Invalid confirmation code.')

  if user and found.user != user:
    raise DataModelException('Invalid confirmation code.')

  return found

def delete_team_invite(code, user=None):
  found = lookup_team_invite(code, user)

  team = found.team
  inviter = found.inviter

  found.delete_instance()

  return (team, inviter)


def confirm_team_invite(code, user):
  found = lookup_team_invite(code)

  # If the invite is for a specific user, we have to confirm that here.
  if found.user is not None and found.user != user:
    message = """This invite is intended for user "%s".
                 Please login to that account and try again.""" % found.user.username
    raise DataModelException(message)

  # Add the user to the team.
  try:
    add_user_to_team(user, found.team)
  except UserAlreadyInTeam:
    # Ignore.
    pass

  # Delete the invite and return the team.
  team = found.team
  inviter = found.inviter
  found.delete_instance()
  return (team, inviter)

def cancel_repository_build(build, work_queue):
  with config.app_config['DB_TRANSACTION_FACTORY'](db):
    # Reload the build for update.
    try:
      build = db_for_update(RepositoryBuild.select().where(RepositoryBuild.id == build.id)).get()
    except RepositoryBuild.DoesNotExist:
      return False

    if build.phase != BUILD_PHASE.WAITING or not build.queue_id:
      return False

    # Try to cancel the queue item.
    if not work_queue.cancel(build.queue_id):
      return False

    # Delete the build row.
    build.delete_instance()
    return True

def get_repository_pushes(repository, time_delta):
  since = date.today() - time_delta
  push_repo = LogEntryKind.get(name = 'push_repo')
  return (LogEntry.select()
                  .where(LogEntry.repository == repository)
                  .where(LogEntry.kind == push_repo)
                  .where(LogEntry.datetime >= since)
                  .count())

def get_repository_pulls(repository, time_delta):
  since = date.today() - time_delta
  repo_pull = LogEntryKind.get(name = 'pull_repo')
  repo_verb = LogEntryKind.get(name = 'repo_verb')
  return (LogEntry.select()
                  .where(LogEntry.repository == repository)
                  .where((LogEntry.kind == repo_pull) | (LogEntry.kind == repo_verb))
                  .where(LogEntry.datetime >= since)
                  .count())


def get_repository_usage():
  one_month_ago = date.today() - timedelta(weeks=4)
  repo_pull = LogEntryKind.get(name = 'pull_repo')
  repo_verb = LogEntryKind.get(name = 'repo_verb')
  return (LogEntry.select(LogEntry.ip, LogEntry.repository)
                  .where((LogEntry.kind == repo_pull) | (LogEntry.kind == repo_verb))
                  .where(~(LogEntry.repository >> None))
                  .where(LogEntry.datetime >= one_month_ago)
                  .group_by(LogEntry.ip, LogEntry.repository)
                  .count())

def archivable_buildlogs_query():
  presumed_dead_date = datetime.utcnow() - PRESUMED_DEAD_BUILD_AGE
  return (RepositoryBuild.select()
    .where((RepositoryBuild.phase == BUILD_PHASE.COMPLETE) |
           (RepositoryBuild.phase == BUILD_PHASE.ERROR) |
           (RepositoryBuild.started < presumed_dead_date), RepositoryBuild.logs_archived == False))


def star_repository(user, repository):
  """ Stars a repository. """
  star = Star.create(user=user.id, repository=repository.id)
  star.save()


def unstar_repository(user, repository):
  """ Unstars a repository. """
  try:
    star = (Star
            .delete()
            .where(Star.repository == repository.id, Star.user == user.id)
            .execute())
  except Star.DoesNotExist:
    raise DataModelException('Star not found.')


def get_user_starred_repositories(user, limit=None, page=None):
  """ Retrieves all of the repositories a user has starred. """
  query = (Repository
           .select()
           .join(Star)
           .join(User)
           .where(User.id == user.id)
           .order_by(Star.created))

  if page and limit:
    query = query.paginate(page, limit)
  elif limit:
    query = query.limit(limit)

  return query


def repository_is_starred(user, repository):
  """ Determines whether a user has starred a repository or not. """
  try:
    (Star
     .select()
     .where(Star.repository == repository.id, Star.user == user.id)
     .get())
    return True
  except Star.DoesNotExist:
    return False


def revert_tag(repository, tag_name, docker_image_id):
  """ Reverts a tag to a specific image ID. """
  # Verify that the image ID already existed under this repository under the
  # tag.
  try:
    (RepositoryTag.select()
                  .join(Image)
                  .where(RepositoryTag.repository == repository)
                  .where(RepositoryTag.name == tag_name)
                  .where(Image.docker_image_id == docker_image_id)
                  .get())
  except RepositoryTag.DoesNotExist:
    raise DataModelException('Cannot revert to unknown or invalid image')

  return create_or_update_tag(repository.namespace_user.username, repository.name,
                              tag_name, docker_image_id, reversion=True)