package com.atlassian.bitbucket.internal.defaultreviewers.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.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import net.java.ao.Query;

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

import static java.util.Optional.ofNullable;

public class AoPullRequestConditionDao extends AbstractAoDao implements PullRequestConditionDao {

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

    @Nonnull
    @Override
    public AoPullRequestCondition create(int repositoryId,
                                         @Nonnull RefMatcher sourceMatcher, @Nonnull RefMatcher targetMatcher,
                                         @Nonnull Set<Integer> userIds, int requiredApprovals) {
        getByMatchers(repositoryId, sourceMatcher, targetMatcher)
                .ifPresent(existing ->
                        new IllegalStateException("Ref restriction already exists"));

        AoPullRequestCondition refCondition = createRefCondition(repositoryId, sourceMatcher, targetMatcher, requiredApprovals);
        userIds.stream().forEach(id -> createReviewer(refCondition, id));
        return ao.get(AoPullRequestCondition.class, refCondition.getId());
    }

    @Override
    public void delete(@Nonnull AoPullRequestCondition pullRequestCondition) {
        ao.delete(pullRequestCondition.getReviewers());
        ao.delete(pullRequestCondition);
    }

    @Override
    public void deleteForRepository(int repositoryId) {
        Arrays.asList(ao.find(
                AoPullRequestCondition.class,
                Query.select().where(AoPullRequestCondition.REPOSITORY_ID_COLUMN + "= ?", repositoryId)))
                .forEach(this::delete);
    }

