package com.atlassian.bitbucket.internal.branch.model;

import com.atlassian.activeobjects.external.ActiveObjects;
import com.atlassian.bitbucket.NoSuchEntityException;
import com.atlassian.bitbucket.branch.model.BranchModel;
import com.atlassian.bitbucket.branch.model.BranchModelService;
import com.atlassian.bitbucket.event.repository.RepositoryCreatedEvent;
import com.atlassian.bitbucket.event.repository.RepositoryDeletedEvent;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.internal.branch.model.configuration.*;
import com.atlassian.bitbucket.internal.branch.model.dao.BranchModelDao;
import com.atlassian.bitbucket.internal.branch.model.dao.PersistentBranchModelConfiguration;
import com.atlassian.bitbucket.internal.branch.model.exception.BranchModelConfigurationException;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.permission.PermissionValidationService;
import com.atlassian.bitbucket.repository.*;
import com.atlassian.bitbucket.server.ApplicationPropertiesService;
import com.atlassian.event.api.EventListener;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

import static com.atlassian.bitbucket.internal.branch.model.InternalBranchTypes.*;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Sets.filter;
import static org.apache.commons.lang3.StringUtils.isNotEmpty;

/**
 * An implementation of {@link BranchModelService} and {@link BranchModelConfigurationService} backed by ActiveObjects.
 *
 * @since 2.8
 */
public class DefaultBranchModelService implements BranchModelService, BranchModelConfigurationService {

    private static final Logger log = LoggerFactory.getLogger(DefaultBranchModelService.class);

    private static final String BRANCHMODEL_PROP_PREFIX = "plugin.bitbucket-branch-model.";

    private static final int DEFAULT_PREFIX_MAX_LENGTH = 30;
    static final String PROP_PREFIX_MAX_LENGTH = BRANCHMODEL_PROP_PREFIX + "validation.prefix.length";

    public static final String DEFAULT_VERSION_COMPONENT_SEPARATOR = "[_\\-.+]";
    static final String PROP_VERSION_COMPONENT_SEPARATOR = BRANCHMODEL_PROP_PREFIX + "version.separator";

    private static final String DEVELOPMENT_ID = "DEVELOPMENT";
    private static final String PRODUCTION_ID = "PRODUCTION";

    private final ActiveObjects ao;
    private final ApplicationPropertiesService applicationPropertiesService;
    private final BranchModelDao branchModelDao;
    private final I18nService i18nService;
    private final PermissionValidationService permissionValidationService;
    private final RefService refService;
    private final RepositoryService repositoryService;

    public DefaultBranchModelService(ActiveObjects ao, ApplicationPropertiesService applicationPropertiesService,
                                     BranchModelDao branchModelDao, I18nService i18nService,
                                     PermissionValidationService permissionValidationService,
                                     RefService refService, RepositoryService repositoryService) {
        this.ao = ao;
        this.applicationPropertiesService = applicationPropertiesService;
        this.branchModelDao = branchModelDao;
        this.i18nService = i18nService;
        this.permissionValidationService = permissionValidationService;
        this.refService = refService;
        this.repositoryService = repositoryService;
    }

    @Nullable
    @Override
    public BranchModel getModel(@Nonnull Repository repository) {
        checkNotNull(repository, "repository");
        return ao.executeInTransaction(() -> {
            permissionValidationService.validateForRepository(repository, Permission.REPO_READ);
            if (repositoryService.isEmpty(repository)) {
                throw new EmptyRepositoryException(i18nService.createKeyedMessage("bitbucket.branchmodel.error.emptyrepository", repository));
            }
            return toBranchModel(branchModelDao.getByRepository(repository), repository);
        });
    }

    @Nullable
    @Override
    public BranchModelConfiguration getConfiguration(@Nonnull Repository repository) {
        checkNotNull(repository, "repository");
        return ao.executeInTransaction(() -> {
            permissionValidationService.validateForRepository(repository, Permission.REPO_ADMIN);
            return getByRepository(repository);
        });
    }

