package com.atlassian.stash.internal.cluster;

import com.atlassian.security.random.SecureTokenGenerator;
import com.atlassian.stash.internal.ApplicationSettings;
import com.atlassian.stash.internal.config.ConfigurationService;
import com.atlassian.stash.internal.jdbc.DataSourceConfiguration;
import com.google.common.io.Files;
import org.apache.commons.io.Charsets;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.Nonnull;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;

import static com.atlassian.stash.internal.cluster.ClusterJoinCheckAction.PASSIVATE_ANY_NODE;
import static com.atlassian.stash.internal.cluster.ClusterJoinCheckAction.PASSIVATE_OTHER_NODE;
import static com.atlassian.stash.internal.cluster.ClusterJoinCheckAction.PASSIVATE_THIS_NODE;

/**
 * Verifies that two nodes joining share the same shared home and database. There are three possible outcomes:
 * <ul>
 *     <li>The nodes share have the same shared home and database - the check succeeds</li>
 *     <li>The nodes are unrelated: they have different shared homes _and_ databases - the check fails and prevents the
 *         nodes from forming a cluster. No node is passivated.</li>
 *     <li>The nodes connect to the same database but have different shared homes (or vice versa) - the check fails
 *         and causes one of the nodes to be passivated. The most recently started node will be passivated.</li>
 * </ul>
 */
@Component
public class SharedHomeAndDatabaseJoinCheck implements ClusterJoinCheck {

    private final ApplicationSettings applicationSettings;
    private final ConfigurationService configurationService;
    private final DataSourceConfiguration dataSourceConfiguration;
    private final SecureTokenGenerator secureTokenGenerator;

    @Autowired
    public SharedHomeAndDatabaseJoinCheck(ApplicationSettings applicationSettings,
                                          ConfigurationService configurationService,
                                          DataSourceConfiguration dataSourceConfiguration,
                                          SecureTokenGenerator secureTokenGenerator) {
        this.applicationSettings = applicationSettings;
        this.configurationService = configurationService;
        this.dataSourceConfiguration = dataSourceConfiguration;
        this.secureTokenGenerator = secureTokenGenerator;
    }

    @Nonnull
    @Override
    public ClusterJoinCheckResult accept(@Nonnull ClusterJoinRequest request) throws IOException {
        File tmpFile = null;
        boolean sharedHomeMatches = false;
        try {
            tmpFile = File.createTempFile("cluster-join", ".txt", applicationSettings.getSharedHomeDir());
            String token = secureTokenGenerator.generateToken();
            Files.write(token, tmpFile, Charsets.UTF_8);

            request.out().writeUTF(tmpFile.getName());

            if (request.in().readBoolean()) {
                // remote node found the token file, validate the token
                sharedHomeMatches = token.equals(request.in().readUTF());
            }
        } finally {
            if (tmpFile != null && !tmpFile.delete()) {
                tmpFile.deleteOnExit();
            }
        }

        String remoteJdbcUrl = request.in().readUTF();
        String remoteJdbcUser = request.in().readUTF();
        boolean databaseMatches = dataSourceConfiguration.getUrl().equals(remoteJdbcUrl) &&
                dataSourceConfiguration.getUser().equals(remoteJdbcUser);

        if (sharedHomeMatches) {
            if (databaseMatches) {
                return ClusterJoinCheckResult.OK;
            }

            // databases don't match, check whether the in-memory config is up to date with the on-disk config
            DataSourceConfiguration onDiskConfiguration = configurationService.loadDataSourceConfiguration();
            boolean onDiskConfigMatchesCurrent =
                    Objects.equals(onDiskConfiguration.getUrl(), dataSourceConfiguration.getUrl()) &&
                            Objects.equals(onDiskConfiguration.getUser(), dataSourceConfiguration.getUser());

            return ClusterJoinCheckResult.passivate(
                    onDiskConfigMatchesCurrent ? PASSIVATE_OTHER_NODE : PASSIVATE_THIS_NODE,
                    "Nodes are connected to the same shared home but different databases");
        }

        // sharedHomeMatches == false
        return databaseMatches ?
                ClusterJoinCheckResult.passivate(PASSIVATE_ANY_NODE,
                        "Nodes are connected to the same database but different shared homes") :
                ClusterJoinCheckResult.disconnect("Node is part of an unrelated cluster");
    }

    @Nonnull
    @Override
    public ClusterJoinCheckResult connect(@Nonnull ClusterJoinRequest request) throws IOException {
        File tmpFile = new File(applicationSettings.getSharedHomeDir(), request.in().readUTF());
        boolean fileExists = tmpFile.isFile();
        request.out().writeBoolean(fileExists);
        if (fileExists) {
            String token = Files.toString(tmpFile, StandardCharsets.UTF_8);
            request.out().writeUTF(token);
        }

        request.out().writeUTF(dataSourceConfiguration.getUrl());
        request.out().writeUTF(dataSourceConfiguration.getUser());

        return ClusterJoinCheckResult.OK;
    }

    @Nonnull
    @Override
    public String getName() {
        return getClass().getName();
    }

    @Override
    public int getOrder() {
        return 1;
    }

    @Nonnull
    @Override
    public ClusterJoinCheckResult onUnknown(@Nonnull ClusterJoinRequest request) {
        return ClusterJoinCheckResult.passivate(PASSIVATE_ANY_NODE,
                "Cannot verify whether the nodes connect to the same database and shared home");
    }
}
