/*
 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the License. You may obtain a
 * copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package org.apache.geode.session.tests;

import static java.nio.file.Files.createTempDirectory;

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.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.function.IntSupplier;
import java.util.regex.Pattern;

/**
 * Provides logic for installation of tomcat. This makes the modifications to the tomcat install as
 * described in <a href=
 * "https://geode.apache.org/docs/guide/latest/tools_modules/http_session_mgmt/session_mgmt_tomcat.html">
 * the geode docs</a>.
 */
public class TomcatInstall extends ContainerInstall {
  /**
   * Version of tomcat that this class will install
   *
   * <p>
   * Includes the download URL for the each version, the version number associated with each
   * version, and other properties or XML attributes needed to setup tomcat containers within Cargo
   *
   * <p>
   * Note: TOMCAT6-9 are kept for backward compatibility with upgradeTest but are not actively
   * used in distributedTest (Jakarta EE 10 migration requires Tomcat 10+)
   */
  public enum TomcatVersion {
    // Legacy versions - kept for upgradeTest compatibility only
    TOMCAT6(6, "tomcat-6.0.37.zip"),
    TOMCAT7(7, "tomcat-7.0.109.zip"),
    TOMCAT8(8, "tomcat-8.5.66.zip"),
    TOMCAT9(9, "tomcat-9.0.62.zip"),

    // Jakarta EE 10 compatible version - actively used in distributedTest
    TOMCAT10(10, "tomcat-10.1.33.zip");

    private final int version;

    private final String downloadURL;

    TomcatVersion(int version, String downloadURL) {
      this.version = version;
      this.downloadURL = downloadURL;
    }

    /**
     * Converts the version to an integer
     *
     * @return the version as an integer
     */
    public int toInteger() {
      return getVersion();
    }

    public int getVersion() {
      return version;
    }

    public String getContainerId() {
      return "tomcat" + getVersion() + "x";
    }

    public String getDownloadURL() {
      return downloadURL;
    }

    public String jarSkipPropertyName() {
      switch (this) {
        case TOMCAT6:
          return null;
        case TOMCAT7:
          return "tomcat.util.scan.DefaultJarScanner.jarsToSkip";
        case TOMCAT8:
        case TOMCAT9:
        case TOMCAT10:
          return "tomcat.util.scan.StandardJarScanFilter.jarsToSkip";
        default:
          throw new IllegalArgumentException("Illegal tomcat version option");
      }
    }
  }

  /**
   * This determines the setting for 'enableCommitValve' in Tomcat's context.xml when configuring
   * the DeltaSessionManager.
   */
  public enum CommitValve {
    ENABLED("true"),
    DISABLED("false"),
    DEFAULT(null);

    private final String value;

    CommitValve(String value) {
      this.value = value;
    }

    public String getValue() {
      return value;
    }
  }

  /**
   * If you update this list method to return different dependencies, please also update the Tomcat
   * module documentation! The documentation can be found here:
   *
   * <p>
   * geode-docs/tools_modules/http_session_mgmt/tomcat_installing_the_module.html.md.erb
   */
  private static final String[] tomcatRequiredJars =
      {"antlr", "commons-io", "commons-lang", "commons-validator", "fastutil", "geode-common",
          "geode-core", "geode-unsafe", "geode-deployment-legacy", "geode-log4j", "geode-logging",
          "geode-membership", "geode-management", "geode-serialization", "geode-tcp-server",
          "jakarta.transaction-api", "jgroups", "log4j-api", "log4j-core", "log4j-jul",
          "micrometer",
          "shiro-core", "jetty-server", "jetty-util", "jetty-http", "jetty-io"};

  private final TomcatVersion version;

  private final CommitValve commitValve;

  public TomcatInstall(String name, TomcatVersion version, ConnectionType connectionType,
      IntSupplier portSupplier, CommitValve commitValve) throws Exception {
    this(createTempDirectory("geode_container_install").toAbsolutePath(), name, version,
        connectionType, DEFAULT_MODULE_PATH, GEODE_LIB_PATH, portSupplier, commitValve);
  }

  public TomcatInstall(Path rootDir, String name, TomcatVersion version,
      ConnectionType connectionType,
      IntSupplier portSupplier, CommitValve commitValve) throws Exception {
    this(rootDir, name, version, connectionType, DEFAULT_MODULE_PATH, GEODE_LIB_PATH, portSupplier,
        commitValve);
  }

