package com.airlock.iam.publicselfservice.domain.model.step;

import com.airlock.iam.authentication.api.domain.model.step.AuthenticationStepResult;
import com.airlock.iam.authentication.api.domain.model.step.AuthenticationStepResultBuilder;
import com.airlock.iam.authentication.api.domain.model.step.AuthenticationStepResultFactory;
import com.airlock.iam.base.api.domain.model.plugin.LogicForConfig;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.api.domain.model.authmethod.AuthenticationMethod;
import com.airlock.iam.common.api.domain.model.secretquestions.SecretQuestionsAnswer;
import com.airlock.iam.common.domain.model.principal.UserPrincipal;
import com.airlock.iam.common.domain.model.token.secretquestions.SecretQuestion;
import com.airlock.iam.common.domain.model.token.secretquestions.SecretQuestionsRepository;
import com.airlock.iam.core.domain.model.authentication.factor.KnownAuthenticationFactorDetail;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.flow.api.domain.model.step.StepSession;
import com.airlock.iam.publicselfservice.api.domain.model.PublicSelfServiceUser;
import com.airlock.iam.publicselfservice.application.configuration.steps.SecretQuestionsIdentityVerificationStepConfig;
import com.airlock.iam.publicselfservice.domain.model.flow.session.SecretQuestionsIdentityVerificationStepSession;
import com.airlock.iam.publicselfservice.domain.service.SecretQuestionSimulationService;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Random;

@LogicForConfig(SecretQuestionsIdentityVerificationStepConfig.class)
public final class SecretQuestionsIdentityVerificationStepImpl implements SecretQuestionsIdentityVerificationStep {
   private static final ELogger LOG = ELoggerFactory.getLogger(SecretQuestionsIdentityVerificationStepImpl.class);
   private static final String QUESTIONS_ATTRIBUTE = "secretQuestions";
   private final Provider<SecretQuestionsIdentityVerificationStepSession> session;
   private final Provider<PublicSelfServiceUser> user;
   private final Provider<UserPrincipal> userPrincipal;
   private final Provider<SecretQuestionSimulationService> simulationService;
   private final SecretQuestionsIdentityVerificationStepSettings settings;
   private final Provider<SecretQuestionsRepository> repository;

   @Inject
   public SecretQuestionsIdentityVerificationStepImpl(
      Provider<SecretQuestionsIdentityVerificationStepSession> session,
      Provider<PublicSelfServiceUser> user,
      Provider<UserPrincipal> userPrincipal,
      Provider<SecretQuestionSimulationService> simulationService,
      SecretQuestionsIdentityVerificationStepSettings settings,
      Provider<SecretQuestionsRepository> repository
   ) {
      this.session = session;
      this.user = user;
      this.userPrincipal = userPrincipal;
      this.simulationService = simulationService;
      this.settings = settings;
      this.repository = repository;
   }

   @Override
   public Optional<StepSession> createSession() {
      return Optional.of(new SecretQuestionsIdentityVerificationStepSession());
   }

   public AuthenticationStepResult verifyAnswers(Map<String, SecretQuestionsAnswer> answers) {
      if (!((PublicSelfServiceUser)this.user.get()).isPublicSelfServiceAllowed()
         && !((SecretQuestionsIdentityVerificationStepSession)this.session.get()).isSimulated()) {
         LOG.info("Public self-service is no longer allowed for user " + this.quotedName() + " - switching to simulation mode");
         ((SecretQuestionsIdentityVerificationStepSession)this.session.get()).setSimulated();
      }

      if (((SecretQuestionsIdentityVerificationStepSession)this.session.get()).isSimulated()) {
         LOG.info("Simulation mode active - rejecting answers without checking");
         return this.handleFailedAttempt();
      } else {
         return this.checkAnswers(answers) ? this.stepResultFactory().success().build() : this.handleFailedAttempt();
      }
   }

   public AuthenticationStepResult retrieveQuestionKeys() {
      return this.stepResultFactory()
         .stay(SecretQuestionsIdentityVerificationStep.SecretQuestionsStepAction.SECRET_QUESTIONS_ANSWER_REQUIRED)
         .resultData(
            new SecretQuestionsIdentityVerificationStep.SecretQuestions(
               ((SecretQuestionsIdentityVerificationStepSession)this.session.get())
                  .getQuestions()
                  .stream()
                  .map(SecretQuestion::getQuestionResourceKey)
                  .toList()
            )
         )
         .build();
   }

   @Override
   public AuthenticationStepResult initialize() {
      if (!((PublicSelfServiceUser)this.user.get()).isPublicSelfServiceAllowed()) {
         LOG.info(String.format("User %s is not allowed to perform public self-service. Simulating secret questions step.", this.quotedName()));
         ((SecretQuestionsIdentityVerificationStepSession)this.session.get()).setSimulated();
      } else if (!((UserPrincipal)this.userPrincipal.get()).user().isSecretQuestionsEnabled()) {
         LOG.info(
            String.format("Secret Questions are disabled for user %s. Secret questions step is simulated with pseudo random questions.", this.quotedName())
         );
         ((SecretQuestionsIdentityVerificationStepSession)this.session.get()).setSimulated();
      }

      List<SecretQuestion> allQuestions = this.getAllQuestions();
      if (allQuestions.size() == this.settings.getNumberOfRequiredQuestions()) {
         ((SecretQuestionsIdentityVerificationStepSession)this.session.get()).setQuestions(allQuestions);
      } else if (allQuestions.size() > this.settings.getNumberOfRequiredQuestions()) {
         LOG.info(
            String.format(
               "User should not have more questions than configured. User %s has %s questions but only %s were expected. Secret questions step is continued with a subset of questions.",
               this.quotedName(),
               allQuestions.size(),
               this.settings.getNumberOfRequiredQuestions()
            )
         );
         ((SecretQuestionsIdentityVerificationStepSession)this.session.get()).setQuestions(this.pseudoRandomSubsetOfQuestions(allQuestions));
      } else {
         LOG.info(
            String.format(
               "User has not provided enough answers. User %s has %s answered questions but %s are expected. Secret questions step is simulated with pseudo random questions.",
               this.quotedName(),
               allQuestions.size(),
               this.settings.getNumberOfRequiredQuestions()
            )
         );
         ((SecretQuestionsIdentityVerificationStepSession)this.session.get()).setSimulated();
         ((SecretQuestionsIdentityVerificationStepSession)this.session.get()).setQuestions(this.getPseudoRandomQuestions());
      }

      return this.addQuestions(
            this.stepResultFactory().stay(SecretQuestionsIdentityVerificationStep.SecretQuestionsStepAction.SECRET_QUESTIONS_ANSWER_REQUIRED)
         )
         .build();
   }

