package com.atlassian.stash.internal.repository.ref.restriction.dao;

import com.atlassian.activeobjects.external.ActiveObjects;
import com.atlassian.bitbucket.ao.AbstractAoDao;
import com.atlassian.bitbucket.ao.AoUtils;
import com.atlassian.bitbucket.repository.ref.restriction.RefMatcher;
import com.atlassian.bitbucket.repository.ref.restriction.RefMatcherType;
import com.atlassian.bitbucket.repository.ref.restriction.RefRestrictionType;
import com.atlassian.bitbucket.util.Chainable;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.crowd.embedded.impl.IdentifierUtils;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Functions;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import net.java.ao.Query;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.*;

public class AoRefRestrictionDao extends AbstractAoDao implements RefRestrictionDao {

    public AoRefRestrictionDao(ActiveObjects ao) {
        super(ao);
    }

    @Override
    public int count(int repositoryId) {
        return ao.count(AoRefRestriction.class, Query.select()
                .where(AoRefRestriction.REPOSITORY_ID_COLUMN + " = ?", repositoryId));
    }

    @Nonnull
    @Override
    public AoRefRestriction create(int repositoryId,
                                   @Nonnull RefRestrictionType refRestrictionType,
                                   @Nonnull RefMatcher refMatcher,
                                   @Nonnull Iterable<String> groups,
                                   @Nonnull Iterable<Integer> userIds) {

        AoRefRestriction existing = get(repositoryId, refRestrictionType, refMatcher);
        if (existing != null) {
            throw new IllegalStateException("Ref restriction already exists (repo = " + repositoryId +
                    ", type = " + refRestrictionType.getId() + ", matcher = {" + refMatcher.getType().getId() + ", " +
                    refMatcher.getId() + "})");
        }

        AoRefRestriction restriction = ao.create(AoRefRestriction.class, ImmutableMap.<String, Object>of(
                AoRefRestriction.REPOSITORY_ID_COLUMN, repositoryId,
                AoRefRestriction.REF_MATCHER_TYPE_COLUMN, refMatcher.getType().getId(),
                AoRefRestriction.TYPE_COLUMN, refRestrictionType.getId(),
                AoRefRestriction.REF_MATCHER_ID_COLUMN, refMatcher.getId()));

        if (Iterables.isEmpty(groups) && Iterables.isEmpty(userIds)) {
            return restriction;
        }

        groups.forEach(group -> createAccessGrant(restriction, AoAccessGrant.GROUP_NAME_COLUMN, group));
        userIds.forEach(userId -> createAccessGrant(restriction, AoAccessGrant.USER_ID_COLUMN, userId));
        // refetch to get the up-to-date array of grants
        return  ao.get(AoRefRestriction.class, restriction.getId());
    }

    @Override
    public void delete(@Nonnull AoRefRestriction restriction) {
        ao.delete(restriction.getAccessGrants());
        ao.delete(restriction);
    }

    @Override
    public void deleteForGroup(@Nonnull String group) {
        ao.delete(ao.find(AoAccessGrant.class,
                Query.select().where("LOWER(" + quoteColumnName(AoAccessGrant.GROUP_NAME_COLUMN) + ") =  ?", canonicalizeGroup(group))));
    }

    @Override
    public void deleteForRepository(int repositoryId) {
        AoRefRestriction[] refRestrictions = ao.find(AoRefRestriction.class,
                Query.select().where(AoRefRestriction.REPOSITORY_ID_COLUMN + " = ?", repositoryId));
        // delete the grants first
        for (AoRefRestriction restriction : refRestrictions) {
            delete(restriction);
        }
    }

    @Override
    public void deleteForUser(int userId) {
        ao.delete(ao.find(AoAccessGrant.class,
                Query.select().where(AoAccessGrant.USER_ID_COLUMN + " = ?", userId)));
    }

    @VisibleForTesting
    AoAccessGrant[] findGrantsByRestrictionId(int restrictionId) {
        AoRefRestriction restriction = getById(restrictionId);
        return restriction == null ? new AoAccessGrant[]{} : restriction.getAccessGrants();
    }

    @Override
    public AoRefRestriction get(int repositoryId, @Nonnull RefRestrictionType refRestrictionType,
                                @Nonnull RefMatcher matcher) {
        AoRefRestriction[] restrictions = ao.find(
                AoRefRestriction.class,
                AoUtils.newQuery(AoRefRestriction.ID_COLUMN, AoRefRestriction.REPOSITORY_ID_COLUMN,
                        AoRefRestriction.TYPE_COLUMN, AoRefRestriction.REF_MATCHER_TYPE_COLUMN,
                        AoRefRestriction.REF_MATCHER_ID_COLUMN)
                        .where(AoRefRestriction.REPOSITORY_ID_COLUMN + " = ? AND " +
                                        AoRefRestriction.TYPE_COLUMN + " = ? AND " +
                                        AoRefRestriction.REF_MATCHER_TYPE_COLUMN + " = ? AND " +
                                        AoRefRestriction.REF_MATCHER_ID_COLUMN + " = ?",
                                repositoryId, refRestrictionType.getId(), matcher.getType().getId(), matcher.getId())
                        .limit(1));
        return restrictions.length > 0 ? restrictions[0] : null;
    }

