package com.airlock.iam.oauth2.domain.model.remoteconsent;

import com.airlock.iam.base.misc.time.RealTimeProvider;
import com.airlock.iam.common.domain.service.jwt.JwtTicketDecoderImpl;
import com.airlock.iam.common.domain.service.jwt.encryption.JwtTicketDecrypter;
import com.airlock.iam.common.domain.service.jwt.signature.JwtSignatureVerifier;
import com.airlock.iam.core.misc.util.ticket.Ticket;
import com.airlock.iam.core.misc.util.ticket.TicketEncodingException;
import com.airlock.iam.log.audit.AuditLogger;
import com.airlock.iam.oauth2.api.domain.model.OAuth2Scope;
import com.airlock.iam.oauth2.domain.model.settings.OAuth2OidcAuthorizationCodeGrantSettings;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;

public class RemoteConsentVerifier {
   private final JwtSignatureVerifier verifier;
   private final JwtTicketDecrypter decrypter;
   private final Consumer<String> auditLog;
   private final Optional<OAuth2OidcAuthorizationCodeGrantSettings> asSettings;

   public RemoteConsentVerifier(JwtSignatureVerifier verifier, JwtTicketDecrypter decrypter, OAuth2OidcAuthorizationCodeGrantSettings asSettings) {
      this.verifier = verifier;
      this.decrypter = decrypter;
      this.asSettings = Optional.ofNullable(asSettings);
      this.auditLog = AuditLogger::log;
   }

   public RemoteConsentVerificationResult verify(String consentTokenJwt, RemoteConsentSessionNonce sessionNonce) {
      this.auditLogJwt(consentTokenJwt);
      Optional<Ticket> ticket = this.verifyJwtAndReadTicket(consentTokenJwt);
      if (ticket.isEmpty()) {
         return RemoteConsentVerificationResult.jwtInvalid();
      } else {
         Optional<RemoteConsentResponseToken> consentToken = RemoteConsentResponseTokenFactory.createConsentToken(ticket.get(), sessionNonce);
         if (consentToken.isEmpty()) {
            this.auditLog("Unable to create the consent token.");
            return RemoteConsentVerificationResult.jwtInvalid();
         } else if (!consentToken.get().consentGiven()) {
            this.auditLog("The user denied the remote consent.");
            return RemoteConsentVerificationResult.consentDenied();
         } else {
            Set<OAuth2Scope> oAuth2Scopes = consentToken.get().scopes().orElseThrow();
            if (this.asSettings.map(s -> !s.isAllowEmptyScope()).orElse(false) && oAuth2Scopes.isEmpty()) {
               this.auditLog("No scopes were provided but empty scopes are not allowed.");
               return RemoteConsentVerificationResult.consentDenied();
            } else {
               this.auditLog("The token was verified successfully. Scopes: " + OAuth2Scope.convertToScopeNames(oAuth2Scopes));
               return RemoteConsentVerificationResult.consentGivenWithScope(oAuth2Scopes);
            }
         }
      }
   }

   private Optional<Ticket> verifyJwtAndReadTicket(String consentTokenJwt) {
      try {
         Ticket ticket = this.decoder().decode(consentTokenJwt, StandardCharsets.UTF_8.name());
         return Optional.of(ticket);
      } catch (TicketEncodingException e) {
         this.auditLog("Failed to verify the remote consent ticket: " + e.getMessage());
         return Optional.empty();
      }
   }

   private JwtTicketDecoderImpl decoder() {
      return JwtTicketDecoderImpl.builder(this.verifier, new RealTimeProvider()).decrypter(Optional.of(this.decrypter)).issuerIsMandatory(false).build();
   }

   private void auditLogJwt(String consentTokenJwt) {
      this.auditLog("The consent token JWT sent by the remote site is: " + consentTokenJwt);
   }

   private void auditLog(String message) {
      this.auditLog.accept(message);
   }

   @SuppressFBWarnings(justification = "generated code")
   RemoteConsentVerifier(
      JwtSignatureVerifier verifier, JwtTicketDecrypter decrypter, Consumer<String> auditLog, Optional<OAuth2OidcAuthorizationCodeGrantSettings> asSettings
   ) {
      this.verifier = verifier;
      this.decrypter = decrypter;
      this.auditLog = auditLog;
      this.asSettings = asSettings;
   }
}
