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

import com.airlock.iam.application.parameters.domain.model.LogFormat;
import com.airlock.iam.application.webserver.infrastructure.logs.StructuredAccessLog;
import com.airlock.iam.application.webserver.infrastructure.logs.WebserverLogSerializer;
import com.airlock.iam.elog.api.EDefaultLogType;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.system.property.IamSystemProperties;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.annotation.Nullable;
import java.io.CharArrayWriter;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import lombok.NonNull;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.AccessLogValve;
import org.apache.catalina.valves.AbstractAccessLogValve.AccessLogElement;
import org.apache.catalina.valves.AbstractAccessLogValve.ByteSentElement;
import org.apache.catalina.valves.AbstractAccessLogValve.CachedElement;
import org.apache.catalina.valves.AbstractAccessLogValve.ConnectionStatusElement;
import org.apache.catalina.valves.AbstractAccessLogValve.ElapsedTimeElement;
import org.apache.catalina.valves.AbstractAccessLogValve.FirstByteTimeElement;
import org.apache.catalina.valves.AbstractAccessLogValve.HeaderElement;
import org.apache.catalina.valves.AbstractAccessLogValve.HostElement;
import org.apache.catalina.valves.AbstractAccessLogValve.HttpStatusCodeElement;
import org.apache.catalina.valves.AbstractAccessLogValve.LocalAddrElement;
import org.apache.catalina.valves.AbstractAccessLogValve.LocalServerNameElement;
import org.apache.catalina.valves.AbstractAccessLogValve.MethodElement;
import org.apache.catalina.valves.AbstractAccessLogValve.PortElement;
import org.apache.catalina.valves.AbstractAccessLogValve.ProtocolElement;
import org.apache.catalina.valves.AbstractAccessLogValve.QueryElement;
import org.apache.catalina.valves.AbstractAccessLogValve.RemoteAddrElement;
import org.apache.catalina.valves.AbstractAccessLogValve.RequestURIElement;
import org.apache.catalina.valves.AbstractAccessLogValve.SessionIdElement;
import org.apache.catalina.valves.AbstractAccessLogValve.ThreadNameElement;
import org.apache.catalina.valves.AbstractAccessLogValve.UserElement;
import org.apache.catalina.valves.AbstractAccessLogValve.ElapsedTimeElement.Style;
import org.apache.commons.lang3.StringUtils;

public abstract class FormattingAccessLogValve extends AccessLogValve {
   private static final ELogger LOG = ELoggerFactory.getLogger(FormattingAccessLogValve.class);
   private static final String ACCESS_LOG_PROGRAM = "access";
   private final List<FormattingAccessLogValve.AccessLogAppender> accessLogAppenders;
   private final LogFormat formatterType;

   protected FormattingAccessLogValve(LogFormat formatterType) {
      this.formatterType = formatterType;
      if (formatterType == LogFormat.STRUCTURED) {
         this.accessLogAppenders = this.createStructuredLogAppenderList();
         super.logElements = this.accessLogAppenders.toArray(AccessLogElement[]::new);
         super.cachedElements = this.accessLogAppenders.stream().filter(appender -> appender instanceof CachedElement).toArray(CachedElement[]::new);
      } else {
         this.accessLogAppenders = List.of();
      }
   }

   public LogFormat getFormatterType() {
      return this.formatterType;
   }

   public void log(Request request, Response response, long processingTimeNanos) {
      switch (this.formatterType) {
         case SIMPLE:
            super.log(request, response, processingTimeNanos);
            break;
         case STRUCTURED:
            this.structuredLog(request, response, processingTimeNanos);
            break;
         default:
            throw new IllegalStateException("Unhandled formatter type: " + this.formatterType);
      }
   }

   private void structuredLog(@NonNull Request request, @NonNull Response response, long processingTimeNanos) {
      if (request == null) {
         throw new NullPointerException("request is marked non-null but is null");
      }

      if (response == null) {
         throw new NullPointerException("response is marked non-null but is null");
      }

      if (this.shouldLogRequest(request)) {
         StructuredAccessLog.StructuredAccessLogBuilder accessLogBuilder = StructuredAccessLog.builder().program("access");

         try (CharArrayWriter resultWriter = new CharArrayWriter(1024)) {
            for (FormattingAccessLogValve.AccessLogAppender appender : this.accessLogAppenders) {
               appender.addElement(resultWriter, request, response, processingTimeNanos);
               appendElementToBuilder(appender, accessLogBuilder, resultWriter.toString());
               resultWriter.reset();
            }

            IamSystemProperties.getLogLabel().ifPresent(accessLogBuilder::label);
            Optional<String> jsonString = WebserverLogSerializer.writeLogAsJsonString(accessLogBuilder.build());
            jsonString.ifPresent(json -> {
               resultWriter.reset();
               resultWriter.append(json);
               this.log(resultWriter);
            });
         }
      }
   }

