package com.airlock.iam.saml2.infrastructure.sp;

import com.airlock.iam.base.api.domain.model.exception.ViolationException;
import com.airlock.iam.common.api.domain.model.user.ProvidedUsername;
import com.airlock.iam.saml2.domain.model.Saml2RelayState;
import com.airlock.iam.saml2.domain.model.idp.Saml2IdpEntityId;
import com.airlock.iam.saml2.domain.model.sp.Saml2Attributes;
import com.airlock.iam.saml2.domain.model.sp.Saml2InResponseTo;
import com.airlock.iam.saml2.domain.model.sp.Saml2SpEntityId;
import com.sun.identity.saml2.assertion.Assertion;
import com.sun.identity.saml2.assertion.Attribute;
import com.sun.identity.saml2.assertion.AttributeStatement;
import com.sun.identity.saml2.assertion.NameID;
import com.sun.identity.saml2.protocol.Response;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.NonNull;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;

public final class AssertionDataImpl implements AssertionData {
   @NonNull
   private final Assertion assertion;
   @NonNull
   private final NameID nameID;
   @NonNull
   private final Saml2IdpEntityId idpEntityId;
   @NonNull
   private final Saml2SpEntityId spEntityId;
   @NonNull
   private final Optional<Saml2RelayState> relayState;
   @NonNull
   private final Optional<Saml2InResponseTo> inResponseTo;

   public static AssertionData fromResponseMap(Map<?, ?> map) {
      return builder()
         .assertion((Assertion)map.get("Assertion"))
         .nameID((NameID)map.get("NameID"))
         .idpEntityId(Saml2IdpEntityId.idpEntityId((String)map.get("idpEntityID")))
         .spEntityId(Saml2SpEntityId.spEntityId((String)map.get("spEntityID")))
         .relayState(Optional.ofNullable((String)map.get("RelayState")).map(Saml2RelayState::relayState))
         .inResponseTo(Optional.ofNullable(response(map).getInResponseTo()).filter(StringUtils::isNotBlank).map(Saml2InResponseTo::new))
         .build();
   }

   private static Response response(Map<?, ?> map) {
      return (Response)map.get("Response");
   }

   @Override
   public Saml2Attributes getAttributes() {
      return new Saml2Attributes(
         this.extractAttributeStatements()
            .flatMap(stmt -> streamOfNullable(stmt.getAttribute()))
            .filter(attr -> CollectionUtils.isNotEmpty(attr.getAttributeValueString()))
            .collect(
               Collectors.toMap(
                  Attribute::getName, Attribute::getAttributeValueString, (first, second) -> Stream.concat(first.stream(), second.stream()).toList()
               )
            )
      );
   }

   @Override
   public ProvidedUsername getProvidedUsernameFromNameID() {
      if (StringUtils.isBlank(this.nameID.getValue())) {
         throw new ViolationException("Failed to extract user ID from NameID element in Assertion; NameID was empty.");
      } else {
         return ProvidedUsername.providedUsernameOf(this.nameID.getValue());
      }
   }

   private Stream<AttributeStatement> extractAttributeStatements() {
      return streamOfNullable(this.assertion.getAttributeStatements());
   }

   private static <T> Stream<T> streamOfNullable(Collection<T> collection) {
      return collection == null ? Stream.empty() : collection.stream();
   }

   @SuppressFBWarnings(justification = "generated code")
   AssertionDataImpl(
      @NonNull Assertion assertion,
      @NonNull NameID nameID,
      @NonNull Saml2IdpEntityId idpEntityId,
      @NonNull Saml2SpEntityId spEntityId,
      @NonNull Optional<Saml2RelayState> relayState,
      @NonNull Optional<Saml2InResponseTo> inResponseTo
   ) {
      if (assertion == null) {
         throw new NullPointerException("assertion is marked non-null but is null");
      }

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

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

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

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

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

      this.assertion = assertion;
      this.nameID = nameID;
      this.idpEntityId = idpEntityId;
      this.spEntityId = spEntityId;
      this.relayState = relayState;
      this.inResponseTo = inResponseTo;
   }

   @SuppressFBWarnings(justification = "generated code")
   private static AssertionDataImpl.AssertionDataImplBuilder builder() {
      return new AssertionDataImpl.AssertionDataImplBuilder();
   }

   @NonNull
   @SuppressFBWarnings(justification = "generated code")
   @Override
   public Saml2IdpEntityId getIdpEntityId() {
      return this.idpEntityId;
   }

   @NonNull
   @SuppressFBWarnings(justification = "generated code")
   @Override
   public Saml2SpEntityId getSpEntityId() {
      return this.spEntityId;
   }

   @NonNull
   @SuppressFBWarnings(justification = "generated code")
   @Override
   public Optional<Saml2RelayState> getRelayState() {
      return this.relayState;
   }

   @NonNull
   @SuppressFBWarnings(justification = "generated code")
   @Override
   public Optional<Saml2InResponseTo> getInResponseTo() {
      return this.inResponseTo;
   }