    @Nonnull
    @Override
    public Page<AoPullRequestCondition> find(int repositoryId, @Nullable RefMatcher sourceMatcher,
                                             @Nullable RefMatcher targetMatcher, @Nonnull PageRequest pageRequest) {
        final StringBuilder whereClause = new StringBuilder(AoPullRequestCondition.REPOSITORY_ID_COLUMN + " = ?");
        final List<Object> whereArguments = Lists.newArrayList(repositoryId);

        // Add the source and target matchers if they are there
        ofNullable(sourceMatcher).ifPresent(matcher ->
                addRefMatcherToQuery(matcher,
                        AoPullRequestCondition.SOURCE_REF_MATCHER_ID_COLUMN, AoPullRequestCondition.SOURCE_REF_MATCHER_TYPE_COLUMN,
                        whereClause, whereArguments));
        ofNullable(targetMatcher).ifPresent(matcher ->
                addRefMatcherToQuery(matcher,
                        AoPullRequestCondition.TARGET_REF_MATCHER_ID_COLUMN, AoPullRequestCondition.TARGET_REF_MATCHER_TYPE_COLUMN,
                        whereClause, whereArguments));

        Query query = AoUtils.newQuery(AoPullRequestCondition.ID_COLUMN, AoPullRequestCondition.REPOSITORY_ID_COLUMN,
                AoPullRequestCondition.SOURCE_REF_MATCHER_ID_COLUMN, AoPullRequestCondition.SOURCE_REF_MATCHER_TYPE_COLUMN,
                AoPullRequestCondition.TARGET_REF_MATCHER_ID_COLUMN, AoPullRequestCondition.TARGET_REF_MATCHER_TYPE_COLUMN)
                .where(whereClause.toString(), whereArguments.toArray())
                .order(AoPullRequestCondition.SOURCE_REF_MATCHER_TYPE_COLUMN + ", " +
                        AoPullRequestCondition.SOURCE_REF_MATCHER_ID_COLUMN + ", " +
                        AoPullRequestCondition.TARGET_REF_MATCHER_TYPE_COLUMN + ", " +
                        AoPullRequestCondition.TARGET_REF_MATCHER_ID_COLUMN + " ASC");

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

    @Nonnull
    @Override
    public Page<AoPullRequestCondition> find(int repositoryId, @Nonnull PageRequest pageRequest) {
        return find(repositoryId, null, null, pageRequest);
    }

    @Nonnull
    @Override
    public AoPullRequestCondition get(int pullRequestConditionId) {
        return ao.get(AoPullRequestCondition.class, pullRequestConditionId);
    }

    @Override
    public AoPullRequestCondition update(@Nonnull AoPullRequestCondition aoPullRequestCondition,
                                         int repositoryId,
                                         @Nullable RefMatcher newSourceMatcher,
                                         @Nullable RefMatcher newTargetMatcher,
                                         @Nullable Collection<Integer> newReviewerIds,
                                         @Nullable Integer newRequiredApprovals) {
        aoPullRequestCondition.setRepository(repositoryId);

        if (newSourceMatcher != null) {
            aoPullRequestCondition.setSourceMatcherId(newSourceMatcher.getId());
            aoPullRequestCondition.setSourceMatcherType(newSourceMatcher.getType().getId());
        }

        if (newTargetMatcher != null) {
            aoPullRequestCondition.setTargetMatcherId(newTargetMatcher.getId());
            aoPullRequestCondition.setTargetMatcherType(newTargetMatcher.getType().getId());
        }

        if (newReviewerIds != null) {
            ao.delete(aoPullRequestCondition.getReviewers());
            addReviewers(aoPullRequestCondition, newReviewerIds);
        }

        if (newRequiredApprovals != null) {
            aoPullRequestCondition.setRequiredApprovals(newRequiredApprovals);
        }

        aoPullRequestCondition.save();
        return get(aoPullRequestCondition.getId());
    }

    private void addRefMatcherToQuery(RefMatcher refMatcher,
                                      String idColumn, String typeColumn,
                                      StringBuilder whereClause, List<Object> whereArguments) {
        whereClause.append(" AND ")
                .append(idColumn)
                .append(" = ? AND ")
                .append(typeColumn)
                .append(" = ?");
        whereArguments.add(refMatcher.getId());
        whereArguments.add(refMatcher.getType().getId());
    }

    private AoPullRequestCondition addReviewers(@Nonnull AoPullRequestCondition pullRequestCondition, @Nullable Collection<Integer> userIdsOrNull) {
        ofNullable(userIdsOrNull).ifPresent(userIds -> userIds.stream()
                .forEach(userId -> createReviewer(pullRequestCondition, userId)));
        return ao.get(AoPullRequestCondition.class, pullRequestCondition.getId());
    }

    private AoPullRequestCondition createRefCondition(int repositoryId, @Nonnull RefMatcher sourceMatcher, @Nonnull RefMatcher targetMatcher, int requiredApprovals) {
        return ao.create(AoPullRequestCondition.class, ImmutableMap.<String, Object>builder()
                .put(AoPullRequestCondition.REPOSITORY_ID_COLUMN, repositoryId)
                .put(AoPullRequestCondition.SOURCE_REF_MATCHER_ID_COLUMN, sourceMatcher.getId())
                .put(AoPullRequestCondition.SOURCE_REF_MATCHER_TYPE_COLUMN, sourceMatcher.getType().getId())
                .put(AoPullRequestCondition.TARGET_REF_MATCHER_ID_COLUMN, targetMatcher.getId())
                .put(AoPullRequestCondition.TARGET_REF_MATCHER_TYPE_COLUMN, targetMatcher.getType().getId())
                .put(AoPullRequestCondition.REQUIRED_APPROVALS_COLUMN, requiredApprovals)
                .build());
    }

    private AoReviewer createReviewer(AoPullRequestCondition refCondition, Integer id) {
        AoReviewer aoReviewer = ao.create(AoReviewer.class, ImmutableMap.<String, Object>builder()
                .put(AoReviewer.CONDITION_COLUMN, refCondition.getId())
                .put(AoReviewer.USER_ID_COLUMN, id)
                .build());
        aoReviewer.setCondition(refCondition);
        aoReviewer.save();
        return aoReviewer;
    }

    private Optional<AoPullRequestCondition> getByMatchers(int repositoryId,
                                                           @Nonnull RefMatcher sourceMatcher, @Nonnull RefMatcher targetMatcher) {
        AoPullRequestCondition[] condition = ao.find(AoPullRequestCondition.class, AoUtils.newQuery(
                AoPullRequestCondition.ID_COLUMN, AoPullRequestCondition.REPOSITORY_ID_COLUMN,
                AoPullRequestCondition.SOURCE_REF_MATCHER_ID_COLUMN, AoPullRequestCondition.SOURCE_REF_MATCHER_TYPE_COLUMN,
                AoPullRequestCondition.TARGET_REF_MATCHER_ID_COLUMN, AoPullRequestCondition.TARGET_REF_MATCHER_TYPE_COLUMN)
                .where(AoPullRequestCondition.REPOSITORY_ID_COLUMN + " = ? AND " +
                                AoPullRequestCondition.SOURCE_REF_MATCHER_ID_COLUMN + " = ? AND " +
                                AoPullRequestCondition.SOURCE_REF_MATCHER_TYPE_COLUMN + " = ? AND " +
                                AoPullRequestCondition.TARGET_REF_MATCHER_ID_COLUMN + " = ? AND " +
                                AoPullRequestCondition.TARGET_REF_MATCHER_TYPE_COLUMN + "= ?",
                        repositoryId,
                        sourceMatcher.getId(), sourceMatcher.getType().getId(),
                        targetMatcher.getId(), targetMatcher.getType().getId()
                ).limit(1)
        );

        return Optional.ofNullable(condition)
                .map(Arrays::asList)
                .flatMap(list -> list.stream().findFirst());
    }
}
