package com.guidewire.pl.rest;

import com.guidewire.pl.config.PLConfigResourceKeys;
import com.guidewire.pl.integration.watcher.ChangeListener;
import com.guidewire.pl.json.util.IntegrationConfigSourcePluginCache;
import com.guidewire.pl.rest.dispatch.SwaggerRestDispatcher;
import com.guidewire.pl.system.dependency.PLDependencies;
import com.guidewire.pl.system.dependency.ServerDependencies;
import com.guidewire.pl.system.integration.plugins.PluginConfig;
import com.guidewire.pl.system.logging.PLLoggerCategory;
import com.guidewire.pl.system.server.InitTab;
import com.guidewire.pl.system.server.config.manager.SystemConfiguration;
import gw.api.rest.DefaultRestConfigRefreshPlugin;
import gw.api.rest.IRestConfigRefreshPlugin;
import gw.api.system.PLConfigParameters;
import gw.api.system.server.Runlevel;
import gw.lang.reflect.ITypeLoaderListener;
import gw.lang.reflect.RefreshRequest;
import gw.lang.reflect.TypeSystem;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class SwaggerRestServlet extends HttpServlet implements ITypeLoaderListener {
   private final AtomicBoolean _needsUpdate = new AtomicBoolean(false);
   private boolean _isDev;
   private SwaggerRestDispatcher _dispatcher;
   private ChangeListener _changeListener;
   private Collection<String> _extensions;
   private IRestConfigRefreshPlugin _configRefreshPlugin;

   public void init() throws ServletException {
      super.init();
      TypeSystem.addTypeLoaderListenerAsWeakRef(this);
      this._isDev = PLDependencies.getServerMode() != null && PLDependencies.getServerMode().isDev();
      this._dispatcher = new SwaggerRestDispatcher();
      this._configRefreshPlugin = this.loadConfigRefreshPlugin();
      if (this._isDev && !InitTab.isRunningTestsInHarness()) {
         this._extensions = this._configRefreshPlugin.getFileExtensionsToWatch();
         List<Path> integrationRoots = Arrays.stream(PLConfigResourceKeys.INTEGRATION_DIR_KEY.getDir().resolve())
            .filter(File::exists)
            .map(File::toPath)
            .collect(Collectors.toList());
         this._changeListener = ChangeListener.createDefault(integrationRoots, this::isInterested);
      }

      PLDependencies.setRestPathTreeSupplier(() -> this._dispatcher.getPathTree());
      IntegrationConfigSourcePluginCache.getPlugins().forEach(p -> p.registerChangeCallback(this::pluginSourcesChanged));
      this.maybePreload();
      PLLoggerCategory.REST_CONFIG.info("REST Servlet initialized");
   }

   private void maybePreload() {
      SystemConfiguration systemConfiguration = ServerDependencies.getSystemConfiguration();
      if (systemConfiguration != null
         && Boolean.TRUE.equals(systemConfiguration.getParameterValue(PLConfigParameters.PreloadRestServletConfig))
         && InitTab.getInstance().getRunLevel().isLater(Runlevel.SHUTDOWN)) {
         PLLoggerCategory.REST_CONFIG.info("Preloading the REST servlet and related configuration files");
         this.preload();
      } else {
         PLLoggerCategory.REST_CONFIG.info("Preload disabled for the REST servlet, loading will happen lazily on the first REST request");
      }
   }

   private void preload() {
      this._dispatcher.getPathTree();
      this._configRefreshPlugin.preloadConfig();
   }

   private IRestConfigRefreshPlugin loadConfigRefreshPlugin() {
      PluginConfig pluginConfig = PLDependencies.getPluginConfig();
      if (pluginConfig != null && pluginConfig.getPluginDef(IRestConfigRefreshPlugin.class) != null) {
         IRestConfigRefreshPlugin plugin = (IRestConfigRefreshPlugin)pluginConfig.getPlugin(IRestConfigRefreshPlugin.class);
         if (plugin != null) {
            return plugin;
         }
      }

      IRestConfigRefreshPlugin plugin = new DefaultRestConfigRefreshPlugin();
      return plugin;
   }

   protected void service(HttpServletRequest req, HttpServletResponse resp) {
      this.maybeUpdate();
      this._dispatcher.handleRequest(req, resp);
   }

   protected void maybeUpdate() {
      if (this._isDev && this.needsUpdate()) {
         this.update();
      }
   }

   private boolean isInterested(Path path) {
      String fileName = path.toString();

      for (String ext : this._extensions) {
         if (fileName.endsWith(ext)) {
            return true;
         }
      }

      return false;
   }

   private synchronized void update() {
      if (this._needsUpdate.get()) {
         PLLoggerCategory.REST_CONFIG.info("Changes detected to REST config files and/or classes, reloading the REST dispatcher");

         try {
            this._configRefreshPlugin.resetCaches();
            this._dispatcher = new SwaggerRestDispatcher();
         } finally {
            this.updateFinished();
         }
      }
   }

   private void updateFinished() {
      this._needsUpdate.set(false);
   }

   private void pluginSourcesChanged() {
      this.scheduleUpdate();
   }

   private boolean needsUpdate() {
      this.checkIfNeedsUpdate();
      return this._needsUpdate.get();
   }

   private void scheduleUpdate() {
      this._needsUpdate.set(true);
   }

   private void checkIfNeedsUpdate() {
      if (this._changeListener != null && this._changeListener.hasChanges()) {
         this.scheduleUpdate();
      }
   }

   public void refreshedTypes(RefreshRequest request) {
      this.scheduleUpdate();
   }

   public void refreshed() {
      this.scheduleUpdate();
   }

   public void destroy() {
      try {
         if (this._changeListener != null) {
            this._changeListener.close();
         }
      } catch (IOException e) {
         throw new UncheckedIOException(e);
      }
   }
}