   public void setPattern(String pattern) {
      if (this.formatterType == LogFormat.SIMPLE) {
         super.setPattern(pattern);
      }
   }

   private static void appendElementToBuilder(
      FormattingAccessLogValve.AccessLogAppender appender, StructuredAccessLog.StructuredAccessLogBuilder accessLogBuilder, String content
   ) {
      try {
         appender.appendElementToBuilder(accessLogBuilder, content);
      } catch (Throwable e) {
         LOG.error(
            EDefaultLogType.GENERIC_LOG_TYPE,
            String.format("Failed to parse generated access log content for appender type %s. Cause: %s", appender.getClass(), e.getMessage())
         );
         LOG.debug("Unparseable access log content: " + content, e);
      }
   }

   private boolean shouldLogRequest(Request request) {
      return this.getEnabled()
         && this.getState().isAvailable()
         && isRequestOnAllowList(request, this.getConditionIf())
         && !isRequestOnDenyList(request, this.getCondition());
   }

   private static boolean isRequestOnAllowList(Request request, @Nullable String conditionAttribute) {
      return Optional.ofNullable(conditionAttribute).map(c -> request.getRequest().getAttribute(c) != null).orElse(true);
   }

   private static boolean isRequestOnDenyList(Request request, @Nullable String conditionAttribute) {
      return Optional.ofNullable(conditionAttribute).map(c -> request.getRequest().getAttribute(c) != null).orElse(false);
   }

   private List<FormattingAccessLogValve.AccessLogAppender> createStructuredLogAppenderList() {
      return List.of(
         new FormattingAccessLogValve.DateAndTimeElementAppender(),
         new FormattingAccessLogValve.LocalAddressElementAppender(),
         new FormattingAccessLogValve.RemoteAddressElementAppender(),
         new FormattingAccessLogValve.PeerAddressElementAppender(),
         new FormattingAccessLogValve.RemotePortElementAppender(),
         new FormattingAccessLogValve.LocalPortElementAppender(),
         new FormattingAccessLogValve.HostElementAppender(),
         new FormattingAccessLogValve.ProtocolElementAppender(),
         new FormattingAccessLogValve.LocalServerNameElementAppender(),
         new FormattingAccessLogValve.ElapsedTimeElementAppender(),
         new FormattingAccessLogValve.FirstByteTimeElementAppender(),
         new FormattingAccessLogValve.ByteSentElementAppender(),
         new FormattingAccessLogValve.MethodElementAppender(),
         new FormattingAccessLogValve.HttpStatusCodeElementAppender(),
         new FormattingAccessLogValve.PathElementAppender(),
         new FormattingAccessLogValve.QueryElementAppender(),
         new FormattingAccessLogValve.SessionIdElementAppender(),
         new FormattingAccessLogValve.UserElementAppender(),
         new FormattingAccessLogValve.ThreadNameElementAppender(),
         new FormattingAccessLogValve.ConnectionStatusElementAppender(),
         new FormattingAccessLogValve.HeaderElementAppender(FormattingAccessLogValve.AccessLogHeaderType.REFERER),
         new FormattingAccessLogValve.HeaderElementAppender(FormattingAccessLogValve.AccessLogHeaderType.USER_AGENT),
         new FormattingAccessLogValve.HeaderElementAppender(FormattingAccessLogValve.AccessLogHeaderType.ACCEPT_LANGUAGE)
      );
   }

   public interface AccessLogAppender extends AccessLogElement {
      void appendElementToBuilder(StructuredAccessLog.StructuredAccessLogBuilder var1, String var2) throws Exception;

      static String filterEmptyContent(String content) {
         return !StringUtils.isBlank(content) && !"null".equals(content) && !"-".equals(content) ? content : null;
      }

      static int safeParseInteger(String content) throws NumberFormatException {
         String filteredString = filterEmptyContent(content);
         return filteredString == null ? 0 : Integer.parseInt(content);
      }

      static long safeParseLong(String content) throws NumberFormatException {
         String filteredString = filterEmptyContent(content);
         return filteredString == null ? 0L : Long.parseLong(content);
      }
   }

   protected enum AccessLogHeaderType {
      REFERER("Referer"),
      USER_AGENT("User-Agent"),
      ACCEPT_LANGUAGE("Accept-Language");

      private final String identifier;

      @SuppressFBWarnings(justification = "generated code")
      public String getIdentifier() {
         return this.identifier;
      }

      @SuppressFBWarnings(justification = "generated code")
      AccessLogHeaderType(final String identifier) {
         this.identifier = identifier;
      }
   }