    @Override
    @Nonnull
    public BranchModelConfiguration getDefaultConfiguration() {
        return new SimpleBranchModelConfiguration.Builder()
                .development(new SimpleBranchConfiguration(null, true))
                .enabledType(BUGFIX.getId(), BUGFIX.getDefaultPrefix())
                .enabledType(FEATURE.getId(), FEATURE.getDefaultPrefix())
                .enabledType(HOTFIX.getId(), HOTFIX.getDefaultPrefix())
                .enabledType(RELEASE.getId(), RELEASE.getDefaultPrefix())
                .build();
    }

    @Override
    public boolean setEnabled(@Nonnull Repository repository, boolean enabled) {
        checkNotNull(repository, "repository");
        return ao.executeInTransaction(() -> {
            permissionValidationService.validateForRepository(repository, Permission.REPO_ADMIN);
            BranchModelConfiguration branchModelConfig = branchModelDao.getByRepository(repository);
            if (branchModelConfig == null && enabled) {
                // create new default entity associated with the repository
                createDefaultModel(repository);
                return true;
            } else if (branchModelConfig != null) {
                // updateConfiguration state of the existing branch model entity
                return branchModelDao.setEnabled(repository, enabled);
            }
            return false;
        });
    }

    @Nonnull
    @Override
    public BranchModelConfiguration updateConfiguration(@Nonnull Repository repository,
                                                        @Nonnull BranchModelConfigurationRequest updateRequest) {
        checkNotNull(repository, "repository");
        checkNotNull(updateRequest, "updateRequest");
        return ao.executeInTransaction(() -> {
            permissionValidationService.validateForRepository(repository, Permission.REPO_ADMIN);
            PersistentBranchModelConfiguration model = branchModelDao.getByRepository(repository);
            if (model == null || !model.isEnabled()) {
                throw new NoSuchEntityException(i18nService.createKeyedMessage("bitbucket.branchmodel.error.nosuchbranchmodel", repository));
            }
            validate(repository, updateRequest);
            branchModelDao.update(repository, updateRequest);
            return getByRepository(repository);
        });
    }

    @EventListener
    public void onRepositoryCreated(@Nonnull RepositoryCreatedEvent repositoryCreatedEvent) {
        ao.executeInTransaction(() -> {
            createDefaultModel(repositoryCreatedEvent.getRepository());
            return null;
        });
        //TODO Create sensible branch permissions for repository
    }

    @EventListener
    public void onRepositoryDeleted(@Nonnull RepositoryDeletedEvent repositoryDeletedEvent) {
        ao.executeInTransaction(() -> {
            branchModelDao.deleteByRepository(repositoryDeletedEvent.getRepository());
            return null;
        });
    }

    private BranchModelConfiguration addI18nedDisplayNames(BranchModelConfiguration originalModel) {
        ImmutableSet.Builder<BranchTypeConfiguration> i18nedTypes = ImmutableSet.builder();
        for (BranchTypeConfiguration originalType : originalModel.getTypes()) {
            InternalBranchTypes internalType = InternalBranchTypes.forId(originalType.getId());
            i18nedTypes.add(new SimpleBranchTypeConfiguration(internalType.getType(), internalType.getDisplayName(i18nService),
                    originalType.isEnabled(), originalType.getPrefix()));
        }
        return new SimpleBranchModelConfiguration.Builder()
                .development(originalModel.getDevelopment())
                .production(originalModel.getProduction())
                .types(i18nedTypes.build())
                .build();
    }

    private void createDefaultModel(Repository repository) {
        branchModelDao.create(repository, new BranchModelConfigurationRequest.Builder(getDefaultConfiguration()).build());
    }

