package com.airlock.iam.admin.application.service.authentication;

import com.airlock.iam.admin.application.service.token.password.AdminPasswordAdminService;
import com.airlock.iam.admin.domain.model.session.AdminappSession;
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.authentication.Authentee;
import com.airlock.iam.common.api.domain.model.password.Password;
import com.airlock.iam.common.api.domain.model.persistency.exception.NotFoundException;
import com.airlock.iam.common.api.domain.model.persistency.exception.NotUniqueException;
import com.airlock.iam.common.api.domain.model.role.Role;
import com.airlock.iam.common.api.domain.model.user.CurrentLocale;
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.application.service.password.command.ChangePassword;
import com.airlock.iam.common.application.service.session.SessionTerminationService;
import com.airlock.iam.common.domain.model.authentication.AuthenteeImpl;
import com.airlock.iam.common.domain.model.authentication.factor.AuthenticationFactorConverter;
import com.airlock.iam.common.domain.model.contextdata.ContextDataImpl;
import com.airlock.iam.common.domain.model.role.RolesUtil;
import com.airlock.iam.common.domain.model.user.TentativePrincipalInfo;
import com.airlock.iam.common.domain.service.SessionIdService;
import com.airlock.iam.common.domain.service.session.IamSessionService;
import com.airlock.iam.core.domain.model.authentication.factor.KnownAuthenticationFactor;
import com.airlock.iam.core.misc.authen.Authenticator;
import com.airlock.iam.core.misc.authen.AuthenticatorException;
import com.airlock.iam.core.misc.authen.UsernameTransformationManager;
import com.airlock.iam.core.misc.authen.PasswordService.PasswordChangeContext;
import com.airlock.iam.core.misc.authen.credential.Credential;
import com.airlock.iam.core.misc.authen.result.AuthenticationFailed;
import com.airlock.iam.core.misc.authen.result.AuthenticationFailedPassword;
import com.airlock.iam.core.misc.authen.result.AuthenticationResult;
import com.airlock.iam.core.misc.authen.result.PasswordChangeResult;
import com.airlock.iam.core.misc.impl.authen.credential.UserPasswordCredentialImpl;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.log.audit.AuditLogger;
import com.airlock.iam.reporting.api.domain.model.data.ActionStatus;
import com.airlock.iam.reporting.api.domain.model.logger.Reporting;
import com.airlock.iam.reporting.domain.model.ReportingDataFactory;
import com.google.common.base.Preconditions;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.Collections;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

public class AdminappAuthenticationServiceImpl implements AdminappAuthenticationService {
   private static final ELogger LOG = ELoggerFactory.getLogger(AdminappAuthenticationServiceImpl.class);
   private final Provider<Optional<Authenticator>> authenticator;
   private final Provider<CurrentLocale> locale;
   private final Provider<AdminappSession> session;
   private final Provider<IamSessionService<AdminappSession>> sessionService;
   private final Provider<SessionTerminationService> sessionTerminationService;
   private final Provider<AdminPasswordAdminService> passwordChangeService;
   private final Provider<UsernameTransformationManager> usernameTransformationManager;
   private final Provider<Reporting> reporting;
   private final Provider<SessionIdService> sessionIdService;
   private final Provider<TentativePrincipalInfo> principalInfo;

   @Inject
   public AdminappAuthenticationServiceImpl(
      Provider<Optional<Authenticator>> authenticator,
      Provider<CurrentLocale> locale,
      Provider<AdminappSession> session,
      Provider<IamSessionService<AdminappSession>> sessionService,
      Provider<SessionTerminationService> sessionTerminationService,
      Provider<AdminPasswordAdminService> passwordChangeService,
      Provider<UsernameTransformationManager> usernameTransformationManager,
      Provider<Reporting> reporting,
      Provider<SessionIdService> sessionIdService,
      Provider<TentativePrincipalInfo> principalInfo
   ) {
      this.authenticator = authenticator;
      this.locale = locale;
      this.session = session;
      this.sessionService = sessionService;
      this.sessionTerminationService = sessionTerminationService;
      this.passwordChangeService = passwordChangeService;
      this.usernameTransformationManager = usernameTransformationManager;
      this.reporting = reporting;
      this.sessionIdService = sessionIdService;
      this.principalInfo = principalInfo;
   }

