package com.airlock.iam.factor.domain.model.fido;

import com.airlock.iam.base.api.domain.model.mapification.Mapifiable;
import com.airlock.iam.base.api.domain.model.mapification.MapifiedValues;
import com.airlock.iam.base.domain.model.mapification.MapificationUtils;
import com.airlock.iam.base.domain.model.mapification.MapifiedValuesBuilder;
import com.airlock.iam.fido.api.domain.model.FidoPublicKeyCredentialId;
import com.airlock.iam.fido.api.domain.model.FidoRelyingPartyId;
import com.google.common.annotations.VisibleForTesting;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.annotation.Nullable;
import java.time.Instant;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import lombok.NonNull;

public final class FidoPublicKeyCredentialRequestOptions implements Mapifiable {
   private static final String CHALLENGE_KEY = "challenge";
   private static final String ALLOWED_CREDENTIAL_IDS_AND_TRANSPORTS_KEY = "allowedCredentialIdsAndTransports";
   private static final String IS_PASSWORDLESS_KEY = "isPasswordless";
   private static final String AUTHENTICATION_SETTINGS_KEY = "authenticationSettings";
   private static final String CREATED_AT_KEY = "createdAt";
   private static final String RELYING_PARTY_ID_KEY = "relyingPartyId";
   private static final String CHALLENGE_ATTRIBUTE = "challenge";
   private static final String TIMEOUT_ATTRIBUTE = "timeout";
   private static final String USER_VERIFICATION_ATTRIBUTE = "userVerification";
   private static final String RP_ID_ATTRIBUTE = "rpId";
   @VisibleForTesting
   static final String ALLOW_CREDENTIALS_ATTRIBUTE = "allowCredentials";
   @NonNull
   private final FidoChallenge challenge;
   @Nullable
   private final List<FidoCredentialIdAndTransports> allowedCredentialIdsAndTransports;
   @NonNull
   private final FidoCredentialAuthenticationSettings authenticationSettings;
   @NonNull
   private final Instant createdAt;
   @NonNull
   private final Instant validUntil;
   @NonNull
   private final FidoRelyingPartyId relyingPartyId;

   private FidoPublicKeyCredentialRequestOptions(
      @NonNull FidoRelyingPartyId relyingPartyId,
      @NonNull FidoChallenge challenge,
      @Nullable List<FidoCredentialIdAndTransports> allowedCredentialIdsAndTransports,
      @NonNull FidoCredentialAuthenticationSettings authenticationSettings,
      @NonNull Instant createdAt
   ) {
      if (relyingPartyId == null) {
         throw new NullPointerException("relyingPartyId is marked non-null but is null");
      }

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

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

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

      this.relyingPartyId = relyingPartyId;
      this.challenge = challenge;
      this.allowedCredentialIdsAndTransports = allowedCredentialIdsAndTransports;
      this.authenticationSettings = authenticationSettings;
      this.createdAt = createdAt;
      this.validUntil = createdAt.plusMillis(authenticationSettings.authenticationTimeout().toMillis());
   }

   public boolean isValidAt(Instant now) {
      return !now.isBefore(this.createdAt) && !now.isAfter(this.validUntil);
   }

   public Optional<List<FidoPublicKeyCredentialId>> getAllowedCredentialIds() {
      return this.isPasswordless()
         ? Optional.empty()
         : Optional.of(this.allowedCredentialIdsAndTransports.stream().map(FidoCredentialIdAndTransports::publicKeyCredentialId).toList());
   }

   @Override
   public MapifiedValues mapify() {
      return MapifiedValuesBuilder.mapifiedValues()
         .put("relyingPartyId", this.relyingPartyId.getId())
         .put("challenge", this.challenge.mapify())
         .put("isPasswordless", this.isPasswordless())
         .putIfNotEmpty("allowedCredentialIdsAndTransports", this.mapifyAllowedCredentialsAndTransports())
         .put("authenticationSettings", FidoCredentialAuthenticationSettingsMapifier.mapifyFidoCredentialAuthenticationSettings(this.authenticationSettings))
         .put("createdAt", MapificationUtils.mapifyInstant(this.createdAt))
         .build();
   }

