package com.airlock.iam.login.domain.model.credential;

import com.airlock.iam.base.api.domain.model.exception.ViolationException;
import com.airlock.iam.base.api.domain.model.validation.GenericError;
import com.airlock.iam.base.api.domain.model.validation.ValidationResult;
import com.airlock.iam.base.api.domain.model.validation.ValidatorError;
import com.airlock.iam.core.misc.authen.credential.Credential;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.flow.shared.application.configuration.HttpParameterSpec;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;

public final class RequestAttributeExtractor {
   private static final ELogger LOG = ELoggerFactory.getLogger(RequestAttributeExtractor.class);
   private List<HttpParameterSpec> bodyParameterSpec;
   private List<HttpParameterSpec> headerParameterSpec;

   private RequestAttributeExtractor() {
      this.bodyParameterSpec = Collections.emptyList();
      this.headerParameterSpec = Collections.emptyList();
   }

   public Map<String, String> extractAllAttributes(HttpServletRequest request, RequestAttributeExtractor.AttributeType type) {
      return this.extractAttributes(request.getMethod(), this.getValueProvider(request, type), type, false);
   }

   public Map<String, String> extractAllValidAttributes(HttpServletRequest request, RequestAttributeExtractor.AttributeType type) {
      return this.extractAttributes(request.getMethod(), this.getValueProvider(request, type), type, true);
   }

   public Map<String, String> extractAllValidAttributes(Map<String, String> attributes) {
      return this.extractAttributes("POST", this.attributesValueProvider(attributes), RequestAttributeExtractor.AttributeType.BODY, true);
   }

   public Set<String> collectUnexpectedParameters(Map<String, String> attributes, RequestAttributeExtractor.AttributeType type) {
      Set<String> paramNames = this.getSpecForType(type).stream().map(HttpParameterSpec::getAttributeName).collect(Collectors.toSet());
      return paramNames.isEmpty()
         ? attributes.keySet()
         : attributes.entrySet().stream().filter(param -> !paramNames.contains(param.getKey())).map(Entry::getKey).collect(Collectors.toSet());
   }

   public ValidationResult validateAttributes(Map<String, String> attributes, RequestAttributeExtractor.AttributeType type) {
      if (this.getSpecForType(type).isEmpty()) {
         return ValidationResult.valid();
      }

      List<ValidatorError> errors = new ArrayList<>();
      this.getSpecForType(type).forEach(paramSpec -> {
         if (attributes.containsKey(paramSpec.getAttributeName()) && this.getValue(paramSpec, this.attributesValueProvider(attributes), true).isEmpty()) {
            errors.add(ValidatorError.validatorError(paramSpec.getAttributeName(), GenericError.INVALID_VALUE));
         }
      });
      return errors.isEmpty() ? ValidationResult.valid() : ValidationResult.invalid(errors);
   }

   private Map<String, String> extractAttributes(
      String requestMethod, RequestAttributeExtractor.AttributeValueProvider valueProvider, RequestAttributeExtractor.AttributeType type, boolean validOnly
   ) {
      Map<String, String> attributes = new HashMap<>();
      if ("POST".equals(requestMethod)) {
         this.getSpecForType(type)
            .forEach(headerSpec -> this.getValue(headerSpec, valueProvider, validOnly).ifPresent(value -> attributes.put(headerSpec.getAttributeName(), value)));
      }

      return attributes;
   }

   private RequestAttributeExtractor.AttributeValueProvider getValueProvider(HttpServletRequest request, RequestAttributeExtractor.AttributeType type) {
      if (type.equals(RequestAttributeExtractor.AttributeType.HEADER)) {
         return this.valueProvider("HTTP header", request::getHeader);
      } else if (type.equals(RequestAttributeExtractor.AttributeType.BODY)) {
         return this.valueProvider("HTTP parameter", request::getParameter);
      } else {
         throw new ViolationException("Unsupported attribute type: " + type);
      }
   }

   private RequestAttributeExtractor.AttributeValueProvider attributesValueProvider(Map<String, String> attributes) {
      return this.valueProvider("attribute", attributes::get);
   }

   public void addExternalCredentialAttributes(HttpServletRequest request, Credential credential) {
      this.extractAllValidAttributes(request, RequestAttributeExtractor.AttributeType.BODY)
         .entrySet()
         .forEach(attribute -> credential.setAttribute(attribute.getKey(), attribute.getValue()));
      this.extractAllValidAttributes(request, RequestAttributeExtractor.AttributeType.HEADER)
         .entrySet()
         .forEach(attribute -> credential.setAttribute(attribute.getKey(), attribute.getValue()));
   }

