package com.atlassian.stash.internal.catalina.startup;

import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.concurrent.Callable;

/**
 * A shim that decorates arguments to org.apache.catalina.startup.Bootstrap and appends "-config" "/path/to/server.xml"
 * if a server.xml file is found in the currently configured product home or shared home directories. This works by
 * substituting the original Bootstrap class with this class in the various Tomcat scripts used to launch the
 * product / register it as a service on Windows, Linux and OSX.
 * <p>
 * Resolution of the the product home dir is:
 * <ol>
 *     <li>the <code>bitbucket.home</code> system property,</li>
 *     <li>the <code>BITBUCKET_HOME</code> environment variable,</li>
 *     <li>the <code>stash.home</code> system property, or</li>
 *     <li>the <code>STASH_HOME</code> environment variable.</li>
 * </ol>
 * (in that order).
 * <p>
 * Resolution of the the product shared home dir is:
 * <ol>
 *     <li>the <code>bitbucket.shared.home</code> system property,</li>
 *     <li>the <code>BITBUCKET_SHARED_HOME</code> environment variable,</li>
 *     <li>the <code>stash.shared.home</code> system property,</li>
 *     <li>the <code>STASH_SHARED_HOME</code> environment variable, or</li>
 *     <li>the <code>shared</code> directory under the resolved Stash home if one was resolved.</li>
 * * </ol>
 * (in that order).
 * <p>
 * These mechanisms are analogous to how {@code HomeDirectoryResolver} resolves the two directories.
 */
public class Bootstrap {

    private static final String FALLBACK_SERVER_XML = ".default-server.xml";

    private static final String ENV_HOME = "BITBUCKET_HOME";
    private static final String ENV_LEGACY_HOME = "STASH_HOME";
    private static final String ENV_LEGACY_SHARED_HOME = "STASH_SHARED_HOME";
    private static final String ENV_SHARED_HOME = "BITBUCKET_SHARED_HOME";
    
    private static final String SYS_PROP_HOME = "bitbucket.home";
    private static final String SYS_PROP_SHARED_HOME = "bitbucket.shared.home";
    private static final String SYS_PROP_LEGACY_HOME = "stash.home";
    private static final String SYS_PROP_LEGACY_SHARED_HOME = "stash.shared.home";

    private static final String MINIMUM_SUPPORTED_JAVA_VERSION = "8";
    private static final String[] UNSUPPORTED_JAVA_VERSION_PREFIXES = new String[]{"1.6.", "1.7."};

    //Log class not used in declaration to avoid UnsupportedClassVersionError on loading of this class on Java 6
    private static Object log;

    public static void main(String[] rawArgs) throws Exception {
        String version = System.getProperty("java.version", "");
        for (String unsupportedJavaVersionPrefix : UNSUPPORTED_JAVA_VERSION_PREFIXES) {
            if (version.startsWith(unsupportedJavaVersionPrefix)) {
                //A literal "Bitbucket" is used here for debugging clarity and because this module must minimise dependencies
                //on other modules where the product name is centralised
                System.out.println("Atlassian Bitbucket does not support Java " + version +
                        ". Please start the product with Java " + MINIMUM_SUPPORTED_JAVA_VERSION + " or greater.");
                System.exit(16);
            }
        }

        initLog();

        try {
            Class<?> catalinaBootstrap = Class.forName("org.apache.catalina.startup.Bootstrap");
            catalinaBootstrap.getDeclaredMethod("main", String[].class).invoke(null, new Object[]{new ArgumentDecorator().apply(rawArgs)});
        } catch (Exception e) {
            logException("Failed dynamically invoking org.apache.catalina.startup.Bootstrap.main(String[])", e);
            System.exit(1);
        }
    }

    //VisibleForTesting
    protected static void initLog() {
        log = LogFactory.getLog(Bootstrap.class);
    }

    private static void logDebug(String msg) {
        ((Log) log).debug(msg);
    }

    private static void logError(String msg) {
        ((Log) log).error(msg);
    }

    private static void logException(String msg, Exception e) {
        ((Log) log).error(msg, e);
    }

    /**
     * Decorates the arguments to be passed onto org.apache.catalina.startup.Bootstrap.main(String[]) with the server.xml
     * file if one is found in the product home or the product shared home
     */
    protected static class ArgumentDecorator {

