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

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.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.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.common.api.domain.model.authmethod.AuthenticationMethod;
import com.airlock.iam.common.api.domain.model.user.CurrentLocale;
import com.airlock.iam.common.api.domain.model.user.UserIdentity;
import com.airlock.iam.common.domain.model.QueryString;
import com.airlock.iam.common.domain.model.accountlink.AccountSub;
import com.airlock.iam.common.domain.model.provider.ProviderId;
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.redflag.RedFlag;
import com.airlock.iam.flow.api.domain.model.session.FlowSession;
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.api.domain.model.session.UserSession;
import com.airlock.iam.flow.shared.domain.model.acknowledgemessage.AcknowledgeMessageId;
import com.airlock.iam.flow.shared.domain.model.step.acknowledgemessage.AcknowledgeMessageStep;
import com.airlock.iam.flow.shared.domain.model.step.oauth2.OAuth2AuthorizationVerifyingFlowStep;
import com.airlock.iam.log.api.helpdesk.HelpdeskLogger;
import com.airlock.iam.log.api.signing.SigningLogger;
import com.airlock.iam.login.api.domain.model.oauth2.OAuth2Credential;
import com.airlock.iam.oauth2.application.configuration.OAuth2SsoStepConfig;
import com.airlock.iam.oauth2.domain.model.OAuth2Language;
import com.airlock.iam.oauth2.domain.model.accountlinking.OAuth2MissingAccountLinkFlowSessionAttribute;
import com.airlock.iam.oauth2.domain.model.client.OAuth2AuthorizationErrorResponse;
import com.airlock.iam.oauth2.domain.model.client.OAuth2AuthorizationResponse;
import com.airlock.iam.oauth2.domain.model.client.OAuth2AuthorizationSuccessResponse;
import com.airlock.iam.oauth2.domain.model.client.OAuth2ClientSession;
import com.airlock.iam.oauth2.domain.model.client.OAuth2ParseException;
import com.airlock.iam.oauth2.domain.model.client.OpenIDConnectCredential;
import com.airlock.iam.oauth2.domain.model.client.communication.LogoutPropagationAttribute;
import com.airlock.iam.oauth2.domain.model.client.communication.OAuth2ClientCommunicator;
import com.airlock.iam.oauth2.domain.model.oauth2.session.OAuth2CredentialAuthenticationFlowSessionAttribute;
import com.airlock.iam.oauth2.domain.service.AutoRegisteredUser;
import com.airlock.iam.oauth2.domain.service.OAuth2AccountRegistrationService;
import com.airlock.iam.oauth2.domain.service.OAuth2SsoIdentificationService;
import com.airlock.iam.oauth2.domain.service.accountlinking.AccountLinkAccountInfoProvider;
import com.airlock.iam.oauth2.domain.service.logout.LogoutPropagationService;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;

@LogicForConfig(OAuth2SsoStepConfig.class)
public final class OAuth2SsoStepImpl implements OAuth2SsoStep, IdentityProvidingFlowStep {
   private static final ELogger LOG = ELoggerFactory.getLogger(OAuth2SsoStepImpl.class);
   private final Provider<OAuth2SsoStepSession> stepSession;
   private final Provider<FlowSession> flowSession;
   private final OAuth2SsoIdentificationService oAuth2SsoIdentificationService;
   private final Optional<OAuth2AccountRegistrationService> registrationService;
   private final AccountLinkAccountInfoProvider accountInfoProvider;
   private final Provider<UserSession> userSession;
   private final ProviderId providerId;
   private final CurrentLocale locale;
   private final AuthenticationMethod authMethod;
   private final OAuth2ClientCommunicator communicator;
   private final HelpdeskLogger helpdeskLogger;
   private final SigningLogger signingLogger;
   private final Optional<LogoutPropagationService> logoutPropagationService;
   private final Optional<AcknowledgeMessageId> messageId;
   private final Optional<RedFlag> missingAccountLinkRedFlag;

