package com.atlassian.stash.internal.home;

import com.atlassian.bitbucket.Product;
import com.atlassian.bitbucket.env.SystemProperties;
import com.google.common.base.Supplier;

import javax.annotation.Nonnull;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;

import static com.google.common.base.Preconditions.checkState;

/**
 * Utility class to resolve the location of the home directory.
 */
public class HomeDirectoryResolver {

    static {
        //TODO: Put this somewhere else
        //
        //Unfortunately Spring evaluates custom XML attributes extremely early, so this can't be defined in
        //HazelcastConfigFactoryBean or many Hazelcast classes create their loggers before it's set
        System.setProperty("hazelcast.logging.type", "slf4j"); //Because Hazelcast has its own logging abstraction
    }

    static final String ENV_HOME = "BITBUCKET_HOME";
    static final String ENV_LEGACY_HOME = "STASH_HOME";
    static final String ENV_LEGACY_SHARED_HOME = "STASH_SHARED_HOME";
    static final String ENV_SHARED_HOME = "BITBUCKET_SHARED_HOME";

    private final EnvironmentVariableResolver environmentVariableResolver;

    public HomeDirectoryResolver() {
        this(new DefaultEnvironmentVariableResolver());
    }

    public HomeDirectoryResolver(@Nonnull EnvironmentVariableResolver environmentVariableResolver) {
        this.environmentVariableResolver = environmentVariableResolver;
    }

    public HomeDirectoryDetails resolve() {
        File home = getHomeDirectory();
        File sharedHome = getSharedHomeDirectory(home);

        return new HomeDirectoryDetails(home, sharedHome);
    }

    public HomeDirectoryDetails resolveAndValidate() {
        HomeDirectoryDetails details = resolve();
        validate(details);

        return details;
    }

    private File getHomeDirectory() {
        String home = getPropertyValue(VariableBundle.HOME, null);

        checkState(home != null,
                "No home directory is defined using either the system property '%s' or the environment variable '%s'",
                SystemProperties.PROP_LEGACY_HOME_DIR, ENV_LEGACY_HOME);
        checkTilde(home, "home");

        return resolve(home);
    }

    private File getSharedHomeDirectory(final File home) {
        String sharedHome = getPropertyValue(VariableBundle.SHARED_HOME,
                new Supplier<String>() {
                    @Override
                    public String get() {
                        return new File(home, "shared").getAbsolutePath();
                    }
                });

        checkState(sharedHome != null, "Cannot derive shared home value from home value");
        checkTilde(sharedHome, "shared home");

        return resolve(sharedHome);
    }

    private String getPropertyValue(VariableBundle bundle, Supplier<String> missingValueSupplier) {
        String propertyValue = System.getProperty(bundle.getSystem());
        if (propertyValue != null) {
            //When the system property is specified, ensure the legacy property is also defined
            System.setProperty(bundle.getLegacySystem(), propertyValue);

            return propertyValue;
        }

        propertyValue = environmentVariableResolver.getValue(bundle.getEnvironment());
        if (propertyValue == null) {
            //If neither of the properties are specified, try the legacy ones. This is intended to
            //ease the migration from the old values to the new ones
            propertyValue = System.getProperty(bundle.getLegacySystem());
            if (propertyValue == null) {
                propertyValue = environmentVariableResolver.getValue(bundle.getLegacyEnvironment());
            }
        }

        if (propertyValue == null && missingValueSupplier != null) {
            propertyValue = missingValueSupplier.get();
        }

        if (propertyValue != null) {
            //Set the value on _both_ system properties
            System.setProperty(bundle.getLegacySystem(), propertyValue);
            System.setProperty(bundle.getSystem(), propertyValue);
        }

        return propertyValue;
    }

    private void checkTilde(String text, String directoryName) {
        if (text != null && text.startsWith("~")) { //We're not a shell; we don't support tilde expansion
            throw new IllegalStateException("The " + directoryName + " directory [" + text + "] is invalid; " +
                    "tilde expansion is not supported. Please use an absolute path referring to a specific home directory.");
        }
    }

    private void checkNotInCatalinaHome(File directory, String directoryName) {
        String catalinaHome = System.getProperty("catalina.home");
        if (catalinaHome != null) {
            Path directoryPath = directory.toPath();
            Path catalinaHomePath = resolve(catalinaHome).toPath();
            if (directoryPath.startsWith(catalinaHomePath)) {
                throw new InvalidHomeDirectoryException("The %s directory [%s] must not be the same directory as or a subdirectory of the %s installation directory.",
                        directoryName, directory.getAbsolutePath(), Product.NAME);
            }
        }
    }

    private void validate(HomeDirectoryDetails details) {
        validateDirectory(details.getHome(), "home");
        validateDirectory(details.getSharedHome(), "shared home");
    }

    private void validateDirectory(File directory, String directoryName) {
        checkNotInCatalinaHome(directory, directoryName);
    }

    private static File resolve(String dirName) {
        File dir = new File(dirName);
        try {
            return dir.getCanonicalFile();
        } catch (IOException e) {
            return dir.getAbsoluteFile();
        }
    }

    private enum VariableBundle {

        HOME(ENV_HOME, ENV_LEGACY_HOME, SystemProperties.PROP_HOME_DIR, SystemProperties.PROP_LEGACY_HOME_DIR),
        SHARED_HOME(ENV_SHARED_HOME, ENV_LEGACY_SHARED_HOME,
                SystemProperties.PROP_SHARED_HOME_DIR, SystemProperties.PROP_LEGACY_SHARED_HOME_DIR);

        private final String environment;
        private final String legacyEnvironment;
        private final String legacySystem;
        private final String system;

        VariableBundle(String environment, String legacyEnvironment, String system, String legacySystem) {
            this.environment = environment;
            this.legacyEnvironment = legacyEnvironment;
            this.legacySystem = legacySystem;
            this.system = system;
        }

        public String getEnvironment() {
            return environment;
        }

        public String getLegacyEnvironment() {
            return legacyEnvironment;
        }

        public String getLegacySystem() {
            return legacySystem;
        }

        public String getSystem() {
            return system;
        }
    }
}
