package com.airlock.iam.authentication.domain.model.sso;

import com.airlock.iam.authentication.api.domain.model.step.AuthenticationFlowStep;
import com.airlock.iam.authentication.api.domain.model.step.AuthenticationStepResult;
import com.airlock.iam.authentication.api.domain.model.step.AuthenticationStepResultFactory;
import com.airlock.iam.authentication.api.domain.model.step.IdentityProvidingFlowStep;
import com.airlock.iam.authentication.api.domain.model.step.UserIdentifyingStep;
import com.airlock.iam.authentication.api.domain.service.username.IdentificationResult;
import com.airlock.iam.authentication.api.domain.service.username.IdentificationResultConverter;
import com.airlock.iam.authentication.api.domain.service.username.IdentityProvidingService;
import com.airlock.iam.authentication.application.configuration.sso.SsoTicketIdentifyingStepConfigBase;
import com.airlock.iam.authentication.impl.domain.model.authentication.UserIdentificationResultData;
import com.airlock.iam.base.api.domain.model.plugin.LogicForConfig;
import com.airlock.iam.base.api.domain.model.user.UserId;
import com.airlock.iam.base.misc.result.Result;
import com.airlock.iam.common.api.domain.model.authmethod.AuthenticationMethod;
import com.airlock.iam.common.api.domain.model.user.UserIdentity;
import com.airlock.iam.common.domain.model.sso.SsoTicketStepSettings;
import com.airlock.iam.common.domain.service.sso.SsoTicketService;
import com.airlock.iam.core.domain.model.authentication.factor.KnownAuthenticationFactor;
import com.airlock.iam.core.misc.util.ticket.Ticket;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.flow.api.domain.model.session.FlowAttributeRepository;
import com.airlock.iam.flow.api.domain.model.step.StepError;
import com.airlock.iam.flow.api.domain.model.tag.Tag;
import com.airlock.iam.flow.shared.api.domain.model.session.UserSession;
import com.airlock.iam.log.api.helpdesk.HelpdeskLogger;
import com.airlock.iam.log.api.signing.SigningLogger;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

@LogicForConfig(SsoTicketIdentifyingStepConfigBase.class)
public final class SsoTicketIdentifyingStepImpl implements AuthenticationFlowStep, IdentityProvidingFlowStep {
   private static final ELogger LOG = ELoggerFactory.getLogger(SsoTicketIdentifyingStepImpl.class);
   private final Provider<IdentityProvidingService> identificationService;
   private final Provider<FlowAttributeRepository> attributes;
   private final UserSession userSession;
   private final SsoTicketService ticketService;
   private final SsoTicketStepSettings settings;
   private final HelpdeskLogger helpdeskLogger;
   private final SigningLogger signingLogger;
   private final AuthenticationMethod authMethod;
   private final Provider<List<SsoTicketTagExtractor>> tagExtractors;

   @Inject
   public SsoTicketIdentifyingStepImpl(
      Provider<IdentityProvidingService> identificationService,
      Provider<FlowAttributeRepository> attributes,
      UserSession userSession,
      SsoTicketService ticketService,
      SsoTicketStepSettings settings,
      SigningLogger signingLogger,
      HelpdeskLogger helpdeskLogger,
      AuthenticationMethod authMethod,
      Provider<List<SsoTicketTagExtractor>> tagExtractors
   ) {
      this.identificationService = identificationService;
      this.attributes = attributes;
      this.userSession = userSession;
      this.ticketService = ticketService;
      this.settings = settings;
      this.signingLogger = signingLogger;
      this.helpdeskLogger = helpdeskLogger;
      this.authMethod = authMethod;
      this.tagExtractors = tagExtractors;
   }

