package com.airlock.iam.flow.shared.domain.service.devicetoken;

import com.airlock.iam.common.domain.model.jwk.ECJsonWebKey;
import com.airlock.iam.common.domain.model.jwt.signature.JsonWebSignatureToken;
import com.airlock.iam.common.domain.model.jwt.signature.JwsAlgorithmEc;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.flow.shared.domain.model.devicetoken.CheckDeviceTokenJwtRequest;
import com.airlock.iam.flow.shared.domain.model.devicetoken.DeviceToken;
import com.airlock.iam.flow.shared.domain.model.devicetoken.DeviceTokenChallenge;
import com.airlock.iam.flow.shared.domain.model.devicetoken.DeviceTokenInfo;
import com.airlock.iam.flow.shared.domain.model.devicetoken.DeviceTokenRepository;
import com.airlock.iam.log.api.signing.SigningLogger;
import com.airlock.iam.security.util.SeededSecureRandom;
import com.google.common.base.Preconditions;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.codec.binary.Base64;

public class DeviceTokenAuthenticationService {
   private static final ELogger LOG = ELoggerFactory.getLogger(DeviceTokenAuthenticationService.class);
   private static final int CHALLENGE_LENGTH_IN_BYTES = 64;
   private final Provider<DeviceTokenRepository> deviceTokenRepository;
   private final Provider<Set<JwsAlgorithmEc>> allowedSignatureAlgorithms;
   private final Provider<SigningLogger> signingLogger;
   private static final String JWT_CHALLENGE_CLAIM_NAME = "challenge";

   @Inject
   public DeviceTokenAuthenticationService(
      Provider<DeviceTokenRepository> deviceTokenRepository, Provider<Set<JwsAlgorithmEc>> allowedSignatureAlgorithms, Provider<SigningLogger> signingLogger
   ) {
      this.deviceTokenRepository = deviceTokenRepository;
      this.allowedSignatureAlgorithms = allowedSignatureAlgorithms;
      this.signingLogger = signingLogger;
   }

   public boolean isDeviceTokenJwtCorrect(CheckDeviceTokenJwtRequest request, DeviceTokenChallenge sentChallenge) {
      String username = request.getUsername();
      Preconditions.checkNotNull(username);
      Preconditions.checkNotNull(request.getJwt());
      Preconditions.checkNotNull(sentChallenge);
      Optional<JsonWebSignatureToken> jwsToken = JsonWebSignatureToken.parse(request.getJwt());
      if (jwsToken.isEmpty()) {
         LOG.info(String.format("Device token JWT verification failed for user \"%s\": Invalid JWT (parse error).", username));
         return false;
      } else if (!this.isJwtSignatureAlgorithmAllowedAndChallengeValid(jwsToken.get(), sentChallenge)) {
         LOG.info(String.format("Device token JWT verification failed for user \"%s\": Invalid or expired JWT.", username));
         return false;
      } else {
         Optional<DeviceToken> deviceToken = ((DeviceTokenRepository)this.deviceTokenRepository.get())
            .getDeviceToken(username, sentChallenge.getDeviceTokenId());
         if (!isDeviceTokenValidAndSignatureCorrect(deviceToken, jwsToken.get(), username)) {
            LOG.info(String.format("Device token JWT verification failed for user \"%s\": Invalid JWT signature or device token invalid.", username));
            return false;
         } else {
            ((DeviceTokenRepository)this.deviceTokenRepository.get()).updateDeviceTokenPersistedData(deviceToken.orElseThrow());
            ((SigningLogger)this.signingLogger.get())
               .log(
                  "Successfully verified device token JWT signature for user \"%s\" and device ID \"%s\" (using challenge \"%s\").",
                  new Object[]{username, deviceToken.get().getId(), sentChallenge.getChallenge()}
               );
            return true;
         }
      }
   }

   public DeviceTokenChallenge createChallenge(String deviceTokenId) {
      SeededSecureRandom random = SeededSecureRandom.getInstance();
      byte[] values = new byte[64];
      random.nextBytes(values);
      return new DeviceTokenChallenge(deviceTokenId, Base64.encodeBase64URLSafeString(values));
   }

   public boolean hasEnabledAndValidDeviceToken(String username, String deviceTokenId) {
      return ((DeviceTokenRepository)this.deviceTokenRepository.get()).hasEnabledAndValidDeviceToken(username, deviceTokenId, Instant.now());
   }

   public Optional<String> findUserWithDeviceToken(String deviceTokenId) {
      return ((DeviceTokenRepository)this.deviceTokenRepository.get()).findUserWithDeviceToken(deviceTokenId);
   }

   public boolean hasEnabledAndValidDeviceTokens(String username) {
      return !this.getEnabledAndValidDeviceTokens(username).isEmpty();
   }

