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

import com.airlock.iam.application.parameters.domain.model.AdminappParameters;
import com.airlock.iam.application.parameters.domain.model.ApiPolicyServiceParameters;
import com.airlock.iam.application.parameters.domain.model.ApplicationParameters;
import com.airlock.iam.application.parameters.domain.model.ClientSslConnectionParameters;
import com.airlock.iam.application.parameters.domain.model.H2Parameters;
import com.airlock.iam.application.parameters.domain.model.LoginappParameters;
import com.airlock.iam.application.parameters.domain.model.SecureStore;
import com.airlock.iam.application.parameters.domain.model.ServiceContainerParameters;
import com.airlock.iam.application.parameters.domain.model.TransactionApprovalParameters;
import com.airlock.iam.application.parameters.domain.model.WebServerParameters.ConnectorParameters;
import com.airlock.iam.application.resources.infrastructure.ApplicationResource;
import com.airlock.iam.application.resources.infrastructure.ResourceLocator;
import com.airlock.iam.application.webserver.infrastructure.deploy.AdminappDeployer;
import com.airlock.iam.application.webserver.infrastructure.deploy.ApiPolicyServiceDeployer;
import com.airlock.iam.application.webserver.infrastructure.deploy.H2Deployer;
import com.airlock.iam.application.webserver.infrastructure.deploy.HealthCheckDeployer;
import com.airlock.iam.application.webserver.infrastructure.deploy.LoginappDeployer;
import com.airlock.iam.application.webserver.infrastructure.deploy.ResourceDeployer;
import com.airlock.iam.application.webserver.infrastructure.deploy.ServiceContainerDeployer;
import com.airlock.iam.application.webserver.infrastructure.deploy.TransactionApprovalDeployer;
import com.airlock.iam.application.webserver.infrastructure.properties.JavaxNetSslTrustStoreProperties;
import com.airlock.iam.elog.api.EDefaultLogType;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.nio.file.Files;
import java.util.function.Consumer;
import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.core.StandardHost;
import org.apache.catalina.startup.Tomcat;
import org.apache.tomcat.util.descriptor.web.ErrorPage;

public final class WebServerLauncher {
   private static final ELogger LOG = ELoggerFactory.getLogger(WebServerLauncher.class);
   private final ApplicationParameters parameters;

   public WebServerLauncher(ApplicationParameters parameters) {
      this.parameters = parameters;
   }

   public void launch() throws Exception {
      TomcatJuliConfigurator.configureJuli(this.parameters.webServer().webServerLog());
      initJavaxNetTrustStoreProperties(this.parameters.clientSslConnectionParameters(), WebServerLauncher::sslPropertyInit);
      Tomcat tomcat = new Tomcat();
      tomcat.setBaseDir(this.parameters.webServer().workDir().toString());
      TomcatConfigurator.configureConnectors(tomcat.getService(), this.parameters.webServer().connectors());
      StandardHost host = TomcatConfigurator.requireStandardHost(tomcat.getHost());
      TomcatConfigurator.configureFastHost(host);
      TomcatConfigurator.configureSecureHost(host);
      TomcatConfigurator.configureAccessLogValve(host, this.parameters.webServer().accessLog());
      TomcatConfigurator.configureSecureServer(tomcat.getServer());
      TomcatConfigurator.addJreMemoryLeakPreventionListener(tomcat.getServer());
      Files.createDirectories(tomcat.getHost().getCatalinaHome().toPath().resolve(tomcat.getHost().getAppBase()));
      ResourceLocator resourceLocator = new ResourceLocator(System::getProperty);
      new HealthCheckDeployer(this.parameters).deploy(tomcat);
      if (this.parameters.h2().isPresent()) {
         new H2Deployer((H2Parameters)this.parameters.h2().get()).deploy(tomcat);
      }

      if (this.parameters.adminapp().isPresent()) {
         new AdminappDeployer((AdminappParameters)this.parameters.adminapp().get(), resourceLocator).deploy(tomcat);
      }

      if (this.parameters.loginapp().isPresent()) {
         new LoginappDeployer((LoginappParameters)this.parameters.loginapp().get(), resourceLocator).deploy(tomcat);
      }

      if (this.parameters.transactionApproval().isPresent()) {
         new TransactionApprovalDeployer((TransactionApprovalParameters)this.parameters.transactionApproval().get(), resourceLocator).deploy(tomcat);
      }

      if (this.parameters.apiPolicyService().isPresent()) {
         new ApiPolicyServiceDeployer((ApiPolicyServiceParameters)this.parameters.apiPolicyService().get(), resourceLocator).deploy(tomcat);
      }

      if (this.parameters.serviceContainer().isPresent()) {
         new ServiceContainerDeployer((ServiceContainerParameters)this.parameters.serviceContainer().get(), resourceLocator).deploy(tomcat);
      }

      resourceLocator.findPath(ApplicationResource.ROOT_DOC_BASE).ifPresent(docBase -> {
         Context rootContext = new ResourceDeployer("", docBase).deploy(tomcat);
         rootContext.addErrorPage(this.getNotFoundErrorPage());
      });
      this.testConnectorSockets();
      tomcat.start();
      Runtime.getRuntime().addShutdownHook(new WebServerLauncher.GracefulShutdownHook(tomcat));
      tomcat.getServer().await();
   }

   @VisibleForTesting
   static void initJavaxNetTrustStoreProperties(ClientSslConnectionParameters parameters, Consumer<SecureStore> storeConsumer) {
      parameters.trustStore().ifPresent(storeConsumer);
   }

   private static void sslPropertyInit(SecureStore store) {
      new JavaxNetSslTrustStoreProperties(store, System::getProperty).init(System::setProperty);
   }

   private void testConnectorSockets() throws IOException {
      for (ConnectorParameters connector : this.parameters.webServer().connectors()) {
         try (ServerSocket test = new ServerSocket(connector.port(), 0, InetAddress.getByName(connector.address()))) {
            test.setReuseAddress(true);
         } catch (IOException e) {
            throw new IOException("Failed to initialize connector " + connector.toShortString(), e);
         }
      }
   }

   private ErrorPage getNotFoundErrorPage() {
      ErrorPage errorPage = new ErrorPage();
      errorPage.setErrorCode(404);
      errorPage.setLocation("/404.html");
      return errorPage;
   }

   private static final class GracefulShutdownHook extends Thread {
      private final Tomcat tomcat;

      GracefulShutdownHook(Tomcat tomcat) {
         this.tomcat = tomcat;
      }

      @Override
      public void run() {
         try {
            this.tomcat.stop();
         } catch (LifecycleException e) {
            WebServerLauncher.LOG.error(EDefaultLogType.GENERIC_LOG_TYPE, "Failed to stop web server: " + e.getMessage());
            WebServerLauncher.LOG.debug("Tomcat threw exception: " + e, e);
         }
      }
   }
}
