package com.atlassian.stash.internal.merge;

import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.project.Project;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.scm.PluginMergeStrategy;
import com.atlassian.bitbucket.validation.ArgumentValidationException;
import com.atlassian.stash.internal.InternalConverter;
import com.atlassian.stash.internal.annotation.Profiled;
import com.atlassian.stash.internal.scm.InternalScmService;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;

/**
 * @since 4.9
 */
@Component("mergeConfigHelper")
@Profiled
public class DefaultMergeConfigHelper implements MergeConfigHelper {

    private final I18nService i18nService;
    private final ProjectMergeConfigDao projectConfigDao;
    private final RepositoryMergeConfigDao repositoryConfigDao;
    private final ScmMergeConfigDao scmConfigDao;
    private final InternalScmService scmService;

    @Autowired
    public DefaultMergeConfigHelper(I18nService i18nService, ProjectMergeConfigDao projectConfigDao,
                                    RepositoryMergeConfigDao repositoryConfigDao, ScmMergeConfigDao scmConfigDao,
                                    InternalScmService scmService) {
        this.i18nService = i18nService;
        this.projectConfigDao = projectConfigDao;
        this.repositoryConfigDao = repositoryConfigDao;
        this.scmConfigDao = scmConfigDao;
        this.scmService = scmService;
    }

    @Nonnull
    @Override
    public MergeConfig getForProjectAndScm(@Nonnull Project project, @Nonnull String scmId) {
        requireNonNull(project, "project");
        requireNonNull(scmId, "scmId");

        InternalProjectMergeConfig config = projectConfigDao.findByProjectAndScm(project.getId(), scmId);
        if (config == null) {
            return getForScm(scmId);
        }
        return toMergeConfig(scmId, config);
    }

    @Nonnull
    @Override
    public MergeConfig getForRepository(@Nonnull Repository repository) {
        requireNonNull(repository, "repository");

        InternalRepositoryMergeConfig config = repositoryConfigDao.findByRepository(repository.getId());
        if (config == null) {
            return getForProjectAndScm(repository.getProject(), repository.getScmId());
        }
        return toMergeConfig(repository.getScmId(), config);
    }

    @Nonnull
    @Override
    public MergeConfig getForScm(@Nonnull String scmId) {
        requireNonNull(scmId, "scmId");

        return toMergeConfig(scmId, scmConfigDao.findByScm(scmId));
    }

    @Override
    public void setForProjectAndScm(@Nonnull Project project, @Nonnull String scmId, @Nonnull String defaultStrategyId,
                                    @Nonnull Set<String> strategyIds) {
        requireNonNull(project, "project");
        validateStrategies(scmId, defaultStrategyId, strategyIds);

        InternalProjectMergeConfig config = projectConfigDao.findByProjectAndScm(project.getId(), scmId);
        if (config == null) {
            projectConfigDao.create(new InternalProjectMergeConfig(InternalConverter.convertToInternalProject(project),
                    scmId, defaultStrategyId, strategyIds));
        } else {
            config.setDefaultStrategyId(defaultStrategyId);
            config.setStrategyIds(strategyIds);
            projectConfigDao.update(config);
        }
    }

    @Override
    public void setForRepository(@Nonnull Repository repository, @Nonnull String defaultStrategyId,
                                 @Nonnull Set<String> strategyIds) {
        validateStrategies(requireNonNull(repository, "repository").getScmId(), defaultStrategyId, strategyIds);

        InternalRepositoryMergeConfig config = repositoryConfigDao.findByRepository(repository.getId());
        if (config == null) {
            repositoryConfigDao.create(new InternalRepositoryMergeConfig(
                    InternalConverter.convertToInternalRepository(repository),
                    defaultStrategyId, strategyIds));
        } else {
            config.setDefaultStrategyId(defaultStrategyId);
            config.setStrategyIds(strategyIds);
            repositoryConfigDao.update(config);
        }
    }

