package com.airlock.iam.authentication.infrastructure.factory;

import com.airlock.iam.authentication.api.domain.service.username.IdentityProvidingService;
import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.base.api.domain.model.exception.ServiceConfigurationException;
import com.airlock.iam.base.api.infrastructure.plugin.ModuleForConfig;
import com.airlock.iam.base.api.infrastructure.plugin.PluginConfigToLogic;
import com.airlock.iam.base.misc.adapt.Adapter;
import com.airlock.iam.common.api.domain.model.authmethod.AuthenticationMethod;
import com.airlock.iam.common.api.domain.model.store.user.InsertingUserStore;
import com.airlock.iam.common.api.domain.model.store.user.UserStore;
import com.airlock.iam.common.application.configuration.accountlink.persistence.AccountLinkPersisterConfig;
import com.airlock.iam.common.domain.model.provider.ProviderId;
import com.airlock.iam.common.domain.service.accountlink.persistence.AccountLinkPersister;
import com.airlock.iam.core.misc.impl.authen.AuthenticationMethodFactory;
import com.airlock.iam.flow.api.application.configuration.redflag.RedFlagConfig;
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.infrastructure.step.StepSessionModuleHelper;
import com.airlock.iam.flow.shared.domain.model.acknowledgemessage.AcknowledgeMessageId;
import com.airlock.iam.oauth2.application.configuration.OAuth2ClientSettingsConfigSpec;
import com.airlock.iam.oauth2.application.configuration.OAuth2SsoFlowClientSettingsBase;
import com.airlock.iam.oauth2.application.configuration.OAuth2SsoStepConfig;
import com.airlock.iam.oauth2.application.configuration.OpenIDConnectSsoFlowClientSettingsBase;
import com.airlock.iam.oauth2.application.configuration.accountlinking.AccountLinkingSelfServiceConfig;
import com.airlock.iam.oauth2.domain.model.accountregistration.AccountRegistrationUserFactory;
import com.airlock.iam.oauth2.domain.model.client.communication.OAuth2ClientCommunicator;
import com.airlock.iam.oauth2.domain.model.client.communication.OAuth2ClientCommunicatorFactory;
import com.airlock.iam.oauth2.domain.model.settings.OAuth2SsoIdentificationServiceSettings;
import com.airlock.iam.oauth2.domain.model.sso.OAuth2SsoStepSession;
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.accountlinking.AccountLinkAccountInfoProviderImpl;
import com.airlock.iam.oauth2.domain.service.accountlinking.AccountLinkRepository;
import com.airlock.iam.oauth2.domain.service.accountlinking.AccountLinkRepositoryImpl;
import com.airlock.iam.oauth2.domain.service.accountlinking.UserLookupRepository;
import com.airlock.iam.oauth2.domain.service.logout.LogoutPropagationService;
import com.airlock.iam.oauth2.domain.service.logout.LogoutPropagationServiceImpl;
import com.google.inject.Exposed;
import com.google.inject.PrivateModule;
import com.google.inject.Provides;
import jakarta.annotation.Nonnull;
import jakarta.inject.Provider;
import java.util.Optional;

@ModuleForConfig(OAuth2SsoStepConfig.class)
public class OAuth2SsoStepModule extends PrivateModule {
   @Provides
   @Exposed
   AuthenticationMethod provideAuthenticationMethod(OAuth2SsoStepConfig config) {
      return AuthenticationMethodFactory.authMethodFor(config.getAuthenticationMethodId());
   }

   @Exposed
   @Provides
   OAuth2SsoStepSession provideOauth2SsoStepSession(FlowSession flowSession) {
      return StepSessionModuleHelper.retrieveStepSession(flowSession, OAuth2SsoStepSession.class);
   }

   @Exposed
   @Provides
   ProviderId provideProviderId(OAuth2SsoStepConfig config) {
      return config.getProviderId().providerId();
   }

   @Provides
   OAuth2SsoFlowClientSettingsBase provideFlowClientSettings(ProviderId providerId, Optional<OAuth2ClientSettingsConfigSpec> clientSettings) {
      Optional<OAuth2SsoFlowClientSettingsBase> stepClientSettings = clientSettings.flatMap(spec -> spec.findFlowClientSettings(providerId));
      return ServiceConfigurationException.checkPresent(
         stepClientSettings, String.format("No OAuth 2.0 flow based settings with provider %s configured in OAuth 2.0 SSO settings.", providerId.id())
      );
   }

   @Provides
   OAuth2SsoIdentificationServiceSettings provideOAuth2SsoIdentificationServiceSettings(OAuth2SsoFlowClientSettingsBase flowClientSetting) {
      return OAuth2SsoIdentificationServiceSettings.builder()
         .accountInfo(flowClientSetting.getAccountLinkingSelfService().flatMap(AccountLinkingSelfServiceConfig::getAccountInfo))
         .autoLinkExistingUsersContextDataField(flowClientSetting.getAutoLinkExistingUsersContextDataField())
         .build();
   }

   @Provides
   Optional<AccountRegistrationUserFactory> provideAccountRegistrationUserFactory(OAuth2SsoFlowClientSettingsBase flowClientSetting, UserStore userStore) {
      return flowClientSetting.getAccountRegistrationConfig().map(accountRegistrationConfig -> accountRegistrationConfig.logic(userStore));
   }

