package com.atlassian.stash.internal.user;

import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.project.Project;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.crowd.embedded.impl.IdentifierUtils;
import com.atlassian.stash.internal.AbstractHibernateDao;
import com.atlassian.stash.internal.ApplicationConstants;
import com.atlassian.stash.internal.hibernate.HibernatePageUtils;
import com.google.common.collect.Collections2;
import org.hibernate.Criteria;
import org.hibernate.Query;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.*;
import org.hibernate.sql.JoinType;
import org.springframework.beans.factory.annotation.Autowired;

import javax.annotation.Nonnull;
import java.util.Collections;
import java.util.Set;
import java.util.function.Predicate;

@org.springframework.stereotype.Repository("effectivePermissionDao")
public class HibernateEffectivePermissionDao
        extends AbstractHibernateDao<Long, InternalGrantedPermission> implements EffectivePermissionDao {

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

    @Nonnull
    @Override
    public Page<InternalGrantedPermission> findByGroup(@Nonnull String group, @Nonnull PageRequest pageRequest) {
        Query query = session().createQuery(
                "select p from InternalGrantedPermission p, InternalPermissionType t " +
                        "where p.permission = t.id and p.user is null and p.group = :groupName order by id asc")
                .setString("groupName", IdentifierUtils.toLowerCase(group));

        return HibernatePageUtils.pageQuery(query, pageRequest);
    }

    @Nonnull
    @Override
    public Page<InternalGrantedPermission> findByUserId(@Nonnull Integer userId, @Nonnull PageRequest pageRequest) {
        Query query = session().createQuery(
                "select p from InternalGrantedPermission p, InternalPermissionType t " +
                        "where p.permission = t.id and p.user = :userId and p.group is null order by id asc")
                .setInteger("userId", userId);

        return HibernatePageUtils.pageQuery(query, pageRequest);
    }

    @Override
    @Nonnull
    public Page<String> findGroups(@Nonnull Permission permission, @Nonnull PageRequest pageRequest) {
        Criteria criteria = DetachedCriteria.forClass(InternalGlobalPermission.class)
                .add(Restrictions.in("permission", permission.getInheritingPermissions()))
                .add(Restrictions.isNull("user"))
                .add(Restrictions.isNotNull("group"))
                .addOrder(Order.asc("group"))
                .setProjection(Projections.distinct(Projections.property("group")))
                .getExecutableCriteria(session())
                        // We are able to cache finding groups with permissions as there are no inner
                        // queries required and hibernate has issues caching such queries.
                        // This cache is used for licensed user calculation
                .setCacheable(true)
                .setCacheRegion(ApplicationConstants.QUERY_CACHE_PERMISSIONS);
        return HibernatePageUtils.pageCriteria(criteria, pageRequest);
    }

    @Override
    @Nonnull
    public Page<ApplicationUser> findUsers(@Nonnull Permission permission, @Nonnull PageRequest pageRequest, @Nonnull Predicate<? super ApplicationUser> predicate) {
        DetachedCriteria permissionCriteria = DetachedCriteria.forClass(InternalGlobalPermission.class)
                .add(Restrictions.in("permission", permission.getInheritingPermissions()))
                .add(Restrictions.isNotNull("user"))
                .add(Restrictions.isNull("group"))
                .setProjection(Projections.distinct(Projections.property("user.id")));

        // find the users matching the permissions
        Criteria userCriteria = session().createCriteria(InternalNormalUser.class)
                .add(Subqueries.propertyIn("id", permissionCriteria))
                .addOrder(Order.asc("username"));
        return HibernatePageUtils.pageCriteria(userCriteria, pageRequest, predicate);
    }

    @Override
    public boolean isGrantedToGroup(@Nonnull GroupPermissionCriteria criteria) {
        Permission permission = criteria.getPermission();
        Set<String> groups = criteria.getGroups();
        DetachedCriteria permissionCriteria = DetachedCriteria.forClass(InternalGrantedPermission.class)
                .add(Restrictions.in("permission", permission.getInheritingPermissions()))
                .add(Restrictions.isNull("user"))
                // from HibernateGrantedPermissionDao, prior to 5fc1a0d6f9cbdc7563f0aea2c689691aa5881766:
                //Note: When performing a matchesAny query, we take implicit permissions into consideration. That means if
                //      permitted group is in the collection _or_ if the permitted group is null (which means the permission
                //      has been granted to all groups), there is a match
                .add(Restrictions.or(
                        Restrictions.isNull("group"),
                        Restrictions.in("group", Collections2.transform(groups, IdentifierUtils.TO_LOWER_CASE))))
                .setProjection(Projections.count("id"));

        includeResourceIds(criteria, permissionCriteria);

        return ((Long) permissionCriteria.getExecutableCriteria(session())
                // We are able to cache finding groups with permissions as there are no inner
                // queries required and hibernate has issues caching such queries.
                // This cache is useful as users will often be part of the same
                // groups leading to cached results between different threads.
                .setCacheable(true)
                .setCacheRegion(ApplicationConstants.QUERY_CACHE_PERMISSIONS)
                .uniqueResult()) > 0;
    }

    @Override
    public boolean isGrantedToUser(@Nonnull UserPermissionCriteria criteria) {
        Permission permission = criteria.getPermission();
        Integer userId = criteria.getUserId();
        DetachedCriteria permissionCriteria = DetachedCriteria.forClass(InternalGrantedPermission.class)
                .add(Restrictions.in("permission", permission.getInheritingPermissions()))
                .add(Restrictions.or(Restrictions.isNull("user"), Restrictions.eq("user.id", userId)))
                .add(Restrictions.isNull("group"));

        includeResourceIds(criteria, permissionCriteria);

        return ((Long) permissionCriteria.getExecutableCriteria(session())
                .setProjection(Projections.count("id"))
                .uniqueResult()) > 0;
    }

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

    private void includeResourceIds(PermissionCriteria criteria, DetachedCriteria permissionCriteria) {
        Permission permission = criteria.getPermission();
        Integer projectId = criteria.getProjectId();
        Integer repositoryId = criteria.getRepositoryId();

        if (permission.isGlobal()) {
            // matches only global permissions
            // (note: this clause is not necessary since non-global permissions would be eliminated
            //  by the 'in' clause on the perm_id column anyways. However, it leads to > 50% CPU/IO cost reduction.)
            permissionCriteria.add(Restrictions.isNull("project")).add(Restrictions.isNull("repository"));
        } else if (permission.isResource(Project.class) && !Permission.PROJECT_VIEW.equals(permission)) {
            // filter out repository permissions
            // (like above, this clause is not necessary since repository permissions would be eliminated by the 'in'
            //  clause on the perm_id column. However, it leads to ~ 20% CPU/IO cost reduction.)
            permissionCriteria.add(Restrictions.isNull("repository"));
            // the project id will be null for queries checking if the user (or group) has any project permission
            // (for example, queries originating from PermissionService.hasAnyUserPermission())
            if (projectId != null) {
                permissionCriteria.add(Restrictions.or(
                        // match global permissions
                        Restrictions.isNull("project"),
                        // match project permissions
                        Restrictions.eq("project.id", projectId)));
            }
        } else if (permission.isResource(Repository.class) || Permission.PROJECT_VIEW.equals(permission)) {
            if (projectId != null || repositoryId != null) {
                Disjunction disjunction = Restrictions.disjunction();
                // match global permissions
                disjunction.add(Restrictions.and(Restrictions.isNull("project"), Restrictions.isNull("repository")));
                // match project permissions
                if (projectId != null) { // assume there are usages for retrieving the repo permissions while ignoring inheritance from project permissions
                    disjunction.add(Restrictions.eq("project.id", projectId));
                }
                // match repository permissions
                if (repositoryId != null) {
                    disjunction.add(Restrictions.eq("repository.id", repositoryId));
                } else if (Permission.PROJECT_VIEW.equals(permission)) {
                    // Perform a left outer join to the repository table but ONLY when
                    // we need to
                    permissionCriteria.createAlias("repository", "joinRepository", JoinType.LEFT_OUTER_JOIN);

                    disjunction.add(Restrictions.eq("joinRepository.project.id", projectId));
                }
                permissionCriteria.add(disjunction);
            }
        } else {
            throw new IllegalArgumentException("Unsupported permission: " + permission);
        }
    }

}
