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

import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.api.domain.model.claims.Claim;
import com.airlock.iam.common.api.domain.model.user.PersistentUser;
import com.airlock.iam.common.domain.model.claims.ClaimSet;
import com.airlock.iam.common.domain.model.claims.StringClaim;
import com.airlock.iam.common.domain.model.claims.SubjectClaim;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.oauth2.api.application.configuration.claims.OAuth2UsernameTransformer;
import com.airlock.iam.oauth2.api.domain.model.claims.OAuth2TokenInformation;
import com.airlock.iam.oauth2.api.domain.model.claims.UserClaimExtractor;
import com.airlock.iam.oauth2.domain.model.claims.DistributedClaims;
import com.airlock.iam.oauth2.domain.model.token.OAuth2AccessToken;
import com.airlock.iam.oauth2.domain.model.userinfo.OpenIDConnectUserInfo;
import com.airlock.iam.oauth2.domain.service.reporting.OAuth2ReportingService;
import com.google.common.collect.ImmutableList.Builder;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.NonNull;

public final class OpenIDConnectUserInfoServiceImpl implements OpenIDConnectUserInfoService {
   @NonNull
   private final List<UserClaimExtractor<?>> userClaimExtractors;
   @NonNull
   private final Optional<DistributedClaims> distributedClaims;
   @NonNull
   private final OAuth2UsernameTransformer usernameTransformer;
   @NonNull
   private final OAuth2ReportingService reportingService;
   @NonNull
   private final ELogger logger;

   @Override
   public OpenIDConnectUserInfo userInfo(OAuth2AccessToken accessToken, PersistentUser persistentUser) {
      String username = this.usernameTransformer.apply(persistentUser);
      SubjectClaim subjectClaim = new SubjectClaim(username);
      OpenIDConnectUserInfo userInfo = OpenIDConnectUserInfo.userInfo(subjectClaim, this.extractClaims(persistentUser, accessToken));
      this.logger
         .info(
            "Sending successful OpenID Connect UserInfo Response to client "
               + LogUtil.quote(accessToken.getClientId().id())
               + " about user "
               + LogUtil.quote(username)
         );
      return userInfo;
   }

   private ClaimSet extractClaims(PersistentUser persistentUser, OAuth2AccessToken accessToken) {
      Builder<Claim<?>> claims = new Builder();
      OAuth2TokenInformation tokenInformation = new OAuth2TokenInformation(
         accessToken.getUserId().getValue(), accessToken.getScopes().scopesAsSet(), accessToken.getClientId(), accessToken.getSessionId()
      );
      this.userClaimExtractors
         .stream()
         .map(extractor -> extractor.extract(persistentUser, tokenInformation))
         .filter(Optional::isPresent)
         .map(Optional::get)
         .forEach(claims::add);
      if (this.distributedClaims.isPresent()) {
         claims.add(
            claimContainingClaimSet(
               "_claim_names",
               this.distributedClaims
                  .get()
                  .getClaimNames()
                  .entrySet()
                  .stream()
                  .map(e -> new StringClaim(e.getKey(), e.getValue()))
                  .collect(Collectors.toList())
            )
         );
         claims.add(
            claimContainingClaimSet(
               "_claim_sources",
               this.distributedClaims
                  .get()
                  .getClaimSources()
                  .entrySet()
                  .stream()
                  .map(e -> new StringClaim(e.getKey(), e.getValue()))
                  .collect(Collectors.toList())
            )
         );
      }

      return ClaimSet.claimSet(claims.build());
   }

   private static Claim<ClaimSet> claimContainingClaimSet(final String claimName, final Collection<Claim<?>> claims) {
      return new Claim<ClaimSet>() {
         @Override
         public String name() {
            return claimName;
         }

         public ClaimSet value() {
            return ClaimSet.claimSet(claims);
         }
      };
   }

   @SuppressFBWarnings(justification = "generated code")
   OpenIDConnectUserInfoServiceImpl(
      @NonNull List<UserClaimExtractor<?>> userClaimExtractors,
      @NonNull Optional<DistributedClaims> distributedClaims,
      @NonNull OAuth2UsernameTransformer usernameTransformer,
      @NonNull OAuth2ReportingService reportingService,
      @NonNull ELogger logger
   ) {
      if (userClaimExtractors == null) {
         throw new NullPointerException("userClaimExtractors is marked non-null but is null");
      }

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

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

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

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

      this.userClaimExtractors = userClaimExtractors;
      this.distributedClaims = distributedClaims;
      this.usernameTransformer = usernameTransformer;
      this.reportingService = reportingService;
      this.logger = logger;
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public static class OpenIDConnectUserInfoServiceImplBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private List<UserClaimExtractor<?>> userClaimExtractors;
      @SuppressFBWarnings(justification = "generated code")
      private Optional<DistributedClaims> distributedClaims;
      @SuppressFBWarnings(justification = "generated code")
      private OAuth2UsernameTransformer usernameTransformer;
      @SuppressFBWarnings(justification = "generated code")
      private OAuth2ReportingService reportingService;
      @SuppressFBWarnings(justification = "generated code")
      private ELogger logger;

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

      @SuppressFBWarnings(justification = "generated code")
      public OpenIDConnectUserInfoServiceImpl.OpenIDConnectUserInfoServiceImplBuilder userClaimExtractors(
         @NonNull List<UserClaimExtractor<?>> userClaimExtractors
      ) {
         if (userClaimExtractors == null) {
            throw new NullPointerException("userClaimExtractors is marked non-null but is null");
         }

         this.userClaimExtractors = userClaimExtractors;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OpenIDConnectUserInfoServiceImpl.OpenIDConnectUserInfoServiceImplBuilder distributedClaims(@NonNull Optional<DistributedClaims> distributedClaims) {
         if (distributedClaims == null) {
            throw new NullPointerException("distributedClaims is marked non-null but is null");
         }

         this.distributedClaims = distributedClaims;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OpenIDConnectUserInfoServiceImpl.OpenIDConnectUserInfoServiceImplBuilder usernameTransformer(
         @NonNull OAuth2UsernameTransformer usernameTransformer
      ) {
         if (usernameTransformer == null) {
            throw new NullPointerException("usernameTransformer is marked non-null but is null");
         }

         this.usernameTransformer = usernameTransformer;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OpenIDConnectUserInfoServiceImpl.OpenIDConnectUserInfoServiceImplBuilder reportingService(@NonNull OAuth2ReportingService reportingService) {
         if (reportingService == null) {
            throw new NullPointerException("reportingService is marked non-null but is null");
         }

         this.reportingService = reportingService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OpenIDConnectUserInfoServiceImpl.OpenIDConnectUserInfoServiceImplBuilder logger(@NonNull ELogger logger) {
         if (logger == null) {
            throw new NullPointerException("logger is marked non-null but is null");
         }

         this.logger = logger;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public OpenIDConnectUserInfoServiceImpl build() {
         return new OpenIDConnectUserInfoServiceImpl(
            this.userClaimExtractors, this.distributedClaims, this.usernameTransformer, this.reportingService, this.logger
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "OpenIDConnectUserInfoServiceImpl.OpenIDConnectUserInfoServiceImplBuilder(userClaimExtractors="
            + this.userClaimExtractors
            + ", distributedClaims="
            + this.distributedClaims
            + ", usernameTransformer="
            + this.usernameTransformer
            + ", reportingService="
            + this.reportingService
            + ", logger="
            + this.logger
            + ")";
      }
   }
}