   @SuppressFBWarnings(justification = "generated code")
   @Override
   public boolean equals(Object o) {
      if (o == this) {
         return true;
      } else if (!(o instanceof AssertionDataImpl other)) {
         return false;
      } else {
         Object this$assertion = this.assertion;
         Object other$assertion = other.assertion;
         if (this$assertion == null ? other$assertion == null : this$assertion.equals(other$assertion)) {
            Object this$nameID = this.nameID;
            Object other$nameID = other.nameID;
            if (this$nameID == null ? other$nameID == null : this$nameID.equals(other$nameID)) {
               Object this$idpEntityId = this.idpEntityId;
               Object other$idpEntityId = other.idpEntityId;
               if (this$idpEntityId == null ? other$idpEntityId == null : this$idpEntityId.equals(other$idpEntityId)) {
                  Object this$spEntityId = this.spEntityId;
                  Object other$spEntityId = other.spEntityId;
                  if (this$spEntityId == null ? other$spEntityId == null : this$spEntityId.equals(other$spEntityId)) {
                     Object this$relayState = this.relayState;
                     Object other$relayState = other.relayState;
                     if (this$relayState == null ? other$relayState == null : this$relayState.equals(other$relayState)) {
                        Object this$inResponseTo = this.inResponseTo;
                        Object other$inResponseTo = other.inResponseTo;
                        return this$inResponseTo == null ? other$inResponseTo == null : this$inResponseTo.equals(other$inResponseTo);
                     } else {
                        return false;
                     }
                  } 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;
      Object $assertion = this.assertion;
      result = result * 59 + ($assertion == null ? 43 : $assertion.hashCode());
      Object $nameID = this.nameID;
      result = result * 59 + ($nameID == null ? 43 : $nameID.hashCode());
      Object $idpEntityId = this.idpEntityId;
      result = result * 59 + ($idpEntityId == null ? 43 : $idpEntityId.hashCode());
      Object $spEntityId = this.spEntityId;
      result = result * 59 + ($spEntityId == null ? 43 : $spEntityId.hashCode());
      Object $relayState = this.relayState;
      result = result * 59 + ($relayState == null ? 43 : $relayState.hashCode());
      Object $inResponseTo = this.inResponseTo;
      return result * 59 + ($inResponseTo == null ? 43 : $inResponseTo.hashCode());
   }

   @SuppressFBWarnings(justification = "generated code")
   @Override
   public String toString() {
      return "AssertionDataImpl(assertion="
         + this.assertion
         + ", nameID="
         + this.nameID
         + ", idpEntityId="
         + this.getIdpEntityId()
         + ", spEntityId="
         + this.getSpEntityId()
         + ", relayState="
         + this.getRelayState()
         + ", inResponseTo="
         + this.getInResponseTo()
         + ")";
   }

   @SuppressFBWarnings(justification = "generated code")
   private static class AssertionDataImplBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private Assertion assertion;
      @SuppressFBWarnings(justification = "generated code")
      private NameID nameID;
      @SuppressFBWarnings(justification = "generated code")
      private Saml2IdpEntityId idpEntityId;
      @SuppressFBWarnings(justification = "generated code")
      private Saml2SpEntityId spEntityId;
      @SuppressFBWarnings(justification = "generated code")
      private Optional<Saml2RelayState> relayState;
      @SuppressFBWarnings(justification = "generated code")
      private Optional<Saml2InResponseTo> inResponseTo;

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

      @SuppressFBWarnings(justification = "generated code")
      private AssertionDataImpl.AssertionDataImplBuilder assertion(@NonNull Assertion assertion) {
         if (assertion == null) {
            throw new NullPointerException("assertion is marked non-null but is null");
         }

         this.assertion = assertion;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      private AssertionDataImpl.AssertionDataImplBuilder nameID(@NonNull NameID nameID) {
         if (nameID == null) {
            throw new NullPointerException("nameID is marked non-null but is null");
         }

         this.nameID = nameID;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      private AssertionDataImpl.AssertionDataImplBuilder idpEntityId(@NonNull Saml2IdpEntityId idpEntityId) {
         if (idpEntityId == null) {
            throw new NullPointerException("idpEntityId is marked non-null but is null");
         }

         this.idpEntityId = idpEntityId;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      private AssertionDataImpl.AssertionDataImplBuilder spEntityId(@NonNull Saml2SpEntityId spEntityId) {
         if (spEntityId == null) {
            throw new NullPointerException("spEntityId is marked non-null but is null");
         }

         this.spEntityId = spEntityId;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      private AssertionDataImpl.AssertionDataImplBuilder relayState(@NonNull Optional<Saml2RelayState> relayState) {
         if (relayState == null) {
            throw new NullPointerException("relayState is marked non-null but is null");
         }

         this.relayState = relayState;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      private AssertionDataImpl.AssertionDataImplBuilder inResponseTo(@NonNull Optional<Saml2InResponseTo> inResponseTo) {
         if (inResponseTo == null) {
            throw new NullPointerException("inResponseTo is marked non-null but is null");
         }

         this.inResponseTo = inResponseTo;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      private AssertionDataImpl build() {
         return new AssertionDataImpl(this.assertion, this.nameID, this.idpEntityId, this.spEntityId, this.relayState, this.inResponseTo);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "AssertionDataImpl.AssertionDataImplBuilder(assertion="
            + this.assertion
            + ", nameID="
            + this.nameID
            + ", idpEntityId="
            + this.idpEntityId
            + ", spEntityId="
            + this.spEntityId
            + ", relayState="
            + this.relayState
            + ", inResponseTo="
            + this.inResponseTo
            + ")";
      }
   }
}