   @Provides
   @Exposed
   Optional<OAuth2AccountRegistrationService> provideOAuth2AccountRegistrationService(
      Optional<AccountRegistrationUserFactory> accountRegistrationUserFactory,
      AccountLinkAccountInfoProvider accountInfoProvider,
      Provider<IdentityProvidingService> identificationService,
      ProviderId providerId,
      Provider<InsertingUserStore> userStore,
      Optional<AccountLinkRepository> accountLinkRepository
   ) {
      return accountRegistrationUserFactory.map(
         factory -> OAuth2AccountRegistrationService.builder()
            .accountRegistrationUserFactory(factory)
            .identificationService(identificationService)
            .providerId(providerId)
            .userStore((InsertingUserStore)userStore.get())
            .accountLinkRepository(accountLinkRepository)
            .accountInfoProvider(accountInfoProvider)
            .build()
      );
   }

   @Provides
   Optional<String> provideAutoLinkExistingUsersContextDataField(OAuth2SsoFlowClientSettingsBase flowClientSetting) {
      return flowClientSetting.getAutoLinkExistingUsersContextDataField();
   }

   @Provides
   Optional<AccountLinkPersister> provideAccountLinkPersister(Optional<OAuth2ClientSettingsConfigSpec> configSpecOptional) {
      return configSpecOptional.map(OAuth2ClientSettingsConfigSpec::getAccountLinkPersister).map(AccountLinkPersisterConfig::persister);
   }

   @Provides
   Optional<AccountLinkRepository> provideAccountLinkRepository(
      Optional<AccountLinkPersister> optionalAccountLinkPersister, OAuth2SsoFlowClientSettingsBase flowClientSetting
   ) {
      if (flowClientSetting.isEnableAccountLinking()) {
         AccountLinkPersister accountLinkPersister = ServiceConfigurationException.checkPresent(
            optionalAccountLinkPersister, "No Account Link Persister configured in OAuth 2.0/OIDC Client settings."
         );
         return Optional.of(AccountLinkRepositoryImpl.builder().persister(accountLinkPersister).build());
      } else {
         return Optional.empty();
      }
   }

   @Provides
   Optional<UserLookupRepository> provideUserLookupRepository(OAuth2SsoFlowClientSettingsBase flowClientSettings, UserStore userStore) {
      Optional<String> linkExistingUsersContextDataField = flowClientSettings.getAutoLinkExistingUsersContextDataField();
      return linkExistingUsersContextDataField.map(field -> new UserLookupRepository(userStore, field));
   }

   @Provides
   InsertingUserStore provideInsertingUserStore(UserStore userStore) {
      return Adapter.adapt(userStore)
         .ifPossibleTo(InsertingUserStore.class)
         .orElseThrow(() -> new FailureException("Configured user store does not support insertion of a user."));
   }

   @Provides
   @Exposed
   OAuth2ClientCommunicator provideCommunicator(PluginConfigToLogic configToLogic, OAuth2SsoFlowClientSettingsBase flowClientSettings) {
      OAuth2ClientCommunicatorFactory communicatorFactory = configToLogic.getLogic(flowClientSettings);
      return communicatorFactory.initCommunicator(configToLogic.getLogic(flowClientSettings.getClientRedirectURI()));
   }

   @Provides
   @Exposed
   Optional<LogoutPropagationService> provideLogoutPropagation(OAuth2SsoFlowClientSettingsBase config) {
      return config instanceof OpenIDConnectSsoFlowClientSettingsBase
         ? this.logoutPropagationService((OpenIDConnectSsoFlowClientSettingsBase)config)
         : Optional.empty();
   }

   @Provides
   @Exposed
   Optional<AcknowledgeMessageId> provideAcknowledgeMessageId(OAuth2SsoStepConfig config) {
      return Optional.ofNullable(config.getMessageId()).map(AcknowledgeMessageId::messageId);
   }

   @Provides
   @Exposed
   Optional<RedFlag> provideMissingAccountLinkRedflag(OAuth2SsoFlowClientSettingsBase config) {
      return config.getMissingAccountLinkRedFlag().map(RedFlagConfig::toRedFlag).filter(flag -> config.isEnableAccountLinking());
   }

   protected void configure() {
      this.bind(AccountLinkAccountInfoProvider.class).to(AccountLinkAccountInfoProviderImpl.class);
      this.bind(OAuth2SsoIdentificationService.class);
      this.expose(AccountLinkAccountInfoProvider.class);
      this.expose(OAuth2SsoIdentificationService.class);
   }

   @Nonnull
   private Optional<LogoutPropagationService> logoutPropagationService(OpenIDConnectSsoFlowClientSettingsBase config) {
      if (config.getEndSessionEndpoint() != null) {
         LogoutPropagationService logoutPropagationService = new LogoutPropagationServiceImpl(
            config.supportsEndSessionEndpoint(), config.getEndSessionEndpoint(), config.getPostLogoutRedirectUrl()
         );
         return Optional.of(logoutPropagationService);
      } else {
         return Optional.empty();
      }
   }
}