   private static final class ByteSentElementAppender extends ByteSentElement implements FormattingAccessLogValve.AccessLogAppender {
      private ByteSentElementAppender() {
         super(false);
      }

      @Override
      public void appendElementToBuilder(StructuredAccessLog.StructuredAccessLogBuilder builder, String content) {
         builder.bytesSent(FormattingAccessLogValve.AccessLogAppender.safeParseLong(content));
      }
   }

   private static final class ConnectionStatusElementAppender extends ConnectionStatusElement implements FormattingAccessLogValve.AccessLogAppender {
      @Override
      public void appendElementToBuilder(StructuredAccessLog.StructuredAccessLogBuilder builder, String content) {
         builder.connectionStatus(content);
      }
   }

   private static final class DateAndTimeElementAppender implements FormattingAccessLogValve.AccessLogAppender {
      public void addElement(CharArrayWriter buf, Request request, Response response, long processingTimeNanos) {
         long requestEndTime = request.getCoyoteRequest().getStartTime() + TimeUnit.NANOSECONDS.toMillis(processingTimeNanos);
         buf.append(WebserverLogSerializer.instantToIso8601Timestamp(Instant.ofEpochMilli(requestEndTime)));
      }

      @Override
      public void appendElementToBuilder(StructuredAccessLog.StructuredAccessLogBuilder builder, String content) {
         builder.timestamp(content).time(content);
      }
   }

   private static final class ElapsedTimeElementAppender extends ElapsedTimeElement implements FormattingAccessLogValve.AccessLogAppender {
      private ElapsedTimeElementAppender() {
         super(Style.MILLISECONDS);
      }

      @Override
      public void appendElementToBuilder(StructuredAccessLog.StructuredAccessLogBuilder builder, String content) {
         builder.elapsedTimeMillis(FormattingAccessLogValve.AccessLogAppender.safeParseLong(content));
      }
   }

   private static final class FirstByteTimeElementAppender extends FirstByteTimeElement implements FormattingAccessLogValve.AccessLogAppender {
      @Override
      public void appendElementToBuilder(StructuredAccessLog.StructuredAccessLogBuilder builder, String content) {
         String filteredContent = FormattingAccessLogValve.AccessLogAppender.filterEmptyContent(content);
         if (filteredContent == null) {
            builder.firstByteTime(-1L);
         } else {
            builder.firstByteTime(FormattingAccessLogValve.AccessLogAppender.safeParseLong(filteredContent));
         }
      }
   }

   private static final class HeaderElementAppender extends HeaderElement implements FormattingAccessLogValve.AccessLogAppender {
      private final FormattingAccessLogValve.AccessLogHeaderType type;

      private HeaderElementAppender(FormattingAccessLogValve.AccessLogHeaderType type) {
         super(type.getIdentifier());
         this.type = type;
      }

      @Override
      public void appendElementToBuilder(StructuredAccessLog.StructuredAccessLogBuilder builder, String content) {
         String filteredContent = FormattingAccessLogValve.AccessLogAppender.filterEmptyContent(content);
         switch (this.type) {
            case REFERER:
               builder.referrer(filteredContent);
               break;
            case USER_AGENT:
               builder.userAgent(filteredContent);
               break;
            case ACCEPT_LANGUAGE:
               builder.acceptLanguage(filteredContent);
               break;
            default:
               throw new IllegalStateException("Unhandled header type; " + this.type);
         }
      }
   }

   private final class HostElementAppender extends HostElement implements FormattingAccessLogValve.AccessLogAppender {
      private HostElementAppender() {
         super(FormattingAccessLogValve.this);
      }

      @Override
      public void appendElementToBuilder(StructuredAccessLog.StructuredAccessLogBuilder builder, String content) {
         builder.remoteHostName(FormattingAccessLogValve.AccessLogAppender.filterEmptyContent(content));
      }
   }

   private static final class HttpStatusCodeElementAppender extends HttpStatusCodeElement implements FormattingAccessLogValve.AccessLogAppender {
      @Override
      public void appendElementToBuilder(StructuredAccessLog.StructuredAccessLogBuilder builder, String content) {
         builder.httpStatusCode(FormattingAccessLogValve.AccessLogAppender.safeParseInteger(content));
      }
   }

   private static final class LocalAddressElementAppender extends LocalAddrElement implements FormattingAccessLogValve.AccessLogAppender {
      private LocalAddressElementAppender() {
         super(false);
      }

      @Override
      public void appendElementToBuilder(StructuredAccessLog.StructuredAccessLogBuilder builder, String content) {
         builder.localAddress(FormattingAccessLogValve.AccessLogAppender.filterEmptyContent(content));
      }
   }

   private final class LocalPortElementAppender extends PortElement implements FormattingAccessLogValve.AccessLogAppender {
      private LocalPortElementAppender() {
         super(FormattingAccessLogValve.this, "local");
      }

