package net.i2p.router.startup;
/*
 * free (adj.): unencumbered; not under the control of others
 * Released into the public domain
 * with no warranty of any kind, either expressed or implied.
 */

import java.io.File;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.OutputStreamWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;

import net.i2p.data.DataHelper;
import net.i2p.router.RouterContext;
import net.i2p.util.FileUtil;
import net.i2p.util.I2PSSLSocketFactory;
import net.i2p.util.SecureFileOutputStream;
import net.i2p.util.VersionComparator;

/**
 *  Migrate the clients.config and jetty.xml files
 *  from Jetty 5/6 to Jetty 7/8.
 *  Also migrate jetty.xml from Jetty 7/8 to Jetty 9.
 *
 *  For each client for class org.mortbay.jetty.Server:
 *<pre>
 *  Let $D be the dir that jetty.xml is in (usually ~/.i2p/eepsite)
 *  Saves $D/jetty.xml to $D/jetty6.xml
 *  Copies $I2P/eepsite-jetty7/jetty.xml to $D/jetty.xml, edited for $D
 *  Copies $I2P/eepsite-jetty7/jetty-ssl.xml to $D/jetty-ssl.xml, edited for $D
 *  Copies $I2P/eepsite-jetty7/jetty-rewrite.xml to $D/jetty-rewrite.xml
 *  Copies $I2P/eepsite-jetty7/context/base-context.xml to $D/jetty.xml, edited for $D
 *  Copies $I2P/eepsite-jetty7/context/cgi-context.xml to $D/jetty.xml, edited for $D
 *  Copies $I2P/eepsite-jetty7/etc/* to $D/etc
 *  Changes main class in clients.config
 *</pre>
 *  Copies clients.config to clients.config.jetty6;
 *  Saves new clients.config.
 *
 *  Does NOT preserve port number, thread counts, etc. in the migration to 7/8.
 *  DOES preserve everything in the migration to 9.
 *
 *  @since Jetty 6
 */
abstract class MigrateJetty {
    private static boolean _wasChecked;
    private static boolean _hasLatestJetty;

    private static final String OLD_CLASS = "org.mortbay.jetty.Server";
    private static final String OLD_CLASS_6 = "org.mortbay.start.Main";
    private static final String NEW_CLASS = "net.i2p.jetty.JettyStart";
    private static final String TEST_CLASS = "org.eclipse.jetty.server.Server";
    private static final String BACKUP_SUFFIX = ".jetty6";
    private static final String BACKUP_SUFFIX_8 = ".jetty8";
    private static final String JETTY_TEMPLATE_DIR = "eepsite-jetty9";
    private static final String JETTY_TEMPLATE_PKGDIR = "eepsite";
    private static final String BASE_CONTEXT = "contexts/base-context.xml";
    private static final String CGI_CONTEXT = "contexts/cgi-context.xml";
    private static final String PROP_JETTY9_MIGRATED = "router.startup.jetty9.migrated";
    
