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

import com.airlock.iam.base.api.domain.model.plugin.LogicForConfig;
import com.airlock.iam.base.api.domain.model.user.UserId;
import com.airlock.iam.common.domain.model.user.PrincipalInfo;
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.StepResult;
import com.airlock.iam.flow.api.domain.model.step.StepSession;
import com.airlock.iam.flow.shared.domain.model.ssi.VerifiableCredentialStepSession;
import com.airlock.iam.flow.shared.domain.service.PollingService;
import com.airlock.iam.flow.types.api.domain.model.session.KnownMultiFlowType;
import com.airlock.iam.flow.types.api.domain.model.session.MultiFlowType;
import com.airlock.iam.log.api.helpdesk.HelpdeskLogger;
import com.airlock.iam.ssi.application.configuration.step.SsiPersistedIssuanceStepConfig;
import com.airlock.iam.ssi.application.configuration.step.SsiSelfRegistraionIssuanceStepConfig;
import com.airlock.iam.ssi.domain.model.SsiIssuanceSession;
import com.airlock.iam.ssi.domain.model.vc.VerifiableCredential;
import com.airlock.iam.ssi.domain.model.vc.VerifiableCredentialId;
import com.airlock.iam.ssi.domain.model.vc.VerifiableCredentialRepository;
import com.airlock.iam.ssi.domain.service.SsiAttributeAssemblerService;
import com.airlock.iam.ssi.domain.service.SsiService;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.net.URI;
import java.util.Objects;
import java.util.Optional;

@LogicForConfig({SsiPersistedIssuanceStepConfig.class, SsiSelfRegistraionIssuanceStepConfig.class})
public final class SsiIssuanceStepImpl implements SsiIssuanceStep {
   private static final ELogger LOG = ELoggerFactory.getLogger(SsiIssuanceStepImpl.class);
   private final Provider<SsiService> service;
   private final Provider<SsiIssuanceStepSession> session;
   private final Provider<SsiIssuanceStepSettings> settings;
   private final PollingService<SsiIssuanceStepSession, StepResult> acceptancePolling;
   private final Provider<SsiAttributeAssemblerService> attributeAssembler;
   private final Provider<Optional<VerifiableCredentialRepository>> vcRepository;
   private final Provider<MultiFlowType> multiFlowType;
   private final Provider<FlowAttributeRepository> flowAttributeRepository;
   private final Provider<PrincipalInfo> principalInfo;
   private final HelpdeskLogger helpdeskLogger;

   @Inject
   public SsiIssuanceStepImpl(
      Provider<SsiService> service,
      Provider<SsiIssuanceStepSession> session,
      Provider<SsiIssuanceStepSettings> settings,
      Provider<SsiAttributeAssemblerService> attributeAssembler,
      Provider<Optional<VerifiableCredentialRepository>> vcRepository,
      Provider<MultiFlowType> multiFlowType,
      Provider<FlowAttributeRepository> flowAttributeRepository,
      Provider<PrincipalInfo> principalInfo,
      HelpdeskLogger helpdeskLogger
   ) {
      this.service = service;
      this.session = session;
      this.settings = settings;
      this.attributeAssembler = attributeAssembler;
      this.vcRepository = vcRepository;
      this.multiFlowType = multiFlowType;
      this.flowAttributeRepository = flowAttributeRepository;
      this.principalInfo = principalInfo;
      this.helpdeskLogger = helpdeskLogger;
      this.acceptancePolling = new PollingService<>(
         SsiIssuanceStepSession::getAcceptanceCache, this::pollCredentialAcceptanceInternal, s -> this.acceptanceRequiredResult()
      );
   }

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

   @Override
   public StepResult initialize() {
      Optional<SsiIssuanceSession> credentialIssuance = ((SsiService)this.service.get())
         .issueCredential(
            ((SsiIssuanceStepSettings)this.settings.get()).credentialSchema(),
            ((SsiAttributeAssemblerService)this.attributeAssembler.get()).assembleAttributes()
         );
      if (credentialIssuance.isEmpty()) {
         LOG.info("Credential issuance could not be started.");
         this.helpdeskLogger.log(this.currentUser(), "Credential issuance could not be started.");
         return this.stepResultFactory().fail(SsiIssuanceStep.SsiIssuanceErrors.SSI_ISSUANCE_FAILED).build();
      } else {
         URI invitationLink = URI.create(credentialIssuance.get().urlForQrCode());
         String invitationQrString = SsiUtils.generateQrAsString(invitationLink.toASCIIString());
         ((SsiIssuanceStepSession)this.session.get()).credentialIssued(credentialIssuance.get(), invitationLink, invitationQrString);
         this.persistVerifiableCredential(credentialIssuance.get().credentialId());
         return this.acceptanceRequiredResult();
      }
   }

   @Override
   public StepResult pollCredentialAcceptance(boolean queryOnly) {
      return this.acceptancePolling.poll((SsiIssuanceStepSession)this.session.get(), queryOnly);
   }

