package com.airlock.iam.oauth2.domain.service;

import com.airlock.iam.authentication.api.domain.service.username.IdentificationResult;
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.user.UserId;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.api.domain.model.user.PersistentUser;
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.api.domain.model.user.transformation.AccountLinkingUsernameTransformationImpl;
import com.airlock.iam.common.domain.model.accountlink.AccountInfo;
import com.airlock.iam.common.domain.model.accountlink.AccountSub;
import com.airlock.iam.common.domain.model.accountlink.persistence.AccountLinkData;
import com.airlock.iam.common.domain.model.provider.ProviderId;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.login.api.domain.model.oauth2.OAuth2Credential;
import com.airlock.iam.oauth2.domain.model.settings.OAuth2SsoIdentificationServiceSettings;
import com.airlock.iam.oauth2.domain.service.accountlinking.AccountLinkAccountInfoProvider;
import com.airlock.iam.oauth2.domain.service.accountlinking.AccountLinkRepository;
import com.airlock.iam.oauth2.domain.service.accountlinking.UserLookupRepository;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.Optional;

public class OAuth2SsoIdentificationService {
   private static final ELogger LOG = ELoggerFactory.getLogger(OAuth2SsoIdentificationService.class);
   private final OAuth2SsoIdentificationServiceSettings settings;
   private final AccountLinkAccountInfoProvider accountInfoProvider;
   private final ProviderId providerId;
   private final Optional<AccountLinkRepository> accountLinkRepository;
   private final Optional<UserLookupRepository> userLookupRepository;
   private final Provider<IdentityProvidingService> identificationService;

   @Inject
   public OAuth2SsoIdentificationService(
      OAuth2SsoIdentificationServiceSettings settings,
      AccountLinkAccountInfoProvider accountInfoProvider,
      ProviderId providerId,
      Optional<AccountLinkRepository> accountLinkRepository,
      Optional<UserLookupRepository> userLookupRepository,
      Provider<IdentityProvidingService> identificationService
   ) {
      this.settings = settings;
      this.accountInfoProvider = accountInfoProvider;
      this.providerId = providerId;
      this.accountLinkRepository = accountLinkRepository;
      this.userLookupRepository = userLookupRepository;
      this.identificationService = identificationService;
   }

   public IdentificationResult identify(OAuth2Credential oAuth2Credential) {
      return this.isAccountLinkingEnabled()
         ? this.accountLinkingCredential(oAuth2Credential)
            .map(credential -> UserId.userId(credential.getUsername().getValue()))
            .map(AccountLinkingUsernameTransformationImpl::new)
            .map(transformation -> ((IdentityProvidingService)this.identificationService.get()).tryIdentify(oAuth2Credential.getUsername(), transformation))
            .orElse(
               IdentificationResult.userNotFound(new UserIdentity(UserId.userId(oAuth2Credential.getUsername().getValue()), oAuth2Credential.getUsername()))
            )
         : ((IdentityProvidingService)this.identificationService.get()).tryIdentify(oAuth2Credential.getUsername());
   }

   private boolean isAccountLinkingEnabled() {
      return this.accountLinkRepository.isPresent();
   }

   private Optional<OAuth2Credential> accountLinkingCredential(OAuth2Credential credential) {
      AccountSub accountSub = AccountSub.accountSub(credential.getUsername());
      return this.findAccountLinkUsernameForProvider(accountSub)
         .map(
            username -> {
               LOG.info(
                  String.format(
                     "User %s with provider ID %s has account linking to local user %s",
                     LogUtil.logSafeQuote(accountSub.sub()),
                     LogUtil.logSafeQuote(this.providerId.id()),
                     LogUtil.logSafeQuote(username)
                  )
               );
               return credential.toNewCredential(ProvidedUsername.providedUsernameOf(username));
            }
         )
         .or(() -> this.autoLinkedCredential(credential));
   }

   private Optional<String> findAccountLinkUsernameForProvider(AccountSub accountSub) {
      return this.accountLinkRepository
         .<Optional<String>>map(repository -> repository.findUsernameForProvider(this.providerId, accountSub))
         .orElseThrow(() -> new FailureException("account link persister is required for account linking."));
   }

