package com.airlock.iam.application.webserver.infrastructure;

import com.airlock.iam.application.parameters.domain.model.LogFormat;
import com.airlock.iam.application.parameters.domain.model.SecureStore;
import com.airlock.iam.application.parameters.domain.model.WebServerLogParameters;
import com.airlock.iam.application.parameters.domain.model.WebServerParameters.ConnectorParameters;
import com.airlock.iam.application.parameters.domain.model.WebServerParameters.SecureConnectorParameters;
import com.airlock.iam.application.webserver.infrastructure.hardening.SecureErrorReportValve;
import com.airlock.iam.application.webserver.infrastructure.valve.ConsoleAccessLogValve;
import com.airlock.iam.application.webserver.infrastructure.valve.FileAccessLogValve;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Collection;
import org.apache.catalina.Host;
import org.apache.catalina.Server;
import org.apache.catalina.Service;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.JreMemoryLeakPreventionListener;
import org.apache.catalina.core.StandardHost;
import org.apache.catalina.security.SecurityListener;
import org.apache.tomcat.util.buf.EncodedSolidusHandling;
import org.apache.tomcat.util.net.SSLHostConfig;
import org.apache.tomcat.util.net.SSLHostConfigCertificate;
import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type;

public final class TomcatConfigurator {
   static final String AUTO_DEPLOY_PROPERTY = "iam.internal.webserver.auto-deploy";
   static final String STRUCTURED_ACCESS_LOG_FILENAME = "access-structured.log";

   public static void configureFastHost(StandardHost host) {
      host.setStartStopThreads(0);
   }

   public static void configureSecureHost(StandardHost host) {
      boolean autoDeploy = Boolean.parseBoolean(System.getProperty("iam.internal.webserver.auto-deploy", "false"));
      host.setDeployOnStartup(autoDeploy);
      host.setAutoDeploy(autoDeploy);
      host.setErrorReportValveClass(SecureErrorReportValve.class.getName());
   }

   public static StandardHost requireStandardHost(Host host) {
      if (!(host instanceof StandardHost)) {
         throw new IllegalStateException("Expected host to be of type " + StandardHost.class.getName());
      } else {
         return (StandardHost)host;
      }
   }

   public static void configureSecureServer(Server server) {
      SecurityListener securityListener = new SecurityListener();
      if (isRunningOnWwaf()) {
         securityListener.setCheckedOsUsers("");
      }

      server.addLifecycleListener(securityListener);
   }

   @SuppressFBWarnings("DMI_HARDCODED_ABSOLUTE_FILENAME")
   private static boolean isRunningOnWwaf() {
      return Files.exists(Paths.get("/opt/airlock/buildcode"));
   }

   public static void addJreMemoryLeakPreventionListener(Server server) {
      server.addLifecycleListener(new JreMemoryLeakPreventionListener());
   }

   public static void configureAccessLogValve(StandardHost host, WebServerLogParameters accessLogParameters) {
      if (accessLogParameters.simpleFileOutputEnabled()) {
         host.addValve(newSimpleFileAccessLogValve(accessLogParameters));
      }

      if (accessLogParameters.structuredFileOutputEnabled()) {
         host.addValve(newStructuredFileAccessLogValve(accessLogParameters));
      }

      if (accessLogParameters.consoleOutputEnabled()) {
         host.addValve(newConsoleAccessLogValve(accessLogParameters));
      }
   }