  public TomcatInstall(String name, TomcatVersion version, ConnectionType connType,
      String modulesJarLocation, String extraJarsPath, IntSupplier portSupplier,
      CommitValve commitValve)
      throws Exception {
    this(createTempDirectory("geode_container_install").toAbsolutePath(), name, version, connType,
        Paths.get(modulesJarLocation), Paths.get(extraJarsPath), portSupplier, commitValve);
  }

  public TomcatInstall(Path rootDir, String name, TomcatVersion version, ConnectionType connType,
      String modulesJarLocation, String extraJarsPath, IntSupplier portSupplier,
      CommitValve commitValve)
      throws Exception {
    this(rootDir, name, version, connType, Paths.get(modulesJarLocation), Paths.get(extraJarsPath),
        portSupplier, commitValve);
  }

  /**
   * Download and setup an installation tomcat using the {@link ContainerInstall} constructor and
   * some extra functions this class provides
   *
   * <p>
   * Specifically, this function uses {@link #copyTomcatGeodeReqFiles(Path, Path)} to install
   * geode session into Tomcat, {@link #setupDefaultSettings()} to modify the context and server XML
   * files within the installation's 'conf' folder, and {@link #updateProperties()} to set the jar
   * skipping properties needed to speedup container startup.
   *
   * @param rootDir The root folder used by default for cargo logs, container configs and other
   *        files and directories
   * @param name used to name install directory
   * @param version the version of Tomcat to use
   * @param connType Enum representing the connection type of this installation (either client
   *        server or peer to peer)
   * @param modulesJarLocation the path to the module jars
   * @param extraJarsPath the path to the extra jars
   * @param portSupplier the port supplier
   * @param commitValve the commit valve
   *
   * @throws Exception if an exception is encountered
   */
  public TomcatInstall(Path rootDir, String name, TomcatVersion version, ConnectionType connType,
      Path modulesJarLocation, Path extraJarsPath, IntSupplier portSupplier,
      CommitValve commitValve)
      throws Exception {
    // Does download and install from URL
    super(rootDir, name, version.getDownloadURL(), connType, "tomcat", modulesJarLocation,
        portSupplier);

    this.version = version;
    this.commitValve = commitValve;
    modulesJarLocation = Paths.get(getModulePath()).toAbsolutePath().resolve("lib");

    // Install geode sessions into tomcat install
    copyTomcatGeodeReqFiles(modulesJarLocation, extraJarsPath);
    // Set some default XML attributes in server and cache XMLs
    setupDefaultSettings();

    // Add required jars copied to jar skips so container startup is faster
    if (version.jarSkipPropertyName() != null) {
      updateProperties();
    }
  }

  /**
   * Modifies the context and server XML files in the installation's 'conf' directory so that they
   * contain the session manager class ({@link #getContextSessionManagerClass()}) and life cycle
   * listener class ({@link #getServerLifeCycleListenerClass()}) respectively
   */
  public void setupDefaultSettings() {
    HashMap<String, String> attributes = new HashMap<>();

    // Set the session manager class within the context XML file
    attributes.put("className", getContextSessionManagerClass());
    editXMLFile(getDefaultContextXMLFile(), "Tomcat", "Manager", "Context",
        attributes);

    // Set the server lifecycle listener within the server XML file
    attributes.put("className", getServerLifeCycleListenerClass());
    editXMLFile(getDefaultServerXMLFile(), "Tomcat", "Listener", "Server",
        attributes);
  }

  /**
   * Get the server life cycle class that should be used
   *
   * Generates the class based on whether the installation's connection type {@link
   * ContainerInstall#getConnectionType()} is client server or peer to peer.
   *
   * @return the server life cycle class that should be used
   */
  public String getServerLifeCycleListenerClass() {
    String className = "org.apache.geode.modules.session.catalina.";
    switch (getConnectionType()) {
      case PEER_TO_PEER:
        className += "PeerToPeer";
        break;
      case CLIENT_SERVER:
      case CACHING_CLIENT_SERVER:
        className += "ClientServer";
        break;
      default:
        throw new IllegalArgumentException(
            "Bad connection type. Must be either PEER_TO_PEER or CLIENT_SERVER");
    }

    className += "CacheLifecycleListener";
    return className;
  }

