package com.airlock.iam.saml2.domain.model.session;

import com.airlock.iam.base.api.domain.model.mapification.MapifiedValues;
import com.airlock.iam.base.api.domain.model.user.UserId;
import com.airlock.iam.base.domain.model.mapification.MapifiedValuesBuilder;
import com.airlock.iam.base.misc.adapt.Adapter;
import com.airlock.iam.base.misc.log.LogUtil;
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.flow.shared.api.domain.model.session.UserSessionScopedFlowAttribute;
import com.airlock.iam.saml2.domain.model.idp.Saml2FederationSessionIndex;
import com.google.common.collect.MoreCollectors;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;

public final class FlowFederationSession implements UserSessionScopedFlowAttribute<FlowFederationSession> {
   static final String AUTHENTICATION_INSTANT_KEY = "authInstant";
   private static final String VALUE_CONTAINER_KEY = "values";
   private static final String SINGLE_LOGOUT_IN_PROGRESS_KEY = "singleLogoutInProgress";
   private static final String BLOCK_REST_REQUESTS_KEY = "blockRestRequests";
   private static final String PROPAGATION_ATTRIBUTES_KEY = "propagationAttributes";
   private static final String AUTHENTICATED_USER_ID_KEY = "userId";
   private static final String SESSION_ID_KEY = "sessionId";
   private static final ELogger LOG = ELoggerFactory.getLogger(FlowFederationSession.class);
   private final Map<String, List<String>> valueContainer;
   private FlowFederationSession.SingleLogoutInProgress singleLogoutInProgress;
   private boolean clearGatewaySessionOnSlo;
   private boolean blockRestRequests;
   private Optional<Saml2PropagationAttributes> propagationAttributes;
   private Optional<UserId> authenticatedUserId;
   private final FlowFederationSessionId sessionId;

   public FlowFederationSession(
      Map<String, List<String>> valueContainer,
      FlowFederationSession.SingleLogoutInProgress singleLogoutInProgress,
      boolean blockRestRequests,
      Optional<Saml2PropagationAttributes> propagationAttributes,
      Optional<UserId> authenticatedUserId,
      FlowFederationSessionId sessionId
   ) {
      this.singleLogoutInProgress = singleLogoutInProgress;
      this.valueContainer = valueContainer;
      this.blockRestRequests = blockRestRequests;
      this.propagationAttributes = propagationAttributes;
      this.authenticatedUserId = authenticatedUserId;
      this.sessionId = sessionId;
      this.clearGatewaySessionOnSlo = true;
   }

   public FlowFederationSession mergeWith(FlowFederationSession newAttribute) {
      return newAttribute;
   }

   public FlowFederationSession copyAttribute() {
      return demapify(this.mapify());
   }

   private FlowFederationSession() {
      this(
         new HashMap<>(),
         FlowFederationSession.SingleLogoutInProgress.NO_SLO_IN_PROGRESS,
         false,
         Optional.empty(),
         Optional.empty(),
         FlowFederationSessionId.randomFederationSessionId()
      );
   }

   public static FlowFederationSession emptySession() {
      return new FlowFederationSession();
   }

   public Optional<List<String>> getValues(String key) {
      Validate.notBlank(key);
      if ("FORCE_SESSION_UPGRADE".equals(key)) {
         return Optional.of(List.of("true"));
      } else {
         return "authInstant".equals(key)
            ? this.propagationAttributes
               .map(Saml2PropagationAttributes::getAuthenticationInstant)
               .map(i -> i.truncatedTo(ChronoUnit.SECONDS))
               .map(DateTimeFormatter.ISO_INSTANT::format)
               .map(List::of)
            : Optional.ofNullable(this.valueContainer.get(key));
      }
   }

   public Optional<String> getOnlyValue(String key) {
      return this.getValues(key).map(values -> values.stream().filter(StringUtils::isNotBlank).collect(MoreCollectors.onlyElement()));
   }

   public Optional<Saml2FederationSessionIndex> getSessionIndex() {
      return this.getValues("SAML2IDPSessionIndex")
         .stream()
         .flatMap(Collection::stream)
         .collect(MoreCollectors.toOptional())
         .map(Saml2FederationSessionIndex::saml2FederationSessionIndex);
   }

   public FlowFederationSession setPropagationAttributes(Saml2PropagationAttributes attrs) {
      this.propagationAttributes = Optional.of(attrs);
      return this;
   }

   public void resetPropagationAttributes() {
      LOG.debug("Resetting SAML2 propagation attributes for " + this);
      this.propagationAttributes = Optional.empty();
   }

   public FlowFederationSession setValues(String key, List<String> values) {
      Validate.notBlank(key);
      Objects.requireNonNull(values);
      this.valueContainer.put(key, List.copyOf(values));
      return this;
   }

   public FlowFederationSession setValue(String key, String value) {
      return this.setValues(key, Collections.singletonList(value));
   }

   public boolean isSingleLogoutInProgress() {
      return this.singleLogoutInProgress != FlowFederationSession.SingleLogoutInProgress.NO_SLO_IN_PROGRESS;
   }

