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

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.service.username.IdentificationResult;
import com.airlock.iam.authentication.api.domain.service.username.IdentityProvidingService;
import com.airlock.iam.authentication.application.configuration.kerberos.KerberosAuthStepConfigBase;
import com.airlock.iam.base.api.domain.model.plugin.LogicForConfig;
import com.airlock.iam.common.api.domain.model.authmethod.AuthenticationMethod;
import com.airlock.iam.common.api.domain.model.user.ProvidedUsername;
import com.airlock.iam.common.api.domain.model.user.UserIdentity;
import com.airlock.iam.common.domain.model.authentication.factor.AuthenticationFactorConverter;
import com.airlock.iam.core.domain.model.authentication.factor.AuthenticationFactorUsageImpl;
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.GenericStepError;
import com.airlock.iam.flow.api.domain.model.step.StepError;
import com.airlock.iam.flow.api.domain.model.step.StepSession;
import com.airlock.iam.flow.domain.model.result.NotAuthorizedData;
import com.airlock.iam.flow.shared.domain.model.step.access.AccessFlowStep;
import com.airlock.iam.log.api.helpdesk.HelpdeskLogger;
import com.airlock.iam.log.api.signing.SigningLogger;
import com.airlock.iam.login.application.service.kerberos.KerberosService;
import com.airlock.iam.login.application.service.kerberos.KerberosValidationResult;
import com.airlock.iam.reporting.api.domain.model.logger.Reporting;
import com.airlock.iam.reporting.domain.model.ReportingDataFactory;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.Optional;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;

@LogicForConfig(KerberosAuthStepConfigBase.class)
public final class KerberosAuthStep implements IdentityProvidingFlowStep, AuthenticationFlowStep, KerberosStep, AccessFlowStep {
   private static final ELogger LOG = ELoggerFactory.getLogger(KerberosAuthStep.class);
   private final Provider<KerberosAuthStepSession> stepSession;
   private final Provider<IdentityProvidingService> identificationService;
   private final Provider<KerberosService> kerberosService;
   private final Provider<SpnegoTicketExtractor> spnegoExtractor;
   private final Provider<KerberosAuthSettings> settings;
   private final Provider<AuthenticationMethod> authMethod;
   private final HelpdeskLogger helpdeskLogger;
   private final SigningLogger signingLogger;
   private final Reporting reportingLogger;

   @Inject
   public KerberosAuthStep(
      Provider<KerberosAuthStepSession> stepSession,
      Provider<IdentityProvidingService> identificationService,
      Provider<KerberosService> kerberosService,
      Provider<SpnegoTicketExtractor> spnegoExtractor,
      Provider<KerberosAuthSettings> settings,
      Provider<AuthenticationMethod> authMethod,
      HelpdeskLogger helpdeskLogger,
      SigningLogger signingLogger,
      Reporting reportingLogger
   ) {
      this.stepSession = stepSession;
      this.identificationService = identificationService;
      this.kerberosService = kerberosService;
      this.spnegoExtractor = spnegoExtractor;
      this.settings = settings;
      this.authMethod = authMethod;
      this.helpdeskLogger = helpdeskLogger;
      this.signingLogger = signingLogger;
      this.reportingLogger = reportingLogger;
   }

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

   @Override
   public AuthenticationStepResult initialize() {
      return this.stepResultFactory().stay(AccessFlowStep.AccessStepActions.ACCESS_CALL_REQUIRED).build();
   }

   public AuthenticationStepResult access() {
      Optional<SpnegoTicket> negotiateTicket = ((SpnegoTicketExtractor)this.spnegoExtractor.get()).extractSpnegoTicketHeader();
      if (negotiateTicket.isEmpty()) {
         return this.stepResultFactory()
            .failRetry(GenericStepError.NOT_AUTHORIZED, AccessFlowStep.AccessStepActions.ACCESS_CALL_REQUIRED)
            .resultData(NotAuthorizedData.negotiate())
            .isFailedFactor(false)
            .build();
      }

      KerberosValidationResult validateResult = ((KerberosService)this.kerberosService.get())
         .validateSpnegoData(negotiateTicket.get().rawTicket(), ((KerberosAuthStepSession)this.stepSession.get()).getGssContext());

      return switch (validateResult.outcome()) {
         case SUCCESS -> this.handleKerberosSuccess(validateResult);
         case UNFINISHED -> this.handleKerberosUnfinished(validateResult);
         case FAILED -> this.handleKerberosFailed();
         case EXCEPTION -> this.handleKerberosException();
      };
   }