   @Override
   public AuthenticationStepResult initialize() {
      Result<SsoTicketService.SsoTicket, SsoTicketService.SsoTicketError> result = this.ticketService.extractSsoTicket();
      if (result.isFailure()) {
         return this.stepResultFactory().fail(toStepError(result.getFailure())).build();
      }

      SsoTicketService.SsoTicket ticket = result.get();
      IdentificationResult identifyResult = ((IdentityProvidingService)this.identificationService.get()).tryIdentify(ticket.username());
      if (identifyResult.isFailure() && identifyResult.hasError(IdentificationResult.IdentificationFailure.IdentificationError.DIFFERENT_USERNAME)) {
         return IdentificationResultConverter.convertToAuthStepResult(
               identifyResult.getFailure().orElseThrow(), this.stepResultFactory(), UserIdentifyingStep.UserIdentifyingStepAction.USERNAME_REQUIRED
            )
            .build();
      }

      if (this.isFailedIdentification(ticket, identifyResult)) {
         identifyResult.getUserIdentity().ifPresent(id -> this.logFailure(id, identifyResult.getFailure().orElseThrow().error()));
         return this.stepResultFactory().fail(SsoTicketIdentifyingStepImpl.SsoTicketIdentifyStepErrors.SSO_TOKEN_VERIFICATION_FAILED).build();
      }

      UserIdentity identity = identifyResult.getUserIdentity().orElseThrow();
      if (ticket.representerId().isPresent()) {
         UserIdentity identityWithRepresenter = identity.withRepresenter(ticket.representerId().get());
         this.logRepresentationStart(identityWithRepresenter);
         return this.userIdentifiedSuccessResult(identityWithRepresenter, this.extractTags(ticket.ticket()));
      }

      if (this.settings.ignoreTicketDataOfTicketsForAuthenticatedUser()) {
         Optional<UserIdentity> verifiedUserSessionIdentity = this.userSession.userId().filter(id -> id.hasSameId(identity));
         if (verifiedUserSessionIdentity.isPresent()) {
            this.logIgnoringTicket(verifiedUserSessionIdentity.orElseThrow());
            return this.userIdentifiedSuccessResult(verifiedUserSessionIdentity.orElseThrow(), Set.of());
         }
      }

      if (!this.addSsoAttributesToFlowSession(ticket)) {
         LOG.info("Rejecting SSO ticket because another SSO Ticket has already been accepted in the current user session.");
         return this.stepResultFactory().fail(SsoTicketIdentifyingStepImpl.SsoTicketIdentifyStepErrors.SSO_TOKEN_UNEXPECTED).build();
      } else {
         this.logSuccess(identity);
         return this.userIdentifiedSuccessResult(identity, this.extractTags(ticket.ticket()));
      }
   }

   private Set<Tag> extractTags(Ticket ticket) {
      return ((List)this.tagExtractors.get()).stream().flatMap(extractor -> extractor.extract(ticket).stream()).collect(Collectors.toSet());
   }

   private static StepError toStepError(SsoTicketService.SsoTicketError failure) {
      return switch (failure.error()) {
         case SSO_TOKEN_MISSING -> SsoTicketIdentifyingStepImpl.SsoTicketIdentifyStepErrors.SSO_TOKEN_MISSING;
         case SSO_TOKEN_VERIFICATION_FAILED -> SsoTicketIdentifyingStepImpl.SsoTicketIdentifyStepErrors.SSO_TOKEN_VERIFICATION_FAILED;
      };
   }

   private boolean addSsoAttributesToFlowSession(SsoTicketService.SsoTicket ticket) {
      if (((FlowAttributeRepository)this.attributes.get()).tryGetAttribute(SsoFlowSessionAttribute.class).isEmpty()) {
         ((FlowAttributeRepository)this.attributes.get()).putOrMergeAttribute(new SsoFlowSessionAttribute(ticket.roles(), ticket.contextData()));
         return true;
      } else {
         return false;
      }
   }

   private boolean isFailedIdentification(SsoTicketService.SsoTicket ticket, IdentificationResult result) {
      if (!result.isFailure()) {
         return false;
      } else if (!result.hasError(IdentificationResult.IdentificationFailure.IdentificationError.USER_LOCKED)) {
         return true;
      } else {
         UserId userId = result.getUserIdentity().orElseThrow().id();
         if (ticket.representerId().isPresent() && this.settings.allowLockedUsers()) {
            this.signingLogger.log("User '%s' is locked, but is allowed to continue because of representation settings.", new Object[]{userId});
            return false;
         } else {
            return true;
         }
      }
   }

   private void logSuccess(UserIdentity userIdentity) {
      this.signingLogger.log("User '%s' was successfully identified using an SSO ticket.", new Object[]{userIdentity.id()});
      this.helpdeskLogger.log(userIdentity.id(), "User ${affectedUser} was successfully identified using an SSO ticket.");
   }

   private void logRepresentationStart(UserIdentity userIdentity) {
      String representerId = userIdentity.representerId().orElseThrow().value();
      this.signingLogger
         .log("User '%s' was successfully identified using an SSO ticket and is represented by '%s'.", new Object[]{userIdentity.id(), representerId});
      this.helpdeskLogger
         .log(
            userIdentity.id(), "User ${affectedUser} was successfully identified using an SSO ticket and is represented by '%s'.", new Object[]{representerId}
         );
      this.helpdeskLogger
         .log(representerId, "User ${affectedUser} represents user '%s' after successful SSO ticket identification.", new Object[]{userIdentity.id()});
   }

   private void logFailure(UserIdentity userIdentity, IdentificationResult.IdentificationFailure.IdentificationError error) {
      String logMessage = "Identification of user '%s' failed, additional information: %s";
      this.signingLogger.log(logMessage, new Object[]{userIdentity.id(), error});
      this.helpdeskLogger.log(userIdentity.id(), logMessage, new Object[]{userIdentity.id(), error});
   }

