package com.airlock.iam.login.app.domain;

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.authentication.Authentee;
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.persistency.exception.PersisterException;
import com.airlock.iam.common.api.domain.model.request.Request;
import com.airlock.iam.common.api.domain.model.role.Role;
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.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.role.RolesUtil;
import com.airlock.iam.core.domain.model.authentication.factor.AuthenticationFactorUsageImpl;
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.elog.api.LogTypes;
import com.airlock.iam.login.app.application.configuration.oauth.OAuth2TokenRequestAuthenticationConfig;
import com.airlock.iam.oauth2.api.domain.model.OAuth2Scope;
import com.airlock.iam.oauth2.domain.model.token.OAuth2AccessToken;
import com.airlock.iam.oauth2.domain.service.token.OAuth2LoginTokenService;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;

@LogicForConfig(OAuth2TokenRequestAuthenticationConfig.class)
public final class OAuth2TokenRequestAuthentication implements RequestAuthentication {
   private static final ELogger LOG = ELoggerFactory.getLogger(OAuth2TokenRequestAuthentication.class);
   private static final String BEARER_PREFIX = "Bearer ";
   private final Provider<OAuth2LoginTokenService> tokenService;
   private final Provider<OAuth2TokenRequestAuthentication.OAuth2TokenRequestAuthenticationSettings> settings;

   @Inject
   public OAuth2TokenRequestAuthentication(
      Provider<OAuth2LoginTokenService> tokenService, Provider<OAuth2TokenRequestAuthentication.OAuth2TokenRequestAuthenticationSettings> settings
   ) {
      this.tokenService = tokenService;
      this.settings = settings;
   }

   @Override
   public Optional<AuthenticationResult> extractCredentialAndAuthenticate(Request request) {
      Optional<OAuth2AccessToken> accessToken = this.extractCredential(request);
      if (accessToken.isEmpty()) {
         return Optional.empty();
      }

      UserIdentity identity = createIdentity(accessToken.get());
      Authentee authentee = new AuthenteeImpl(identity, this.extractRoles(accessToken.get()), ContextDataImpl.emptyContextData());
      return !validateAccessToken(accessToken.get())
         ? Optional.of(
            AuthenticationResultImpl.failedAuthenticationWithFactorCheck(
               identity, true, AuthenticationFactorUsageImpl.failedFactorUsage(KnownAuthenticationFactorDetail.TOKEN_OAUTH2)
            )
         )
         : Optional.of(
            AuthenticationResultImpl.successfulAuthenticationWithFactorCheck(
               authentee, AuthenticationFactorUsageImpl.succeededFactorUsage(KnownAuthenticationFactorDetail.TOKEN_OAUTH2)
            )
         );
   }

   private static UserIdentity createIdentity(OAuth2AccessToken accessToken) {
      return new UserIdentity(accessToken.getUserId(), ProvidedUsername.providedUsernameOf(accessToken.getUserId().getValue()));
   }

   private Set<Role> extractRoles(OAuth2AccessToken accessToken) {
      return ((OAuth2TokenRequestAuthentication.OAuth2TokenRequestAuthenticationSettings)this.settings.get()).addScopesAsRoles()
         ? RolesUtil.rolesFrom(OAuth2Scope.convertToScopeNames(accessToken.getScopes().scopesAsSet()))
         : Collections.emptySet();
   }

   private Optional<OAuth2AccessToken> extractCredential(Request request) {
      Optional<String> token = extractBearerToken(request);
      if (token.isEmpty()) {
         LOG.debug("No bearer token found in Authorization header.");
         return Optional.empty();
      }

      Optional<OAuth2AccessToken> accessToken = this.loadAccessToken(token.get());
      if (accessToken.isEmpty()) {
         LOG.debug("Access token could not be resolved or is not valid.");
      }

      return accessToken;
   }

   private Optional<OAuth2AccessToken> loadAccessToken(String token) {
      try {
         return ((OAuth2LoginTokenService)this.tokenService.get()).getAccessTokenWithoutClientIdValidation(token);
      } catch (PersisterException e) {
         throw new FailureException(e);
      }
   }

   private static boolean validateAccessToken(OAuth2AccessToken accessToken) {
      if (accessToken.getTokenBinding().isPresent()) {
         LOG.warn(
            LogTypes.CONFIGURATION_ERROR,
            "Access Token for user "
               + LogUtil.quote(accessToken.getUserId())
               + " is certificate-bound. Rejecting token because certificate verification is not yet implemented."
         );
         return false;
      } else {
         return true;
      }
   }

   private static Optional<String> extractBearerToken(Request request) {
      return request.extractHeaderValue("Authorization")
         .<String>map(StringUtils::trimToNull)
         .filter(header -> Strings.CI.startsWith(header, "Bearer "))
         .map(header -> Strings.CI.removeStart(header, "Bearer "))
         .map(StringUtils::trimToNull);
   }

   public record OAuth2TokenRequestAuthenticationSettings(boolean addScopesAsRoles) {
   }
}