   @Inject
   public OAuth2SsoStepImpl(
      Provider<OAuth2SsoStepSession> stepSession,
      Provider<FlowSession> flowSession,
      OAuth2SsoIdentificationService oAuth2SsoIdentificationService,
      Optional<OAuth2AccountRegistrationService> registrationService,
      AccountLinkAccountInfoProvider accountInfoProvider,
      Provider<UserSession> userSession,
      ProviderId providerId,
      CurrentLocale locale,
      AuthenticationMethod authMethod,
      OAuth2ClientCommunicator communicator,
      HelpdeskLogger helpdeskLogger,
      SigningLogger signingLogger,
      Optional<LogoutPropagationService> logoutPropagationService,
      Optional<AcknowledgeMessageId> messageId,
      Optional<RedFlag> missingAccountLinkRedFlag
   ) {
      this.stepSession = stepSession;
      this.flowSession = flowSession;
      this.accountInfoProvider = accountInfoProvider;
      this.userSession = userSession;
      this.oAuth2SsoIdentificationService = oAuth2SsoIdentificationService;
      this.registrationService = registrationService;
      this.providerId = providerId;
      this.locale = locale;
      this.authMethod = authMethod;
      this.communicator = communicator;
      this.helpdeskLogger = helpdeskLogger;
      this.signingLogger = signingLogger;
      this.logoutPropagationService = logoutPropagationService;
      this.messageId = messageId;
      this.missingAccountLinkRedFlag = missingAccountLinkRedFlag;
   }

   @Override
   public AuthenticationStepResult initialize() {
      OAuth2ClientSession clientSession = new OAuth2ClientSession(this.providerId);
      ((OAuth2SsoStepSession)this.stepSession.get()).setClientSession(clientSession);
      LOG.debug(String.format("Starting OAuth 2.0 handshake for provider %s", LogUtil.quote(this.providerId.id())));
      return this.communicator.needsFreshAuthzRequestOnAccess()
         ? this.stepResultFactory()
            .stay(OAuth2AuthorizationVerifyingFlowStep.OAuth2AuthorizationVerifyingFlowStepAction.OAUTH2_CLIENT_AUTHORIZATION_URI_RETRIEVAL_REQUIRED)
            .build()
         : this.stepResultFactory()
            .stay(OAuth2AuthorizationVerifyingFlowStep.OAuth2AuthorizationVerifyingFlowStepAction.OAUTH2_CLIENT_AUTHORIZATION_REQUIRED)
            .addAdditionalAttribute(
               "authorizationRequestUri", this.buildAuthzCodeRequestUrl(((OAuth2SsoStepSession)this.stepSession.get()).getClientSession().orElseThrow())
            )
            .build();
   }

   @Override
   public StepResult buildAuthorizationRequest() {
      try {
         return this.stepResultFactory()
            .stay(
               this.communicator.needsFreshAuthzRequestOnAccess()
                  ? OAuth2AuthorizationVerifyingFlowStep.OAuth2AuthorizationVerifyingFlowStepAction.OAUTH2_CLIENT_AUTHORIZATION_URI_RETRIEVAL_REQUIRED
                  : OAuth2AuthorizationVerifyingFlowStep.OAuth2AuthorizationVerifyingFlowStepAction.OAUTH2_CLIENT_AUTHORIZATION_REQUIRED
            )
            .resultData(
               new OAuth2AuthorizationUri(this.buildAuthzCodeRequestUrl(((OAuth2SsoStepSession)this.stepSession.get()).getClientSession().orElseThrow()))
            )
            .build();
      } catch (FailureException e) {
         LOG.info("Failed to build authorization request: " + e.getMessage());
         return this.stepResultFactory().internalError().build();
      }
   }

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

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

   @Override
   public String getDescription() {
      return "OAuth 2.0 SSO Step";
   }

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