   private void logIgnoringTicket(UserIdentity userIdentity) {
      this.signingLogger.log("User '%s' already authenticated in user session, ignoring SSO ticket with the same identity.", new Object[]{userIdentity.id()});
      this.helpdeskLogger.log(userIdentity.id(), "User ${affectedUser} already authenticated in user session, ignoring SSO ticket with the same identity.");
   }

   private AuthenticationStepResult userIdentifiedSuccessResult(UserIdentity identity, Set<Tag> extractedTags) {
      return this.stepResultFactory().success().internalResultData(new UserIdentificationResultData(identity)).obtainedTags(extractedTags).build();
   }

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

   @Override
   public String getDescription() {
      return "SSO Ticket Identifying Step";
   }

   public AuthenticationStepResultFactory stepResultFactory() {
      return AuthenticationStepResultFactory.forFactor(KnownAuthenticationFactor.TOKEN);
   }

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

   public enum SsoTicketIdentifyStepErrors implements StepError {
      SSO_TOKEN_MISSING,
      SSO_TOKEN_UNEXPECTED,
      SSO_TOKEN_VERIFICATION_FAILED;
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class SsoTicketIdentifyingStepImplBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private Provider<IdentityProvidingService> identificationService;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<FlowAttributeRepository> attributes;
      @SuppressFBWarnings(justification = "generated code")
      private UserSession userSession;
      @SuppressFBWarnings(justification = "generated code")
      private SsoTicketService ticketService;
      @SuppressFBWarnings(justification = "generated code")
      private SsoTicketStepSettings settings;
      @SuppressFBWarnings(justification = "generated code")
      private SigningLogger signingLogger;
      @SuppressFBWarnings(justification = "generated code")
      private HelpdeskLogger helpdeskLogger;
      @SuppressFBWarnings(justification = "generated code")
      private AuthenticationMethod authMethod;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<List<SsoTicketTagExtractor>> tagExtractors;

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

      @SuppressFBWarnings(justification = "generated code")
      public SsoTicketIdentifyingStepImpl.SsoTicketIdentifyingStepImplBuilder identificationService(Provider<IdentityProvidingService> identificationService) {
         this.identificationService = identificationService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SsoTicketIdentifyingStepImpl.SsoTicketIdentifyingStepImplBuilder attributes(Provider<FlowAttributeRepository> attributes) {
         this.attributes = attributes;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SsoTicketIdentifyingStepImpl.SsoTicketIdentifyingStepImplBuilder userSession(UserSession userSession) {
         this.userSession = userSession;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SsoTicketIdentifyingStepImpl.SsoTicketIdentifyingStepImplBuilder ticketService(SsoTicketService ticketService) {
         this.ticketService = ticketService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SsoTicketIdentifyingStepImpl.SsoTicketIdentifyingStepImplBuilder settings(SsoTicketStepSettings settings) {
         this.settings = settings;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SsoTicketIdentifyingStepImpl.SsoTicketIdentifyingStepImplBuilder signingLogger(SigningLogger signingLogger) {
         this.signingLogger = signingLogger;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SsoTicketIdentifyingStepImpl.SsoTicketIdentifyingStepImplBuilder helpdeskLogger(HelpdeskLogger helpdeskLogger) {
         this.helpdeskLogger = helpdeskLogger;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SsoTicketIdentifyingStepImpl.SsoTicketIdentifyingStepImplBuilder authMethod(AuthenticationMethod authMethod) {
         this.authMethod = authMethod;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SsoTicketIdentifyingStepImpl.SsoTicketIdentifyingStepImplBuilder tagExtractors(Provider<List<SsoTicketTagExtractor>> tagExtractors) {
         this.tagExtractors = tagExtractors;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SsoTicketIdentifyingStepImpl build() {
         return new SsoTicketIdentifyingStepImpl(
            this.identificationService,
            this.attributes,
            this.userSession,
            this.ticketService,
            this.settings,
            this.signingLogger,
            this.helpdeskLogger,
            this.authMethod,
            this.tagExtractors
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "SsoTicketIdentifyingStepImpl.SsoTicketIdentifyingStepImplBuilder(identificationService="
            + this.identificationService
            + ", attributes="
            + this.attributes
            + ", userSession="
            + this.userSession
            + ", ticketService="
            + this.ticketService
            + ", settings="
            + this.settings
            + ", signingLogger="
            + this.signingLogger
            + ", helpdeskLogger="
            + this.helpdeskLogger
            + ", authMethod="
            + this.authMethod
            + ", tagExtractors="
            + this.tagExtractors
            + ")";
      }
   }
}