  /**
   * Location of the context XML file in the installation's 'conf' directory
   *
   * @return the location of the context XML file in the installation's 'conf' directory
   */
  public Path getDefaultContextXMLFile() {
    return getHome().resolve("conf").resolve("context.xml");
  }

  /**
   * Location of the server XML file in the installation's 'conf' directory
   *
   * @return the location of the server XML file in the installation's 'conf' directory
   */
  public Path getDefaultServerXMLFile() {
    return getHome().resolve("conf").resolve("server.xml");
  }

  /**
   * Gets the TomcatDeltaSessionManager class associated with this {@link #version}. Use's the
   * {@link #version}'s toInteger function to do so.
   */
  @Override
  public String getContextSessionManagerClass() {
    return "org.apache.geode.modules.session.catalina.Tomcat" + version.toInteger()
        + "DeltaSessionManager";
  }

  /**
   * Creates a {@link TomcatContainer} instance off of this installation.
   *
   * @param containerDescriptors Additional descriptors used to identify a container
   */
  @Override
  public TomcatContainer generateContainer(Path rootDir, Path containerConfigHome,
      String containerDescriptors)
      throws IOException {
    return new TomcatContainer(this, rootDir, containerConfigHome, containerDescriptors,
        portSupplier());
  }

  /**
   * The cargo specific installation id needed to setup a cargo container
   *
   * Based on the installation's {@link #version}.
   */
  @Override
  public String getInstallId() {
    return version.getContainerId();
  }

  @Override
  public String getInstallDescription() {
    return version.name() + "_" + getConnectionType().getName();
  }

  public CommitValve getCommitValve() {
    return commitValve;
  }

  /**
   * Copies jars specified by {@link #tomcatRequiredJars} from the {@link #getModulePath()} and the
   * specified other directory passed to the function
   *
   * @throws IOException if the {@link #getModulePath()}, installation lib directory, or extra
   *         directory passed in contain no files.
   */
  private void copyTomcatGeodeReqFiles(Path moduleJarDir, Path extraJarsPath)
      throws IOException {
    List<File> requiredFiles = new ArrayList<>();

    // The library path for the current tomcat installation
    Path tomcatLibPath = getHome().resolve("lib");

    // List of required jars and form version regexps from them
    String versionRegex = "-?[0-9]*.*\\.jar";
    List<Pattern> patterns = new ArrayList<>(tomcatRequiredJars.length);
    for (String jar : tomcatRequiredJars) {
      patterns.add(Pattern.compile(jar + versionRegex));
    }

    // Don't need to copy any jars already in the tomcat install
    File tomcatLib = tomcatLibPath.toFile();

    // Find all jars in the tomcatModulePath and add them as required jars
    try {
      for (File file : moduleJarDir.toFile().listFiles()) {
        if (file.isFile() && file.getName().endsWith(".jar")) {
          requiredFiles.add(file);
        }
      }
    } catch (NullPointerException e) {
      throw new IOException(
          "No files found in tomcat module directory " + getModulePath() + "/lib/");
    }

    // Find all the required jars in the extraJarsPath
    try {
      for (File file : extraJarsPath.toFile().listFiles()) {
        for (Pattern pattern : patterns) {
          if (pattern.matcher(file.getName()).find()) {
            requiredFiles.add(file);
            break;
          }
        }
      }
    } catch (NullPointerException e) {
      throw new IOException("No files found in extra jars directory " + extraJarsPath);
    }

    // Copy the required jars to the given tomcat lib folder
    for (File file : requiredFiles) {
      Files.copy(file.toPath(), tomcatLib.toPath().resolve(file.toPath().getFileName()),
          StandardCopyOption.REPLACE_EXISTING);
      logger.debug("Copied required jar from " + file.toPath() + " to "
          + tomcatLibPath.resolve(file.toPath().getFileName()));
    }

    logger.info("Copied required jars into the Tomcat installation");
  }

  /**
   * Update the tomcat installation property file using {@link #editPropertyFile)}
   */
  private void updateProperties() throws Exception {
    String jarsToSkip = "";
    // Adds all the required jars as jars to skip when starting Tomcat
    for (String jarName : tomcatRequiredJars) {
      jarsToSkip += "," + jarName + "*.jar";
    }

    // Add the jars to skip to the catalina property file
    editPropertyFile(getHome().resolve("conf").resolve("catalina.properties"),
        version.jarSkipPropertyName(), jarsToSkip, true);
  }

}