    private BranchModelConfiguration getByRepository(Repository repository) {
        PersistentBranchModelConfiguration configuration = branchModelDao.getByRepository(repository);
        if (configuration == null || !configuration.isEnabled()) {
            return null;
        }
        return addI18nedDisplayNames(configuration);
    }

    @SuppressWarnings("ConstantConditions")
    private Set<InternalBranchType> getEffectiveTypes(PersistentBranchModelConfiguration model) {
        ImmutableSet.Builder<InternalBranchType> types = ImmutableSet.builder();
        for (BranchTypeConfiguration typeConfiguration : model.getTypes()) {
            if (typeConfiguration.isEnabled()) {
                InternalBranchTypes internalType = InternalBranchTypes.forId(typeConfiguration.getId());
                types.add(new SimpleInternalBranchType(internalType.getType(), internalType.getDisplayName(i18nService),
                        typeConfiguration.getPrefix()));
            }
        }
        return types.build();
    }

    private Branch getDevelopmentBranch(PersistentBranchModelConfiguration configuration, Repository repository) {
        if (configuration.getDevelopment().isUseDefault()) {
            return refService.getDefaultBranch(repository);
        }
        Ref ref = refService.resolveRef(new ResolveRefRequest.Builder(repository)
                .refId(configuration.getDevelopment().getRefId())
                .type(StandardRefType.BRANCH)
                .build());
        if (isNotBranch(ref)) {
            log.debug("Ref {} stored as development branch for repository {} does not resolve to a branch",
                    configuration.getDevelopment().getRefId(), repository);
            // this may happen, e.g. if the branch was deleted. Fall back to the default branch
            return refService.getDefaultBranch(repository);
        } else {
            return (Branch) ref;
        }
    }

    private Branch getProductionBranch(PersistentBranchModelConfiguration configuration, Repository repository) {
        if (configuration.getProduction() == null) {
            return null;
        }
        if (configuration.getProduction().isUseDefault()) {
            return refService.getDefaultBranch(repository);
        }
        Ref ref = refService.resolveRef(new ResolveRefRequest.Builder(repository)
                .refId(configuration.getProduction().getRefId())
                .type(StandardRefType.BRANCH)
                .build());
        if (isNotBranch(ref)) {
            log.debug("Ref {} stored as production branch for repository {} does not resolve to a branch",
                    configuration.getProduction().getRefId(), repository);
            // this may happen, e.g. if the branch was deleted. Just return null as production branch does not need to
            // be set
            return null;
        } else {
            return (Branch) ref;
        }
    }

    private boolean isNotBranch(Ref ref) {
        return ref == null || !Branch.class.isInstance(ref);
    }

    @SuppressWarnings("unchecked")
    private BranchModel toBranchModel(PersistentBranchModelConfiguration configuration, Repository repository) {
        if (configuration == null || !configuration.isEnabled()) {
            return null;
        }
        Set<InternalBranchType> types = getEffectiveTypes(configuration);
        return new SimpleBranchModel.Builder()
                .development(getDevelopmentBranch(configuration, repository))
                .production(getProductionBranch(configuration, repository))
                .types((Set) types) // InternalBranchType is always an instance of BranchType so this is valid
                .classifier(new PrefixBranchClassifier(refService, repository, types))
                .versionComponentSeparators(getVersionComponentSeparators())
                .build();
    }

    private void validate(Repository repository, BranchModelConfigurationRequest updateRequest) {
        validateDevelopment(repository, updateRequest);
        validateProduction(repository, updateRequest);
        validatePrefixes(updateRequest);
    }

    private void validateDevelopment(Repository repository, BranchModelConfigurationRequest updateRequest) {
        validateBranchConfiguration(repository, updateRequest.getDevelopment(), DEVELOPMENT_ID);
    }

    private void validateProduction(Repository repository, BranchModelConfigurationRequest updateRequest) {
        if (updateRequest.getProduction() != null) {
            validateBranchConfiguration(repository, updateRequest.getProduction(), PRODUCTION_ID);
        }
    }

