package com.airlock.iam.authentication.domain.service.secretquestions;

import com.airlock.iam.authentication.domain.model.secretquestions.SecretQuestionStatus;
import com.airlock.iam.authentication.domain.model.secretquestions.SecretQuestionStatusList;
import com.airlock.iam.authentication.domain.model.secretquestions.SecretQuestionsProvision;
import com.airlock.iam.base.api.domain.model.exception.ViolationException;
import com.airlock.iam.base.api.domain.model.validation.ValidationResult;
import com.airlock.iam.common.api.domain.model.store.user.UserStore;
import com.airlock.iam.common.application.configuration.secretquestion.GeneralSecretQuestionsSettings;
import com.airlock.iam.common.domain.model.token.secretquestions.SecretQuestion;
import com.airlock.iam.common.domain.model.token.secretquestions.SecretQuestionAnswer;
import com.airlock.iam.common.domain.model.token.secretquestions.SecretQuestionsRepository;
import com.airlock.iam.common.domain.service.exception.FeatureDisabledException;
import com.airlock.iam.common.domain.service.secretquestions.SecretQuestionsService;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.log.api.signing.SigningLogger;
import com.google.common.base.Preconditions;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.stream.Collectors;

public class SecretQuestionsLoginService {
   private static final ELogger LOGGER = ELoggerFactory.getLogger(SecretQuestionsLoginService.class);
   private final SecretQuestionsService commonService;
   private final SecretQuestionHashService hashService;
   private final GeneralSecretQuestionsSettings secretQuestionsSettings;
   private final SecretQuestionsRepository secretQuestionsRepository;
   private final SecretQuestionAnswerValidator secretQuestionAnswerValidator;
   private final SigningLogger signingLogger;

   @Inject
   public SecretQuestionsLoginService(
      SecretQuestionsService commonService,
      SecretQuestionHashService hashService,
      GeneralSecretQuestionsSettings settings,
      SecretQuestionsRepository tokenRepository,
      SecretQuestionAnswerValidator secretQuestionAnswerValidator,
      SigningLogger logger
   ) {
      this.commonService = commonService;
      this.hashService = hashService;
      this.secretQuestionsSettings = settings;
      this.secretQuestionsRepository = tokenRepository;
      this.secretQuestionAnswerValidator = secretQuestionAnswerValidator;
      this.signingLogger = logger;
   }

   public SecretQuestionsLoginService(
      Optional<SecretQuestionsService> commonService,
      GeneralSecretQuestionsSettings secretQuestionsSettings,
      SecretQuestionsRepository secretQuestionsRepository,
      UserStore userStore,
      SecretQuestionAnswerValidator secretQuestionAnswerValidator,
      SigningLogger logger,
      SecretQuestionHashService hashService
   ) {
      this.hashService = hashService;
      if (commonService != null && commonService.isPresent()) {
         this.commonService = commonService.get();
      } else {
         this.commonService = SecretQuestionsService.builder()
            .userStore(userStore)
            .secretQuestionsSettings(secretQuestionsSettings)
            .secretQuestionsRepository(secretQuestionsRepository)
            .signingLogger(logger)
            .build();
      }

      this.secretQuestionsSettings = secretQuestionsSettings;
      this.secretQuestionsRepository = secretQuestionsRepository;
      this.secretQuestionAnswerValidator = secretQuestionAnswerValidator;
      this.signingLogger = logger;
   }

   public List<String> getAvailableQuestions() {
      List<String> questions = new ArrayList<>(this.secretQuestionsSettings.getQuestionResourceKeys());
      Collections.shuffle(questions);
      return questions;
   }

   public List<String> getQuestionsToBeAnswered(String userId) {
      Preconditions.checkNotNull(userId);
      this.checkSecretQuestionsEnabled(userId);
      List<String> questions = this.getAvailableQuestions();

      for (SecretQuestion question : this.getQuestionsForChallengeInternal(userId)) {
         questions.remove(question.getQuestionResourceKey());
      }

      return questions;
   }