    /**
     *  For each entry in apps, if the main class is an old Jetty class,
     *  migrate it to the new Jetty class, and update the Jetty config files.
     */
    public static void migrate(RouterContext ctx, List<ClientAppConfig> apps) {
        if (ctx.getBooleanProperty(PROP_JETTY9_MIGRATED))
            return;
        String installed = ctx.getProperty("router.firstVersion");
        if (installed != null && VersionComparator.comp(installed, "0.9.30") >= 0) {
            ctx.router().saveConfig(PROP_JETTY9_MIGRATED, "true");
            return;
        }
        boolean shouldSave = false;
        boolean jetty9success = false;
        for (int i = 0; i < apps.size(); i++) {
            ClientAppConfig app = apps.get(i);
            String client;
            String backupSuffix;
            if (app.className.equals(NEW_CLASS)) {
                client = "client application " + i + " [" + app.clientName +
                         "] from Jetty 7/8 to Jetty 9";
                backupSuffix = BACKUP_SUFFIX_8;
            } else if (app.className.equals(OLD_CLASS) || app.className.equals(OLD_CLASS_6)) {
                client = "client application " + i + " [" + app.clientName +
                         "] from Jetty 5/6 " + app.className +
                         " to Jetty 9 " + NEW_CLASS;
                backupSuffix = BACKUP_SUFFIX;
            } else {
                continue;
            }
            if (!hasLatestJetty()) {
                System.err.println("WARNING: Jetty 7 unavailable, cannot migrate " + client);
                continue;
            }
            if (app.args == null)
                continue;
            // remove quotes
            String args[] = LoadClientAppsJob.parseArgs(app.args);
            if (args.length == 0)
                continue;
            String xml = args[0];
            File xmlFile = new File(xml);
            if (!xmlFile.isAbsolute())
                xmlFile = new File(ctx.getAppDir(), xml);
            if (!xmlFile.exists()) {
                System.err.println("WARNING: XML file " + xmlFile +
                               " not found, cannot migrate " + client);
                continue;
            }
            File eepsite = xmlFile.getParentFile();
            boolean ok = backupFile(xmlFile, backupSuffix);
            if (!ok) {
                System.err.println("WARNING: Failed to backup up XML file " + xmlFile +
                               ", cannot migrate " + client);
                continue;
            }
            if (app.className.equals(NEW_CLASS)) {
                // Do the migration of 8 to 9, handle additional command-line xml files too
                for (int j = 0; j < args.length; j++) {
                    if (j > 0) {
                        // probably jetty-ssl.xml
                        xmlFile = new File(args[j]);
                        ok = backupFile(xmlFile, backupSuffix);
                        if (!ok) {
                            System.err.println("WARNING: Failed to backup up XML file " + xmlFile +
                                               ", cannot migrate " + client);
                            continue;
                        }
                    }
                    boolean ok9 = migrateToJetty9(xmlFile);
                    if (ok9) {
                        System.err.println("WARNING: Migrated " + client + ".\n" +
                                           "Check the " + xmlFile.getName() + " file in " + eepsite + ".\n" +
                                           "Your old " + xmlFile.getName() + " file was backed up to " + xmlFile.getAbsolutePath() + BACKUP_SUFFIX_8);
                        jetty9success = true;
                    }
                }
                continue;
            }

            // Below here is migration of 5/6 to 9

            File baseEep = new File(ctx.getBaseDir(), JETTY_TEMPLATE_DIR);
            // in packages, or perhaps on an uninstall/reinstall, the files are in eepsite/
            if (!baseEep.exists())
                baseEep = new File(ctx.getBaseDir(), JETTY_TEMPLATE_PKGDIR);
            if (baseEep.equals(eepsite)) {
                // non-split directory yet not an upgrade? shouldn't happen
                System.err.println("Eepsite in non-split directory " + eepsite +
                               ", cannot migrate " + client);
                continue;
            }
            // jetty.xml existed before in jetty 5 version, so check this new file
            // and if it doesn't exist we can't continue
            File baseContext = new File(baseEep, BASE_CONTEXT);
            if (!baseContext.exists()) {
                System.err.println("WARNING: Cannot find new XML file template " + baseContext +
                               ", cannot migrate " + client);
                continue;
            }
            String newPath = eepsite.getAbsolutePath() + File.separatorChar;
            ok = WorkingDir.migrateJettyXml(baseEep, eepsite, "jetty.xml", "./eepsite/", newPath);
            if (!ok) {
                System.err.println("WARNING: Failed to modify XML file " + xmlFile +
                               ", cannot migrate " + client);
                continue;
            }
            // now we're committed, so don't check any more failure codes
            backupAndMigrateFile(baseEep, eepsite, "jetty-ssl.xml", "./eepsite/", newPath);
            (new File(eepsite, "contexts")).mkdir();
            // ContextProvider scanner only looks for files ending in .xml so we can
            // back up to the same directory
            backupAndMigrateFile(baseEep, eepsite, BASE_CONTEXT, "./eepsite/", newPath);
            backupAndMigrateFile(baseEep, eepsite, CGI_CONTEXT, "./eepsite/", newPath);
            backupAndCopyFile(baseEep, eepsite, "jetty-rewrite.xml");
            (new File(eepsite, "etc")).mkdir();
            // realm.properties: No change from 6 to 7
            File to = new File(eepsite, "etc/realm.properties");
            if (!to.exists())
                WorkingDir.copyFile(new File(baseEep, "etc/realm.properties"), to);
            backupAndCopyFile(baseEep, eepsite, "etc/webdefault.xml");
            app.className = NEW_CLASS;
            shouldSave = true;
            System.err.println("WARNING: Migrated " + client + '\n' +
                               "Check the following files in " + eepsite +
                               ": jetty.xml, " + BASE_CONTEXT + ", and " + CGI_CONTEXT + "\n" +
                               "Your old jetty.xml was backed up." + '\n' +
                               "If you modified your jetty.xml to change ports, thread limits, etc, you MUST\n" +
                               "edit it to change them again. Your port was reset to 7658.");
        }
        if (shouldSave) {
            File cfgFile = ClientAppConfig.configFile(ctx);
            boolean ok = backupFile(cfgFile);
            if (ok) {
                try {
                    ClientAppConfig.writeClientAppConfig(ctx, apps);
                    System.err.println("WARNING: Migrated clients config file " + cfgFile +
                                   " from Jetty 5/6 " + OLD_CLASS + '/' + OLD_CLASS_6 +
                                   " to Jetty 9 " + NEW_CLASS);
                } catch (IOException ioe) {
                    ok = false;
                }
            }
            if (!ok) {
                System.err.println("WARNING: Failed to migrate clients config file " + cfgFile +
                               " from Jetty 5/6 " + OLD_CLASS + '/' + OLD_CLASS_6 +
                               " to Jetty 9 " + NEW_CLASS);
            }
        }
        if (jetty9success)
            ctx.router().saveConfig(PROP_JETTY9_MIGRATED, "true");
    }