   private void persistVerifiableCredential(String credentialId) {
      if (Objects.equals(KnownMultiFlowType.USER_SELF_REG, this.multiFlowType.get())) {
         LOG.debug(
            String.format(
               "Writing verifiable credential %s for user %s to session during self registration. It needs to be persisted by an insertion handler after the user is created.",
               credentialId,
               this.currentUser()
            )
         );
         ((FlowAttributeRepository)this.flowAttributeRepository.get()).putOrMergeAttribute(new VerifiableCredentialStepSession(credentialId));
      } else {
         LOG.debug(String.format("Persisting verifiable credential %s for user %s", credentialId, this.currentUser()));
         ((VerifiableCredentialRepository)((Optional)this.vcRepository.get()).orElseThrow())
            .save(new VerifiableCredential(VerifiableCredentialId.verifiableCredentialId(credentialId), this.currentUser()));
      }
   }

   private UserId currentUser() {
      return ((PrincipalInfo)this.principalInfo.get()).getKnownIdentity().orElseThrow().id();
   }

   private StepResult pollCredentialAcceptanceInternal(SsiIssuanceStepSession stepSession) {
      if (stepSession.getIssuanceSession().isEmpty()) {
         return this.stepResultFactory().failDueToUnexpectedCall("Tried to poll credential acceptance, but credential has not been issued.").build();
      }

      return switch (((SsiService)this.service.get()).pollIssuanceStatus(stepSession.getIssuanceSession().orElseThrow())) {
         case PENDING -> this.acceptanceRequiredResult();
         case SUCCESS -> {
            LOG.info("Successfully issued credential.");
            this.helpdeskLogger.log(this.currentUser(), "User accepted credential in wallet");
            yield this.stepResultFactory().success().build();
         }
         case FAILED -> {
            LOG.info("Failed to issue credential.");
            this.helpdeskLogger.log(this.currentUser(), "Credential issuance failed.");
            yield this.stepResultFactory().fail(SsiIssuanceStep.SsiIssuanceErrors.SSI_ISSUANCE_FAILED).build();
         }
      };
   }

   private StepResult acceptanceRequiredResult() {
      return SsiUtils.addInvitationAttributes(
         this.stepResultFactory().stay(SsiIssuanceStep.SsiIssuanceActions.SSI_CREDENTIAL_ACCEPTANCE_REQUIRED),
         ((SsiIssuanceStepSession)this.session.get()).getInvitationLink().orElseThrow(),
         ((SsiIssuanceStepSession)this.session.get()).getInvitationQrCode().orElseThrow()
      );
   }

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

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

   @SuppressFBWarnings(justification = "generated code")
   public static class SsiIssuanceStepImplBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private Provider<SsiService> service;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<SsiIssuanceStepSession> session;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<SsiIssuanceStepSettings> settings;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<SsiAttributeAssemblerService> attributeAssembler;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<Optional<VerifiableCredentialRepository>> vcRepository;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<MultiFlowType> multiFlowType;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<FlowAttributeRepository> flowAttributeRepository;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<PrincipalInfo> principalInfo;
      @SuppressFBWarnings(justification = "generated code")
      private HelpdeskLogger helpdeskLogger;

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

      @SuppressFBWarnings(justification = "generated code")
      public SsiIssuanceStepImpl.SsiIssuanceStepImplBuilder service(Provider<SsiService> service) {
         this.service = service;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SsiIssuanceStepImpl.SsiIssuanceStepImplBuilder session(Provider<SsiIssuanceStepSession> session) {
         this.session = session;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SsiIssuanceStepImpl.SsiIssuanceStepImplBuilder settings(Provider<SsiIssuanceStepSettings> settings) {
         this.settings = settings;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SsiIssuanceStepImpl.SsiIssuanceStepImplBuilder attributeAssembler(Provider<SsiAttributeAssemblerService> attributeAssembler) {
         this.attributeAssembler = attributeAssembler;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SsiIssuanceStepImpl.SsiIssuanceStepImplBuilder vcRepository(Provider<Optional<VerifiableCredentialRepository>> vcRepository) {
         this.vcRepository = vcRepository;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SsiIssuanceStepImpl.SsiIssuanceStepImplBuilder multiFlowType(Provider<MultiFlowType> multiFlowType) {
         this.multiFlowType = multiFlowType;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SsiIssuanceStepImpl.SsiIssuanceStepImplBuilder flowAttributeRepository(Provider<FlowAttributeRepository> flowAttributeRepository) {
         this.flowAttributeRepository = flowAttributeRepository;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SsiIssuanceStepImpl.SsiIssuanceStepImplBuilder principalInfo(Provider<PrincipalInfo> principalInfo) {
         this.principalInfo = principalInfo;
         return this;
      }

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

      @SuppressFBWarnings(justification = "generated code")
      public SsiIssuanceStepImpl build() {
         return new SsiIssuanceStepImpl(
            this.service,
            this.session,
            this.settings,
            this.attributeAssembler,
            this.vcRepository,
            this.multiFlowType,
            this.flowAttributeRepository,
            this.principalInfo,
            this.helpdeskLogger
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "SsiIssuanceStepImpl.SsiIssuanceStepImplBuilder(service="
            + this.service
            + ", session="
            + this.session
            + ", settings="
            + this.settings
            + ", attributeAssembler="
            + this.attributeAssembler
            + ", vcRepository="
            + this.vcRepository
            + ", multiFlowType="
            + this.multiFlowType
            + ", flowAttributeRepository="
            + this.flowAttributeRepository
            + ", principalInfo="
            + this.principalInfo
            + ", helpdeskLogger="
            + this.helpdeskLogger
            + ")";
      }
   }
}