   public AuthenticationStepResult verifyAuthorizationResponse(QueryString queryString) {
      Optional<OAuth2ClientSession> clientSession = ((OAuth2SsoStepSession)this.stepSession.get()).getClientSession();
      if (clientSession.isEmpty()) {
         return this.stepResultFactory().failDueToUnexpectedCall("Client session not found. Authorization start required.").build();
      }

      Optional<OAuth2AuthorizationResponse> response = parseResponse(queryString);
      if (response.isEmpty()) {
         return this.authenticationFailed();
      }

      LOG.debug(String.format("OAuth 2.0 authorization response received for provider %s", LogUtil.quote(this.providerId.id())));
      Optional<OAuth2AuthorizationSuccessResponse> authorizationSuccessResponse = response.get().successResponse();
      if (authorizationSuccessResponse.isEmpty()) {
         return this.handleError(response.get().errorResponse().orElseThrow());
      }

      Optional<OAuth2Credential> credential = this.communicator.getCredential(clientSession.get(), authorizationSuccessResponse.get());
      if (credential.isEmpty()) {
         return this.authenticationFailed();
      }

      IdentificationResult identificationResult = this.oAuth2SsoIdentificationService.identify(credential.get());
      if (identificationResult.hasError(IdentificationResult.IdentificationFailure.IdentificationError.USER_NOT_FOUND)) {
         if (this.registrationService.isPresent()) {
            ((OAuth2SsoStepSession)this.stepSession.get()).setCredential(credential.get());
            ((OAuth2SsoStepSession)this.stepSession.get()).clearClientSession();
            return this.confirmationRequired();
         }

         if (this.missingAccountLinkRedFlag.isPresent()) {
            ((FlowSession)this.flowSession.get()).raiseRedFlag(this.missingAccountLinkRedFlag.get());
            OAuth2MissingAccountLinkFlowSessionAttribute attribute = new OAuth2MissingAccountLinkFlowSessionAttribute(
               this.providerId, AccountSub.accountSub(credential.get().getUsername()), this.accountInfoProvider.accountInfo(credential.get())
            );
            ((FlowSession)this.flowSession.get()).flowAttributes().putOrMergeAttribute(attribute);
            LOG.info("User could not be identified using account linking. Raised missing account link red flag in flow session.");
            return this.stepResultFactory().success().build();
         }
      }

      return this.handleIdentificationResult(identificationResult, credential.get());
   }

   public AuthenticationStepResult acknowledge() {
      Optional<AutoRegisteredUser> autoRegisteredUser = this.registrationService
         .flatMap(service -> ((OAuth2SsoStepSession)this.stepSession.get()).getCredential().map(service::createAutoRegisteredUser));
      if (autoRegisteredUser.isEmpty()) {
         return this.stepResultFactory().failDueToUnexpectedCall("Invalid state. No credential found in session.").build();
      }

      IdentificationResult identificationResult = this.registrationService.orElseThrow().persistAndLinkAutoRegisteredUser(autoRegisteredUser.get());
      return this.handleIdentificationResult(identificationResult, autoRegisteredUser.get().getCredential());
   }

   private AuthenticationStepResult confirmationRequired() {
      AuthenticationStepResultBuilder result = this.stepResultFactory()
         .stay(AcknowledgeMessageStep.AcknowledgeMessageStepActions.MESSAGE_ACKNOWLEDGMENT_REQUIRED)
         .isFailedFactor(false);
      this.messageId.ifPresent(id -> result.addAdditionalAttribute("messageId", id.getId()));
      return result.build();
   }

   private static Optional<OAuth2AuthorizationResponse> parseResponse(QueryString queryString) {
      try {
         return Optional.of(OAuth2AuthorizationResponse.oAuth2AuthorizationResponse(queryString));
      } catch (OAuth2ParseException e) {
         LOG.info(String.format("Failed to parse OAuth 2.0 authorization response: %s", e.getMessage()));
         return Optional.empty();
      }
   }

   private AuthenticationStepResult handleError(OAuth2AuthorizationErrorResponse response) {
      LOG.info(String.format("Received OAuth 2.0 authorization failure response: %s", LogUtil.logSafeString(response.textRepresentation())));
      return this.authenticationFailed();
   }

   private AuthenticationStepResult authenticationFailed() {
      return this.stepResultFactory()
         .fail(OAuth2AuthorizationVerifyingFlowStep.OAuth2AuthorizationVerifyingFlowStepError.OAUTH2_CLIENT_AUTHORIZATION_FAILED)
         .isFailedFactor(false)
         .build();
   }