    /**
     *  Migrate a jetty.xml file to Jetty 9.
     *  Unlike above, where we just migrate the new install file over for Jetty 9,
     *  here we modify the xml file in-place to preserve settings where possible.
     *
     *  @return success
     *  @since Jetty 9
     */
    private static boolean migrateToJetty9(File xmlFile) {
        if (xmlFile.getName().equals("jetty-jmx.xml")) {
            // This is lazy but nobody's using jmx, not worth the trouble
            System.err.println("ERROR: Migration  of " + xmlFile +
                               " file is not supported. Copy new file from $I2P/eepsite-jetty9/jetty-jmx.xml");
            return false;
        }
        // we don't re-migrate from the template, we just add the
        // necessary args for the QueuedThreadPool constructor in-place
        // and fixup the renamed set call
        boolean modified = false;
        File eepsite = xmlFile.getParentFile();
        File newFile = new File(eepsite, xmlFile.getName() + System.currentTimeMillis() + ".tmp");
        FileInputStream in = null;
        PrintWriter out = null;
        try {
            in = new FileInputStream(xmlFile);
            out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(newFile), "UTF-8")));
            String s;
            boolean foundQTP = false;
            boolean foundSTP = false;
            boolean foundETP = false;
            boolean foundSCC = false;
            boolean foundHC = false;
            boolean foundSSCC = false;
            while ((s = DataHelper.readLine(in)) != null) {
                // readLine() doesn't strip \r
                if (s.endsWith("\r"))
                    s = s.substring(0, s.length() - 1);
                if (s.contains("Modified by I2P migration script for Jetty 9.") ||
                    s.contains("This configuration supports Jetty 9.") ||
                    s.contains("http://www.eclipse.org/jetty/configure_9_0.dtd")) {
                    if (!modified)
                        break;
                    // else we've modified it twice?
                } else if (s.contains("org.eclipse.jetty.util.thread.QueuedThreadPool")) {
                    foundQTP = true;
                } else if (foundQTP) {
                    if (!(s.contains("Modified by") || s.contains("<Arg type=\"int\">"))) {
                        out.println("        <!-- Modified by I2P migration script for Jetty 9. Do not remove this line -->");
                        out.println("        <Arg type=\"int\">20</Arg>     <!-- maxThreads, overridden below -->");
                        out.println("        <Arg type=\"int\">3</Arg>      <!-- minThreads, overridden below -->");
                        out.println("        <Arg type=\"int\">60000</Arg>  <!-- maxIdleTimeMs, overridden below -->");
                        modified = true;
                    }
                    foundQTP = false;
                }
                if (s.contains("<Set name=\"maxIdleTimeMs\">")) {
                    // <Set name="maxIdleTimeMs">60000</Set>
                    s = s.replace("<Set name=\"maxIdleTimeMs\">", "<Set name=\"idleTimeout\">");
                    modified = true;
                } else if (s.contains("<Set name=\"ThreadPool\">")) {
                    // <Set name="ThreadPool">, must be changed to constructor arg
                    out.println("    <!-- Modified by I2P migration script for Jetty 9. Do not remove this line -->");
                    s = s.replace("<Set name=\"ThreadPool\">", "<Arg>");
                    foundSTP = true;
                    modified = true;
                } else if (foundSTP && !foundETP && s.contains("</Set>") && !s.contains("<Set")) {
                    // </Set> (close of <Set name="ThreadPool">)
                    // All the lines above have <Set>...</Set> on the same line, if they don't, this will break.
                    s = s.replace("</Set>", "</Arg>");
                    foundETP = true;
                } else if (s.contains("org.eclipse.jetty.server.nio.SelectChannelConnector")) {
                    s = s.replace("org.eclipse.jetty.server.nio.SelectChannelConnector", "org.eclipse.jetty.server.ServerConnector");
                    out.println("          <!-- Modified by I2P migration script for Jetty 9. Do not remove this line -->");
                    out.println(s);
                    out.println("            <Arg><Ref id=\"Server\" /></Arg>");
                    out.println("            <Arg type=\"int\">1</Arg>     <!-- number of acceptors -->");
                    out.println("            <Arg type=\"int\">0</Arg>     <!-- default number of selectors -->");
                    out.println("            <Arg>");
                    out.println("              <Array type=\"org.eclipse.jetty.server.ConnectionFactory\">    <!-- varargs so we need an array -->");
                    out.println("                <Item>");
                    out.println("                  <New class=\"org.eclipse.jetty.server.HttpConnectionFactory\">");
                    out.println("                    <Arg>");
                    out.println("                      <New class=\"org.eclipse.jetty.server.HttpConfiguration\">");
                    out.println("                        <Set name=\"sendServerVersion\">false</Set>");
                    out.println("                        <Set name=\"sendDateHeader\">true</Set>");
                    out.println("                      </New>");
                    out.println("                    </Arg>");
                    out.println("                  </New>");
                    out.println("                </Item>");
                    out.println("              </Array>");
                    out.println("            </Arg>");
                    modified = true;
                    continue;
             // SSL starts here
                } else if (s.contains("org.eclipse.jetty.http.ssl.SslContextFactory")) {
                    s = s.replace("org.eclipse.jetty.http.ssl.SslContextFactory", "org.eclipse.jetty.util.ssl.SslContextFactory");
                    out.println("  <!-- Modified by I2P migration script for Jetty 9. Do not remove this line -->");
                    out.println(s);
                    // don't try to migrate from below, just generate a new list
                    out.println("    <Set name=\"ExcludeCipherSuites\">");
                    out.println("      <Array type=\"java.lang.String\">");
                    for (String ss : I2PSSLSocketFactory.EXCLUDE_CIPHERS) {
                        out.println("        <Item>" + ss + "</Item>");
                    }
                    out.println("      </Array>");
                    out.println("    </Set>");
                    out.println("    <Set name=\"ExcludeProtocols\">");
                    out.println("      <Array type=\"java.lang.String\">");
                    for (String ss : I2PSSLSocketFactory.EXCLUDE_PROTOCOLS) {
                        out.println("        <Item>" + ss + "</Item>");
                    }
                    out.println("      </Array>");
                    out.println("    </Set>");
                    modified = true;
                    continue;
                } else if (s.contains("org.eclipse.jetty.server.ssl.SslSelectChannelConnector")) {
                    s = s.replace("org.eclipse.jetty.server.ssl.SslSelectChannelConnector", "org.eclipse.jetty.server.ServerConnector");
                    out.println("      <!-- Modified by I2P migration script for Jetty 9. Do not remove this line -->");
                    out.println(s);
                    out.println("        <Arg><Ref id=\"Server\" /></Arg>");
                    out.println("        <Arg type=\"int\">1</Arg>     <!-- number of acceptors -->");
                    out.println("        <Arg type=\"int\">0</Arg>     <!-- default number of selectors -->");
                    out.println("        <Arg>");
                    out.println("           <Array type=\"org.eclipse.jetty.server.ConnectionFactory\">    <!-- varargs so we need an array -->");
                    out.println("              <Item>");
                    out.println("                <New class=\"org.eclipse.jetty.server.SslConnectionFactory\">");
                    out.println("                  <Arg><Ref id=\"sslContextFactory\" /></Arg>");
                    out.println("                  <Arg>http/1.1</Arg>");
                    out.println("                </New>");
                    out.println("              </Item>");
                    out.println("              <Item>");
                    out.println("                <New class=\"org.eclipse.jetty.server.HttpConnectionFactory\">");
                    out.println("                  <Arg>");
                    out.println("                    <New class=\"org.eclipse.jetty.server.HttpConfiguration\">");
                    out.println("                      <Set name=\"sendServerVersion\">false</Set>");
                    out.println("                      <Set name=\"sendDateHeader\">true</Set>");
                    out.println("                    </New>");
                    out.println("                  </Arg>");
                    out.println("                </New>");
                    out.println("              </Item>");
                    out.println("            </Array>");
                    out.println("        </Arg>");
                    foundSSCC = true;
                    modified = true;
                    continue;
                } else if (foundSSCC && s.contains("<Set name=\"ExcludeCipherSuites\">")) {
                    // delete the old ExcludeCipherSuites in this section
                    do {
                        s = DataHelper.readLine(in);
                    } while(s != null && !s.contains("</Set>"));
                    modified = true;
                    continue;
                } else if (foundSSCC &&
                           s.contains("<Ref id=\"sslContextFactory\"")) {
                    // delete old one in this section, replaced above
                    modified = true;
                    continue;
                } else if (s.contains("<Set name=\"KeyStore\">")) {
                    s = s.replace("<Set name=\"KeyStore\">", "<Set name=\"KeyStorePath\">");
                    modified = true;
                } else if (s.contains("<Set name=\"TrustStore\">")) {
                    s = s.replace("<Set name=\"TrustStore\">", "<Set name=\"TrustStorePath\">");
                    modified = true;
             // SSL ends here
                } else if (s.contains("class=\"org.eclipse.jetty.deploy.providers.ContextProvider\">")) {
                    // WebAppProvider now also does what ContextProvider used to do
                    out.println("        <!-- Modified by I2P migration script for Jetty 9. Do not remove this line -->");
                    s = s.replace("class=\"org.eclipse.jetty.deploy.providers.ContextProvider\">", "class=\"org.eclipse.jetty.deploy.providers.WebAppProvider\">");
                    modified = true;
                } else if (s.contains("<Set name=\"maxIdleTime\">")) {
                    s = s.replace("<Set name=\"maxIdleTime\">", "<Set name=\"idleTimeout\">");
                    modified = true;
                } else if (s.contains("<Set name=\"gracefulShutdown\">")) {
                    s = s.replace("<Set name=\"gracefulShutdown\">", "<Set name=\"stopTimeout\">");
                    modified = true;
                } else if (s.contains("org.eclipse.jetty.server.HttpConfiguration")) {
                    foundHC = true;
                } else if (!foundHC &&
                           (s.contains("<Set name=\"sendServerVersion\">") ||
                            s.contains("<Set name=\"sendDateHeader\">"))) {
                    // old ones for Server, not in HTTPConfiguration section, delete
                    modified = true;
                    continue;
                } else if (s.contains("<Set name=\"Acceptors\">") ||
                           s.contains("<Set name=\"acceptors\">") ||
                           s.contains("<Set name=\"statsOn\">") ||
                           s.contains("<Set name=\"confidentialPort\">") ||
                           s.contains("<Set name=\"lowResourcesConnections\">") ||
                           s.contains("<Set name=\"lowResourcesMaxIdleTime\">") ||
                           s.contains("<Set name=\"useDirectBuffers\">")) {
                    // delete
                    modified = true;
                    continue;
                }
                out.println(s);
            }
        } catch (IOException ioe) {
            if (in != null) {
                System.err.println("FAILED migration of " + xmlFile + ": " + ioe);
            }
            return false;
        } finally {
            if (in != null) try { in.close(); } catch (IOException ioe) {}
            if (out != null) out.close();
        }
        if (modified) {
            return FileUtil.rename(newFile, xmlFile);
        } else {
            newFile.delete();
            return true;
        }
    }


    /** do we have Jetty 7/8/9? */
    private static boolean hasLatestJetty() {
        if (!_wasChecked) {
            try {
                LoadClientAppsJob.testClient(TEST_CLASS, null);
                _hasLatestJetty = true;
            } catch (ClassNotFoundException cnfe) {}
            _wasChecked = true;
        }
        return _hasLatestJetty;
    }

    /**
     *  Backup a file
     *  @return success
     *  @since Jetty 7
     */
    private static boolean backupFile(File from) {
        return backupFile(from, BACKUP_SUFFIX);
    }

    /**
     *  Backup a file with given suffix
     *  @return success
     *  @since Jetty 9
     */
    private static boolean backupFile(File from, String suffix) {
        if (!from.exists())
            return true;
        File to = new File(from.getAbsolutePath() + suffix);
        if (to.exists())
            to = new File(to.getAbsolutePath() + "." + System.currentTimeMillis());
        boolean rv = WorkingDir.copyFile(from, to);
        if (rv)
            System.err.println("Backed up file " + from + " to " + to);
        else
            System.err.println("WARNING: Failed to back up file " + from + " to " + to);
        return rv;
    }

    /**
     *  Backup a file and migrate new XML
     *  @return success
     *  @since Jetty 7
     */
    private static boolean backupAndMigrateFile(File templateDir, File toDir, String filename, String fromString, String toString) {
        File to = new File(toDir, filename);
        boolean rv = backupFile(to);
        boolean rv2 = WorkingDir.migrateJettyXml(templateDir, toDir, filename, fromString, toString);
        return rv && rv2;
    }

    /**
     *  Backup a file and copy new
     *  @return success
     *  @since Jetty 7
     */
    private static boolean backupAndCopyFile(File templateDir, File toDir, String filename) {
        File to = new File(toDir, filename);
        boolean rv = backupFile(to);
        File from = new File(templateDir, filename);
        boolean rv2 = WorkingDir.copyFile(from, to);
        return rv && rv2;
    }
}