   public boolean isIdpInitiatedSingleLogoutInProgressOnIdp() {
      return this.singleLogoutInProgress == FlowFederationSession.SingleLogoutInProgress.IDP_INITIATED_ON_IDP;
   }

   public boolean isSpInitiatedSingleLogoutInProgressOnIdp() {
      return this.singleLogoutInProgress == FlowFederationSession.SingleLogoutInProgress.SP_INITIATED_ON_IDP;
   }

   public boolean isSingleLogoutInProgressOnIdp() {
      return this.isIdpInitiatedSingleLogoutInProgressOnIdp() || this.isSpInitiatedSingleLogoutInProgressOnIdp();
   }

   public boolean isIdpInitiatedSingleLogoutInProgressOnSp() {
      return this.singleLogoutInProgress == FlowFederationSession.SingleLogoutInProgress.IDP_INITIATED_ON_SP;
   }

   public boolean isSpInitiatedSingleLogoutInProgressOnSp() {
      return this.singleLogoutInProgress == FlowFederationSession.SingleLogoutInProgress.SP_INITIATED_ON_SP;
   }

   public boolean isSingleLogoutInProgressOnSp() {
      return this.isIdpInitiatedSingleLogoutInProgressOnSp() || this.isSpInitiatedSingleLogoutInProgressOnSp();
   }

   public FlowFederationSession.SingleLogoutInProgress getSingleLogoutInProgress() {
      return this.singleLogoutInProgress;
   }

   public FlowFederationSession setIdpInitiatedSingleLogoutInProgressOnIdp() {
      this.singleLogoutInProgress = FlowFederationSession.SingleLogoutInProgress.IDP_INITIATED_ON_IDP;
      return this;
   }

   public FlowFederationSession setSpInitiatedSingleLogoutInProgressOnIdp() {
      this.singleLogoutInProgress = FlowFederationSession.SingleLogoutInProgress.SP_INITIATED_ON_IDP;
      return this;
   }

   public FlowFederationSession setIdpInitiatedSingleLogoutInProgressOnSp() {
      this.singleLogoutInProgress = FlowFederationSession.SingleLogoutInProgress.IDP_INITIATED_ON_SP;
      return this;
   }

   public FlowFederationSession setSpInitiatedSingleLogoutInProgressOnSp() {
      this.singleLogoutInProgress = FlowFederationSession.SingleLogoutInProgress.SP_INITIATED_ON_SP;
      return this;
   }

   public void doNotClearGatewaySessionOnSlo() {
      this.clearGatewaySessionOnSlo = false;
   }

   public FlowFederationSession setAuthenticatedUserId(UserId id) {
      this.authenticatedUserId = Optional.of(id);
      return this;
   }

   public boolean isValid() {
      return this.authenticatedUserId.isPresent() || this.isSingleLogoutInProgress();
   }

   public FlowFederationSession invalidate() {
      this.authenticatedUserId = Optional.empty();
      this.propagationAttributes = Optional.empty();
      this.singleLogoutInProgress = FlowFederationSession.SingleLogoutInProgress.NO_SLO_IN_PROGRESS;
      this.valueContainer.clear();
      LOG.info(LogTypes.TRACE_MESSAGE, "Federation Session invalidated.");
      return this;
   }

   public boolean hasAuthenticatedUserId(UserId userId) {
      return this.authenticatedUserId.isPresent() && this.authenticatedUserId.get().equals(userId);
   }

   @Override
   public String toString() {
      return Stream.of(
            this.authenticatedUserId.<String>map(id -> "userId=" + LogUtil.logSafeQuote(id)).orElse(null),
            "singleLogoutInProgress=" + this.singleLogoutInProgressHint()
         )
         .filter(StringUtils::isNotBlank)
         .collect(Collectors.joining(", ", "Federation Session[", "]"));
   }

   private String singleLogoutInProgressHint() {
      if (this.singleLogoutInProgress == FlowFederationSession.SingleLogoutInProgress.IDP_INITIATED_ON_IDP
         || this.singleLogoutInProgress == FlowFederationSession.SingleLogoutInProgress.IDP_INITIATED_ON_SP) {
         return "true (IdP-initiated)";
      } else {
         return this.singleLogoutInProgress != FlowFederationSession.SingleLogoutInProgress.SP_INITIATED_ON_IDP
               && this.singleLogoutInProgress != FlowFederationSession.SingleLogoutInProgress.SP_INITIATED_ON_SP
            ? "false"
            : "true (SP-initiated)";
      }
   }

   public static <X extends Throwable> FlowFederationSession toFlowFederationSessionOrThrow(Object o, Supplier<? extends X> exceptionSupplier) throws X {
      return Adapter.adapt(o).ifPossibleTo(FlowFederationSession.class).orElseThrow(exceptionSupplier);
   }

   public void clearSessionIndex() {
      this.valueContainer.remove("SAML2IDPSessionIndex");
   }