   private AuthenticationStepResult handleIdentificationResult(IdentificationResult identificationResult, OAuth2Credential credential) {
      if (identificationResult.isFailure()) {
         identificationResult.getUserIdentity().ifPresent(id -> this.logFailure(id, identificationResult.getFailure().orElseThrow().error()));
         AuthenticationStepResultBuilder result = IdentificationResultConverter.convertToAuthStepResult(
            identificationResult.getFailure().orElseThrow(), this.stepResultFactory(), UserIdentifyingStep.UserIdentifyingStepAction.USERNAME_REQUIRED
         );
         return result.isFailedFactor(false).build();
      } else {
         UserIdentity userIdentity = identificationResult.getUserIdentity().orElseThrow();
         this.addCredentialAttributeToSession(credential);
         this.addLogoutPropagationAttribute(credential);
         this.logSuccess(userIdentity);
         return this.stepResultFactory().success().internalResultData(new UserIdentificationResultData(userIdentity)).build();
      }
   }

   private void addLogoutPropagationAttribute(OAuth2Credential credential) {
      if (this.logoutPropagationService.isPresent() && this.logoutPropagationService.get().isEnabled()) {
         if (!(credential instanceof OpenIDConnectCredential oidcCredential)) {
            throw new FailureException("Credential is not of type OpenIDConnect credential");
         }

         LogoutPropagationAttribute logoutAttribute = new LogoutPropagationAttribute(
            List.of(this.logoutPropagationService.get().provideLogoutUri(oidcCredential.idToken()))
         );
         ((UserSession)this.userSession.get()).addUserSessionAttribute(logoutAttribute);
      }
   }

   private void addCredentialAttributeToSession(OAuth2Credential oAuth2Credential) {
      OAuth2CredentialAuthenticationFlowSessionAttribute.OAuth2CredentialAuthenticationFlowSessionAttributeBuilder attribute = OAuth2CredentialAuthenticationFlowSessionAttribute.builder()
         .providerId(this.providerId)
         .username(oAuth2Credential.getUsername())
         .roles(oAuth2Credential.getUserRoles())
         .contextData(contextData(oAuth2Credential))
         .accessToken(oAuth2Credential.accessToken());
      if (oAuth2Credential instanceof OpenIDConnectCredential) {
         attribute.idToken(((OpenIDConnectCredential)oAuth2Credential).idToken());
      }

      ((FlowSession)this.flowSession.get()).flowAttributes().putOrMergeAttribute(attribute.build());
   }

   private static Map<String, Object> contextData(OAuth2Credential credential) {
      return credential.getAttributeKeys().stream().collect(Collectors.toMap(Function.identity(), credential::getAttributeValue));
   }

   private OAuth2Language oauth2Language() {
      return OAuth2Language.oAuth2Language(this.locale.get().toLanguageTag());
   }

