package com.airlock.iam.common.domain.model.credential;

import com.airlock.iam.base.api.domain.model.plugin.LogicForConfig;
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.authentication.AuthenticationResult;
import com.airlock.iam.common.api.domain.model.credential.RequestAuthentication;
import com.airlock.iam.common.api.domain.model.request.Request;
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.UsernameTransformation;
import com.airlock.iam.common.application.configuration.credential.BasicAuthRequestAuthenticationConfig;
import com.airlock.iam.common.domain.model.authentication.AuthenteeImpl;
import com.airlock.iam.common.domain.model.authentication.AuthenticationResultImpl;
import com.airlock.iam.common.domain.model.contextdata.ContextDataImpl;
import com.airlock.iam.common.domain.model.password.PasswordAuthenticationResult;
import com.airlock.iam.common.domain.model.principal.UserPrincipal;
import com.airlock.iam.common.domain.model.transform.TransformWithLogContextUpdate;
import com.airlock.iam.common.domain.service.credential.HttpBasicAuthExtractor;
import com.airlock.iam.common.domain.service.password.UsernamePasswordAuthenticationService;
import com.airlock.iam.common.domain.service.principal.UserPrincipalService;
import com.airlock.iam.common.domain.service.username.UsernameValidationService;
import com.airlock.iam.core.domain.model.authentication.factor.AuthenticationFactorUsageImpl;
import com.airlock.iam.core.domain.model.authentication.factor.KnownAuthenticationFactor;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.inject.Provider;
import java.util.List;
import java.util.Optional;

@LogicForConfig(BasicAuthRequestAuthenticationConfig.class)
public final class BasicAuthRequestAuthentication implements RequestAuthentication {
   public static final String BASIC_AUTH_REQUEST_AUTHENTICATION_USER_PRINCIPAL_SERVICE = "BasicAuthRequestAuthenticationUserPrincipalService";
   private static final ELogger LOG = ELoggerFactory.getLogger(BasicAuthRequestAuthentication.class);
   private final Provider<UsernameTransformation> usernameTransformation;
   private final Provider<UsernamePasswordAuthenticationService> passwordAuthenticationService;
   private final Provider<BasicAuthSettings> settings;
   private final Provider<UserPrincipalService> userPrincipalService;
   private final Provider<UsernameValidationService> usernameValidationService;

   @Inject
   public BasicAuthRequestAuthentication(
      Provider<UsernameTransformation> usernameTransformation,
      Provider<UsernamePasswordAuthenticationService> passwordAuthenticationService,
      Provider<BasicAuthSettings> settings,
      @Named("BasicAuthRequestAuthenticationUserPrincipalService") Provider<UserPrincipalService> userPrincipalService,
      Provider<UsernameValidationService> usernameValidationService
   ) {
      this.usernameTransformation = usernameTransformation;
      this.passwordAuthenticationService = passwordAuthenticationService;
      this.settings = settings;
      this.userPrincipalService = userPrincipalService;
      this.usernameValidationService = usernameValidationService;
   }

   @Override
   public Optional<AuthenticationResult> extractCredentialAndAuthenticate(Request request) {
      Optional<HttpBasicAuthExtractor.BasicAuthHeader> header = HttpBasicAuthExtractor.extractBasicAuthHeader(
         request.extractHeaderValue("Authorization"), ((BasicAuthSettings)this.settings.get()).charset()
      );
      if (header.isEmpty()) {
         return Optional.empty();
      }

      ProvidedUsername providedUsername = header.get().username();
      if (!((UsernameValidationService)this.usernameValidationService.get()).isValid(providedUsername)) {
         LOG.info("Provided username rejected by global username filter.");
         return Optional.empty();
      }

      UserId transformedUserId = TransformWithLogContextUpdate.transformAndUpdateLogContext(
         (UsernameTransformation)this.usernameTransformation.get(), providedUsername
      );
      UserIdentity userIdentity = new UserIdentity(transformedUserId, providedUsername);
      UserPrincipal identifiedPrincipal = ((UserPrincipalService)this.userPrincipalService.get()).initializePrincipal(userIdentity);
      PasswordAuthenticationResult result = ((UsernamePasswordAuthenticationService)this.passwordAuthenticationService.get())
         .checkPassword(identifiedPrincipal, header.get().password());
      if (result.isUserInvalid() || result.isUserLocked()) {
         LOG.info("Basic Authentication not possible for user " + LogUtil.logSafeQuote(transformedUserId) + ": user is locked or invalid.");
         return Optional.of(
            AuthenticationResultImpl.failedAuthenticationWithoutFactorCheck(
               userIdentity, AuthenticationFactorUsageImpl.failedFactorUsage(KnownAuthenticationFactor.PASSWORD)
            )
         );
      }

      if (!result.isPasswordCorrect()) {
         if (result.isPasswordCheckPerformed()) {
            LOG.info("Basic Authentication check failed for user " + LogUtil.logSafeQuote(transformedUserId) + ": password is not correct.");
         }

         return Optional.of(
            new AuthenticationResultImpl(
               List.of(AuthenticationFactorUsageImpl.failedFactorUsage(KnownAuthenticationFactor.PASSWORD)),
               userIdentity,
               result.isPasswordCheckPerformed(),
               true,
               Optional.empty()
            )
         );
      } else {
         if (!result.isPasswordExpired() && !result.isPasswordChangeEnforced() && !result.hasPasswordPolicyViolations()) {
            return Optional.of(
               new AuthenticationResultImpl(
                  List.of(AuthenticationFactorUsageImpl.succeededFactorUsage(KnownAuthenticationFactor.PASSWORD)),
                  userIdentity,
                  true,
                  true,
                  Optional.of(new AuthenteeImpl(userIdentity, result.getPasswordCheckRoles(), ContextDataImpl.emptyContextData()))
               )
            );
         }

         LOG.info("Basic Authentication not possible for user " + LogUtil.logSafeQuote(transformedUserId) + ": mandatory password change required.");
         return Optional.of(
            AuthenticationResultImpl.failedAuthenticationWithFactorCheck(
               userIdentity, false, AuthenticationFactorUsageImpl.failedFactorUsage(KnownAuthenticationFactor.PASSWORD)
            )
         );
      }
   }
}
