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

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.service.username.IdentificationResult;
import com.airlock.iam.authentication.api.domain.service.username.IdentityProvidingService;
import com.airlock.iam.authentication.impl.domain.model.authentication.UserIdentificationResultData;
import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.base.api.domain.model.plugin.LogicForConfig;
import com.airlock.iam.base.misc.log.LogUtil;
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.ProvidedUsername;
import com.airlock.iam.common.api.domain.model.user.UserIdentity;
import com.airlock.iam.common.domain.model.user.PrincipalInfo;
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.session.FlowAttributeRepository;
import com.airlock.iam.flow.api.domain.model.step.StepSession;
import com.airlock.iam.flow.shared.domain.service.PollingService;
import com.airlock.iam.log.api.helpdesk.HelpdeskLogger;
import com.airlock.iam.log.api.signing.SigningLogger;
import com.airlock.iam.ssi.application.configuration.step.SsiAuthenticationStepConfig;
import com.airlock.iam.ssi.application.configuration.step.SsiPasswordlessAuthenticationStepConfig;
import com.airlock.iam.ssi.domain.model.SsiPollingStatus;
import com.airlock.iam.ssi.domain.model.SsiVerificationSession;
import com.airlock.iam.ssi.domain.model.SsiVerifiedData;
import com.airlock.iam.ssi.domain.model.attribute.SsiVerificationData;
import com.airlock.iam.ssi.domain.service.SsiService;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.Optional;

@LogicForConfig({SsiAuthenticationStepConfig.class, SsiPasswordlessAuthenticationStepConfig.class})
public final class SsiAuthenticationStepImpl implements SsiAuthenticationStep {
   private static final ELogger LOG = ELoggerFactory.getLogger(SsiAuthenticationStepImpl.class);
   private final Provider<SsiService> service;
   private final Provider<SsiAuthenticationStepSession> session;
   private final Provider<SsiAuthenticationStepSettings> settings;
   private final Provider<AuthenticationMethod> authMethod;
   private final Provider<PrincipalInfo> user;
   private final SigningLogger signingLogger;
   private final HelpdeskLogger helpdeskLogger;
   private final Provider<IdentityProvidingService> identificationService;
   private final Provider<FlowAttributeRepository> attributes;
   private final PollingService<SsiAuthenticationStepSession, AuthenticationStepResult> verificationPolling;

   @Inject
   public SsiAuthenticationStepImpl(
      Provider<SsiService> service,
      Provider<SsiAuthenticationStepSession> session,
      Provider<SsiAuthenticationStepSettings> settings,
      Provider<AuthenticationMethod> authMethod,
      Provider<PrincipalInfo> user,
      Provider<FlowAttributeRepository> attributes,
      SigningLogger signingLogger,
      HelpdeskLogger helpdeskLogger,
      Provider<IdentityProvidingService> identificationService
   ) {
      this.service = service;
      this.session = session;
      this.settings = settings;
      this.authMethod = authMethod;
      this.user = user;
      this.attributes = attributes;
      this.signingLogger = signingLogger;
      this.helpdeskLogger = helpdeskLogger;
      this.identificationService = identificationService;
      this.verificationPolling = new PollingService<>(
         SsiAuthenticationStepSession::getVerificationCache, this::pollVerificationInternal, s -> this.verificationRequiredResult()
      );
   }

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

   public AuthenticationStepResult pollProofVerification(boolean queryOnly) {
      return this.verificationPolling.poll((SsiAuthenticationStepSession)this.session.get(), queryOnly);
   }

   private AuthenticationStepResult pollVerificationInternal(SsiAuthenticationStepSession stepSession) {
      if (stepSession.getVerificationSession().isEmpty()) {
         return this.stepResultFactory().failDueToUnexpectedCall("Tried to poll proof verification, but proof has not been requested.").build();
      }

      Result<SsiVerifiedData, SsiPollingStatus> result = ((SsiService)this.service.get())
         .pollProofStatus(stepSession.getVerificationSession().get(), ((SsiAuthenticationStepSettings)this.settings.get()).parameters());
      if (result.isPresent()) {
         return this.handleVerificationSuccess(result.get());
      }

      return switch ((SsiPollingStatus)result.getFailure()) {
         case PENDING -> this.verificationRequiredResult();
         case FAILED -> {
            LOG.info("Failed to verify presentation.");
            yield this.stepResultFactory().fail(SsiVerificationStep.SsiVerificationErrors.SSI_VERIFICATION_FAILED).build();
         }
         case SUCCESS -> throw new IllegalStateException("not possible");
      };
   }