   private URI buildAuthzCodeRequestUrl(OAuth2ClientSession clientSession) {
      return this.communicator.buildAndPrepareAuthzCodeRequest(clientSession, this.oauth2Language()).toURI();
   }

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

   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});
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public static class OAuth2SsoStepImplBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private Provider<OAuth2SsoStepSession> stepSession;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<FlowSession> flowSession;
      @SuppressFBWarnings(justification = "generated code")
      private OAuth2SsoIdentificationService oAuth2SsoIdentificationService;
      @SuppressFBWarnings(justification = "generated code")
      private Optional<OAuth2AccountRegistrationService> registrationService;
      @SuppressFBWarnings(justification = "generated code")
      private AccountLinkAccountInfoProvider accountInfoProvider;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<UserSession> userSession;
      @SuppressFBWarnings(justification = "generated code")
      private ProviderId providerId;
      @SuppressFBWarnings(justification = "generated code")
      private CurrentLocale locale;
      @SuppressFBWarnings(justification = "generated code")
      private AuthenticationMethod authMethod;
      @SuppressFBWarnings(justification = "generated code")
      private OAuth2ClientCommunicator communicator;
      @SuppressFBWarnings(justification = "generated code")
      private HelpdeskLogger helpdeskLogger;
      @SuppressFBWarnings(justification = "generated code")
      private SigningLogger signingLogger;
      @SuppressFBWarnings(justification = "generated code")
      private Optional<LogoutPropagationService> logoutPropagationService;
      @SuppressFBWarnings(justification = "generated code")
      private Optional<AcknowledgeMessageId> messageId;
      @SuppressFBWarnings(justification = "generated code")
      private Optional<RedFlag> missingAccountLinkRedFlag;

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

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2SsoStepImpl.OAuth2SsoStepImplBuilder stepSession(Provider<OAuth2SsoStepSession> stepSession) {
         this.stepSession = stepSession;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2SsoStepImpl.OAuth2SsoStepImplBuilder flowSession(Provider<FlowSession> flowSession) {
         this.flowSession = flowSession;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2SsoStepImpl.OAuth2SsoStepImplBuilder oAuth2SsoIdentificationService(OAuth2SsoIdentificationService oAuth2SsoIdentificationService) {
         this.oAuth2SsoIdentificationService = oAuth2SsoIdentificationService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2SsoStepImpl.OAuth2SsoStepImplBuilder registrationService(Optional<OAuth2AccountRegistrationService> registrationService) {
         this.registrationService = registrationService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2SsoStepImpl.OAuth2SsoStepImplBuilder accountInfoProvider(AccountLinkAccountInfoProvider accountInfoProvider) {
         this.accountInfoProvider = accountInfoProvider;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2SsoStepImpl.OAuth2SsoStepImplBuilder userSession(Provider<UserSession> userSession) {
         this.userSession = userSession;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2SsoStepImpl.OAuth2SsoStepImplBuilder providerId(ProviderId providerId) {
         this.providerId = providerId;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2SsoStepImpl.OAuth2SsoStepImplBuilder locale(CurrentLocale locale) {
         this.locale = locale;
         return this;
      }

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

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2SsoStepImpl.OAuth2SsoStepImplBuilder communicator(OAuth2ClientCommunicator communicator) {
         this.communicator = communicator;
         return this;
      }

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

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

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2SsoStepImpl.OAuth2SsoStepImplBuilder logoutPropagationService(Optional<LogoutPropagationService> logoutPropagationService) {
         this.logoutPropagationService = logoutPropagationService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2SsoStepImpl.OAuth2SsoStepImplBuilder messageId(Optional<AcknowledgeMessageId> messageId) {
         this.messageId = messageId;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2SsoStepImpl.OAuth2SsoStepImplBuilder missingAccountLinkRedFlag(Optional<RedFlag> missingAccountLinkRedFlag) {
         this.missingAccountLinkRedFlag = missingAccountLinkRedFlag;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2SsoStepImpl build() {
         return new OAuth2SsoStepImpl(
            this.stepSession,
            this.flowSession,
            this.oAuth2SsoIdentificationService,
            this.registrationService,
            this.accountInfoProvider,
            this.userSession,
            this.providerId,
            this.locale,
            this.authMethod,
            this.communicator,
            this.helpdeskLogger,
            this.signingLogger,
            this.logoutPropagationService,
            this.messageId,
            this.missingAccountLinkRedFlag
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "OAuth2SsoStepImpl.OAuth2SsoStepImplBuilder(stepSession="
            + this.stepSession
            + ", flowSession="
            + this.flowSession
            + ", oAuth2SsoIdentificationService="
            + this.oAuth2SsoIdentificationService
            + ", registrationService="
            + this.registrationService
            + ", accountInfoProvider="
            + this.accountInfoProvider
            + ", userSession="
            + this.userSession
            + ", providerId="
            + this.providerId
            + ", locale="
            + this.locale
            + ", authMethod="
            + this.authMethod
            + ", communicator="
            + this.communicator
            + ", helpdeskLogger="
            + this.helpdeskLogger
            + ", signingLogger="
            + this.signingLogger
            + ", logoutPropagationService="
            + this.logoutPropagationService
            + ", messageId="
            + this.messageId
            + ", missingAccountLinkRedFlag="
            + this.missingAccountLinkRedFlag
            + ")";
      }
   }
}