   public List<DeviceTokenInfo> getEnabledAndValidDeviceTokens(String username) {
      return ((DeviceTokenRepository)this.deviceTokenRepository.get())
         .getEnabledAndValidDeviceTokens(username)
         .stream()
         .map(DeviceTokenInfo::deviceTokenInfo)
         .toList();
   }

   private boolean isAllowedSignatureAlgorithm(String jwtAlgorithm) {
      return ((Set)this.allowedSignatureAlgorithms.get())
         .stream()
         .map(Enum::name)
         .anyMatch(allowedAlgorithm -> allowedAlgorithm.equalsIgnoreCase(jwtAlgorithm));
   }

   private static boolean isJwtSignatureCorrect(DeviceToken deviceToken, JsonWebSignatureToken jwsToken) {
      ECJsonWebKey jwk = deviceToken.getJwk();
      return jwk.verifier().map(v -> v.verify(jwsToken)).orElse(false);
   }

   private static Optional<String> getSignedChallengeFromJwt(JsonWebSignatureToken jwsToken) {
      return jwsToken.claimsSet().claim("challenge").map(Object::toString);
   }

   private boolean isJwtSignatureAlgorithmAllowedAndChallengeValid(JsonWebSignatureToken jwsToken, DeviceTokenChallenge sentChallenge) {
      String jwtAlgorithm = jwsToken.header().algorithmName();
      if (!this.isAllowedSignatureAlgorithm(jwtAlgorithm)) {
         LOG.info(
            "Device token JWT verification failed: Invalid JWT signature algorithm \""
               + jwtAlgorithm
               + "\". Allowed algorithms are "
               + ((Set)this.allowedSignatureAlgorithms.get()).stream().map(Enum::name).collect(Collectors.joining(", ", "\"", "\""))
               + "."
         );
         return false;
      } else {
         Optional<String> signedChallengeFromJwt = getSignedChallengeFromJwt(jwsToken);
         if (signedChallengeFromJwt.isEmpty()) {
            LOG.info("Device token JWT verification failed: Cannot extract challenge from JWT.");
            return false;
         } else if (!sentChallenge.getChallenge().equals(signedChallengeFromJwt.get())) {
            LOG.info("Device token JWT verification failed: Answered wrong challenge.");
            return false;
         } else if (sentChallenge.hasExpired()) {
            LOG.info("Challenge has expired (the client took too long to answer).");
            return false;
         } else {
            return true;
         }
      }
   }

   private static boolean isDeviceTokenValidAndSignatureCorrect(Optional<DeviceToken> deviceToken, JsonWebSignatureToken jwsToken, String username) {
      if (deviceToken.isEmpty()) {
         LOG.info("Could not load device token data from persistence layer for user \"" + username + "\".");
         return false;
      } else if (!deviceToken.get().isEnabled()) {
         LOG.info("Device token JWT verification failed: Device token is disabled.");
         return false;
      } else if (deviceToken.get().hasExpired()) {
         LOG.info("Device token JWT verification failed: Device token expired.");
         return false;
      } else if (!isJwtSignatureCorrect(deviceToken.get(), jwsToken)) {
         LOG.info("Device token JWT verification failed: Signature invalid.");
         return false;
      } else {
         return true;
      }
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public static class DeviceTokenAuthenticationServiceBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private Provider<DeviceTokenRepository> deviceTokenRepository;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<Set<JwsAlgorithmEc>> allowedSignatureAlgorithms;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<SigningLogger> signingLogger;

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

      @SuppressFBWarnings(justification = "generated code")
      public DeviceTokenAuthenticationService.DeviceTokenAuthenticationServiceBuilder deviceTokenRepository(
         Provider<DeviceTokenRepository> deviceTokenRepository
      ) {
         this.deviceTokenRepository = deviceTokenRepository;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DeviceTokenAuthenticationService.DeviceTokenAuthenticationServiceBuilder allowedSignatureAlgorithms(
         Provider<Set<JwsAlgorithmEc>> allowedSignatureAlgorithms
      ) {
         this.allowedSignatureAlgorithms = allowedSignatureAlgorithms;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DeviceTokenAuthenticationService.DeviceTokenAuthenticationServiceBuilder signingLogger(Provider<SigningLogger> signingLogger) {
         this.signingLogger = signingLogger;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public DeviceTokenAuthenticationService build() {
         return new DeviceTokenAuthenticationService(this.deviceTokenRepository, this.allowedSignatureAlgorithms, this.signingLogger);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "DeviceTokenAuthenticationService.DeviceTokenAuthenticationServiceBuilder(deviceTokenRepository="
            + this.deviceTokenRepository
            + ", allowedSignatureAlgorithms="
            + this.allowedSignatureAlgorithms
            + ", signingLogger="
            + this.signingLogger
            + ")";
      }
   }
}