   private List<Object> mapifyAllowedCredentialsAndTransports() {
      return this.isPasswordless()
         ? Collections.emptyList()
         : MapificationUtils.mapifyCollection(this.allowedCredentialIdsAndTransports, FidoCredentialIdAndTransports::mapify);
   }

   public static FidoPublicKeyCredentialRequestOptions demapify(MapifiedValues values) {
      return new FidoPublicKeyCredentialRequestOptions(
         FidoRelyingPartyId.relyingPartyId(values.get("relyingPartyId")),
         values.demapifyValue("challenge", FidoChallenge::demapify),
         demapifyAllowedCredentials(values),
         values.demapifyValue("authenticationSettings", FidoCredentialAuthenticationSettingsMapifier::demapifyFidoCredentialAuthenticationSettings),
         values.demapifyValue("createdAt", MapificationUtils::demapifyInstant)
      );
   }

   private boolean isPasswordless() {
      return this.allowedCredentialIdsAndTransports == null;
   }

   private static List<FidoCredentialIdAndTransports> demapifyAllowedCredentials(MapifiedValues values) {
      return values.getBoolean("isPasswordless")
         ? null
         : values.<MapifiedValues>getSet("allowedCredentialIdsAndTransports").stream().map(FidoCredentialIdAndTransports::demapify).toList();
   }

   public Map<String, Object> asAdditionalAttribute() {
      Map<String, Object> attribute = new HashMap<>();
      attribute.put("challenge", this.challenge.base64Url());
      attribute.put("timeout", this.authenticationSettings.authenticationTimeout().toMillis());
      attribute.put("userVerification", this.authenticationSettings.userVerification().name().toLowerCase(Locale.ROOT));
      attribute.put("rpId", this.relyingPartyId.getId());
      if (this.allowedCredentialIdsAndTransports != null) {
         attribute.put("allowCredentials", this.allowCredentials(this.allowedCredentialIdsAndTransports));
      }

      return attribute;
   }

   private List<Map<String, Object>> allowCredentials(List<FidoCredentialIdAndTransports> credentialIdsAndTransports) {
      return credentialIdsAndTransports.stream().filter(this::isCredentialWithConfiguredOrEmptyTransports).map(this::mapAllowCredentials).toList();
   }

   private boolean isCredentialWithConfiguredOrEmptyTransports(FidoCredentialIdAndTransports fidoCredentialIdAndTransports) {
      return noTransportsPersistedAtRegistration(fidoCredentialIdAndTransports) ? true : this.matchesConfiguredTransports(fidoCredentialIdAndTransports);
   }

   private boolean matchesConfiguredTransports(FidoCredentialIdAndTransports fidoCredentialIdAndTransports) {
      List<String> configuredTransports = this.authenticationSettings.transportsAsStrings();
      return configuredTransports.isEmpty() ? true : fidoCredentialIdAndTransports.transports().stream().anyMatch(configuredTransports::contains);
   }

   private static boolean noTransportsPersistedAtRegistration(FidoCredentialIdAndTransports fidoCredentialIdAndTransports) {
      return fidoCredentialIdAndTransports.transports().isEmpty();
   }

   private Map<String, Object> mapAllowCredentials(FidoCredentialIdAndTransports credentialIdAndTransports) {
      Map<String, Object> allowCredentials = new HashMap<>(Map.of("id", credentialIdAndTransports.publicKeyCredentialId().base64Url(), "type", "public-key"));
      if (noTransportsPersistedAtRegistration(credentialIdAndTransports)) {
         this.addConfiguredTransports(allowCredentials);
      } else {
         allowCredentials.put("transports", credentialIdAndTransports.transports().stream().toList());
      }

      return allowCredentials;
   }

   private void addConfiguredTransports(Map<String, Object> allowCredentials) {
      List<String> configuredTransports = this.authenticationSettings.transportsAsStrings();
      if (!configuredTransports.isEmpty()) {
         allowCredentials.put("transports", configuredTransports);
      }
   }