   private AuthenticationStepResult verificationRequiredResult() {
      return SsiUtils.addInvitationAttributes(
         this.stepResultFactory().stay(SsiVerificationStep.SsiVerificationActions.SSI_PROOF_VERIFICATION_REQUIRED),
         ((SsiAuthenticationStepSession)this.session.get()).getInvitationLink().orElseThrow(),
         ((SsiAuthenticationStepSession)this.session.get()).getInvitationQrCode().orElseThrow()
      );
   }

   private AuthenticationStepResult handleVerificationSuccess(SsiVerifiedData result) {
      SsiAttributeSettings ssiAttributeSettings = ((SsiAuthenticationStepSettings)this.settings.get()).userAttributeSettings();
      String verifiedUserAttribute = result.receivedAttributes().get(ssiAttributeSettings);
      if (verifiedUserAttribute == null) {
         LOG.debug("Did not find configured user claim attribute in response.");
         return this.stepResultFactory().fail(SsiVerificationStep.SsiVerificationErrors.SSI_VERIFICATION_FAILED).build();
      }

      IdentificationResult identificationResult = ((IdentityProvidingService)this.identificationService.get())
         .tryIdentify(ProvidedUsername.providedUsernameOf(verifiedUserAttribute), ((SsiAuthenticationStepSettings)this.settings.get()).transformation());
      if (!identificationResult.isFailure() && !identificationResult.getUserIdentity().isEmpty()) {
         UserIdentity identity = identificationResult.getUserIdentity().get();
         AuthenticationStepResult stepResult = this.createStepResult(identity, verifiedUserAttribute);
         if (stepResult.isSuccess()) {
            Map<String, String> receivedAttributes = result.providerKeysToValues();
            ((FlowAttributeRepository)this.attributes.get()).putOrMergeAttribute(new SsiVerificationData(receivedAttributes));
            List<String> attributes = receivedAttributes.keySet().stream().sorted().toList();
            String info = "Received verified attributes %s. Identity of user %s verified by SSI verification of attribute %s using value %s."
               .formatted(attributes, LogUtil.quote(identity.id()), LogUtil.quote(ssiAttributeSettings.name()), LogUtil.logSafeQuote(verifiedUserAttribute));
            this.signingLogger.log(info);
            this.helpdeskLogger.log(identity.id(), info);
            LOG.info(info);
         }

         return stepResult;
      } else {
         LOG.info("The provided verifiable credentials are not associated with any user.");
         return this.stepResultFactory().fail(SsiVerificationStep.SsiVerificationErrors.SSI_VERIFICATION_FAILED).build();
      }
   }

   private AuthenticationStepResult createStepResult(UserIdentity identity, String attribute) {
      LOG.debug("Successfully verified attribute with value " + LogUtil.logSafeQuote(attribute) + ", transformed to " + LogUtil.logSafeQuote(identity.id()));
      if (((SsiAuthenticationStepSettings)this.settings.get()).passwordless()) {
         return this.stepResultFactory().success().internalResultData(new UserIdentificationResultData(identity)).build();
      }

      if (((PrincipalInfo)this.user.get())
         .getKnownIdentity()
         .orElseThrow(() -> new FailureException("The user needs to be already identified for this step."))
         .hasSameId(identity)) {
         return this.stepResultFactory().success().build();
      }

      LOG.info("The provided verifiable credential is not associated with the current user.");
      return this.stepResultFactory().fail(SsiVerificationStep.SsiVerificationErrors.SSI_VERIFICATION_FAILED).build();
   }

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

   @Override
   public AuthenticationStepResult initialize() {
      Optional<SsiVerificationSession> proofRequest = ((SsiService)this.service.get())
         .initiateProofRequest(((SsiAuthenticationStepSettings)this.settings.get()).parameters());
      if (proofRequest.isEmpty()) {
         LOG.info("The authentication proof request could not be initiated.");
         return this.stepResultFactory().fail(SsiVerificationStep.SsiVerificationErrors.SSI_VERIFICATION_FAILED).build();
      } else {
         URI invitationLink = URI.create(proofRequest.get().urlForQrCode());
         String invitationQrString = SsiUtils.generateQrAsString(invitationLink.toASCIIString());
         ((SsiAuthenticationStepSession)this.session.get()).proofRequested(proofRequest.get(), invitationLink, invitationQrString);
         return this.verificationRequiredResult();
      }
   }

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

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