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

import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.api.domain.model.authentication.factor.AuthenticationFactor;
import com.airlock.iam.common.domain.model.authentication.factor.AuthenticationFactorConverter;
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 com.airlock.iam.oauth2.api.domain.model.OAuth2ClientId;
import com.airlock.iam.oauth2.api.domain.model.clientauthentication.OAuth2AuthClientId;
import com.airlock.iam.oauth2.api.domain.model.clientauthentication.OAuth2ClientAuthentication;
import com.airlock.iam.oauth2.api.domain.model.clientauthentication.OAuth2ClientAuthenticationPayload;
import com.airlock.iam.oauth2.api.domain.model.clientauthentication.OAuth2ClientAuthenticationVerificationMethod;
import com.airlock.iam.oauth2.api.domain.service.clientauthentication.OAuth2ClientAuthenticationVerifier;
import com.airlock.iam.oauth2.domain.model.clientauthentication.method.KnownClientAuthenticationMethod;
import com.airlock.iam.oauth2.domain.service.clientauthentication.OAuth2ClientAuthenticationService;
import com.airlock.iam.oauth2.domain.service.reporting.OAuth2ReportingService;
import com.airlock.iam.reporting.api.domain.model.data.AuthnFactor;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import lombok.NonNull;

public final class DefaultOAuth2ClientAuthenticationService<T extends OAuth2ClientAuthenticationPayload> implements OAuth2ClientAuthenticationService<T> {
   private static final ELogger LOG = ELoggerFactory.getLogger(DefaultOAuth2ClientAuthenticationService.class);
   private static final Map<? extends OAuth2ClientAuthenticationVerificationMethod, AuthenticationFactor> KNOWN_METHOD_TO_AUTH_FACTOR = Map.of(
      KnownClientAuthenticationMethod.BASIC_AUTH_CLIENT_SECRET,
      KnownAuthenticationFactor.PASSWORD,
      KnownClientAuthenticationMethod.POST_PARAMETERS_CLIENT_SECRET,
      KnownAuthenticationFactor.PASSWORD,
      KnownClientAuthenticationMethod.MTLS_CERTIFICATE,
      KnownAuthenticationFactor.CERTIFICATE,
      KnownClientAuthenticationMethod.PRIVATE_KEY_JWT,
      KnownAuthenticationFactor.TOKEN
   );
   @NonNull
   private final OAuth2ClientAuthenticationVerifier<T> verifier;
   @NonNull
   private final List<? extends OAuth2ClientAuthenticationVerificationMethod> enforcedMethods;
   @NonNull
   private final OAuth2ReportingService reportingService;

   @Override
   public Optional<OAuth2ClientAuthentication> authorize(T authenticationPayload) {
      Optional<? extends OAuth2ClientAuthenticationVerificationMethod> verifiedMethod = this.enforcedMethods
         .stream()
         .filter(method -> method.matches(authenticationPayload.method()))
         .findFirst();
      if (verifiedMethod.isEmpty()) {
         this.firstReportableEnforcedMethod()
            .or(this::firstEnforcedMethod)
            .ifPresent(method -> report(authnFactor -> this.reportFailure(authenticationPayload.clientId(), authnFactor), method));
         return Optional.empty();
      }

      Optional<OAuth2ClientAuthentication> result = this.verifier.verify(authenticationPayload);
      if (result.isPresent()) {
         OAuth2ClientAuthenticationPayload payload = result.get().getPayload();
         if (!verifiedMethod.get().matches(KnownClientAuthenticationMethod.NONE)) {
            report(authnFactor -> this.reportSuccess(payload.clientId(), authnFactor), payload.method());
         }
      } else {
         report(authnFactor -> this.reportFailure(authenticationPayload.clientId(), authnFactor), verifiedMethod.get());
      }

      return result;
   }

   private Optional<OAuth2ClientAuthenticationVerificationMethod> firstReportableEnforcedMethod() {
      return this.enforcedMethods.stream().filter(KNOWN_METHOD_TO_AUTH_FACTOR::containsKey).findFirst().map(Function.identity());
   }

   private Optional<OAuth2ClientAuthenticationVerificationMethod> firstEnforcedMethod() {
      return this.enforcedMethods.stream().findFirst().map(Function.identity());
   }

   private void reportFailure(Optional<OAuth2AuthClientId> clientId, AuthnFactor authnFactor) {
      clientId.map(id -> OAuth2ClientId.oAuth2ClientId(id.id()))
         .ifPresentOrElse(id -> this.reportingService.reportFactorUseFailure(id, authnFactor), () -> this.reportingService.reportFactorUseFailure(authnFactor));
      this.reportingService.reportLoginFailure();
   }

   private void reportSuccess(Optional<OAuth2AuthClientId> clientId, AuthnFactor authnFactor) {
      this.reportingService.reportFactorUseSuccess(OAuth2ClientId.oAuth2ClientId(clientId.orElseThrow().id()), authnFactor);
      this.reportingService.reportLoginSuccess();
   }

   private static void report(Consumer<AuthnFactor> report, OAuth2ClientAuthenticationVerificationMethod method) {
      if (!method.matches(KnownClientAuthenticationMethod.NONE)) {
         Optional.ofNullable(KNOWN_METHOD_TO_AUTH_FACTOR.get(method))
            .ifPresentOrElse(
               factor -> report.accept(AuthenticationFactorConverter.convertFactor(factor)),
               () -> LOG.info(
                  String.format("Unable to report for unknown OAuth 2.0 authentication verification method (class: %s).", LogUtil.quote(method.getClass()))
               )
            );
      }
   }

   @SuppressFBWarnings(justification = "generated code")
   public DefaultOAuth2ClientAuthenticationService(
      @NonNull OAuth2ClientAuthenticationVerifier<T> verifier,
      @NonNull List<? extends OAuth2ClientAuthenticationVerificationMethod> enforcedMethods,
      @NonNull OAuth2ReportingService reportingService
   ) {
      if (verifier == null) {
         throw new NullPointerException("verifier is marked non-null but is null");
      }

      if (enforcedMethods == null) {
         throw new NullPointerException("enforcedMethods is marked non-null but is null");
      }

      if (reportingService == null) {
         throw new NullPointerException("reportingService is marked non-null but is null");
      }

      this.verifier = verifier;
      this.enforcedMethods = enforcedMethods;
      this.reportingService = reportingService;
   }
}
