package com.atlassian.stash.internal.commit;

import com.atlassian.stash.internal.HibernateUtils;
import com.atlassian.stash.internal.comment.CommentCounts;
import com.atlassian.stash.internal.content.InternalChangeLocation;
import com.atlassian.stash.internal.pull.AbstractHibernateDiffCommentAnchorDao;
import com.google.common.collect.Maps;
import org.hibernate.Criteria;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Restrictions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import static java.util.Optional.ofNullable;

/**
 * @since 2.11
 */
@Repository("commitDiscussionCommentAnchorDao")
public class HibernateCommitDiscussionCommentAnchorDao
        extends AbstractHibernateDiffCommentAnchorDao<InternalCommitDiscussion, InternalCommitDiscussionCommentAnchor>
        implements CommitDiscussionCommentAnchorDao {

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

    @Override
    public InternalCommitDiscussionCommentAnchor getAnchorByComment(@Nonnull InternalCommitDiscussion discussion,
                                                                    long commentId) {
        return HibernateUtils.initialize((InternalCommitDiscussionCommentAnchor) session().createQuery("select a " +
                "from InternalCommitDiscussionCommentAnchor a, InternalComment c " +
                "where a.discussion.id = :discussionId " +
                "and c.id = :commentId " +
                "and (a.comment.id = c.id or a.comment.id = c.root.id)")
                .setLong("commentId", commentId)
                .setLong("discussionId", discussion.getId())
                .uniqueResult());
    }

    @Nonnull
    @Override
    public Map<InternalChangeLocation, CommentCounts> getCountsByLocation(@Nonnull InternalCommitDiscussion discussion) {
        return getCountsByLocation(discussion, null);
    }

    @Nonnull
    @Override
    public Map<InternalChangeLocation, CommentCounts> getCountsByLocation(@Nonnull InternalCommitDiscussion discussion,
                                                                          @Nullable Long pullRequestId) {
        List<Object[]> results = getRawCounts(discussion, pullRequestId);

        if (results.isEmpty()) {
            return Collections.emptyMap();
        }

        Map<InternalChangeLocation, CommentCounts> countsByLocation = Maps.newHashMapWithExpectedSize(results.size());
        for (Object[] columns : results) {
            InternalChangeLocation location = new InternalChangeLocation.Builder()
                    .path((String) columns[0])
                    .srcPath((String) columns[1])
                    .build();

            countsByLocation.put(location, new CommentCounts(((Number) columns[2]).intValue(), 0));
        }
        return countsByLocation;
    }

    @Override
    @Nonnull
    public List<InternalCommitDiscussionCommentAnchor> search(@Nonnull CommitDiscussionAnchorSearchCriteria searchCriteria) {
        Criteria criteria = session().createCriteria(InternalCommitDiscussionCommentAnchor.class)
                .add(Restrictions.eq("discussion.id", searchCriteria.getDiscussion().getId()))
                .add(Restrictions.eq("toHash", searchCriteria.getToHash()));
        Optional<String> fromHash = searchCriteria.getFromHash();
        if (fromHash.isPresent()) {
            criteria.add(Restrictions.eq("fromHash", fromHash.get()));
        } else {
            criteria.add(Restrictions.isNull("fromHash"));
        }

        Optional<Long> pullRequestScopedId = searchCriteria.getPullRequestScopedId();
        if (pullRequestScopedId.isPresent()) {
            criteria.createAlias("pullRequest", "p")
                    .add(Restrictions.eq("p.scopedId", pullRequestScopedId.get()));
        } else {
            criteria.add(Restrictions.isNull("pullRequest"));
        }

        return search(criteria, searchCriteria);
    }

    @Nonnull
    @Override
    public InternalCommitDiscussionCommentAnchor update(InternalCommitDiscussionCommentAnchor entity) {
        throw new UnsupportedOperationException("CommitDiscussionCommentAnchors are immutable and cannot be updated");
    }

    @SuppressWarnings("unchecked")
    private List<Object[]> getRawCounts(InternalCommitDiscussion discussion, Long p) {
        return ofNullable(p)
                .map(pullRequestId -> session().createQuery(
                        "select path, srcPath, count(*) " +
                                "from InternalCommitDiscussionCommentAnchor " +
                                "where pullRequest.toRef.repository.id = :repositoryId " +
                                "and pullRequest.scopedId = :pullRequestId " +
                                "and discussion.id = :discussionId " +
                                "group by path, srcPath " +
                                "order by path, srcPath")
                        .setLong("repositoryId", discussion.getRepository().getId())
                        .setLong("pullRequestId", pullRequestId)
                        .setLong("discussionId", discussion.getId()))
                .orElse(session().createQuery(
                        "select path, srcPath, count(*) " +
                                "from InternalCommitDiscussionCommentAnchor " +
                                "where discussion.id = :discussionId " +
                                "and pullRequest is null " +
                                "group by path, srcPath " +
                                "order by path, srcPath")
                        .setLong("discussionId", discussion.getId()))
                .list();
    }
}