   @Override
   public AdminappAuthenticationService.PasswordCheckResult checkPassword(String providedUsername, Password password) {
      if (((Optional)this.authenticator.get()).isEmpty()) {
         return AdminappAuthenticationService.PasswordCheckResult.SSO_TICKET_MISSING;
      }

      String transformedUsername;
      try {
         transformedUsername = this.transformedUsername(providedUsername);
      } catch (NotFoundException e) {
         return AdminappAuthenticationService.PasswordCheckResult.USERNAME_PASSWORD_WRONG;
      } catch (NotUniqueException e) {
         return AdminappAuthenticationService.PasswordCheckResult.USER_AMBIGUOUS;
      }

      AuthenticationResult result = this.callAuthenticator(new UserIdentity(UserId.userId(transformedUsername), providedUsername), password);
      if (result.authenticationSucceeded()) {
         this.authenticateSession(this.extractAuthentee(result, providedUsername));
         return AdminappAuthenticationService.PasswordCheckResult.OK;
      } else {
         return this.mapFailure(result);
      }
   }

   @Override
   public AdminappAuthenticationService.PasswordCheckAndChangeResult checkAndChangePassword(String providedUsername, Password oldPassword, Password newPassword) {
      if (((Optional)this.authenticator.get()).isEmpty()) {
         return new AdminappAuthenticationService.PasswordCheckAndChangeResult(
            Optional.of(AdminappAuthenticationService.PasswordCheckResult.SSO_TICKET_MISSING), Collections.emptyList()
         );
      }

      String transformedUsername;
      try {
         transformedUsername = this.transformedUsername(providedUsername);
      } catch (NotFoundException e) {
         return new AdminappAuthenticationService.PasswordCheckAndChangeResult(
            Optional.of(AdminappAuthenticationService.PasswordCheckResult.USERNAME_PASSWORD_WRONG), Collections.emptyList()
         );
      } catch (NotUniqueException e) {
         return new AdminappAuthenticationService.PasswordCheckAndChangeResult(
            Optional.of(AdminappAuthenticationService.PasswordCheckResult.USER_AMBIGUOUS), Collections.emptyList()
         );
      }

      AuthenticationResult checkResult = this.callAuthenticator(new UserIdentity(UserId.userId(transformedUsername), providedUsername), oldPassword);
      if (!this.isSuccessfulCheck(checkResult)) {
         return new AdminappAuthenticationService.PasswordCheckAndChangeResult(Optional.of(this.mapFailure(checkResult)), Collections.emptyList());
      }

      Authentee authentee = this.extractAuthentee(checkResult, providedUsername);
      PasswordChangeResult changeResult = this.changePassword(
         authentee.userIdentity(), oldPassword, newPassword, checkResult.isOfType(AuthenticationFailedPassword.PASSWORD_CHANGE_REQUIRED)
      );
      if (changeResult.didPasswordChangeSucceed()) {
         this.authenticateSession(authentee);
      }

      Preconditions.checkState(!changeResult.isExistingPasswordWrong(), "Existing password cannot be wrong at this point");
      return new AdminappAuthenticationService.PasswordCheckAndChangeResult(Optional.empty(), changeResult.getPasswordPolicyViolations());
   }

   @Override
   public boolean hasAuthenticatedSession() {
      return ((AdminappSession)this.session.get()).isAuthenticated();
   }

   private boolean isSuccessfulCheck(AuthenticationResult result) {
      return result.authenticationSucceeded() || result.isOfType(AuthenticationFailedPassword.PASSWORD_CHANGE_REQUIRED);
   }

   private String transformedUsername(String providedUsername) throws NotFoundException, NotUniqueException {
      return ((UsernameTransformationManager)this.usernameTransformationManager.get()).transformProvidedUsername(providedUsername);
   }