   private Optional<String> getValue(HttpParameterSpec paramSpec, RequestAttributeExtractor.AttributeValueProvider provider, boolean validOnly) {
      String name = paramSpec.getAttributeName();
      Optional<String> value = provider.getValue(name);
      if (value.isPresent()) {
         if (validOnly) {
            if (this.isValueValid(paramSpec, value.get())) {
               LOG.debug("Accepting valid " + provider.getDescription() + " attribute: \"" + name + "\" = \"" + value.get() + "\"");
               return value;
            } else {
               LOG.debug("Rejecting invalid " + provider.getDescription() + " attribute: \"" + name + "\" = \"" + value.get() + "\"");
               return Optional.empty();
            }
         } else {
            LOG.debug("Accepting " + provider.getDescription() + " attribute: \"" + name + "\" = \"" + value.get() + "\"");
            return value;
         }
      } else {
         return Optional.empty();
      }
   }

   private boolean isValueValid(HttpParameterSpec paramSpec, String value) {
      return this.valueIsNotTooLong(paramSpec, value) && this.valueConformsToValidationPattern(paramSpec, value);
   }

   private boolean valueConformsToValidationPattern(HttpParameterSpec paramSpec, String value) {
      return paramSpec.getValueValidatorPattern().isEmpty() || paramSpec.getValueValidatorPattern().get().matcher(value).matches();
   }

   private boolean valueIsNotTooLong(HttpParameterSpec paramSpec, String value) {
      return value.length() <= paramSpec.getMaxValueLength();
   }

   private RequestAttributeExtractor.AttributeValueProvider valueProvider(final String description, final Function<String, String> resolver) {
      return new RequestAttributeExtractor.AttributeValueProvider() {
         @Override
         public Optional<String> getValue(String key) {
            String value = resolver.apply(key);
            return Optional.ofNullable(value).map(StringUtils::trimToEmpty);
         }

         @Override
         public String getDescription() {
            return description;
         }
      };
   }

   private List<HttpParameterSpec> getSpecForType(RequestAttributeExtractor.AttributeType type) {
      if (type.equals(RequestAttributeExtractor.AttributeType.HEADER)) {
         return this.headerParameterSpec;
      } else if (type.equals(RequestAttributeExtractor.AttributeType.BODY)) {
         return this.bodyParameterSpec;
      } else {
         throw new ViolationException("Unsupported attribute type: " + type);
      }
   }

   public static RequestAttributeExtractor.RequestAttributeExtractorBuilder builder() {
      return new RequestAttributeExtractor().toBuilder();
   }

   @SuppressFBWarnings(justification = "generated code")
   public RequestAttributeExtractor.RequestAttributeExtractorBuilder toBuilder() {
      return new RequestAttributeExtractor.RequestAttributeExtractorBuilder()
         .bodyParameterSpec(this.bodyParameterSpec)
         .headerParameterSpec(this.headerParameterSpec);
   }

   @SuppressFBWarnings(justification = "generated code")
   public RequestAttributeExtractor(List<HttpParameterSpec> bodyParameterSpec, List<HttpParameterSpec> headerParameterSpec) {
      this.bodyParameterSpec = bodyParameterSpec;
      this.headerParameterSpec = headerParameterSpec;
   }

   public enum AttributeType {
      HEADER,
      BODY;
   }

   private interface AttributeValueProvider {
      String getDescription();

      Optional<String> getValue(String var1);
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class RequestAttributeExtractorBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private List<HttpParameterSpec> bodyParameterSpec;
      @SuppressFBWarnings(justification = "generated code")
      private List<HttpParameterSpec> headerParameterSpec;

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

      @SuppressFBWarnings(justification = "generated code")
      public RequestAttributeExtractor.RequestAttributeExtractorBuilder bodyParameterSpec(List<HttpParameterSpec> bodyParameterSpec) {
         this.bodyParameterSpec = bodyParameterSpec;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public RequestAttributeExtractor.RequestAttributeExtractorBuilder headerParameterSpec(List<HttpParameterSpec> headerParameterSpec) {
         this.headerParameterSpec = headerParameterSpec;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public RequestAttributeExtractor build() {
         return new RequestAttributeExtractor(this.bodyParameterSpec, this.headerParameterSpec);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "RequestAttributeExtractor.RequestAttributeExtractorBuilder(bodyParameterSpec="
            + this.bodyParameterSpec
            + ", headerParameterSpec="
            + this.headerParameterSpec
            + ")";
      }
   }
}