   @Override
   public String getDescription() {
      return "Secret Questions Identity Verification Step";
   }

   private AuthenticationStepResult handleFailedAttempt() {
      ((SecretQuestionsIdentityVerificationStepSession)this.session.get()).increaseFailedAttempts();
      return ((SecretQuestionsIdentityVerificationStepSession)this.session.get()).getFailedAttempts() > this.settings.getMaxFailedAttempts()
         ? this.stepResultFactory().fail(SecretQuestionsIdentityVerificationStep.SecretQuestionsStepError.ANSWER_WRONG).build()
         : this.addQuestions(
               this.stepResultFactory()
                  .failRetry(
                     SecretQuestionsIdentityVerificationStep.SecretQuestionsStepError.ANSWER_WRONG,
                     SecretQuestionsIdentityVerificationStep.SecretQuestionsStepAction.SECRET_QUESTIONS_ANSWER_REQUIRED
                  )
            )
            .build();
   }

   private List<SecretQuestion> getPseudoRandomQuestions() {
      return ((SecretQuestionSimulationService)this.simulationService.get())
         .getPseudoRandomQuestions(((PublicSelfServiceUser)this.user.get()).getIdentity().id());
   }

   private boolean checkAnswers(Map<String, SecretQuestionsAnswer> answers) {
      if (answers.size() != ((SecretQuestionsIdentityVerificationStepSession)this.session.get()).getQuestions().size()) {
         LOG.info("Secret questions verification failed for user " + this.quotedName() + ". User did not answer enough questions.");
         return false;
      }

      for (SecretQuestion secretQuestion : ((SecretQuestionsIdentityVerificationStepSession)this.session.get()).getQuestions()) {
         if (!this.checkQuestion(answers, secretQuestion)) {
            LOG.info("Secret questions verification failed for user " + this.quotedName() + ". The answer provided by the user was wrong.");
            return false;
         }
      }

      return true;
   }

   private boolean checkQuestion(Map<String, SecretQuestionsAnswer> answers, SecretQuestion secretQuestion) {
      Optional<SecretQuestionsAnswer> answerToQuestion = Optional.ofNullable(answers.get(secretQuestion.getQuestionResourceKey()));
      if (answerToQuestion.isEmpty()) {
         LOG.info(
            "Secret questions verification failed for user "
               + this.quotedName()
               + ". User did not answer question "
               + secretQuestion.getQuestionResourceKey()
               + "."
         );
         return false;
      } else {
         com.airlock.iam.common.domain.model.token.secretquestions.SecretQuestionAnswer answer = com.airlock.iam.common.domain.model.token.secretquestions.SecretQuestionAnswer.builder()
            .answer(answerToQuestion.get().getAnswer())
            .build();
         return secretQuestion.isAnsweredBy(answer, this.settings.isCheckUsingLatin1Encoding(), this.settings.getPasswordHash());
      }
   }

   private List<SecretQuestion> getAllQuestions() {
      return ((SecretQuestionsRepository)this.repository.get())
         .query()
         .forUser(((PublicSelfServiceUser)this.user.get()).getIdentity().id().getValue())
         .withoutRemovedQuestions()
         .getResults();
   }

   private String quotedName() {
      return LogUtil.logSafeQuote(((PublicSelfServiceUser)this.user.get()).getIdentity().id());
   }

   @SuppressFBWarnings(value = "PREDICTABLE_RANDOM", justification = "Only used for pseudorandom ordering based on username.")
   private List<SecretQuestion> pseudoRandomSubsetOfQuestions(List<SecretQuestion> allQuestions) {
      allQuestions.sort(Comparator.comparing(SecretQuestion::getQuestionResourceKey));
      int seed = ((PublicSelfServiceUser)this.user.get()).getIdentity().id().hashCode();
      Random pseudoRandom = new Random(seed);
      Collections.shuffle(allQuestions, pseudoRandom);
      return allQuestions.subList(0, this.settings.getNumberOfRequiredQuestions());
   }

   private AuthenticationStepResultBuilder addQuestions(AuthenticationStepResultBuilder builder) {
      builder.addAdditionalAttribute(
         "secretQuestions",
         ((SecretQuestionsIdentityVerificationStepSession)this.session.get()).getQuestions().stream().map(SecretQuestion::getQuestionResourceKey).toList()
      );
      return builder;
   }

   @Override
   public AuthenticationMethod getAuthenticationMethod() {
      return this.settings.getAuthMethodId();
   }

   public AuthenticationStepResultFactory stepResultFactory() {
      return AuthenticationStepResultFactory.forFactorDetail(KnownAuthenticationFactorDetail.CRAM_SECRET_QUESTIONS);
   }
}