   public AuthenticationStepResult handleKerberosNotPossible() {
      LOG.info("Client unable to perform Kerberos authentication - aborting flow.");
      return this.stepResultFactory()
         .fail(KerberosAuthStep.KerberosTicketIdentifyStepErrors.KERBEROS_AUTHENTICATION_NOT_POSSIBLE)
         .isFailedFactor(false)
         .build();
   }

   private AuthenticationStepResult handleKerberosSuccess(KerberosValidationResult validateResult) {
      ((KerberosAuthStepSession)this.stepSession.get()).setGssContext(Optional.empty());
      IdentificationResult identifyResult = this.identifyResult(validateResult);
      if (identifyResult.isFailure() && identifyResult.hasError(IdentificationResult.IdentificationFailure.IdentificationError.DIFFERENT_USERNAME)) {
         LOG.info("Kerberos ticket user different than identity result");
         return this.stepResultFactory().fail(KerberosAuthStep.KerberosTicketIdentifyStepErrors.KERBEROS_TICKET_VERIFICATION_FAILED).build();
      } else if (identifyResult.isFailure()) {
         identifyResult.getUserIdentity().ifPresent(id -> this.logFailure(id, identifyResult.getFailure().orElseThrow().error()));
         LOG.info("failed to validate user identity: " + identifyResult.getFailure().orElseThrow().error());
         return this.stepResultFactory().fail(KerberosAuthStep.KerberosTicketIdentifyStepErrors.KERBEROS_TICKET_VERIFICATION_FAILED).build();
      } else {
         UserIdentity identity = identifyResult.getUserIdentity().orElseThrow();
         this.logSuccess(identity);
         return this.stepResultFactory().asAuthenticationStepResult(this.userIdentifiedSuccessResult(identity)).build();
      }
   }

   private AuthenticationStepResult handleKerberosUnfinished(KerberosValidationResult validateResult) {
      LOG.debug("SPNEGO/Kerberos handshake not yet finished. Sending challenge data");
      ((KerberosAuthStepSession)this.stepSession.get()).setGssContext(validateResult.gssContext());
      return this.stepResultFactory()
         .failRetry(GenericStepError.NOT_AUTHORIZED, AccessFlowStep.AccessStepActions.ACCESS_CALL_REQUIRED)
         .isFailedFactor(false)
         .resultData(NotAuthorizedData.negotiate(Base64.encodeBase64String(validateResult.outputToken().orElseThrow())))
         .build();
   }

   private AuthenticationStepResult handleKerberosFailed() {
      this.logFailedFactor();
      LOG.debug("SPNEGO/Kerberos handshake failed.");
      ((KerberosAuthStepSession)this.stepSession.get()).setGssContext(Optional.empty());
      return this.stepResultFactory().fail(KerberosAuthStep.KerberosTicketIdentifyStepErrors.KERBEROS_TICKET_VERIFICATION_FAILED).build();
   }

   private AuthenticationStepResult handleKerberosException() {
      this.logFailedFactor();
      ((KerberosAuthStepSession)this.stepSession.get()).setGssContext(Optional.empty());
      return this.stepResultFactory().fail(KerberosAuthStep.KerberosTicketIdentifyStepErrors.KERBEROS_TICKET_VERIFICATION_FAILED).build();
   }

   private IdentificationResult identifyResult(KerberosValidationResult validateResult) {
      String username = this.usernameFromValidResult(validateResult);
      return ((IdentityProvidingService)this.identificationService.get()).tryIdentify(ProvidedUsername.providedUsernameOf(username));
   }

   private String usernameFromValidResult(KerberosValidationResult validateResult) {
      String username = validateResult.username().orElseThrow();
      return ((KerberosAuthSettings)this.settings.get()).stripUsername() ? StringUtils.substringBefore(username, "@") : username;
   }

   private void logSuccess(UserIdentity userIdentity) {
      String logMessage = "Successfully authenticated using SPNEGO/Kerberos (more authentication steps may follow)";
      this.signingLogger.log(logMessage, new Object[]{userIdentity.id()});
      this.helpdeskLogger.log(userIdentity.id(), logMessage);
   }

   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 logFailedFactor() {
      this.reportingLogger
         .report(
            ReportingDataFactory.dataForFailedFactorUse(
                  AuthenticationFactorConverter.convertFactorUsage(
                     AuthenticationFactorUsageImpl.failedFactorUsage(KnownAuthenticationFactorDetail.TOKEN_KERBEROS)
                  )
               )
               .build()
         );
   }

   @Override
   public String getDescription() {
      return "Kerberos Authentication Step";
   }

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

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

   enum KerberosTicketIdentifyStepErrors implements StepError {
      KERBEROS_AUTHENTICATION_NOT_POSSIBLE,
      KERBEROS_TICKET_VERIFICATION_FAILED;
   }
}
