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

import com.airlock.iam.oauth2.api.domain.model.AuthorizationServerId;
import com.airlock.iam.oauth2.api.domain.model.OAuth2ClientId;
import com.airlock.iam.oauth2.api.domain.model.OAuth2Scope;
import com.airlock.iam.oauth2.api.domain.model.provider.scope.OAuth2ScopeCondition;
import com.airlock.iam.oauth2.domain.model.codegrant.OAuth2AuthorizationCodeGrantFlowSessionAttribute;
import com.airlock.iam.oauth2.domain.model.consent.OAuth2Consent;
import com.airlock.iam.oauth2.domain.model.consent.OAuth2ConsentStatus;
import com.airlock.iam.oauth2.domain.model.provider.OAuth2ConsentClientProvider;
import com.airlock.iam.oauth2.domain.model.provider.OAuth2ScopeAttribute;
import com.airlock.iam.oauth2.domain.model.settings.OAuth2OidcAuthorizationCodeGrantSettings;
import com.airlock.iam.oauth2.domain.service.OAuth2ScopeTranslationService;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;

public class OAuth2ConsentServiceImpl implements OAuth2ConsentService {
   private final Provider<List<OAuth2ScopeCondition>> conditions;
   private final Provider<OAuth2OidcAuthorizationCodeGrantSettings> authorizationCodeGrantSettings;
   private final Provider<Optional<OAuth2LocalConsentStorageService>> localConsentStorageService;
   private final Provider<OAuth2AuthorizationCodeGrantFlowSessionAttribute> authorizationCodeGrantFlowSessionAttribute;
   private final Provider<OAuth2ScopeTranslationService> scopeTranslationService;
   private final Provider<OAuth2ConsentClientProvider> clientProvider;

   @Inject
   public OAuth2ConsentServiceImpl(
      Provider<List<OAuth2ScopeCondition>> conditions,
      Provider<OAuth2OidcAuthorizationCodeGrantSettings> authorizationCodeGrantSettings,
      Provider<Optional<OAuth2LocalConsentStorageService>> localConsentStorageService,
      Provider<OAuth2AuthorizationCodeGrantFlowSessionAttribute> authorizationCodeGrantFlowSessionAttribute,
      Provider<OAuth2ScopeTranslationService> scopeTranslationService,
      Provider<OAuth2ConsentClientProvider> clientProvider
   ) {
      this.conditions = conditions;
      this.authorizationCodeGrantSettings = authorizationCodeGrantSettings;
      this.localConsentStorageService = localConsentStorageService;
      this.authorizationCodeGrantFlowSessionAttribute = authorizationCodeGrantFlowSessionAttribute;
      this.scopeTranslationService = scopeTranslationService;
      this.clientProvider = clientProvider;
   }

   @Override
   public OAuth2ConsentService.PreProcessConsentResult preProcess(List<OAuth2Scope> requestedScopes) {
      List<OAuth2Scope> resultingScopes = requestedScopes.stream()
         .filter(scope -> ((List)this.conditions.get()).stream().anyMatch(condition -> condition.isAllowed(scope)))
         .filter(this::notAlwaysGrantedScope)
         .toList();
      return this.emptyScopesNotAllowedAndNoScopesToGrant(resultingScopes)
         ? new OAuth2ConsentService.PreProcessConsentResult(OAuth2ConsentService.PreProcessConsentResult.ConsentResultError.EMTPY_SCOPE_NOT_ALLOWED)
         : new OAuth2ConsentService.PreProcessConsentResult(resultingScopes, this.alwaysGrantedScopes());
   }

   @Override
   public Optional<OAuth2ConsentService.PreProcessConsentResult.ConsentResultError> validate(
      List<OAuth2ScopeAttribute> grantableScopes, Set<String> grantedScopes
   ) {
      if (!areAllScopesGrantable(grantableScopes, grantedScopes)) {
         return Optional.of(OAuth2ConsentService.PreProcessConsentResult.ConsentResultError.INVALID_SCOPE);
      } else {
         return !((OAuth2OidcAuthorizationCodeGrantSettings)this.authorizationCodeGrantSettings.get()).isAllowEmptyScope()
               && grantedScopes.isEmpty()
               && this.alwaysGrantedScopes().isEmpty()
            ? Optional.of(OAuth2ConsentService.PreProcessConsentResult.ConsentResultError.EMTPY_SCOPE_NOT_ALLOWED)
            : Optional.empty();
      }
   }