   @SuppressFBWarnings(justification = "generated code")
   public static FidoPublicKeyCredentialRequestOptions.FidoPublicKeyCredentialRequestOptionsBuilder builder() {
      return new FidoPublicKeyCredentialRequestOptions.FidoPublicKeyCredentialRequestOptionsBuilder();
   }

   @SuppressFBWarnings(justification = "generated code")
   public FidoPublicKeyCredentialRequestOptions.FidoPublicKeyCredentialRequestOptionsBuilder toBuilder() {
      return new FidoPublicKeyCredentialRequestOptions.FidoPublicKeyCredentialRequestOptionsBuilder()
         .relyingPartyId(this.relyingPartyId)
         .challenge(this.challenge)
         .allowedCredentialIdsAndTransports(this.allowedCredentialIdsAndTransports)
         .authenticationSettings(this.authenticationSettings)
         .createdAt(this.createdAt);
   }

   @NonNull
   @SuppressFBWarnings(justification = "generated code")
   public FidoChallenge getChallenge() {
      return this.challenge;
   }

   @Nullable
   @SuppressFBWarnings(justification = "generated code")
   public List<FidoCredentialIdAndTransports> getAllowedCredentialIdsAndTransports() {
      return this.allowedCredentialIdsAndTransports;
   }

   @NonNull
   @SuppressFBWarnings(justification = "generated code")
   public FidoCredentialAuthenticationSettings getAuthenticationSettings() {
      return this.authenticationSettings;
   }

   @NonNull
   @SuppressFBWarnings(justification = "generated code")
   public Instant getCreatedAt() {
      return this.createdAt;
   }

   @NonNull
   @SuppressFBWarnings(justification = "generated code")
   public Instant getValidUntil() {
      return this.validUntil;
   }

   @NonNull
   @SuppressFBWarnings(justification = "generated code")
   public FidoRelyingPartyId getRelyingPartyId() {
      return this.relyingPartyId;
   }