   public static void configureConnectors(Service service, Collection<ConnectorParameters> connectors) {
      for (ConnectorParameters parameters : connectors) {
         Connector connector = new Connector();
         connector.setProperty("address", parameters.address());
         connector.setPort(parameters.port());
         connector.setURIEncoding("UTF-8");
         connector.setProperty("maxThreads", Integer.toString(parameters.maxThreads()));
         connector.setProperty("maxHttpHeaderSize", Integer.toString(parameters.maxHttpHeaderSize()));
         connector.setProperty("compression", parameters.compression() ? "on" : "off");
         connector.setEncodedSolidusHandling(
            Boolean.getBoolean("org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH")
               ? EncodedSolidusHandling.DECODE.getValue()
               : EncodedSolidusHandling.REJECT.getValue()
         );
         connector.setAllowBackslash(Boolean.getBoolean("org.apache.catalina.connector.CoyoteAdapter.ALLOW_BACKSLASH"));
         parameters.secureParameters().ifPresent(secure -> {
            connector.setSecure(true);
            connector.setScheme("https");
            connector.setProperty("SSLEnabled", "true");
            connector.addSslHostConfig(getSslHostConfig(secure));
         });
         service.addConnector(connector);
      }
   }

   private static SSLHostConfig getSslHostConfig(SecureConnectorParameters secure) {
      SSLHostConfig sslHostConfig = new SSLHostConfig();
      sslHostConfig.setProtocols(String.join("+", secure.tlsProtocols()));
      setKeystore(sslHostConfig, secure.keyStore());
      secure.trustStore().ifPresent(truststore -> setTruststore(sslHostConfig, truststore));
      sslHostConfig.setCertificateVerification(secure.clientAuth());
      setCipherSuites(sslHostConfig, secure.cipherSuites());
      return sslHostConfig;
   }

   private static void setCipherSuites(SSLHostConfig sslHostConfig, String cipherSuites) {
      sslHostConfig.setHonorCipherOrder(true);
      sslHostConfig.setCiphers(cipherSuites);
   }

   private static void setKeystore(SSLHostConfig sslHostConfig, SecureStore store) {
      SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(sslHostConfig, Type.UNDEFINED);
      certificate.setCertificateKeystoreType(store.type());
      certificate.setCertificateKeystoreFile(store.file().toString());
      certificate.setCertificateKeystorePassword(store.password());
      sslHostConfig.addCertificate(certificate);
   }

   private static void setTruststore(SSLHostConfig sslHostConfig, SecureStore store) {
      sslHostConfig.setTruststoreType(store.type());
      sslHostConfig.setTruststoreFile(store.file().toString());
      sslHostConfig.setTruststorePassword(store.password());
   }

   private static FileAccessLogValve newSimpleFileAccessLogValve(WebServerLogParameters accessLogParameters) {
      FileAccessLogValve fileValve = new FileAccessLogValve(LogFormat.SIMPLE);
      fileValve.setPattern(accessLogParameters.pattern());
      fileValve.setDirectory(accessLogParameters.directory().toString());
      fileValve.setPrefix(accessLogParameters.prefix());
      fileValve.setSuffix(accessLogParameters.suffix());
      fileValve.setMaxDays(accessLogParameters.maxDays());
      fileValve.setRenameOnRotate(true);
      fileValve.setConditionUnless("NoAccessLogFilter");
      return fileValve;
   }

   private static FileAccessLogValve newStructuredFileAccessLogValve(WebServerLogParameters accessLogParameters) {
      FileAccessLogValve fileValve = new FileAccessLogValve(LogFormat.STRUCTURED);
      fileValve.setDirectory(accessLogParameters.directory().toString());
      fileValve.setPrefix("access-structured.log");
      fileValve.setSuffix("");
      fileValve.setMaxDays(accessLogParameters.maxDays());
      fileValve.setRenameOnRotate(true);
      fileValve.setConditionUnless("NoAccessLogFilter");
      return fileValve;
   }

   private static ConsoleAccessLogValve newConsoleAccessLogValve(WebServerLogParameters accessLogParameters) {
      ConsoleAccessLogValve stdoutValve = new ConsoleAccessLogValve(accessLogParameters.consoleFormat());
      stdoutValve.setPattern(accessLogParameters.pattern());
      stdoutValve.setConditionUnless("NoAccessLogFilter");
      return stdoutValve;
   }

   private TomcatConfigurator() {
      throw new AssertionError();
   }
}
