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

import com.atlassian.activeobjects.external.ActiveObjects;
import com.atlassian.bitbucket.ao.AbstractAoDao;
import com.atlassian.bitbucket.internal.branch.model.BranchTypes;
import com.atlassian.bitbucket.internal.branch.model.configuration.*;
import com.atlassian.bitbucket.repository.Repository;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import net.java.ao.Query;

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

import static com.atlassian.bitbucket.internal.branch.model.dao.AoBranchModelConfiguration.*;
import static com.atlassian.bitbucket.internal.branch.model.dao.AoBranchTypeConfiguration.*;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.nullToEmpty;
import static org.apache.commons.lang3.StringUtils.isNotEmpty;

public class AoBranchModelDao extends AbstractAoDao implements BranchModelDao {

    public AoBranchModelDao(@Nonnull ActiveObjects ao) {
        super(ao);
    }

    @Nonnull
    @Override
    public PersistentBranchModelConfiguration create(@Nonnull Repository repository,
                                                     @Nonnull BranchModelConfigurationRequest request) {
        checkNotNull(repository, "repository");

        AoBranchModelConfiguration model = ao.create(AoBranchModelConfiguration.class,
                toAoModelColumns(repository.getId(), request));
        for (BranchTypeConfiguration configuration : request.getTypes()) {
            ao.create(AoBranchTypeConfiguration.class, toAoTypeColumns(model, configuration));
        }

        //noinspection ConstantConditions
        return getByRepository(repository);
    }

    @Override
    public void deleteByRepository(@Nonnull Repository repository) {
        deleteAoTypes(repository);
        deleteAoModel(repository);
    }

    @Nullable
    @Override
    public PersistentBranchModelConfiguration getByRepository(@Nonnull Repository repository) {
        checkNotNull(repository, "repository");

        return toBranchModelConfiguration(getAoModel(repository), findAoTypes(repository.getId()));
    }

    @Override
    public boolean setEnabled(@Nonnull Repository repository, boolean enabled) {
        AoBranchModelConfiguration aoBranchModel = getAoModelForUpdate(repository);
        if (aoBranchModel.isEnabled() != enabled) {
            aoBranchModel.setEnabled(enabled);
            aoBranchModel.save();
            return true;
        } else {
            return false;
        }
    }

    @Override
    public void update(@Nonnull Repository repository, @Nonnull BranchModelConfigurationRequest updateRequest) {
        AoBranchModelConfiguration aoModel = getAoModelForUpdate(repository);
        applyUpdateToDevelopment(updateRequest, aoModel);
        applyUpdateToProduction(updateRequest, aoModel);
        applyUpdateToTypes(updateRequest, aoModel, findAoTypes(repository.getId()));
        fixRefIds(aoModel);
        aoModel.save();
    }

    private void applyUpdateToTypes(BranchModelConfigurationRequest request, AoBranchModelConfiguration aoModel,
                                    AoBranchTypeConfiguration[] aoTypes) {
        Map<String, BranchTypeConfiguration> typesMap = toTypesMap(request.getTypes());
        for (AoBranchTypeConfiguration aoTypeConfiguration : aoTypes) {
            BranchTypeConfiguration typeConfiguration = typesMap.remove(aoTypeConfiguration.getTypeId());
            if (typeConfiguration != null) {
                aoTypeConfiguration.setEnabled(typeConfiguration.isEnabled());
                aoTypeConfiguration.setPrefix(typeConfiguration.getPrefix());
            } else {
                aoTypeConfiguration.setEnabled(false);
                aoTypeConfiguration.setPrefix(null);
            }
            fixPrefix(aoTypeConfiguration);
            aoTypeConfiguration.save();
        }
        for (Map.Entry<String, BranchTypeConfiguration> newType : typesMap.entrySet()) {
            ao.create(AoBranchTypeConfiguration.class, toAoTypeColumns(aoModel, newType.getValue()));
        }
    }

    private void applyUpdateToDevelopment(BranchModelConfigurationRequest updateRequest,
                                          AoBranchModelConfiguration model) {
        model.setDevelopmentBranchId(updateRequest.getDevelopment().getRefId());
        model.setDevelopmentBranchUseDefault(updateRequest.getDevelopment().isUseDefault());
    }

    private void applyUpdateToProduction(BranchModelConfigurationRequest updateRequest,
                                         AoBranchModelConfiguration model) {
        if (updateRequest.getProduction() != null) {
            model.setProductionBranchId(updateRequest.getProduction().getRefId());
            model.setProductionBranchUseDefault(updateRequest.getProduction().isUseDefault());
        } else {
            model.setProductionBranchId(null);
            model.setProductionBranchUseDefault(false);
        }
    }

    private void deleteAoTypes(Repository repository) {
        ao.delete(findAoTypes(repository.getId()));
    }

    private void deleteAoModel(Repository repository) {
        AoBranchModelConfiguration model = getAoModel(repository);
        if (model != null) {
            ao.delete(model);
        }
    }

    private AoBranchTypeConfiguration[] findAoTypes(int repositoryId) {
        return ao.find(AoBranchTypeConfiguration.class,
                Query.select().where(BRANCH_MODEL_ID_COLUMN + " = ?", repositoryId));
    }