    @Override
    public void setForScm(@Nonnull String scmId, @Nonnull String defaultStrategyId, @Nonnull Set<String> strategyIds) {
        validateStrategies(scmId, defaultStrategyId, strategyIds);

        InternalScmMergeConfig config = scmConfigDao.findByScm(scmId);
        if (config == null) {
            scmConfigDao.create(new InternalScmMergeConfig(scmId, defaultStrategyId, strategyIds));
        } else {
            config.setDefaultStrategyId(defaultStrategyId);
            config.setStrategyIds(strategyIds);
            scmConfigDao.update(config);
        }
    }

    private static List<String> sort(Set<String> ids) {
        return ids.stream().sorted().collect(toList());
    }

    private MergeConfig toMergeConfig(@Nonnull String scmId, @Nullable InternalAbstractMergeConfig config) {
        Set<PluginMergeStrategy> availableStrategies = scmService.getMergeStrategies(scmId)
                .getStrategies();

        String defaultStrategyId = null;
        Set<String> enabledStrategyIds = new HashSet<>();
        if (config != null) {
            //If configuration was set, the default and enabled strategy IDs are drawn from that. However,
            //they first need to be filtered against the available strategies
            enabledStrategyIds.addAll(config.getStrategyIds());
            //Remove any enabled strategies that the SCM doesn't support
            enabledStrategyIds.retainAll(availableStrategies.stream()
                    .map(PluginMergeStrategy::getId)
                    .collect(toSet()));

            //After the enabled strategies are filtered, apply the configuration's default strategy only
            //if it's still enabled.
            if (enabledStrategyIds.contains(config.getDefaultStrategyId())) {
                defaultStrategyId = config.getDefaultStrategyId();
            } else {
                //If the default is no longer supported, default to the "first" enabled strategy that is
                //still supported. Since the enabled IDs are stored in a Set, which doesn't guarantee an
                //order for its elements, sort the IDs first so that the default will be consistent even
                //if the enabled IDs iterate in different orders
                defaultStrategyId = enabledStrategyIds.stream()
                        .sorted()
                        .findFirst()
                        .orElse(null);
            }
        }
        //Ensure a default is selected
        if (defaultStrategyId == null) {
            PluginMergeStrategy defaultStrategy = availableStrategies.stream()
                    .filter(PluginMergeStrategy::isDefault)
                    //Find the default strategy for the SCM
                    .findFirst()
                    //The SCM should have elected a default, but if it didn't just use the first
                    .orElseGet(() -> Iterables.getFirst(availableStrategies, null));

            defaultStrategyId = defaultStrategy.getId();
        }
        //Ensure the default is enabled
        enabledStrategyIds.add(defaultStrategyId);

        return new SimpleMergeConfig(availableStrategies, defaultStrategyId, enabledStrategyIds,
                config == null ? MergeConfigType.DEFAULT : config.getType());
    }

    private void validateStrategies(@Nonnull String scmId, @Nonnull String defaultStrategyId,
                                    @Nonnull Set<String> strategyIds) {
        requireNonNull(scmId, "scmId");
        //Ensure the default and enabled strategy IDs were provided
        requireNonNull(defaultStrategyId, "defaultStrategyId");
        requireNonNull(strategyIds, "strategyIds");

        //Ensure the default strategy is enabled
        if (strategyIds.contains(defaultStrategyId)) {
            //If the default is enabled, ensure all of the enabled strategies are supported by the SCM. Note
            //that if the SCM doesn't support merge strategies getMergeStrategies(String) will throw
            Set<String> supportedIds = scmService.getMergeStrategies(scmId).getStrategies().stream()
                    .map(PluginMergeStrategy::getId)
                    .collect(toSet());
            if (!supportedIds.containsAll(strategyIds)) {
                Set<String> unsupportedIds = Sets.difference(strategyIds, supportedIds);

                throw new ArgumentValidationException(i18nService.createKeyedMessage(
                        "bitbucket.config.mergestrategies.unsupported",
                        sort(unsupportedIds), unsupportedIds.size(), sort(supportedIds),
                        supportedIds.size(), scmId));
            }
        } else {
            throw new ArgumentValidationException(i18nService.createKeyedMessage(
                    "bitbucket.config.mergestrategies.defaultnotenabled",
                    defaultStrategyId, sort(strategyIds), strategyIds.size()));
        }
    }
}
