package com.airlock.iam.servicecontainer.app.application.configuration;

import com.airlock.iam.base.misc.concurrent.ThreadUtils;
import com.airlock.iam.common.application.configuration.event.OutboxRepositoryConfig;
import com.airlock.iam.common.application.configuration.logging.CorrelationIdSettingsConfig;
import com.airlock.iam.common.application.configuration.usertrail.UserTrailLogRepositoryConfig;
import com.airlock.iam.common.domain.service.sso.AcceptedSsoTicketService;
import com.airlock.iam.common.domain.service.sso.AcceptedSsoTicketServiceImpl;
import com.airlock.iam.common.infrastructure.factory.AcceptedSsoTicketRepositoryFactory;
import com.airlock.iam.core.application.configuration.AcceptedSsoTicketRepositoryConfig;
import com.airlock.iam.core.application.configuration.store.user.UserStoreProvider;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.elog.api.LogTypes;
import com.airlock.iam.plugin.api.annotation.config.ConfigurationValidator;
import com.airlock.iam.plugin.api.annotation.config.Groups;
import com.airlock.iam.plugin.api.annotation.config.Plugin;
import com.airlock.iam.plugin.api.annotation.lifecycle.AfterConfigActivation;
import com.airlock.iam.plugin.api.annotation.lifecycle.BeforePluginDeactivation;
import com.airlock.iam.plugin.api.annotation.property.GroupProperty;
import com.airlock.iam.plugin.api.annotation.property.IntegerProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginListProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginProperty;
import com.airlock.iam.plugin.api.annotation.property.StringProperty;
import com.airlock.iam.plugin.api.config.ConfigurationContext;
import com.airlock.iam.plugin.api.config.ConfigurationEnvironment;
import com.airlock.iam.plugin.api.config.Environment;
import com.airlock.iam.plugin.api.config.PluginConfiguration;
import com.airlock.iam.plugin.api.exception.PropertyConfigurationException;
import com.airlock.iam.plugin.api.exception.PropertyConstraintViolationException;
import com.airlock.iam.plugin.api.valuereader.PluginPropertyValueReader;
import com.airlock.iam.plugin.framework.misc.AppId;
import com.airlock.iam.servicecontainer.api.domain.model.task.StoppableTask;
import com.airlock.iam.servicecontainer.app.infrastructure.event.EventFactories;
import com.airlock.iam.servicecontainer.app.infrastructure.server.Controller;
import com.airlock.iam.servicecontainer.app.infrastructure.server.threadpool.ThreadPool;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Sets;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