        public String[] apply(String[] rawArgs) {
            boolean isStop = false;
            for (String arg : rawArgs) {
                if ("-config".equals(arg)) {
                    logDebug("Arguments to Tomcat Bootstrap already contain \"-config\" - not decorating");
                    return rawArgs;
                }
                if ("stop".equals(arg)) {
                    isStop = true;
                }
            }

            String[] args = rawArgs;
            File homeDir = getHomeDir();
            File serverXml = findServerXmlFile(homeDir);
            if (serverXml == null) {
                serverXml = findServerXmlFile(getSharedHomeDir(homeDir));
            }
            if (serverXml == null) {
                serverXml = findServerXmlFile(getInstallationConfDir());
            }
            if (serverXml == null && !isStop) {
                serverXml = copyFallbackServerXmlTo(getSharedHomeDir(homeDir));
            }
            if (serverXml == null) {
                serverXml = findServerXmlFile(getInstallationConfDir(), FALLBACK_SERVER_XML);
            }

            if (serverXml == null) {
                logError("Could not find a server.xml file to use. Stash will fail to start.");
            } else {
                serverXml = tryGetCanonicalFile(serverXml);
                logDebug("Using " + serverXml.getAbsolutePath());
                Collection<String> newArgs = new ArrayList<String>(args.length + 2);
                newArgs.add("-config");
                newArgs.add(serverXml.getAbsolutePath());
                newArgs.addAll(Arrays.asList(args));
                args = newArgs.toArray(new String[newArgs.size()]);
            }
            return args;
        }

        //Visible for testing
        protected String getEnvVariable(String name) {
            return System.getenv(name);
        }

        private File findServerXmlFile(File dir) {
            return findServerXmlFile(dir, "server.xml");
        }

        private File findServerXmlFile(File dir, String fileName) {
            if (dir == null) {
                return null;
            }

            File serverXml = new File(dir.getAbsolutePath(), fileName);
            boolean exists = serverXml.exists();
            boolean isFile = serverXml.isFile();
            boolean canRead = serverXml.canRead();

            logDebug(serverXml.getAbsolutePath() + ": exists = " + exists + ", is file = " + isFile + ", can read = " + canRead);
            if (!(exists && isFile && canRead)) {
                return null;
            }
            return serverXml;
        }

        private File copyFallbackServerXmlTo(File dir) {
            if (dir == null) {
                return null;
            }

            File defaultServerXml = findServerXmlFile(getInstallationConfDir(), FALLBACK_SERVER_XML);
            if (defaultServerXml == null) {
                return null;
            }
            logError("Copying fallback default-server.xml to shared home; no other server.xml file was found");

            File serverXml = new File(dir.getAbsolutePath(), "server.xml");
            FileChannel sourceChannel = null;
            FileChannel destChannel = null;
            try {
                serverXml.getParentFile().mkdirs();
                serverXml.createNewFile();
                try {
                    sourceChannel = new FileInputStream(defaultServerXml).getChannel();
                    destChannel = new FileOutputStream(serverXml).getChannel();
                    destChannel.transferFrom(sourceChannel, 0, sourceChannel.size());
                } finally {
                    if (sourceChannel != null) {
                        sourceChannel.close();
                    }
                    if (destChannel != null) {
                        destChannel.close();
                    }
                }
            } catch (IOException e) {
                logException("Failed to copy " + defaultServerXml.getAbsolutePath() + " to " +
                        serverXml.getAbsolutePath(), e);
                return null;
            }

            return serverXml;
        }

        private File getInstallationConfDir() {
            File installationDirectory = resolve(getPropertyValue("catalina.base", null));
            if (installationDirectory == null) {
                return null;
            }

            File confDirectory = new File(installationDirectory, "conf");
            if (!confDirectory.exists() || !confDirectory.isDirectory()) {
                return null;
            }
            return confDirectory;
        }

        private File getHomeDir() {
            String home = getPropertyValue(SYS_PROP_HOME, ENV_HOME);
            if (home == null) {
                home = getPropertyValue(SYS_PROP_LEGACY_HOME, ENV_LEGACY_HOME);
            }
            return resolve(home);
        }

        private String getPropertyValue(String propertyName, String environmentName) {
            String propertyValue = System.getProperty(propertyName);
            if (propertyValue != null) {
                logDebug("Found system property " + propertyName + ": " + propertyValue);
                return propertyValue;
            }

            propertyValue = getEnvVariable(environmentName);
            if (propertyValue != null) {
                logDebug("Found environment variable " + environmentName + ": " + propertyValue);
                return propertyValue;
            }

            logDebug("System property " + propertyName + " and environment variable " + environmentName + " not found");
            return null;
        }

        private File getSharedHomeDir(final File home) {
            String sharedHome = getPropertyValue(SYS_PROP_SHARED_HOME, ENV_SHARED_HOME);
            if (sharedHome == null) {
                sharedHome = getPropertyValue(SYS_PROP_LEGACY_SHARED_HOME, ENV_LEGACY_SHARED_HOME);
            }
            if (sharedHome == null) {
                if (home == null) {
                    return null;
                } else {
                    logDebug("Using default subdirectory " + home.getAbsolutePath() + File.pathSeparator + "shared");
                    return new File(home, "shared").getAbsoluteFile();
                }
            }
            return resolve(sharedHome);
        }

        private File resolve(String dirName) {
            if (dirName == null || dirName.trim().length() == 0) {
                return null;
            }

            File dir = new File(dirName);
            return tryGetCanonicalFile(dir);
        }

        private File tryGetCanonicalFile(File dir) {
            try {
                return dir.getCanonicalFile();
            } catch (IOException e) {
                return dir.getAbsoluteFile();
            }
        }
    }

    private interface UncheckedCallable<V> extends Callable<V> {

        @Override
        V call();
    }
}
