package com.atlassian.stash.internal.user;

import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.permission.PermittedGroup;
import com.atlassian.bitbucket.permission.PermittedUser;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.user.UserType;
import com.atlassian.crowd.embedded.impl.IdentifierUtils;
import com.atlassian.stash.experimental.user.RepositoryPermission;
import com.atlassian.stash.internal.hibernate.HibernatePageUtils;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.google.common.collect.Collections2;
import com.google.common.collect.Sets;
import org.hibernate.Criteria;
import org.hibernate.Query;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.springframework.beans.factory.annotation.Autowired;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import static com.atlassian.stash.internal.HibernateUtils.initializePage;

@org.springframework.stereotype.Repository("grantedRepositoryPermissionDao")
public class HibernateRepositoryPermissionDao extends AbstractHibernateGrantedPermissionDao<InternalRepositoryPermission> implements RepositoryPermissionDao {

    @Autowired
    public HibernateRepositoryPermissionDao(SessionFactory sessionFactory) {
        super(sessionFactory);
    }

    @Override
    protected Criteria createHasPermissionEntryCriteria(InternalRepositoryPermission grantedPermission) {
        return super.createHasPermissionEntryCriteria(grantedPermission)
                .add(Restrictions.eq("repository", grantedPermission.getRepository()));
    }

    private Criterion createRestriction(int repositoryId) {
        return Restrictions.eq("repository.id", repositoryId);
    }

    @Nonnull
    @Override
    public Page<PermittedUser> findHighestPermissionPerUser(int repositoryId, @Nonnull UserType userType, @Nullable String filter, @Nonnull PageRequest pageRequest) {
        //First, find a page of users that have repository permissions.
        //This will do two things for us:
        //1. Apply pagination
        //2. Sort the results
        Page<ApplicationUser> page = findUsersWithPermission(userType, filter, pageRequest, createRestriction(repositoryId));
        if (page.getIsLastPage() && page.getSize() == 0) {
            return HibernatePageUtils.createEmptyPage(pageRequest);
        }

        // Secondly we query for the userIds and highest permissions
        Query query = createFindHighestPermissionPerUserQuery(page, "repository.id = :repositoryId")
                .setInteger("repositoryId", repositoryId);

        // Lastly we pair the results of the 2 queries
        return pairUsersWithPermissions(page, query);
    }

    @Nonnull
    @Override
    public Page<ApplicationUser> findUsersWithPermission(int repositoryId, @Nonnull UserType userType, @Nonnull PageRequest pageRequest) {
        return findUsersWithPermission(userType, null, pageRequest, createRestriction(repositoryId));
    }

    @Nonnull
    @Override
    public Page<PermittedGroup> findHighestPermissionPerGroup(int repositoryId, @Nullable String filter, @Nonnull PageRequest pageRequest) {
        Query query = createFindHighestPermissionPerGroupQuery(filter, "repository.id = :repositoryId")
                .setInteger("repositoryId", repositoryId);
        return HibernatePageUtils.pageQuery(query, pageRequest);
    }

    @Nonnull
    @Override
    public Page<String> findGroupsWithPermission(int repositoryId, @Nonnull PageRequest pageRequest) {
        return findGroupsWithPermission(pageRequest, createRestriction(repositoryId));
    }

    @Override
    public int revoke(@Nonnull InternalRepositoryPermission grantedPermission) {
        return createRevokeQuery(grantedPermission, "repository.id = :repositoryId")
                .setInteger("repositoryId", grantedPermission.getRepository().getId())
                .executeUpdate();
    }

    @Override
    public long countWithPermission(int userId, @Nonnull PartitionedGroups allGroups, int projectId) {
        Iterator<Collection<String>> i = allGroups.iterator();

        // if there is only one partition, perform on a count query instead of fetching each repository id in memory
        Collection<String> first = i.next();
        if (i.hasNext()) {
            Set<Long> ids = Sets.newHashSet();
            ids.addAll(getRepositoriesWithPermission(userId, first, projectId));
            while (i.hasNext()) {
                Collection<String> groups = i.next();
                ids.addAll(getRepositoriesWithPermission(userId, groups, projectId));
            }
            return ids.size();
        } else {
            return countRepositoriesWithPermission(userId, first, projectId);
        }
    }

    @Nullable
    @Override
    public Permission getHighestPermissionForUser(int userId, int repositoryId) {
        Query query = session().createQuery(
                "select max(t.weight) " +
                        "from InternalRepositoryPermission p, InternalPermissionType t " +
                        "where p.permission = t.id " +
                        "and p.repository.id = :repositoryId " +
                        "and p.group is null " +
                        "and p.user.id = :userId")
                .setParameter("userId", userId).setParameter("repositoryId", repositoryId);

        Integer maxWeight = (Integer) query.uniqueResult();
        return maxWeight == null ? null : Permission.fromWeight(maxWeight);
    }

    @Nonnull
    @Override
    public Page<RepositoryPermission> findHighestPermissionsForUser(int userId, @Nonnull PageRequest pageRequest) {
        Query query = session().createQuery(
                "select new com.atlassian.stash.internal.user.InternalGrantedRepositoryPermission(r, max(t.weight)) " +
                        "from InternalRepositoryPermission p, InternalPermissionType t, InternalRepository r, InternalProject pj " +
                        "where p.permission = t.id " +
                        "and p.repository.id = r.id " +
                        "and r.project.id = pj.id " +
                        "and p.group is null " +
                        "and p.user.id = :userId " +
                        "group by r.id, pj.name, r.name " +
                        "order by pj.name, r.name ").setParameter("userId", userId);
        return initializePage(HibernatePageUtils.<RepositoryPermission>pageQuery(query, pageRequest));
    }

    public long countRepositoriesWithPermission(int userId, Collection<String> groups, int projectId) {
        return (Long) repositoriesWithPermission(userId, groups, projectId)
                .setProjection(Projections.countDistinct("repository.id"))
                .uniqueResult();
    }

    public List<Long> getRepositoriesWithPermission(int userId, Collection<String> groups, int projectId) {
        //noinspection unchecked
        return (List<Long>) repositoriesWithPermission(userId, groups, projectId)
                .setProjection(Projections.distinct(Projections.property("repository.id")))
                .list();
    }

    private Criteria repositoriesWithPermission(int userId, Collection<String> groups, int projectId) {
        return session().createCriteria(entityClass)
                .createAlias("repository", "joinRepository") // do an inner join to the repository table
                .add(Restrictions.in("permission", Permission.getPermissionsOn(Repository.class))) // assume that all repo permissions implies REPO_READ
                .add(Restrictions.eq("joinRepository.project.id", projectId))
                .add(groups.isEmpty() ?
                        Restrictions.and(Restrictions.eq("user.id", userId), Restrictions.isNull("group")) :
                        Restrictions.or(
                                Restrictions.and(Restrictions.eq("user.id", userId), Restrictions.isNull("group")),
                                Restrictions.and(Restrictions.isNull("user"), Restrictions.in("group", Collections2.transform(groups, IdentifierUtils.TO_LOWER_CASE)))));
    }

}