   private Authentee extractAuthentee(AuthenticationResult result, String providedUsername) {
      if (result.authenticationSucceeded()) {
         return toAuthentee(result.asSuccessfulAuthResult().getAuthentee(), providedUsername);
      } else if (result.isOfType(AuthenticationFailedPassword.PASSWORD_CHANGE_REQUIRED)) {
         return toAuthentee(
            Objects.requireNonNull(AuthenticationFailedPassword.PASSWORD_CHANGE_REQUIRED.getAuthenteeForPasswordChange(result)), providedUsername
         );
      } else {
         throw new IllegalStateException("Unexpected authentication result type while extracting authentee.");
      }
   }

   private static Authentee toAuthentee(com.airlock.iam.core.misc.authen.Authentee authentee, String providedUsername) {
      Set<Role> roles = RolesUtil.rolesFrom(authentee.getGrantedRoleNames());
      return new AuthenteeImpl(
         new UserIdentity(UserId.userId(authentee.getName()), ProvidedUsername.providedUsernameOf(providedUsername)),
         roles,
         new ContextDataImpl(authentee.getContextDataContainer())
      );
   }

   private PasswordChangeResult changePassword(UserIdentity identity, Password oldPassword, Password newPassword, boolean enforced) {
      ChangePassword parameters = ChangePassword.builder()
         .userId(identity.id().getValue())
         .oldPassword(oldPassword.getValue())
         .newPassword(newPassword.getValue())
         .context(enforced ? PasswordChangeContext.ENFORCED : PasswordChangeContext.VOLUNTARY)
         .build();
      return ((AdminPasswordAdminService)this.passwordChangeService.get()).changePassword(parameters);
   }

   @Override
   public void logout() {
      ((AdminappSession)this.session.get()).deauthenticate();
      ((SessionTerminationService)this.sessionTerminationService.get()).terminateSessions();
   }

   private AuthenticationResult callAuthenticator(UserIdentity unauthenticatedAdmin, Password password) {
      ((TentativePrincipalInfo)this.principalInfo.get()).setTentativeIdentity(unauthenticatedAdmin);
      Credential credential = new UserPasswordCredentialImpl(unauthenticatedAdmin.id().getValue(), password.getValue());
      credential.setDisplayLanguage(((CurrentLocale)this.locale.get()).getLanguage());

      try {
         Authenticator adminAuthenticator = (Authenticator)((Optional)this.authenticator.get()).orElseThrow();
         AuthenticationResult result = adminAuthenticator.authenticate(adminAuthenticator.getAuthenticationSession(), credential);
         ((Reporting)this.reporting.get())
            .report(
               ReportingDataFactory.dataForAuthnFactorUse(AuthenticationFactorConverter.convertFactor(KnownAuthenticationFactor.PASSWORD))
                  .status(this.isSuccessfulCheck(result) ? ActionStatus.SUCCESS : ActionStatus.FAILURE)
                  .build()
            );
         LOG.debug("Result from calling admin authenticator: " + result);
         return result;
      } catch (AuthenticatorException e) {
         throw new FailureException(e);
      }
   }

   private AdminappAuthenticationService.PasswordCheckResult mapFailure(AuthenticationResult result) {
      if (this.userJustGotLocked(result)) {
         return AdminappAuthenticationService.PasswordCheckResult.USER_LOCKED;
      } else if (result.isOfType(AuthenticationFailed.USER_NOT_FOUND)
         || result.isOfType(AuthenticationFailedPassword.PASSWORD_WRONG)
         || result.isOfType(AuthenticationFailedPassword.PASSWORD_WRONG_TRY_AGAIN)) {
         return AdminappAuthenticationService.PasswordCheckResult.USERNAME_PASSWORD_WRONG;
      } else if (result.isOfType(AuthenticationFailedPassword.PASSWORD_CHANGE_REQUIRED)) {
         return AdminappAuthenticationService.PasswordCheckResult.MANDATORY_PASSWORD_CHANGE;
      } else if (result.isOfType(AuthenticationFailed.USER_LOCKED)) {
         return AdminappAuthenticationService.PasswordCheckResult.USER_LOCKED;
      } else {
         return result.isOfType(AuthenticationFailed.USER_INVALID)
            ? AdminappAuthenticationService.PasswordCheckResult.USER_INVALID
            : AdminappAuthenticationService.PasswordCheckResult.UNSPECIFIED;
      }
   }