   @Override
   public MapifiedValues mapify() {
      return MapifiedValuesBuilder.mapifiedValues()
         .put("values", this.valueContainer)
         .put("singleLogoutInProgress", this.singleLogoutInProgress.name())
         .put("blockRestRequests", this.blockRestRequests)
         .putIfPresent("propagationAttributes", this.propagationAttributes.map(Saml2PropagationAttributes::mapify))
         .putIfPresent("userId", this.authenticatedUserId.map(UserId::getValue))
         .put("sessionId", this.sessionId.value())
         .build();
   }

   public static FlowFederationSession demapify(MapifiedValues values) {
      return new FlowFederationSession(
         new HashMap<>(values.get("values")),
         values.simpleValue("singleLogoutInProgress", FlowFederationSession.SingleLogoutInProgress::valueOf),
         values.<Boolean>getOptional("blockRestRequests").orElse(false),
         values.demapifyOptional("propagationAttributes", Saml2PropagationAttributes::demapify),
         values.transformOptional("userId", UserId::userId),
         values.simpleValue("sessionId", FlowFederationSessionId::federationSessionId)
      );
   }

   @SuppressFBWarnings(justification = "generated code")
   @Override
   public boolean equals(Object o) {
      if (o == this) {
         return true;
      } else if (!(o instanceof FlowFederationSession other)) {
         return false;
      } else {
         if (this.clearGatewaySessionOnSlo != other.clearGatewaySessionOnSlo) {
            return false;
         }

         if (this.blockRestRequests != other.blockRestRequests) {
            return false;
         }

         Object this$valueContainer = this.valueContainer;
         Object other$valueContainer = other.valueContainer;
         if (this$valueContainer == null ? other$valueContainer == null : this$valueContainer.equals(other$valueContainer)) {
            Object this$singleLogoutInProgress = this.singleLogoutInProgress;
            Object other$singleLogoutInProgress = other.singleLogoutInProgress;
            if (this$singleLogoutInProgress == null ? other$singleLogoutInProgress == null : this$singleLogoutInProgress.equals(other$singleLogoutInProgress)) {
               Object this$propagationAttributes = this.propagationAttributes;
               Object other$propagationAttributes = other.propagationAttributes;
               if (this$propagationAttributes == null ? other$propagationAttributes == null : this$propagationAttributes.equals(other$propagationAttributes)) {
                  Object this$authenticatedUserId = this.authenticatedUserId;
                  Object other$authenticatedUserId = other.authenticatedUserId;
                  if (this$authenticatedUserId == null ? other$authenticatedUserId == null : this$authenticatedUserId.equals(other$authenticatedUserId)) {
                     Object this$sessionId = this.sessionId;
                     Object other$sessionId = other.sessionId;
                     return this$sessionId == null ? other$sessionId == null : this$sessionId.equals(other$sessionId);
                  } else {
                     return false;
                  }
               } else {
                  return false;
               }
            } else {
               return false;
            }
         } else {
            return false;
         }
      }
   }

   @SuppressFBWarnings(justification = "generated code")
   @Override
   public int hashCode() {
      int PRIME = 59;
      int result = 1;
      result = result * 59 + (this.clearGatewaySessionOnSlo ? 79 : 97);
      result = result * 59 + (this.blockRestRequests ? 79 : 97);
      Object $valueContainer = this.valueContainer;
      result = result * 59 + ($valueContainer == null ? 43 : $valueContainer.hashCode());
      Object $singleLogoutInProgress = this.singleLogoutInProgress;
      result = result * 59 + ($singleLogoutInProgress == null ? 43 : $singleLogoutInProgress.hashCode());
      Object $propagationAttributes = this.propagationAttributes;
      result = result * 59 + ($propagationAttributes == null ? 43 : $propagationAttributes.hashCode());
      Object $authenticatedUserId = this.authenticatedUserId;
      result = result * 59 + ($authenticatedUserId == null ? 43 : $authenticatedUserId.hashCode());
      Object $sessionId = this.sessionId;
      return result * 59 + ($sessionId == null ? 43 : $sessionId.hashCode());
   }

   @SuppressFBWarnings(justification = "generated code")
   public boolean isClearGatewaySessionOnSlo() {
      return this.clearGatewaySessionOnSlo;
   }

   @SuppressFBWarnings(justification = "generated code")
   public boolean isBlockRestRequests() {
      return this.blockRestRequests;
   }

   @SuppressFBWarnings(justification = "generated code")
   public void setBlockRestRequests(boolean blockRestRequests) {
      this.blockRestRequests = blockRestRequests;
   }

   @SuppressFBWarnings(justification = "generated code")
   public Optional<Saml2PropagationAttributes> getPropagationAttributes() {
      return this.propagationAttributes;
   }

   @SuppressFBWarnings(justification = "generated code")
   public Optional<UserId> getAuthenticatedUserId() {
      return this.authenticatedUserId;
   }

   @SuppressFBWarnings(justification = "generated code")
   public FlowFederationSessionId getSessionId() {
      return this.sessionId;
   }

   public enum SingleLogoutInProgress {
      NO_SLO_IN_PROGRESS,
      IDP_INITIATED_ON_IDP,
      SP_INITIATED_ON_IDP,
      IDP_INITIATED_ON_SP,
      SP_INITIATED_ON_SP;
   }
}
