package com.guidewire.pl.system.servlet;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Stopwatch;
import com.google.common.collect.Lists;
import com.guidewire.modules.key.MultiFileKey;
import com.guidewire.pl.config.PLConfigResourceKeys;
import com.guidewire.pl.system.dependency.PLDependencies;
import com.guidewire.pl.system.logging.PLLoggerCategory;
import com.guidewire.pl.system.service.GWEnvironment;
import com.guidewire.pl.web.util.UnhandledExceptionLogger;
import gw.api.profiler.GosuServletProfiler;
import gw.lang.reflect.IAnnotationInfo;
import gw.lang.reflect.IType;
import gw.lang.reflect.ITypeLoaderListener;
import gw.lang.reflect.RefreshRequest;
import gw.lang.reflect.TypeSystem;
import gw.servlet.IServletAnnotation;
import gw.util.concurrent.LockingLazyVar;
import gw.xsd.config.servlets.Param;
import gw.xsd.config.servlets.Servlets;
import gw.xsd.config.servlets.anonymous.elements.Servlets_Servlet;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;

public class GosuServlet extends HttpServlet implements ITypeLoaderListener {
   private final GosuServletIntentionalLogger IL;
   private static final ThreadLocal<HttpServletRequest> _requests = new ThreadLocal<>();
   private LockingLazyVar<Map<String, Map<IServletAnnotation, HttpServlet>>> _servlets = new LockingLazyVar<Map<String, Map<IServletAnnotation, HttpServlet>>>() {
      protected Map<String, Map<IServletAnnotation, HttpServlet>> init() {
         IType servletAnnotationType = GosuServlet.getServletAnnotationType();
         Map<String, Map<IServletAnnotation, HttpServlet>> rtnValue = new HashMap<>();
         Stream<GosuServlet.Entry> stream = GosuServlet.getAllServlets();
         stream.forEach(entry -> GosuServlet.this.addServlet(rtnValue, servletAnnotationType, entry.getType(), entry.getParams()));
         return Collections.unmodifiableMap(rtnValue);
      }
   };
   private static GosuServlet _instance;

   static GosuServlet getInstance() {
      return _instance;
   }