@Plugin(
   description = "The server component used to run services such as the RADIUS interface or the task scheduler.",
   displayName = "Service Container",
   typeShortName = "ServiceContainer"
)
@Groups(groupSequence = {"Basic Settings", "Event Settings", "Advanced Settings"}, openGroups = "Basic Settings")
public final class Server {
   static final String EVENT_SETTINGS = "Event Settings";
   public static final AppId SERVICE_CONTAINER_CONFIG_IDENTIFIER = AppId.appId("servicecontainerapp");
   public static final String SERVICE_CONTAINER_DISPLAY_NAME = "Service Container";
   private static final ELogger LOG = ELoggerFactory.getLogger(Server.class);
   private final Map<Service, Thread> threadsByService = new LinkedHashMap<>();
   private final long serverStartTime = System.currentTimeMillis();
   private ThreadPool threadPool;
   private AcceptedSsoTicketService acceptedSsoTicketService;
   @PluginListProperty(description = "The list of services to run.", optional = true)
   @GroupProperty(groupIdentifier = "Basic Settings")
   private List<ServiceConfig> services;
   @PluginProperty(
      description = "<p>Settings for the event outbox repository. If configured, all events are stored in an outbox database table until they\nhave successfully been forwarded to a message broker.</p>\n<p>If no outbox repository is configured, no events are stored. To forward events to a message broker, configure an\nEvent Outbox Processor Service.</p>\n<p>Currently, only the \"Delete Users Task\" and the \"Lock Inactive Accounts Task\" publish events</p>\n",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Event Settings")
   private OutboxRepositoryConfig outboxRepository;
   private static final String OUTBOX_REPOSITORY_PROPERTY = "outboxRepository";
   @PluginProperty(
      description = "<p>User store to load context data values that are added as <code>customData</code> to the stored events. If not configured,\nno custom data is added to the events.</p>\n<p>The data contains all context data values configured in this user store. Note that these values might contain\nsensitive information such as email addresses or phone numbers.</p>\n",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Event Settings")
   private UserStoreProvider userStore;
   private static final String USER_STORE_PROPERTY = "userStore";
   @IntegerProperty(
      displayName = "Session Idle Timeout [m]",
      description = "Session idle timeout of the service container UI in minutes.",
      exampleValues = {10, 30},
      optional = true,
      defaultValue = 30,
      minValue = 1
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private int sessionTimeout;
   @IntegerProperty(
      displayName = "Statistics Log Interval [s]",
      description = "The server logs a line with statistic information periodically. This setting defines the amount of seconds between such logs.",
      minValue = 10,
      defaultValue = 60,
      optional = true,
      exampleValues = {10, 60, 600, 3600}
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private int statisticLogInterval;
   @IntegerProperty(description = "Minimum number of threads in pool.", optional = true, minValue = 1, defaultValue = 10, exampleValues = {1, 10, 20})
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private int minimumThreads;
   @IntegerProperty(description = "Maximum number of threads in pool.", optional = true, minValue = 1, defaultValue = 100, exampleValues = {100, 500, 1000})
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private int maximumThreads;
   @StringProperty(
      description = "The service container secret is used to access the service container from the Adminapp. The shared secret will be used to encrypt the SSO ticket, sent from the Adminapp to the service container in order to authenticate the admin. The shared secret must be identical to the property <b>Service Container Shared Secret</b> in the Adminapp (Advanced Settings).",
      optional = false,
      isSensitive = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private String serviceContainerSharedSecret;
   @PluginProperty(
      description = "<p>Configures the repository used to store accepted SSO tickets and reject previously accepted ones.</p>\n<p>The in-memory repository cannot be used if multiple instances of IAM are deployed in parallel (failover, horizontal scaling).\nFurthermore, the in-memory repository does not preserve previously accepted SSO tickets across IAM restarts.</p>",
      displayName = "Accepted SSO Tickets Repository",
      optional = false,
      globalPluginReference = "globalAcceptedSsoTicketSettings"
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private AcceptedSsoTicketRepositoryConfig acceptedSsoTicketRepository;
   @PluginProperty(
      description = "<p>Configures the database settings to use when persisting user trail log entries.</p>\n<p>If this value is defined, then all user trail log messages generated by the Service\nContainer App module will additionally be forwarded to the database configured within\nthe referenced repository plugin.</p>\n<p>All forwarded log entries are stored inside the table \"USER_TRAIL_LOG\". Note that\nsetting this value does not disable writing log messages to the Service Container App\nlog file.</p>",
      globalPluginReference = "globalUserTrailLogRepository",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private UserTrailLogRepositoryConfig logUserTrailToDatabase;
   @PluginProperty(
      displayName = "Correlation ID Settings",
      description = "<p>Defines settings for correlation ID transfer and logging inside the Service Container module.</p>\n<p>If undefined, no correlation ID will be logged for this module.</p>",
      globalPluginReference = "globalCorrelationIdSettings",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private CorrelationIdSettingsConfig correlationIdSettings;

   @ConfigurationValidator
   static void validateEventSettings(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment valEnv) throws PropertyConfigurationException {
      PluginConfiguration userStore = PluginPropertyValueReader.effectivePluginPropertyValue(config, "userStore", env, context);
      PluginConfiguration repository = PluginPropertyValueReader.effectivePluginPropertyValue(config, "outboxRepository", env, context);
      if (userStore != null && repository == null) {
         throw new PropertyConstraintViolationException(
            "userStore", config, env, context, "User Store can only be configured if the Outbox Repository is configured as well."
         );
      }
   }

   public Optional<UserTrailLogRepositoryConfig> getUserTrailLogRepositoryConfig() {
      return Optional.ofNullable(this.logUserTrailToDatabase);
   }

   public Optional<CorrelationIdSettingsConfig> getCorrelationIdSettingsConfig() {
      return Optional.ofNullable(this.correlationIdSettings);
   }

   public Server(
      List<ServiceConfig> services,
      AcceptedSsoTicketRepositoryConfig acceptedSsoTicketRepository,
      int sessionTimeout,
      int statisticLogInterval,
      int minimumThreads,
      int maximumThreads,
      String serviceContainerSharedSecret,
      UserTrailLogRepositoryConfig logUserTrailToDatabase,
      CorrelationIdSettingsConfig correlationIdSettings,
      OutboxRepositoryConfig outboxRepository,
      UserStoreProvider userStore
   ) {
      this.services = services;
      this.acceptedSsoTicketRepository = acceptedSsoTicketRepository;
      this.sessionTimeout = sessionTimeout;
      this.statisticLogInterval = statisticLogInterval;
      this.minimumThreads = minimumThreads;
      this.maximumThreads = maximumThreads;
      this.serviceContainerSharedSecret = serviceContainerSharedSecret;
      this.logUserTrailToDatabase = logUserTrailToDatabase;
      this.correlationIdSettings = correlationIdSettings;
      this.outboxRepository = outboxRepository;
      this.userStore = userStore;
   }

   @VisibleForTesting
   public void setAcceptedSsoTicketService(AcceptedSsoTicketService acceptedSsoTicketService) {
      this.acceptedSsoTicketService = acceptedSsoTicketService;
   }

   public static Server get() {
      return Controller.getInstance().getServer();
   }

   public int getNumberOfLoadedServices() {
      synchronized (this.threadsByService) {
         return this.threadsByService.size();
      }
   }

   public long getNumberOfRunningServices() {
      synchronized (this.threadsByService) {
         return this.threadsByService.keySet().stream().filter(Service::isRunning).count();
      }
   }

   @AfterConfigActivation
   void afterConfigActivation() {
      this.acceptedSsoTicketService = new AcceptedSsoTicketServiceImpl(
         () -> AcceptedSsoTicketRepositoryFactory.createAcceptedSsoTicketRepository(this.acceptedSsoTicketRepository)
      );
      EventFactories.buildEventFactories(Optional.ofNullable(this.outboxRepository), Optional.ofNullable(this.userStore).map(UserStoreProvider::getUserStore));
      if (this.threadPool == null) {
         this.threadPool = new ThreadPool(this.maximumThreads);
         this.threadPool.start(this.minimumThreads);
      }

      synchronized (this.threadsByService) {
         this.stopAndRemoveNoLongerConfiguredServices();
         this.startNewlyConfiguredServices();
      }
   }

   private void stopAndRemoveNoLongerConfiguredServices() {
      for (Service serviceToRemove : this.threadsByService
         .keySet()
         .stream()
         .filter(service -> !this.configuredServices().contains(service))
         .collect(Collectors.toSet())) {
         this.threadsByService.remove(serviceToRemove);
         if (serviceToRemove.isRunning()) {
            this.stopService(serviceToRemove, 1000L);
         }
      }
   }

   private void startNewlyConfiguredServices() {
      for (ServiceConfig serviceConfig : this.services) {
         if (!this.threadsByService.containsKey(serviceConfig.getService())) {
            this.threadsByService.put(serviceConfig.getService(), null);
         }

         if (serviceConfig.isAutoStart() && !serviceConfig.getService().isRunning()) {
            this.startService(serviceConfig.getService());
         }
      }
   }

   public AcceptedSsoTicketService getAcceptedSsoTicketsService() {
      return this.acceptedSsoTicketService;
   }

   @BeforePluginDeactivation
   public void stopAndWait() {
      if (this.threadPool != null) {
         this.threadPool.stopAllThreads();
      }

      this.threadPool = null;
   }

   private Set<Service> configuredServices() {
      Set<Service> set = Sets.newIdentityHashSet();
      this.services.forEach(s -> set.add(s.getService()));
      return set;
   }

   public boolean executeTask(StoppableTask task, String taskName) {
      return this.threadPool.executeTask(task, false, taskName);
   }

   public int getNumberOfThreads() {
      return this.threadPool.getNumberOfThreads();
   }

   public int getNumberOfSleepingThreads() {
      return this.threadPool.getNumberOfSleepingThreads();
   }

   public int getNumberOfWorkingThreads() {
      return this.threadPool.getNumberOfWorkingThreads();
   }

   public void startService(Service service) {
      Objects.requireNonNull(service, "service null");
      Thread t = new Thread(service);
      t.setDaemon(true);
      t.setName(service.getThreadName());
      synchronized (this.threadsByService) {
         this.threadsByService.put(service, t);
      }

      t.start();
   }

   public void stopService(Service service, long maxWaitMillis) {
      this.requestServiceStop(service);
      this.waitForServiceStopped(service, maxWaitMillis);
   }

   private void requestServiceStop(Service service) {
      if (service.isRunning()) {
         LOG.info(LogTypes.SERVER_INFO_SYS, "Stopping service " + service.getName());
         Thread t;
         synchronized (this.threadsByService) {
            t = this.threadsByService.get(service);
         }

         service.pleaseStop();
         if (t != null) {
            t.interrupt();
         }
      }
   }

   private void waitForServiceStopped(Service service, long maxWaitMillis) {
      long now = System.currentTimeMillis();
      long checkInterval = 50L;

      while (service.isRunning()) {
         ThreadUtils.sleepInterruptibly(50L, java.util.concurrent.TimeUnit.MILLISECONDS);
         if (System.currentTimeMillis() - now > maxWaitMillis) {
            if (LOG.isDebugEnabled()) {
               LOG.debug("Service " + service.getName() + " is still running after waiting for " + maxWaitMillis + "ms.");
            }

            return;
         }
      }

      if (LOG.isDebugEnabled()) {
         LOG.debug("Service " + service.getName() + " has stopped after about " + (System.currentTimeMillis() - now) + "ms.");
      }
   }

   public void stopAllServices(long maxWaitMillisPerService) {
      synchronized (this.threadsByService) {
         Set<Service> servicesToStop = this.threadsByService.keySet();
         servicesToStop.forEach(this::requestServiceStop);
         servicesToStop.forEach(s -> this.waitForServiceStopped(s, maxWaitMillisPerService));
      }
   }

   public Service[] getServices() {
      synchronized (this.threadsByService) {
         return this.threadsByService.keySet().toArray(new Service[0]);
      }
   }

   public Duration getSessionTimeout() {
      return Duration.ofMinutes(this.sessionTimeout);
   }

   public String getServiceContainerSharedSecret() {
      return this.serviceContainerSharedSecret;
   }

   @VisibleForTesting
   public void setSharedSecret(String aServiceContainerSharedSecret) {
      this.serviceContainerSharedSecret = aServiceContainerSharedSecret;
   }

   @VisibleForTesting
   public void removeAllServices() {
      this.threadsByService.clear();
   }

   public long getServerUptimeMillis() {
      return System.currentTimeMillis() - this.serverStartTime;
   }

   public long getServerStartTime() {
      return this.serverStartTime;
   }

   @SuppressFBWarnings(justification = "generated code")
   public static Server.ServerBuilder builder() {
      return new Server.ServerBuilder();
   }

   @SuppressFBWarnings(justification = "generated code")
   public Server() {
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class ServerBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<ServiceConfig> services;
      @SuppressFBWarnings(justification = "generated code")
      private AcceptedSsoTicketRepositoryConfig acceptedSsoTicketRepository;
      @SuppressFBWarnings(justification = "generated code")
      private int sessionTimeout;
      @SuppressFBWarnings(justification = "generated code")
      private int statisticLogInterval;
      @SuppressFBWarnings(justification = "generated code")
      private int minimumThreads;
      @SuppressFBWarnings(justification = "generated code")
      private int maximumThreads;
      @SuppressFBWarnings(justification = "generated code")
      private String serviceContainerSharedSecret;
      @SuppressFBWarnings(justification = "generated code")
      private UserTrailLogRepositoryConfig logUserTrailToDatabase;
      @SuppressFBWarnings(justification = "generated code")
      private CorrelationIdSettingsConfig correlationIdSettings;
      @SuppressFBWarnings(justification = "generated code")
      private OutboxRepositoryConfig outboxRepository;
      @SuppressFBWarnings(justification = "generated code")
      private UserStoreProvider userStore;

      @SuppressFBWarnings(justification = "generated code")
      ServerBuilder() {
      }

      @SuppressFBWarnings(justification = "generated code")
      public Server.ServerBuilder service(ServiceConfig service) {
         if (this.services == null) {
            this.services = new ArrayList<>();
         }

         this.services.add(service);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Server.ServerBuilder services(Collection<? extends ServiceConfig> services) {
         if (services == null) {
            throw new NullPointerException("services cannot be null");
         }

         if (this.services == null) {
            this.services = new ArrayList<>();
         }

         this.services.addAll(services);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Server.ServerBuilder clearServices() {
         if (this.services != null) {
            this.services.clear();
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Server.ServerBuilder acceptedSsoTicketRepository(AcceptedSsoTicketRepositoryConfig acceptedSsoTicketRepository) {
         this.acceptedSsoTicketRepository = acceptedSsoTicketRepository;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Server.ServerBuilder sessionTimeout(int sessionTimeout) {
         this.sessionTimeout = sessionTimeout;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Server.ServerBuilder statisticLogInterval(int statisticLogInterval) {
         this.statisticLogInterval = statisticLogInterval;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Server.ServerBuilder minimumThreads(int minimumThreads) {
         this.minimumThreads = minimumThreads;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Server.ServerBuilder maximumThreads(int maximumThreads) {
         this.maximumThreads = maximumThreads;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Server.ServerBuilder serviceContainerSharedSecret(String serviceContainerSharedSecret) {
         this.serviceContainerSharedSecret = serviceContainerSharedSecret;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Server.ServerBuilder logUserTrailToDatabase(UserTrailLogRepositoryConfig logUserTrailToDatabase) {
         this.logUserTrailToDatabase = logUserTrailToDatabase;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Server.ServerBuilder correlationIdSettings(CorrelationIdSettingsConfig correlationIdSettings) {
         this.correlationIdSettings = correlationIdSettings;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Server.ServerBuilder outboxRepository(OutboxRepositoryConfig outboxRepository) {
         this.outboxRepository = outboxRepository;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Server.ServerBuilder userStore(UserStoreProvider userStore) {
         this.userStore = userStore;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Server build() {
         return new Server(
            switch (this.services == null ? 0 : this.services.size()) {
               case 0 -> Collections.emptyList();
               case 1 -> Collections.singletonList(this.services.get(0));
               default -> Collections.unmodifiableList(new ArrayList<>(this.services));
            },
            this.acceptedSsoTicketRepository,
            this.sessionTimeout,
            this.statisticLogInterval,
            this.minimumThreads,
            this.maximumThreads,
            this.serviceContainerSharedSecret,
            this.logUserTrailToDatabase,
            this.correlationIdSettings,
            this.outboxRepository,
            this.userStore
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "Server.ServerBuilder(services="
            + this.services
            + ", acceptedSsoTicketRepository="
            + this.acceptedSsoTicketRepository
            + ", sessionTimeout="
            + this.sessionTimeout
            + ", statisticLogInterval="
            + this.statisticLogInterval
            + ", minimumThreads="
            + this.minimumThreads
            + ", maximumThreads="
            + this.maximumThreads
            + ", serviceContainerSharedSecret="
            + this.serviceContainerSharedSecret
            + ", logUserTrailToDatabase="
            + this.logUserTrailToDatabase
            + ", correlationIdSettings="
            + this.correlationIdSettings
            + ", outboxRepository="
            + this.outboxRepository
            + ", userStore="
            + this.userStore
            + ")";
      }
   }
}