   private Optional<OAuth2Credential> autoLinkedCredential(OAuth2Credential credential) {
      if (this.settings.getAutoLinkExistingUsersContextDataField().isPresent()) {
         Optional<PersistentUser> matchingUser = this.findMatchingUserForAutoLinkExistingUsersContextDataField(credential);
         if (matchingUser.isPresent()) {
            return this.autoLinkedCredentialsForMatchingUser(credential, matchingUser.get());
         }
      }

      return Optional.empty();
   }

   private Optional<PersistentUser> findMatchingUserForAutoLinkExistingUsersContextDataField(OAuth2Credential credential) {
      return this.userLookupRepository.flatMap(service -> service.findMatchingUser(credential));
   }

   private Optional<OAuth2Credential> autoLinkedCredentialsForMatchingUser(OAuth2Credential credential, PersistentUser matchingUser) {
      AccountSub accountSub = AccountSub.accountSub(credential.getUsername());
      LOG.info(
         String.format(
            "User %s with provider ID %s has auto account linking to local user %s",
            LogUtil.logSafeQuote(accountSub.sub()),
            LogUtil.logSafeQuote(this.providerId.id()),
            LogUtil.logSafeQuote(matchingUser)
         )
      );
      AccountLinkData accountLinkData = this.persistAndLoadAccountLink(matchingUser.getName(), accountSub, this.accountInfoProvider.accountInfo(credential));
      return Optional.of(credential.toNewCredential(ProvidedUsername.providedUsernameOf(accountLinkData.getUsername())));
   }

   private AccountLinkData persistAndLoadAccountLink(String username, AccountSub accountSub, AccountInfo accountInfo) {
      return this.accountLinkRepository
         .<AccountLinkData>map(repository -> repository.persistAndLoadAccountLink(username, this.providerId, accountSub, accountInfo))
         .orElseThrow(() -> new FailureException("account link persister is required for account linking."));
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public static class OAuth2SsoIdentificationServiceBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private OAuth2SsoIdentificationServiceSettings settings;
      @SuppressFBWarnings(justification = "generated code")
      private AccountLinkAccountInfoProvider accountInfoProvider;
      @SuppressFBWarnings(justification = "generated code")
      private ProviderId providerId;
      @SuppressFBWarnings(justification = "generated code")
      private Optional<AccountLinkRepository> accountLinkRepository;
      @SuppressFBWarnings(justification = "generated code")
      private Optional<UserLookupRepository> userLookupRepository;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<IdentityProvidingService> identificationService;

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

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2SsoIdentificationService.OAuth2SsoIdentificationServiceBuilder settings(OAuth2SsoIdentificationServiceSettings settings) {
         this.settings = settings;
         return this;
      }

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

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

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2SsoIdentificationService.OAuth2SsoIdentificationServiceBuilder accountLinkRepository(Optional<AccountLinkRepository> accountLinkRepository) {
         this.accountLinkRepository = accountLinkRepository;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2SsoIdentificationService.OAuth2SsoIdentificationServiceBuilder userLookupRepository(Optional<UserLookupRepository> userLookupRepository) {
         this.userLookupRepository = userLookupRepository;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2SsoIdentificationService.OAuth2SsoIdentificationServiceBuilder identificationService(
         Provider<IdentityProvidingService> identificationService
      ) {
         this.identificationService = identificationService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OAuth2SsoIdentificationService build() {
         return new OAuth2SsoIdentificationService(
            this.settings, this.accountInfoProvider, this.providerId, this.accountLinkRepository, this.userLookupRepository, this.identificationService
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "OAuth2SsoIdentificationService.OAuth2SsoIdentificationServiceBuilder(settings="
            + this.settings
            + ", accountInfoProvider="
            + this.accountInfoProvider
            + ", providerId="
            + this.providerId
            + ", accountLinkRepository="
            + this.accountLinkRepository
            + ", userLookupRepository="
            + this.userLookupRepository
            + ", identificationService="
            + this.identificationService
            + ")";
      }
   }
}
