package com.atlassian.bitbucket.mesh.app;

import com.atlassian.bitbucket.mesh.ApplicationDirs;
import com.atlassian.bitbucket.mesh.Product;
import com.atlassian.bitbucket.mesh.env.SystemProperties;
import com.atlassian.bitbucket.mesh.io.MoreFiles;
import com.atlassian.bitbucket.mesh.util.MoreCollectors;
import com.google.common.base.CharMatcher;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.PropertyResolver;

import javax.annotation.Nonnull;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
import java.util.Locale;

import static java.util.Objects.requireNonNull;
import static org.apache.commons.lang3.StringUtils.isBlank;

/**
 * Utility class that resolves the location of the application home and data directories from system properties or
 * environment variables.
 */
public class ApplicationDirResolver {

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

    private final PropertyResolver propertyResolver;
    private final PropertySetter propertySetter;

    public ApplicationDirResolver(@Nonnull PropertyResolver propertyResolver) {
        this(propertyResolver, System::setProperty);
    }

    public ApplicationDirResolver(@Nonnull PropertyResolver propertyResolver, @Nonnull PropertySetter propertySetter) {
        this.propertyResolver = propertyResolver;
        this.propertySetter = propertySetter;
    }

    @Nonnull
    public List<Path> resolveDataDirs() {
        String homeDir = getPropertyValue(SystemProperties.PROP_HOME_DIR, null);
        String defaultStoreDir = homeDir == null ? null : homeDir + File.separator + ApplicationDirs.DEFAULT_STORE;
        List<Path> configured = resolveDirs("data", SystemProperties.PROP_DATA_DIRS, defaultStoreDir, true);
        if (defaultStoreDir != null) {
            Path defaultStorePath = Paths.get(defaultStoreDir);
            try {
                defaultStorePath = MoreFiles.toRealPath(defaultStorePath);
            } catch (IOException e) {
                //Ignore
            }
            if (!configured.contains(defaultStorePath) && Files.isDirectory(defaultStorePath)) {
                log.info("Found a data store at the default store location ({}). Rename the directory if the store " +
                         "should not be included.", defaultStorePath);
                // the default store directory exists, but hasn't been configured. Add it to the list anyway
                return ImmutableList.<Path>builderWithExpectedSize(configured.size() + 1)
                        .add(defaultStorePath)
                        .addAll(configured)
                        .build();
            }
        }
        return configured;
    }

    @Nonnull
    public Path resolveDir(@Nonnull String directoryName, @Nonnull String propertyName, String defaultValue) {
        requireNonNull(directoryName, "directoryName");
        requireNonNull(propertyName, "propertyName");

        String propertyValue = getPropertyValue(propertyName, defaultValue);

        if (isBlank(propertyValue)) {
            throw missingApplicationDir(directoryName, propertyName);
        }
        checkTilde(propertyValue, directoryName);

        Path applicationDir = resolve(propertyValue);
        checkNotInInstallDir(applicationDir, directoryName);
        return applicationDir;
    }

    @Nonnull
    public List<Path> resolveDirs(@Nonnull String directoryName, @Nonnull String propertyName,
                                  String defaultValue, boolean failOnMissing) {
        requireNonNull(directoryName, "directoryName");
        requireNonNull(propertyName, "propertyName");

        List<String> paths = getPropertyValueList(propertyName);
        if (paths.isEmpty()) {
            String propertyValue = getPropertyValue(propertyName, defaultValue);
            if (isBlank(propertyValue)) {
                if (failOnMissing) {
                    throw missingApplicationDir(directoryName, propertyName);
                }
                return Collections.emptyList();
            }
            paths = ImmutableList.copyOf(Splitter.on(CharMatcher.anyOf("," + File.pathSeparator)).split(propertyValue));
        }

        return paths.stream()
                .map(StringUtils::trim)
                .peek(dir -> checkTilde(dir, directoryName))
                .map(ApplicationDirResolver::resolve)
                .peek(dir -> checkNotInInstallDir(dir, directoryName))
                .distinct()
                .collect(MoreCollectors.toImmutableList());
    }

    @Nonnull
    public Path resolveHomeDir() {
        return resolveDir("home", SystemProperties.PROP_HOME_DIR, null);
    }

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

    @Nonnull
    private List<String> getPropertyValueList(@Nonnull String propertyName) {
        ImmutableList.Builder<String> builder = ImmutableList.builder();
        int index = 0;
        while (true) {
            String propertyValue = propertyResolver.getProperty(propertyName + "[" + index++ + "]");
            if (isBlank(propertyValue)) {
                return builder.build();
            }
            builder.add(propertyValue);
        }
    }

    private static MissingApplicationDirectoryException missingApplicationDir(String directoryName, String propertyName) {
        return new MissingApplicationDirectoryException("The " + directoryName + " directory is not configured",
                String.format("Configure the %s directory using either the system property '%s' or the " +
                              "environment variable '%s'",
                        directoryName, propertyName, toEnvironmentVariable(propertyName)));
    }

    private static Path resolve(String dirName) {
        Path dir = Paths.get(dirName);
        try {
            return dir.toRealPath();
        } catch (IOException e) {
            return dir.toAbsolutePath();
        }
    }

    /**
     * Converts a system property name to an environment variable, using the same logic that Spring uses; the
     * property name is upper-cased and all '.' characters are replaced with '_'.
     *
     * @param propertyName the property name
     * @return the matching environment variable
     */
    private static String toEnvironmentVariable(String propertyName) {
        return propertyName.toUpperCase(Locale.ROOT).replace('.', '_');
    }

    private void checkNotInInstallDir(Path directory, String directoryName) {
        String value = propertyResolver.getProperty(SystemProperties.PROP_INSTALL_DIR);
        if (value == null) {
            return;
        }

        Path installDir = resolve(value);
        if (directory.startsWith(installDir)) {
            String message = String.format("The %s directory [%s] must not be the same directory as or a subdirectory" +
                                           " of the %s installation directory.",
                    directoryName, directory.toAbsolutePath(), Product.NAME);
            throw new InvalidApplicationDirectoryException(message, "Configure a different " + directoryName + " directory");
        }
    }

    private String getPropertyValue(String propertyName, String defaultValue) {
        String propertyValue = propertyResolver.getProperty(propertyName);
        if (propertyValue == null) {
            propertyValue = defaultValue;
        }
        if (propertyValue != null) {
            //Set the value as a system property
            propertySetter.setProperty(propertyName, propertyValue);
        }

        return propertyValue;
    }
}