    private void fixRefIds(AoBranchModelConfiguration branchModel) {
        // work-around for AO-468, AO fails to save a null string on MSSQL
        if (branchModel.getDevelopmentBranchId() == null) {
            branchModel.setDevelopmentBranchId("");
        }
        if (branchModel.getProductionBranchId() == null) {
            branchModel.setProductionBranchId("");
        }
    }

    private void fixPrefix(AoBranchTypeConfiguration configuration) {
        // work-around for AO-468, AO fails to save a null string on MSSQL
        if (configuration.getPrefix() == null) {
            configuration.setPrefix("");
        }
    }

    private AoBranchModelConfiguration getAoModel(Repository repository) {
        return ao.get(AoBranchModelConfiguration.class, repository.getId());
    }

    private AoBranchModelConfiguration getAoModelForUpdate(Repository repository) {
        checkNotNull(repository, "repository");

        AoBranchModelConfiguration aoBranchModel = getAoModel(repository);
        checkArgument(aoBranchModel != null, "No model has been created for %s yet", repository);

        return aoBranchModel;
    }

    private BranchConfiguration getDevelopmentBranchConfiguration(AoBranchModelConfiguration aoBranchModel) {
        return new SimpleBranchConfiguration(aoBranchModel.getDevelopmentBranchId(),
                aoBranchModel.isDevelopmentBranchUseDefault());
    }

    private BranchConfiguration getProductionBranchConfiguration(AoBranchModelConfiguration aoBranchModel) {
        if (isNotEmpty(aoBranchModel.getProductionBranchId()) || aoBranchModel.isProductionBranchUseDefault()) {
            return new SimpleBranchConfiguration(aoBranchModel.getProductionBranchId(),
                    aoBranchModel.isProductionBranchUseDefault());
        } else {
            return null;
        }
    }

    private void maybePut(ImmutableMap.Builder<String, Object> builder, String key, Object value) {
        if (value != null) {
            builder.put(key, value);
        }
    }

    private PersistentBranchModelConfiguration toBranchModelConfiguration(AoBranchModelConfiguration aoBranchModel,
                                                                          AoBranchTypeConfiguration[] aoTypes) {
        if (aoBranchModel == null) {
            return null;
        }
        return new SimplePersistentBranchModelConfiguration.Builder()
                .enabled(aoBranchModel.isEnabled())
                .development(getDevelopmentBranchConfiguration(aoBranchModel))
                .production(getProductionBranchConfiguration(aoBranchModel))
                .types(toTypes(aoTypes))
                .build();
    }

    private Set<BranchTypeConfiguration> toTypes(AoBranchTypeConfiguration[] aoTypes) {
        ImmutableSet.Builder<BranchTypeConfiguration> types = ImmutableSet.builder();
        for (AoBranchTypeConfiguration aoTypeConfig : aoTypes) {
            BranchTypes type = BranchTypes.forId(aoTypeConfig.getTypeId());
            types.add(new SimpleBranchTypeConfiguration(type, aoTypeConfig.isEnabled(),
                    aoTypeConfig.getPrefix()));
        }
        return types.build();
    }

    private Map<String, BranchTypeConfiguration> toTypesMap(Set<BranchTypeConfiguration> types) {
        Map<String, BranchTypeConfiguration> result = Maps.newHashMap();
        for (BranchTypeConfiguration configuration : types) {
            result.put(configuration.getId(), configuration);
        }
        return result;
    }

    private Map<String, Object> toAoTypeColumns(AoBranchModelConfiguration model, BranchTypeConfiguration config) {
        ImmutableMap.Builder<String, Object> builder = ImmutableMap.builder();
        maybePut(builder, BRANCH_MODEL_ID_COLUMN, model.getRepositoryId());
        maybePut(builder, TYPE_ID_COLUMN, config.getId());
        maybePut(builder, TYPE_PREFIX_COLUMN, nullToEmpty(config.getPrefix()));
        maybePut(builder, TYPE_ENABLED_COLUMN, config.isEnabled());
        return builder.build();
    }

    private Map<String, Object> toAoModelColumns(Integer repositoryId, BranchModelConfigurationRequest request) {
        ImmutableMap.Builder<String, Object> builder = ImmutableMap.builder();
        maybePut(builder, REPOSITORY_ID_COLUMN, repositoryId);
        maybePut(builder, ENABLED_COLUMN, true);
        maybePut(builder, DEVELOPMENT_BRANCH_REF_ID_COLUMN, request.getDevelopment().getRefId());
        maybePut(builder, DEVELOPMENT_BRANCH_USE_DEFAULT_COLUMN, request.getDevelopment().isUseDefault());
        if (request.getProduction() != null) {
            maybePut(builder, PRODUCTION_BRANCH_REF_ID_COLUMN, request.getProduction().getRefId());
            maybePut(builder, PRODUCTION_BRANCH_USE_DEFAULT_COLUMN, request.getProduction().isUseDefault());
        } else {
            maybePut(builder, PRODUCTION_BRANCH_REF_ID_COLUMN, null);
            maybePut(builder, PRODUCTION_BRANCH_USE_DEFAULT_COLUMN, false);
        }
        return builder.build();
    }
}