   @Override
   public void saveConsentResults(Set<String> grantedScopes, Set<String> deniedScopes) {
      ((Optional)this.localConsentStorageService.get())
         .ifPresent(service -> service.saveScopes(this.authorizationServerId(), this.clientId(), grantedScopes, deniedScopes));
   }

   @Override
   public List<OAuth2ScopeAttribute> loadSavedConsents(List<OAuth2Scope> scopes) {
      Collection<OAuth2Consent> savedConsents = this.findSavedConsents();
      return scopes.stream()
         .map(
            scope -> savedConsents.stream()
               .filter(consent -> consent.getScope().equals(scope))
               .findFirst()
               .map(consent -> this.scopeAttributeWithConsent(scope, consent))
               .orElseGet(() -> this.scopeAttributeWithoutConsent(scope))
         )
         .toList();
   }

   @Override
   public boolean isConsentStorageEnabled() {
      return ((Optional)this.localConsentStorageService.get()).isPresent();
   }

   private boolean emptyScopesNotAllowedAndNoScopesToGrant(Collection<OAuth2Scope> scopesToGrant) {
      return !((OAuth2OidcAuthorizationCodeGrantSettings)this.authorizationCodeGrantSettings.get()).isAllowEmptyScope()
         && scopesToGrant.isEmpty()
         && this.alwaysGrantedScopes().isEmpty();
   }

   private Set<OAuth2Scope> alwaysGrantedScopes() {
      Set<OAuth2Scope> grantedScopes = new HashSet<>();
      grantedScopes.addAll(((OAuth2ConsentClientProvider)this.clientProvider.get()).provide().alwaysGrantedScopes());
      grantedScopes.addAll(((OAuth2OidcAuthorizationCodeGrantSettings)this.authorizationCodeGrantSettings.get()).getAlwaysGrantedScopes());
      return grantedScopes;
   }

   private boolean notAlwaysGrantedScope(OAuth2Scope scope) {
      return !this.alwaysGrantedScopes().contains(scope);
   }

   private OAuth2ScopeAttribute scopeAttributeWithConsent(OAuth2Scope scope, OAuth2Consent consent) {
      return new OAuth2ScopeAttribute(
         scope.name(),
         ((OAuth2ScopeTranslationService)this.scopeTranslationService.get()).translateScope(this.authorizationServerId(), scope),
         consent.getStatus(),
         Optional.of(consent.getUpdatedAt())
      );
   }

   private OAuth2ScopeAttribute scopeAttributeWithoutConsent(OAuth2Scope scope) {
      return new OAuth2ScopeAttribute(
         scope.name(),
         ((OAuth2ScopeTranslationService)this.scopeTranslationService.get()).translateScope(this.authorizationServerId(), scope),
         OAuth2ConsentStatus.UNDECIDED,
         Optional.empty()
      );
   }

   private AuthorizationServerId authorizationServerId() {
      return ((OAuth2AuthorizationCodeGrantFlowSessionAttribute)this.authorizationCodeGrantFlowSessionAttribute.get()).authorizationServerId();
   }

   private OAuth2ClientId clientId() {
      return ((OAuth2AuthorizationCodeGrantFlowSessionAttribute)this.authorizationCodeGrantFlowSessionAttribute.get()).request().clientId();
   }

   private Collection<OAuth2Consent> findSavedConsents() {
      return ((Optional)this.localConsentStorageService.get())
         .<Collection<OAuth2Consent>>map(service -> service.findSavedConsents(this.authorizationServerId(), this.clientId()))
         .orElseGet(List::of);
   }

   private static boolean areAllScopesGrantable(List<OAuth2ScopeAttribute> scopes, Set<String> grantedScopes) {
      return grantedScopes.stream().allMatch(scopeName -> isScopeGrantable(scopeName, scopes));
   }

   private static boolean isScopeGrantable(String scopeName, List<OAuth2ScopeAttribute> grantableScopes) {
      return grantableScopes.stream().anyMatch(scope -> scope.getScope().equals(scopeName));
   }
}