   public List<SecretQuestion> getQuestionsForChallenge(String userId) {
      this.checkSecretQuestionsEnabled(userId);
      return this.getQuestionsForChallengeInternal(userId);
   }

   public List<SecretQuestion> getImpersonalChallenge(String userId) {
      List<SecretQuestion> secretQuestions = new ArrayList<>();

      for (String resourceKey : this.secretQuestionsSettings.getQuestionResourceKeys()) {
         SecretQuestion secretQuestion = SecretQuestion.builder().questionResourceKey(resourceKey).isValid(true).build();
         secretQuestions.add(secretQuestion);
      }

      return this.shuffleAndTrimChallenge(secretQuestions, userId);
   }

   public List<SecretQuestion> getChallenge(String userId) {
      Preconditions.checkNotNull(userId);
      this.checkSecretQuestionsEnabled(userId);
      List<SecretQuestion> secretQuestions = this.getQuestionsForChallengeInternal(userId);
      return this.shuffleAndTrimChallenge(secretQuestions);
   }

   private List<SecretQuestion> shuffleAndTrimChallenge(List<SecretQuestion> secretQuestions) {
      Collections.shuffle(secretQuestions);
      return this.trimChallenge(secretQuestions);
   }

   @SuppressFBWarnings(value = "PREDICTABLE_RANDOM", justification = "Random solely used once for unpredictable but deterministic ordering based on username.")
   private List<SecretQuestion> shuffleAndTrimChallenge(List<SecretQuestion> secretQuestions, String userId) {
      int seed = userId.hashCode();
      Random pseudoRandom = new Random(seed);
      Collections.shuffle(secretQuestions, pseudoRandom);
      return this.trimChallenge(secretQuestions);
   }

   private List<SecretQuestion> trimChallenge(List<SecretQuestion> secretQuestions) {
      Optional<Integer> numberOfChallengeQuestions = this.secretQuestionsSettings.getNumberOfChallengeQuestions();
      int requiredNumberOfProvisionedAnswers = this.secretQuestionsSettings.getRequiredNumberOfProvisionedAnswers();
      if (numberOfChallengeQuestions.isPresent()) {
         if (secretQuestions.size() >= numberOfChallengeQuestions.get()) {
            return secretQuestions.subList(0, numberOfChallengeQuestions.get());
         }
      } else if (secretQuestions.size() >= requiredNumberOfProvisionedAnswers) {
         return secretQuestions.subList(0, requiredNumberOfProvisionedAnswers);
      }

      return Collections.emptyList();
   }

   public boolean isSecretQuestionsEnabled(String userId) {
      return this.commonService.isSecretQuestionsEnabled(userId);
   }

   public int getNumberOfMissingAnswers(String userId) {
      return this.commonService.getNumberOfMissingAnswers(userId);
   }

   public boolean isProvisioningRequired(String userId) {
      return this.commonService.isProvisioningRequired(userId);
   }

   public boolean verifyChallengeAnswers(List<SecretQuestionAnswer> answers) {
      Preconditions.checkNotNull(answers);
      if (answers.isEmpty()) {
         return false;
      }

      this.checkAnswersHaveSameUserId(answers);
      String userId = answers.getFirst().getUserId();
      this.checkSecretQuestionsEnabled(userId);
      List<String> alreadyVerified = new ArrayList<>();
      int counter = 0;

      for (SecretQuestionAnswer answer : answers) {
         if (answer.isSet() && !alreadyVerified.contains(answer.getQuestionResourceKey())) {
            alreadyVerified.add(answer.getQuestionResourceKey());
            if (!this.commonService.verifyAnswer(answer)) {
               this.signingLogger.log("Could not verify challenge for user \"%s\"", new Object[]{userId});
               return false;
            }

            counter++;
         }
      }

      Optional<Integer> numberOfChallengeAnswers = this.secretQuestionsSettings.getNumberOfChallengeAnswers();
      if (numberOfChallengeAnswers.isPresent()) {
         boolean challengeVerified = counter >= numberOfChallengeAnswers.get();
         this.signingLogger.log("%s challenge for user \"%s\"", new Object[]{challengeVerified ? "Verified" : "Could not verify", userId});
         return challengeVerified;
      } else if (counter >= this.secretQuestionsSettings.getRequiredNumberOfProvisionedAnswers()) {
         this.signingLogger.log("Verified challenge for user \"%s\"", new Object[]{userId});
         return true;
      } else {
         this.signingLogger.log("Could not verify challenge for user \"%s\"", new Object[]{userId});
         return false;
      }
   }