   public static Stream<GosuServlet.Entry> getAllServlets() {
      MultiFileKey multiFile = PLConfigResourceKeys.SERVLET_SERVLETS_XML;
      File[] files = multiFile.getFiles();
      return getServletTypes(Arrays.stream(files).map(file -> {
         try {
            return new FileInputStream(file);
         } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
         }
      }).collect(Collectors.toList()), PLLoggerCategory.SERVER, PLDependencies.getEnvironment());
   }

   static Stream<GosuServlet.Entry> getServletTypes(List<InputStream> files, Logger logger, GWEnvironment gwEnv) {
      IType servletAnnotationType = getServletAnnotationType();
      Map<String, GosuServlet.Entry> servletTypes = new HashMap<>();

      for (InputStream inputStream : Lists.reverse(files)) {
         Servlets servlets = Servlets.parse(inputStream);

         for (Servlets_Servlet servlet : servlets.getServlet()) {
            String forServer = servlet.getServer();
            String forEnv = servlet.getEnv();
            String typeName = servlet.get_Class();
            if (gwEnv.matches(forServer, forEnv)) {
               GosuServlet.Entry doing = servletTypes.get(typeName);
               if (doing != null) {
                  if (doing.getServer() != null ? forServer == null : forServer == null && doing.getEnv() != null && forEnv == null) {
                     continue;
                  }

                  logger.warn("Gosu servlet " + typeName + ", previously defined in configurations, was overridden.");
               }

               IType clazz;
               if (servlet.getDisabled() != null && servlet.getDisabled()) {
                  clazz = null;
               } else {
                  clazz = TypeSystem.getByFullNameIfValid(typeName);
                  if (clazz == null || !clazz.getTypeInfo().hasAnnotation(servletAnnotationType)) {
                     logger.warn("Gosu servlet " + typeName + ", specified in servlet configuration, does not exist or does not contain @Servlet annotation.");
                     continue;
                  }
               }

               Map<String, String> params = extractParams(servlet.getParam(), typeName, gwEnv, logger);
               servletTypes.put(typeName, new GosuServlet.Entry(forEnv, forServer, clazz, params, servlet.getInfoPath(), servlet.getInfoDescription()));
            }
         }
      }

      return servletTypes.values().stream().filter(e -> e.getType() != null);
   }

   private static Map<String, String> extractParams(List<Param> params, String typeName, GWEnvironment gwEnv, Logger logger) {
      Map<String, String> rtn = new HashMap<>();
      if (params != null && !params.isEmpty()) {
         params.stream().filter(param -> gwEnv.matches(param.getServer(), param.getEnv())).forEach(param -> {
            String oldValue = rtn.put(param.getName(), param.getValue());
            if (oldValue != null) {
               logger.warn("Gosu servlet " + typeName + ", specified in servlet configuration, has duplicate param " + param.getName());
            }
         });
         return rtn;
      } else {
         return Collections.EMPTY_MAP;
      }
   }

   private void addServlet(
      Map<String, Map<IServletAnnotation, HttpServlet>> rtnValue, IType servletAnnotationType, IType servletType, Map<String, String> params
   ) {
      try {
         PLLoggerCategory.CONFIG.debug("GosuServlet adding servlet " + servletType.getDisplayName());
         List<IAnnotationInfo> annotations = servletType.getTypeInfo().getAnnotationsOfType(servletAnnotationType);
         if (!annotations.isEmpty()) {
            HttpServlet instance = (HttpServlet)servletType.getTypeInfo().getConstructor(new IType[0]).getConstructor().newInstance(new Object[0]);
            instance.init(this.getServletConfig(params));

            for (IAnnotationInfo annotation : annotations) {
               IServletAnnotation servletAnnotation = (IServletAnnotation)annotation.getInstance();
               Map<IServletAnnotation, HttpServlet> map = rtnValue.get(servletAnnotation.getServletPath());
               if (map == null) {
                  map = new HashMap<>();
                  rtnValue.put(servletAnnotation.getServletPath(), map);
               }

               map.put(servletAnnotation, instance);
            }
         }

         PLLoggerCategory.CONFIG.debug("GosuServlet finished init for servlet " + servletType.getDisplayName());
      } catch (ServletException e) {
         PLLoggerCategory.CONFIG.error("failed to initialize servlet " + servletType, e);
      }
   }

   private ServletConfig getServletConfig(final Map<String, String> params) {
      return params.isEmpty()
         ? this.getServletConfig()
         : (ServletConfig)Proxy.newProxyInstance(
            GosuServlet.AddedParameterServletConfig.class.getClassLoader(),
            this.filterProprietaryInterfaces(this.getServletConfig().getClass().getInterfaces()),
            new GosuServlet.AddedParameterServletConfig(this.getServletConfig(), params)
         );
   }

   private Class<?>[] filterProprietaryInterfaces(Class<?>[] interfaces) {
      List<Class<?>> saved = new ArrayList<>();

      for (Class<?> iface : interfaces) {
         if ("com.ibm.wsspi.webcontainer.servlet.IServletConfig".equals(iface.getName())) {
            saved.addAll(Arrays.asList(iface.getInterfaces()));
         } else {
            saved.add(iface);
         }
      }

      return saved.toArray(new Class[0]);
   }

   protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
      String pathInfo = req.getPathInfo();
      HttpServlet servletToForward = this.getServlet((Map<IServletAnnotation, HttpServlet>)((Map)this._servlets.get()).get(req.getServletPath()), pathInfo);
      if (servletToForward == null) {
         servletToForward = this.getServlet((Map<IServletAnnotation, HttpServlet>)((Map)this._servlets.get()).get(null), pathInfo);
      }

      if (servletToForward == null) {
         resp.sendError(404);
      } else {
         Stopwatch stopwatch = Stopwatch.createStarted();
         this.IL.logServletRequestStart(req, servletToForward);
         GosuServletProfiler gosuServletProfiler = new GosuServletProfiler(servletToForward.getClass().getName());
         gosuServletProfiler.maybeEnterProfiling();

         try {
            _requests.set(req);
            servletToForward.service(req, resp);
         } catch (Throwable t) {
            this.IL.logServletRequestErrorAndFail(req, servletToForward, stopwatch.elapsed(TimeUnit.MILLISECONDS), t);
            UnhandledExceptionLogger exceptionLogger = new UnhandledExceptionLogger(t).asUncaught();
            if (!resp.isCommitted()) {
               resp.sendError(500);
               PrintWriter out1 = resp.getWriter();
               out1.write(exceptionLogger.getAsBasicHtml());
               out1.flush();
            } else {
               exceptionLogger.logErrorMessages();
            }
         } finally {
            _requests.remove();
            gosuServletProfiler.maybeExitProfiling();
         }

         this.IL.logServletRequestStop(req, servletToForward, stopwatch.elapsed(TimeUnit.MILLISECONDS));
      }
   }

   private HttpServlet getServlet(Map<IServletAnnotation, HttpServlet> map, String pathInfo) {
      if (map != null) {
         for (IServletAnnotation annota : map.keySet()) {
            if (annota.matchesPath(pathInfo)) {
               return map.get(annota);
            }
         }
      }

      return null;
   }

   public void destroy() {
      this.destroyChildren();
      super.destroy();
   }

   private void destroyChildren() {
      if (this._servlets.isLoaded()) {
         Map<String, Map<IServletAnnotation, HttpServlet>> rtn = (Map<String, Map<IServletAnnotation, HttpServlet>>)this._servlets.clear();

         for (Map<IServletAnnotation, HttpServlet> map : rtn.values()) {
            for (HttpServlet servlet : map.values()) {
               PLLoggerCategory.CONFIG.info("GosuServlet destroying servlet " + TypeSystem.getTypeFromObject(servlet).getDisplayName());
               servlet.destroy();
            }
         }
      }
   }

   @VisibleForTesting
   GosuServlet(GosuServletIntentionalLogger intentionalLogger) {
      this.IL = intentionalLogger;
   }

   public GosuServlet() {
      this(new GosuServletIntentionalLogger());
      TypeSystem.addTypeLoaderListenerAsWeakRef(this);
      _instance = this;
   }

   public static IType getServletAnnotationType() {
      return TypeSystem.getByFullName("gw.servlet.Servlet");
   }

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

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

   public static HttpServletRequest getRequest() {
      return _requests.get();
   }

   static final class AddedParameterServletConfig implements InvocationHandler {
      final ServletConfig _proxied;
      final Map<String, String> _params;

      AddedParameterServletConfig(ServletConfig config, Map<String, String> params) {
         this._proxied = config;
         this._params = params;
      }

      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
         if (method.getName().equals("getInitParameterNames") && method.getParameterTypes().length == 0) {
            final Enumeration<String> initParameterNames = this._proxied.getInitParameterNames();
            final Iterator<String> iterator = this._params.keySet().iterator();
            return new Enumeration<String>() {
               @Override
               public boolean hasMoreElements() {
                  return initParameterNames.hasMoreElements() || iterator.hasNext();
               }

               public String nextElement() {
                  return iterator.hasNext() ? iterator.next() : initParameterNames.nextElement();
               }
            };
         }

         if (method.getName().equals("getInitParameter")) {
            String arg1 = (String)args[0];
            String rtn = this._params.get(arg1);
            if (rtn == null) {
               rtn = this._proxied.getInitParameter(arg1);
            }

            return rtn;
         } else {
            return method.invoke(this._proxied, args);
         }
      }
   }

   public static class Entry {
      private final String _env;
      private final String _server;
      private final IType _type;
      private final Map<String, String> _params;
      private final String _path;
      private final String _desc;

      public String getEnv() {
         return this._env;
      }

      public String getServer() {
         return this._server;
      }

      public IType getType() {
         return this._type;
      }

      public Map<String, String> getParams() {
         return this._params;
      }

      public String getPath() {
         return this._path;
      }

      public String getDesc() {
         return this._desc;
      }

      private Entry(String env, String server, IType type, Map<String, String> params, String infoPath, String infoDescription) {
         this._env = env;
         this._server = server;
         this._type = type;
         this._params = params;
         this._path = infoPath;
         this._desc = infoDescription;
      }
   }
}