    private void validateBranchConfiguration(Repository repository, BranchConfiguration config, String branchTypeId) {
        if (StringUtils.isEmpty(config.getRefId()) && !config.isUseDefault()) {
            throw new BranchModelConfigurationException(
                    i18nService.createKeyedMessage("bitbucket.branchmodel.error.invalidBranchConfiguration",
                            getBranchI18nName(branchTypeId)), branchTypeId);
        }
        if (!config.isUseDefault() && isNotEmpty(config.getRefId())) {
            validateRefId(repository, config.getRefId(), branchTypeId);
        }
    }

    @SuppressWarnings("ConstantConditions")
    private void validateNoOverlap(BranchTypeConfiguration first, BranchTypeConfiguration second) {
        String firstPrefix = first.getPrefix(), secondPrefix = second.getPrefix();
        if (firstPrefix.startsWith(secondPrefix) || secondPrefix.startsWith(firstPrefix)) {
            throw new BranchModelConfigurationException(i18nService.createKeyedMessage("bitbucket.branchmodel.error.prefixoverlap",
                    firstPrefix, secondPrefix),
                    first.getId(),
                    second.getId()
            );
        }
    }

    private void validatePrefixes(BranchModelConfigurationRequest updateRequest) {
        List<BranchTypeConfiguration> enabledTypes = getEnabledTypes(updateRequest);
        for (BranchTypeConfiguration typeConfiguration : enabledTypes) {
            validatePrefixNonEmpty(typeConfiguration);
            validatePrefixLength(typeConfiguration);
        }
        for (int i = 0; i < enabledTypes.size(); i++) {
            for (int j = i + 1; j < enabledTypes.size(); j++) {
                validateNoOverlap(enabledTypes.get(i), enabledTypes.get(j));
            }
        }
    }

    @SuppressWarnings("ConstantConditions")
    private void validatePrefixLength(BranchTypeConfiguration typeConfiguration) {
        int prefixMaxLength = getBranchPrefixLimit();
        if (typeConfiguration.getPrefix().length() > prefixMaxLength) {
            throw new BranchModelConfigurationException(i18nService.createKeyedMessage("bitbucket.branchmodel.error.prefixtoolong", typeConfiguration.getPrefix(), prefixMaxLength),
                    typeConfiguration.getId());
        }
    }

    private void validatePrefixNonEmpty(BranchTypeConfiguration typeConfiguration) {
        if (StringUtils.isBlank(typeConfiguration.getPrefix())) {
            throw new BranchModelConfigurationException(i18nService.createKeyedMessage("bitbucket.branchmodel.error.emptyprefix"), typeConfiguration.getId());
        }
    }

    private void validateRefId(Repository repository, String refId, String branchTypeId) {
        if (isNotBranch(refService.resolveRef(new ResolveRefRequest.Builder(repository).refId(refId).build()))) {
            throw new InvalidRefNameException(i18nService.createKeyedMessage("bitbucket.branchmodel.error.invalidBranchId",
                    refId, getBranchI18nName(branchTypeId)), refId);
        }
    }

    private List<BranchTypeConfiguration> getEnabledTypes(BranchModelConfigurationRequest updateRequest) {
        return ImmutableList.copyOf(filter(updateRequest.getTypes(), BranchTypeConfiguration::isEnabled));
    }

    private String getBranchI18nName(String branchId) {
        return i18nService.getMessage("bitbucket.branchmodel." + branchId);
    }

    private String getVersionComponentSeparators() {
        return applicationPropertiesService.getPluginProperty(PROP_VERSION_COMPONENT_SEPARATOR,
                DEFAULT_VERSION_COMPONENT_SEPARATOR);
    }

    private int getBranchPrefixLimit() {
        return applicationPropertiesService.getPluginProperty(PROP_PREFIX_MAX_LENGTH, DEFAULT_PREFIX_MAX_LENGTH);
    }
}
