package com.atlassian.stash.internal.merge;

import com.atlassian.bitbucket.pull.PullRequestMergeStrategy;
import com.atlassian.bitbucket.validation.annotation.RequiredString;
import com.atlassian.stash.internal.ApplicationConstants;
import com.atlassian.stash.internal.entity.InternalAbstractEntity;
import com.atlassian.stash.internal.project.InternalProject;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.annotations.NaturalId;
import org.hibernate.annotations.NaturalIdCache;

import javax.annotation.Nonnull;
import javax.persistence.*;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import static java.util.Objects.requireNonNull;

/**
 * Stores a set of enabled {@link PullRequestMergeStrategy#getId strategy IDs} for a given
 * SCM within a specific {@link InternalProject project}. Strategies configured at the project level supersede
 * any configured at the {@link InternalScmMergeConfig SCM level} but will be superseded by any configured
 * at the {@link InternalRepositoryMergeConfig repository level}.
 * <p>
 * <b>Implementation Note</b>: As you probably noticed right off, this class has a public constructor instead of
 * a builder, and the {@link #strategyIds} are mutable. What gives? Unfortunately, those are the realities of
 * using Hibernate's {@code ElementCollection}. If you build a full new instance, <i>even if it has the same ID</i>,
 * changes to the strategy IDs <i>will not be persisted</i>. To change the strategy IDs, you <i>must</i> mutate the
 * {@code PersistentSet} Hibernate returns when an instance is retrieved from the database. To use builders and keep
 * this object fully immutable the DAO would have to delete the entire object, and associated collection, and create
 * a new one each time the entity is updated.
 *
 * @since 4.9
 */
@Cacheable
@Entity
@NaturalIdCache
@Table(name = InternalProjectMergeConfig.TABLE, uniqueConstraints = {
    @UniqueConstraint(name = "uq_bb_proj_merge_config", columnNames = {"project_id", "scm_id"})
})
@TableGenerator(allocationSize = 5, pkColumnValue = InternalProjectMergeConfig.TABLE,
        name = InternalAbstractEntity.ID_GEN, table = ApplicationConstants.ID_SEQUENCE_TABLE)
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class InternalProjectMergeConfig extends InternalAbstractMergeConfig {

    public static final String TABLE = "bb_proj_merge_config";

    @Column(name = "project_id", unique = true, updatable = false)
    @NaturalId
    private final int projectId;

    @Column(name = "scm_id", nullable = false, updatable = false)
    @NaturalId
    @RequiredString
    private final String scmId;

    @CollectionTable(name = "bb_proj_merge_strategy", foreignKey = @ForeignKey(name = "fk_bb_proj_merge_strategy"),
            joinColumns = @JoinColumn(name = "config_id"))
    @Column(name = "strategy_id", nullable = false)
    @ElementCollection
    @OrderBy //For a little extra sanity elsewhere, ensure strategy IDs are always in a consistent order
    @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
    private final Set<String> strategyIds;

    //zomfgwtf! A public constructor?! Read the class Javadocs.
    public InternalProjectMergeConfig(@Nonnull InternalProject project, @Nonnull String scmId,
                                      @Nonnull String defaultStrategyId, @Nonnull Set<String> strategyIds) {
        super(defaultStrategyId);

        this.projectId = requireNonNull(project, "project").getId();
        this.scmId = requireNonNull(scmId, "scmId");
        this.strategyIds = new HashSet<>(requireNonNull(strategyIds, "strategyIds"));
    }

    //For Hibernate
    protected InternalProjectMergeConfig() {
        projectId = ApplicationConstants.UNSAVED_ENTITY;
        scmId = null;
        strategyIds = new HashSet<>();
    }

    public int getProjectId() {
        return projectId;
    }

    @Nonnull
    public String getScmId() {
        return scmId;
    }

    @Nonnull
    @Override
    public Set<String> getStrategyIds() {
        return Collections.unmodifiableSet(strategyIds);
    }

    @Nonnull
    @Override
    public MergeConfigType getType() {
        return MergeConfigType.PROJECT;
    }

    //zomfgwtf! A mutator?! Immutability 4 lyfe! Except not with Hibernate. Read the class Javadocs.
    public void setStrategyIds(@Nonnull Set<String> strategyIds) {
        requireNonNull(strategyIds, "strategyIds");

        this.strategyIds.retainAll(strategyIds); //Remove any IDs that shouldn't still be set
        this.strategyIds.addAll(strategyIds);    //Add any new IDs
    }
}