   public void setAnswer(SecretQuestionAnswer answer) throws ViolationException {
      Preconditions.checkNotNull(answer);
      Preconditions.checkNotNull(answer.getUserId());
      this.checkSecretQuestionsEnabled(answer.getUserId());
      ValidationResult validationResult = this.secretQuestionAnswerValidator.validate(answer);
      if (!validationResult.isValid()) {
         throw new ViolationException("The secret question answer is not valid: " + validationResult.getFailure());
      }

      SecretQuestion secretQuestion = this.hashService.convertToSecretQuestion(answer);
      this.deleteAnswerInternal(answer.getUserId(), answer.getQuestionResourceKey());
      this.secretQuestionsRepository.persist(answer.getUserId(), answer.getUserId(), secretQuestion);
      this.signingLogger.log("Set new answer to secret question \"%s\" for user \"%s\"", new Object[]{answer.getQuestionResourceKey(), answer.getUserId()});
   }

   public ValidationResult validateAnswer(SecretQuestionAnswer answer) {
      this.checkSecretQuestionsEnabled(answer.getUserId());
      return this.secretQuestionAnswerValidator.validate(answer);
   }

   private boolean deleteAnswerInternal(String userId, String questionResourceKey) {
      if (this.secretQuestionsRepository.delete(userId, questionResourceKey)) {
         this.signingLogger.log("Deleted Secret Question token for user \"%s\"", new Object[]{userId});
         return true;
      } else {
         return false;
      }
   }

   private List<SecretQuestion> getQuestionsForChallengeInternal(String userId) {
      return this.secretQuestionsRepository.query().forUser(userId).withoutRemovedQuestions().withoutBlockedAnswers().getResults();
   }

   private void checkSecretQuestionsEnabled(String userId) {
      boolean enabled = this.isSecretQuestionsEnabled(userId);
      FeatureDisabledException.checkTrue(enabled, userId, "Secret Questions are not enabled for user '" + userId + "'");
   }

   private void checkAnswersHaveSameUserId(List<SecretQuestionAnswer> answers) {
      if (answers.isEmpty()) {
         throw new ViolationException("At least one answer to a challenge is required");
      }

      String userId = answers.getFirst().getUserId();
      Preconditions.checkNotNull(userId);

      for (SecretQuestionAnswer answer : answers) {
         if (!userId.equals(answer.getUserId())) {
            throw new ViolationException("The user IDs in the list of secret question answers are not identical");
         }
      }
   }

   public SecretQuestionStatusList status(String userId) {
      return SecretQuestionStatusList.builder()
         .missingAnswers(this.commonService.getNumberOfMissingAnswers(userId))
         .secretQuestions(this.statusOfSecretQuestions(userId))
         .build();
   }

   private List<SecretQuestionStatus> statusOfSecretQuestions(String userId) {
      return this.secretQuestionsSettings
         .getQuestionResourceKeys()
         .stream()
         .map(questionKey -> this.statusOfSecretQuestion(userId, questionKey))
         .collect(Collectors.toList());
   }

   private SecretQuestionStatus statusOfSecretQuestion(String userId, String questionResourceKey) {
      Optional<SecretQuestion> secretQuestion = this.secretQuestionsRepository.query().forUser(userId).withQuestionResourceKey(questionResourceKey).getResult();
      return SecretQuestionStatus.builder().id(questionResourceKey).answerProvisioned(secretQuestion.isPresent()).build();
   }

