package com.atlassian.stash.internal.pull;

import com.atlassian.bitbucket.repository.RefChange;
import com.atlassian.stash.internal.ApplicationConstants;
import com.atlassian.stash.internal.HibernateUtils;
import com.atlassian.stash.internal.Initializable;
import com.atlassian.stash.internal.entity.InternalAbstractEntity;
import com.atlassian.stash.internal.repository.InternalRefChange;
import com.atlassian.stash.internal.repository.InternalRepository;
import com.atlassian.stash.internal.user.InternalApplicationUser;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import org.hibernate.Hibernate;

import javax.annotation.Nonnull;
import javax.persistence.*;
import javax.persistence.Entity;
import javax.persistence.ForeignKey;
import javax.persistence.Index;
import javax.persistence.Table;
import java.util.Date;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;

import static com.google.common.base.Preconditions.checkNotNull;
import static java.util.Objects.requireNonNull;

/**
 * Entity for keeping track of pending pull request rescope operations to ensure rescopes are performed even when
 * the server is shut down half way through processing a pull request.
 *
 * @since 1.3
 */
@Entity
@Table(name = InternalRescopeRequest.TABLE, indexes = {
        @Index(name = "idx_sta_pr_rescope_req_repo", columnList = "repo_id")
})
@TableGenerator(allocationSize = 43, pkColumnValue = InternalRescopeRequest.TABLE,
        name = InternalAbstractEntity.ID_GEN, table = ApplicationConstants.ID_SEQUENCE_TABLE)
public class InternalRescopeRequest extends InternalAbstractEntity implements Initializable, Comparable<InternalRescopeRequest> {

    public static final Function<InternalRescopeRequest, Long> TO_ID = InternalRescopeRequest::getId;

    static final String TABLE = "sta_pr_rescope_request";

    @CollectionTable(name = "sta_pr_rescope_request_change", joinColumns = @JoinColumn(name = "request_id", nullable = false))
    @ElementCollection(targetClass = InternalRefChange.class)
    private Set<RefChange> changes;

    @Column(name = "created_timestamp", nullable = false)
    @Temporal(TemporalType.TIMESTAMP)
    private final Date createdDate;

    @JoinColumn(name = "repo_id", nullable = false, foreignKey = @ForeignKey(name = "fk_sta_pr_rescope_req_repo"))
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    private InternalRepository repository;

    @JoinColumn(name = "user_id", nullable = false, foreignKey = @ForeignKey(name = "fk_sta_pr_rescope_req_user"))
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    private InternalApplicationUser user;

    // for hibernate
    protected InternalRescopeRequest() {
        createdDate = null;
        changes = null;
        repository = null;
        user = null;
    }

    public InternalRescopeRequest(Builder builder) {
        super(builder);

        createdDate = builder.createdDate;
        changes = builder.refChanges;
        repository = builder.repository;
        user = builder.user;
    }

    @Override
    public int compareTo(@Nonnull InternalRescopeRequest o) {
        return createdDate.compareTo(o.createdDate);
    }

    @Nonnull
    public Date getCreatedDate() {
        return createdDate;
    }

    @Nonnull
    public Set<RefChange> getChanges() {
        return changes;
    }

    @Nonnull
    public InternalRepository getRepository() {
        return repository;
    }

    @Nonnull
    public InternalApplicationUser getUser() {
        return user;
    }

    @Override
    public void initialize() {
        repository = HibernateUtils.initialize(getRepository());
        user = HibernateUtils.initialize(getUser());
        Hibernate.initialize(changes);
    }

    public static class Builder extends AbstractEntityBuilder<Builder, InternalRescopeRequest> {

        private final Set<RefChange> refChanges;
        private final InternalRepository repository;
        private final InternalApplicationUser user;

        private Date createdDate;

        public Builder(@Nonnull InternalRescopeRequest request) {
            super(request);

            createdDate = requireNonNull(request.getCreatedDate(), "request.createdDate");
            refChanges = Sets.newHashSet(requireNonNull(request.getChanges(), "request.changes"));
            repository = requireNonNull(request.getRepository(), "request.repository");
            user = requireNonNull(request.getUser(), "request.user");
        }

        public Builder(@Nonnull InternalRepository repository, @Nonnull InternalApplicationUser user) {
            this.createdDate = new Date();
            this.repository = requireNonNull(repository, "repository");
            this.user = requireNonNull(user, "user");

            refChanges = Sets.newHashSet();
        }

        //Note: there is no setter anymore for branchId, since the full ref changes are now being persisted.

        @Nonnull
        @Override
        public InternalRescopeRequest build() {
            return new InternalRescopeRequest(this);
        }

        @Nonnull
        public Builder changes(@Nonnull Iterable<RefChange> value) {
            checkNotNull(value, "changes");
            //Due to misbehaving plugins, it's possible some of the RefChanges in the collection will not be able
            //to be persisted because they do not honor the RefChange contract. For rescoping, we'll allow partial or
            //even missing hashes
            //This addIf filters out any changes which cannot be transformed after padding.
            addIf(Objects::nonNull, refChanges, Iterables.transform(value, InternalRefChange.TRANSFORM_TOLERANT));

            return self();
        }

        @Nonnull
        public Builder createdDate(@Nonnull Date value) {
            createdDate = checkNotNull(value, "createdDate");

            return self();
        }

        @Nonnull
        @Override
        protected Builder self() {
            return this;
        }
    }
}