      @Override
      public void appendElementToBuilder(StructuredAccessLog.StructuredAccessLogBuilder builder, String content) {
         builder.localPort(FormattingAccessLogValve.AccessLogAppender.safeParseInteger(content));
      }
   }

   private final class LocalServerNameElementAppender extends LocalServerNameElement implements FormattingAccessLogValve.AccessLogAppender {
      private LocalServerNameElementAppender() {
         super(FormattingAccessLogValve.this);
      }

      @Override
      public void appendElementToBuilder(StructuredAccessLog.StructuredAccessLogBuilder builder, String content) {
         builder.localServerName(FormattingAccessLogValve.AccessLogAppender.filterEmptyContent(content));
      }
   }

   private static final class MethodElementAppender extends MethodElement implements FormattingAccessLogValve.AccessLogAppender {
      @Override
      public void appendElementToBuilder(StructuredAccessLog.StructuredAccessLogBuilder builder, String content) {
         builder.requestMethod(FormattingAccessLogValve.AccessLogAppender.filterEmptyContent(content));
      }
   }

   private static final class PathElementAppender extends RequestURIElement implements FormattingAccessLogValve.AccessLogAppender {
      @Override
      public void appendElementToBuilder(StructuredAccessLog.StructuredAccessLogBuilder builder, String content) {
         builder.requestPath(FormattingAccessLogValve.AccessLogAppender.filterEmptyContent(content));
      }
   }

   private final class PeerAddressElementAppender extends RemoteAddrElement implements FormattingAccessLogValve.AccessLogAppender {
      private PeerAddressElementAppender() {
         super(FormattingAccessLogValve.this, "peer");
      }

      @Override
      public void appendElementToBuilder(StructuredAccessLog.StructuredAccessLogBuilder builder, String content) {
         builder.peerAddress(FormattingAccessLogValve.AccessLogAppender.filterEmptyContent(content));
      }
   }

   private final class ProtocolElementAppender extends ProtocolElement implements FormattingAccessLogValve.AccessLogAppender {
      private ProtocolElementAppender() {
         super(FormattingAccessLogValve.this);
      }

      @Override
      public void appendElementToBuilder(StructuredAccessLog.StructuredAccessLogBuilder builder, String content) {
         builder.requestProtocol(FormattingAccessLogValve.AccessLogAppender.filterEmptyContent(content));
      }
   }

   private static final class QueryElementAppender extends QueryElement implements FormattingAccessLogValve.AccessLogAppender {
      @Override
      public void appendElementToBuilder(StructuredAccessLog.StructuredAccessLogBuilder builder, String content) {
         builder.queryString(FormattingAccessLogValve.AccessLogAppender.filterEmptyContent(content));
      }
   }

   private final class RemoteAddressElementAppender extends RemoteAddrElement implements FormattingAccessLogValve.AccessLogAppender {
      private RemoteAddressElementAppender() {
         super(FormattingAccessLogValve.this, "remote");
      }

      @Override
      public void appendElementToBuilder(StructuredAccessLog.StructuredAccessLogBuilder builder, String content) {
         builder.remoteAddress(FormattingAccessLogValve.AccessLogAppender.filterEmptyContent(content));
      }
   }

   private final class RemotePortElementAppender extends PortElement implements FormattingAccessLogValve.AccessLogAppender {
      private RemotePortElementAppender() {
         super(FormattingAccessLogValve.this, "remote");
      }

      @Override
      public void appendElementToBuilder(StructuredAccessLog.StructuredAccessLogBuilder builder, String content) {
         builder.remotePort(FormattingAccessLogValve.AccessLogAppender.safeParseInteger(content));
      }
   }

   private static final class SessionIdElementAppender extends SessionIdElement implements FormattingAccessLogValve.AccessLogAppender {
      @Override
      public void appendElementToBuilder(StructuredAccessLog.StructuredAccessLogBuilder builder, String content) {
         builder.sessionId(FormattingAccessLogValve.AccessLogAppender.filterEmptyContent(content));
      }
   }

   private static final class ThreadNameElementAppender extends ThreadNameElement implements FormattingAccessLogValve.AccessLogAppender {
      @Override
      public void appendElementToBuilder(StructuredAccessLog.StructuredAccessLogBuilder builder, String content) {
         builder.threadName(FormattingAccessLogValve.AccessLogAppender.filterEmptyContent(content));
      }
   }

   private static final class UserElementAppender extends UserElement implements FormattingAccessLogValve.AccessLogAppender {
      @Override
      public void appendElementToBuilder(StructuredAccessLog.StructuredAccessLogBuilder builder, String content) {
         builder.remoteUser(FormattingAccessLogValve.AccessLogAppender.filterEmptyContent(content));
      }
   }
}