   public ValidationResult register(String userId, SecretQuestionsProvision provision) {
      this.checkSecretQuestionsEnabled(userId);
      List<SecretQuestionAnswer> answers = provision.getProvisionedQuestions()
         .stream()
         .map(
            questionProvision -> SecretQuestionAnswer.builder()
               .userId(userId)
               .questionResourceKey(questionProvision.getId())
               .answer(questionProvision.getAnswer())
               .build()
         )
         .collect(Collectors.toList());
      ValidationResult validationResult = ValidationResult.valid();
      answers.forEach(answer -> validationResult.mergeWith(this.secretQuestionAnswerValidator.validate(answer, answers)));
      if (!validationResult.isValid()) {
         LOGGER.debug(String.format("User %s provided invalid secret question answers: %s", userId, validationResult));
      } else {
         answers.forEach(this::setAnswer);
      }

      return validationResult;
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public static class SecretQuestionsLoginServiceBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private Optional<SecretQuestionsService> commonService;
      @SuppressFBWarnings(justification = "generated code")
      private GeneralSecretQuestionsSettings secretQuestionsSettings;
      @SuppressFBWarnings(justification = "generated code")
      private SecretQuestionsRepository secretQuestionsRepository;
      @SuppressFBWarnings(justification = "generated code")
      private UserStore userStore;
      @SuppressFBWarnings(justification = "generated code")
      private SecretQuestionAnswerValidator secretQuestionAnswerValidator;
      @SuppressFBWarnings(justification = "generated code")
      private SigningLogger logger;
      @SuppressFBWarnings(justification = "generated code")
      private SecretQuestionHashService hashService;

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

      @SuppressFBWarnings(justification = "generated code")
      public SecretQuestionsLoginService.SecretQuestionsLoginServiceBuilder commonService(Optional<SecretQuestionsService> commonService) {
         this.commonService = commonService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SecretQuestionsLoginService.SecretQuestionsLoginServiceBuilder secretQuestionsSettings(GeneralSecretQuestionsSettings secretQuestionsSettings) {
         this.secretQuestionsSettings = secretQuestionsSettings;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SecretQuestionsLoginService.SecretQuestionsLoginServiceBuilder secretQuestionsRepository(SecretQuestionsRepository secretQuestionsRepository) {
         this.secretQuestionsRepository = secretQuestionsRepository;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SecretQuestionsLoginService.SecretQuestionsLoginServiceBuilder userStore(UserStore userStore) {
         this.userStore = userStore;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SecretQuestionsLoginService.SecretQuestionsLoginServiceBuilder secretQuestionAnswerValidator(
         SecretQuestionAnswerValidator secretQuestionAnswerValidator
      ) {
         this.secretQuestionAnswerValidator = secretQuestionAnswerValidator;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SecretQuestionsLoginService.SecretQuestionsLoginServiceBuilder logger(SigningLogger logger) {
         this.logger = logger;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SecretQuestionsLoginService.SecretQuestionsLoginServiceBuilder hashService(SecretQuestionHashService hashService) {
         this.hashService = hashService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SecretQuestionsLoginService build() {
         return new SecretQuestionsLoginService(
            this.commonService,
            this.secretQuestionsSettings,
            this.secretQuestionsRepository,
            this.userStore,
            this.secretQuestionAnswerValidator,
            this.logger,
            this.hashService
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "SecretQuestionsLoginService.SecretQuestionsLoginServiceBuilder(commonService="
            + this.commonService
            + ", secretQuestionsSettings="
            + this.secretQuestionsSettings
            + ", secretQuestionsRepository="
            + this.secretQuestionsRepository
            + ", userStore="
            + this.userStore
            + ", secretQuestionAnswerValidator="
            + this.secretQuestionAnswerValidator
            + ", logger="
            + this.logger
            + ", hashService="
            + this.hashService
            + ")";
      }
   }
}