   private boolean userJustGotLocked(AuthenticationResult result) {
      return !(result instanceof AuthenticationFailed) ? false : ((AuthenticationFailed)result).isUserGotLocked();
   }

   private void authenticateSession(Authentee authentee) {
      AuditLogger.log(
         this.getClass(), "Administrator " + LogUtil.quote(authentee.userIdentity().id()) + " successfully authenticated with username and password."
      );
      ((AdminappSession)this.session.get()).authenticate(authentee);
      ((SessionIdService)this.sessionIdService.get()).forceNewSessionId();
      ((IamSessionService)this.sessionService.get()).updateSession((AdminappSession)this.session.get());
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public static class AdminappAuthenticationServiceImplBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private Provider<Optional<Authenticator>> authenticator;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<CurrentLocale> locale;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<AdminappSession> session;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<IamSessionService<AdminappSession>> sessionService;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<SessionTerminationService> sessionTerminationService;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<AdminPasswordAdminService> passwordChangeService;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<UsernameTransformationManager> usernameTransformationManager;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<Reporting> reporting;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<SessionIdService> sessionIdService;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<TentativePrincipalInfo> principalInfo;

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

      @SuppressFBWarnings(justification = "generated code")
      public AdminappAuthenticationServiceImpl.AdminappAuthenticationServiceImplBuilder authenticator(Provider<Optional<Authenticator>> authenticator) {
         this.authenticator = authenticator;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AdminappAuthenticationServiceImpl.AdminappAuthenticationServiceImplBuilder locale(Provider<CurrentLocale> locale) {
         this.locale = locale;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AdminappAuthenticationServiceImpl.AdminappAuthenticationServiceImplBuilder session(Provider<AdminappSession> session) {
         this.session = session;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AdminappAuthenticationServiceImpl.AdminappAuthenticationServiceImplBuilder sessionService(
         Provider<IamSessionService<AdminappSession>> sessionService
      ) {
         this.sessionService = sessionService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AdminappAuthenticationServiceImpl.AdminappAuthenticationServiceImplBuilder sessionTerminationService(
         Provider<SessionTerminationService> sessionTerminationService
      ) {
         this.sessionTerminationService = sessionTerminationService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AdminappAuthenticationServiceImpl.AdminappAuthenticationServiceImplBuilder passwordChangeService(
         Provider<AdminPasswordAdminService> passwordChangeService
      ) {
         this.passwordChangeService = passwordChangeService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AdminappAuthenticationServiceImpl.AdminappAuthenticationServiceImplBuilder usernameTransformationManager(
         Provider<UsernameTransformationManager> usernameTransformationManager
      ) {
         this.usernameTransformationManager = usernameTransformationManager;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AdminappAuthenticationServiceImpl.AdminappAuthenticationServiceImplBuilder reporting(Provider<Reporting> reporting) {
         this.reporting = reporting;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AdminappAuthenticationServiceImpl.AdminappAuthenticationServiceImplBuilder sessionIdService(Provider<SessionIdService> sessionIdService) {
         this.sessionIdService = sessionIdService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AdminappAuthenticationServiceImpl.AdminappAuthenticationServiceImplBuilder principalInfo(Provider<TentativePrincipalInfo> principalInfo) {
         this.principalInfo = principalInfo;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public AdminappAuthenticationServiceImpl build() {
         return new AdminappAuthenticationServiceImpl(
            this.authenticator,
            this.locale,
            this.session,
            this.sessionService,
            this.sessionTerminationService,
            this.passwordChangeService,
            this.usernameTransformationManager,
            this.reporting,
            this.sessionIdService,
            this.principalInfo
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "AdminappAuthenticationServiceImpl.AdminappAuthenticationServiceImplBuilder(authenticator="
            + this.authenticator
            + ", locale="
            + this.locale
            + ", session="
            + this.session
            + ", sessionService="
            + this.sessionService
            + ", sessionTerminationService="
            + this.sessionTerminationService
            + ", passwordChangeService="
            + this.passwordChangeService
            + ", usernameTransformationManager="
            + this.usernameTransformationManager
            + ", reporting="
            + this.reporting
            + ", sessionIdService="
            + this.sessionIdService
            + ", principalInfo="
            + this.principalInfo
            + ")";
      }
   }
}
