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.stash.experimental.user.ProjectPermission;
import com.atlassian.stash.internal.hibernate.HibernatePageUtils;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.google.common.collect.ImmutableMap;
import org.hibernate.Criteria;
import org.hibernate.Query;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Restrictions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import javax.annotation.Nonnull;

import java.util.Collections;
import java.util.Map;

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

@Repository("grantedProjectPermissionDao")
public class HibernateProjectPermissionDao
        extends AbstractHibernateGrantedPermissionDao<InternalProjectPermission> implements ProjectPermissionDao {

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

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

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

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

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

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

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

    @Override
    public int revoke(@Nonnull InternalProjectPermission grantedPermission) {
        return createRevokeQuery(grantedPermission, "project.id = :projectId")
                .setInteger("projectId", grantedPermission.getProject().getId())
                .executeUpdate();
    }

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

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

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

    @Nonnull
    @Override
    public Map<Integer, Permission> getDefaultPermissions() {
        Query query = session().createQuery(
                "select p.project.id, max(t.weight) " +
                        "from InternalProjectPermission p, InternalPermissionType t " +
                        "where p.group is null and p.user is null and t.id = p.permission " +
                        "group by p.project");

        ImmutableMap.Builder<Integer, Permission> builder = ImmutableMap.builder();
        scroll(query, result -> builder.put(result.getInteger(0), Permission.fromWeight(result.getInteger(1))));
        return builder.build();
    }

    @Override
    public Permission getHighestDefaultPermission(int projectId) {
        Query query = session().createQuery(
                "select max(t.weight) " +
                        "from InternalProjectPermission p, InternalPermissionType t " +
                        "where p.permission = t.id " +
                        "and p.project.id = :projectId " +
                        "and p.group is null " +
                        "and p.user is null ")
                .setParameter("projectId", projectId);

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

    @Override
    protected Criteria createHasPermissionEntryCriteria(InternalProjectPermission grantedPermission) {
        return super.createHasPermissionEntryCriteria(grantedPermission)
                .add(Restrictions.eq("project", grantedPermission.getProject()));
    }

    @Override
    protected Iterable<Order> getImplicitOrder() {
        return Collections.singleton(Order.asc("id"));
    }

    private Criterion projectIdEq(int projectId) {
        return Restrictions.eq("project.id", projectId);
    }

}