    @Nonnull
    @Override
    public AoRefRestriction setAccessGrants(@Nonnull AoRefRestriction restriction,
                                            @Nonnull Iterable<String> groups,
                                            @Nonnull Iterable<Integer> userIds) {

        Set<String> groupsToAdd = new HashSet<>();
        Set<Integer> usersToAdd = new HashSet<>();
        groups.forEach(groupsToAdd::add);
        userIds.forEach(usersToAdd::add);

        boolean grantsUpdated = false;
        for (AoAccessGrant grant : restriction.getAccessGrants()) {
            if (grant.getUserId() != null && !usersToAdd.remove(grant.getUserId())) {
                // the existing grant is not in the target set of user grants
                ao.delete(grant);
                grantsUpdated = true;
            } else if (grant.getGroup() != null && !groupsToAdd.remove(grant.getGroup())) {
                // the existing grant is not in the target set of group grants
                ao.delete(grant);
                grantsUpdated = true;
            }
        }

        groupsToAdd.forEach(group -> createAccessGrant(restriction, AoAccessGrant.GROUP_NAME_COLUMN, group));
        usersToAdd.forEach(userId -> createAccessGrant(restriction, AoAccessGrant.USER_ID_COLUMN, userId));

        // re-fetch from the database to ensure we get the up-to-date list of grants (if the grants have changed)
        grantsUpdated |= !groupsToAdd.isEmpty() || !usersToAdd.isEmpty();

        return grantsUpdated ? ao.get(AoRefRestriction.class, restriction.getId()) : restriction;
    }

    @Override
    public AoRefRestriction getById(int restrictionId) {
        return ao.get(AoRefRestriction.class, restrictionId);
    }

    @Nonnull
    @Override
    public Page<AoRefRestriction> find(int repositoryId,
                                       @Nullable Collection<RefRestrictionType> types,
                                       @Nullable RefMatcher matcher,
                                       @Nullable Collection<? extends RefMatcherType> matcherTypes,
                                       @Nonnull final PageRequest pageRequest) {

        final StringBuilder whereClause = new StringBuilder(AoRefRestriction.REPOSITORY_ID_COLUMN + " = ?");
        final List<Object> whereArguments = Lists.newArrayList(repositoryId);

        maybeAddTypeClause(whereClause, whereArguments, types);
        maybeAddMatcherClause(whereClause, whereArguments, matcher);
        maybeAddMatcherTypeClause(whereClause, whereArguments, matcherTypes);

        Query query = AoUtils.newQuery(AoRefRestriction.ID_COLUMN, AoRefRestriction.REPOSITORY_ID_COLUMN,
                        AoRefRestriction.TYPE_COLUMN, AoRefRestriction.REF_MATCHER_TYPE_COLUMN,
                        AoRefRestriction.REF_MATCHER_ID_COLUMN)
                        .where(whereClause.toString(), whereArguments.toArray())
                        .order(AoRefRestriction.REF_MATCHER_TYPE_COLUMN + ", " +
                                AoRefRestriction.REF_MATCHER_ID_COLUMN + ", " +
                                AoRefRestriction.TYPE_COLUMN + " ASC");

        return pageQuery(AoRefRestriction.class, query, pageRequest);
    }

    private AoAccessGrant createAccessGrant(AoRefRestriction restriction, String column, Object groupOrUser) {
        AoAccessGrant grant = ao.create(AoAccessGrant.class,
                ImmutableMap.of(
                        AoAccessGrant.RESTRICTION_COLUMN, restriction.getId(),
                        column, groupOrUser));
        grant.setRestriction(restriction);
        grant.save();
        return grant;
    }

    private boolean maybeAddMatcherClause(StringBuilder whereClause, List<Object> arguments, RefMatcher matcher) {
        if (matcher == null) {
            return false;
        }

        whereClause.append(" AND ").append(AoRefRestriction.REF_MATCHER_TYPE_COLUMN)
                .append(" = ? AND ").append(AoRefRestriction.REF_MATCHER_ID_COLUMN).append(" = ?");
        arguments.add(matcher.getType().getId());
        arguments.add(matcher.getId());
        return true;
    }

    private boolean maybeAddMatcherTypeClause(StringBuilder whereClause, List<Object> arguments, Collection<? extends RefMatcherType> matcherTypes) {
        if (matcherTypes == null || matcherTypes.isEmpty()) {
            return false;
        }

        int i = 0;
        for (RefMatcherType matcherType : matcherTypes) {
            whereClause.append((i == 0) ? " AND " : " OR ").append(AoRefRestriction.REF_MATCHER_TYPE_COLUMN)
                    .append(" = ? ");
            arguments.add(matcherType.getId());
            i++;
        }
        return i > 0;
    }

    private boolean maybeAddTypeClause(StringBuilder whereClause, List<Object> arguments, Collection<RefRestrictionType> types) {
        if (types == null || types.isEmpty()) {
            return false;
        }

        whereClause.append(" AND ")
                .append(AoRefRestriction.TYPE_COLUMN)
                .append(" IN (")
                .append(inClausePlaceholder(types))
                .append(")");
        arguments.addAll(Chainable.chain(types).transform(RefRestrictionType::getId).toList());
        return true;
    }

    private String inClausePlaceholder(Iterable<?> values) {
        return Joiner.on(", ").join(Iterables.transform(values, Functions.constant("?")));
    }

    /**
     * Group names need to be **matched** (not stored) case-insensitively, as this is Crowd's behaviour when fulfilling
     * queries. This is required as different directories may contain the same group name varying by case, so the group
     * names reported by Crowd may change when directory order changes. See STASHDEV-2626 for more info.
     */
    private String canonicalizeGroup(String group) {
        return IdentifierUtils.toLowerCase(group);
    }
}