   @SuppressFBWarnings(justification = "generated code")
   @Override
   public boolean equals(Object o) {
      if (o == this) {
         return true;
      } else if (!(o instanceof FidoPublicKeyCredentialRequestOptions other)) {
         return false;
      } else {
         Object this$challenge = this.challenge;
         Object other$challenge = other.challenge;
         if (this$challenge == null ? other$challenge == null : this$challenge.equals(other$challenge)) {
            Object this$allowedCredentialIdsAndTransports = this.allowedCredentialIdsAndTransports;
            Object other$allowedCredentialIdsAndTransports = other.allowedCredentialIdsAndTransports;
            if (this$allowedCredentialIdsAndTransports == null
               ? other$allowedCredentialIdsAndTransports == null
               : this$allowedCredentialIdsAndTransports.equals(other$allowedCredentialIdsAndTransports)) {
               Object this$authenticationSettings = this.authenticationSettings;
               Object other$authenticationSettings = other.authenticationSettings;
               if (this$authenticationSettings == null
                  ? other$authenticationSettings == null
                  : this$authenticationSettings.equals(other$authenticationSettings)) {
                  Object this$createdAt = this.createdAt;
                  Object other$createdAt = other.createdAt;
                  if (this$createdAt == null ? other$createdAt == null : this$createdAt.equals(other$createdAt)) {
                     Object this$validUntil = this.validUntil;
                     Object other$validUntil = other.validUntil;
                     if (this$validUntil == null ? other$validUntil == null : this$validUntil.equals(other$validUntil)) {
                        Object this$relyingPartyId = this.relyingPartyId;
                        Object other$relyingPartyId = other.relyingPartyId;
                        return this$relyingPartyId == null ? other$relyingPartyId == null : this$relyingPartyId.equals(other$relyingPartyId);
                     } 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 $challenge = this.challenge;
      result = result * 59 + ($challenge == null ? 43 : $challenge.hashCode());
      Object $allowedCredentialIdsAndTransports = this.allowedCredentialIdsAndTransports;
      result = result * 59 + ($allowedCredentialIdsAndTransports == null ? 43 : $allowedCredentialIdsAndTransports.hashCode());
      Object $authenticationSettings = this.authenticationSettings;
      result = result * 59 + ($authenticationSettings == null ? 43 : $authenticationSettings.hashCode());
      Object $createdAt = this.createdAt;
      result = result * 59 + ($createdAt == null ? 43 : $createdAt.hashCode());
      Object $validUntil = this.validUntil;
      result = result * 59 + ($validUntil == null ? 43 : $validUntil.hashCode());
      Object $relyingPartyId = this.relyingPartyId;
      return result * 59 + ($relyingPartyId == null ? 43 : $relyingPartyId.hashCode());
   }

   @SuppressFBWarnings(justification = "generated code")
   @Override
   public String toString() {
      return "FidoPublicKeyCredentialRequestOptions(challenge="
         + this.getChallenge()
         + ", allowedCredentialIdsAndTransports="
         + this.getAllowedCredentialIdsAndTransports()
         + ", authenticationSettings="
         + this.getAuthenticationSettings()
         + ", createdAt="
         + this.getCreatedAt()
         + ", validUntil="
         + this.getValidUntil()
         + ", relyingPartyId="
         + this.getRelyingPartyId()
         + ")";
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class FidoPublicKeyCredentialRequestOptionsBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private FidoRelyingPartyId relyingPartyId;
      @SuppressFBWarnings(justification = "generated code")
      private FidoChallenge challenge;
      @SuppressFBWarnings(justification = "generated code")
      private List<FidoCredentialIdAndTransports> allowedCredentialIdsAndTransports;
      @SuppressFBWarnings(justification = "generated code")
      private FidoCredentialAuthenticationSettings authenticationSettings;
      @SuppressFBWarnings(justification = "generated code")
      private Instant createdAt;

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

      @SuppressFBWarnings(justification = "generated code")
      public FidoPublicKeyCredentialRequestOptions.FidoPublicKeyCredentialRequestOptionsBuilder relyingPartyId(@NonNull FidoRelyingPartyId relyingPartyId) {
         if (relyingPartyId == null) {
            throw new NullPointerException("relyingPartyId is marked non-null but is null");
         }

         this.relyingPartyId = relyingPartyId;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public FidoPublicKeyCredentialRequestOptions.FidoPublicKeyCredentialRequestOptionsBuilder challenge(@NonNull FidoChallenge challenge) {
         if (challenge == null) {
            throw new NullPointerException("challenge is marked non-null but is null");
         }

         this.challenge = challenge;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public FidoPublicKeyCredentialRequestOptions.FidoPublicKeyCredentialRequestOptionsBuilder allowedCredentialIdsAndTransports(
         @Nullable List<FidoCredentialIdAndTransports> allowedCredentialIdsAndTransports
      ) {
         this.allowedCredentialIdsAndTransports = allowedCredentialIdsAndTransports;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public FidoPublicKeyCredentialRequestOptions.FidoPublicKeyCredentialRequestOptionsBuilder authenticationSettings(
         @NonNull FidoCredentialAuthenticationSettings authenticationSettings
      ) {
         if (authenticationSettings == null) {
            throw new NullPointerException("authenticationSettings is marked non-null but is null");
         }

         this.authenticationSettings = authenticationSettings;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public FidoPublicKeyCredentialRequestOptions.FidoPublicKeyCredentialRequestOptionsBuilder createdAt(@NonNull Instant createdAt) {
         if (createdAt == null) {
            throw new NullPointerException("createdAt is marked non-null but is null");
         }

         this.createdAt = createdAt;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public FidoPublicKeyCredentialRequestOptions build() {
         return new FidoPublicKeyCredentialRequestOptions(
            this.relyingPartyId, this.challenge, this.allowedCredentialIdsAndTransports, this.authenticationSettings, this.createdAt
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "FidoPublicKeyCredentialRequestOptions.FidoPublicKeyCredentialRequestOptionsBuilder(relyingPartyId="
            + this.relyingPartyId
            + ", challenge="
            + this.challenge
            + ", allowedCredentialIdsAndTransports="
            + this.allowedCredentialIdsAndTransports
            + ", authenticationSettings="
            + this.authenticationSettings
            + ", createdAt="
            + this.createdAt
            + ")";
      }
   }
}
