package com.airlock.iam.common.domain.service.sso;

import com.airlock.iam.base.api.domain.model.user.UserId;
import com.airlock.iam.base.misc.result.Result;
import com.airlock.iam.common.api.domain.model.authentication.Authentee;
import com.airlock.iam.common.api.domain.model.contextdata.ContextData;
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.RepresenterId;
import com.airlock.iam.common.api.domain.model.user.UserIdentity;
import com.airlock.iam.common.api.domain.model.user.transformation.UsernameTransformation;
import com.airlock.iam.common.domain.model.authentication.AuthenteeImpl;
import com.airlock.iam.common.domain.model.contextdata.ContextDataImpl;
import com.airlock.iam.common.domain.model.sso.SsoTicketContextDataExtractor;
import com.airlock.iam.common.domain.model.sso.SsoTicketExtractor;
import com.airlock.iam.common.domain.model.sso.SsoTicketRoleExtractor;
import com.airlock.iam.common.domain.model.sso.SsoTicketStepSettings;
import com.airlock.iam.common.domain.model.transform.TransformWithLogContextUpdate;
import com.airlock.iam.core.misc.authen.LogTypes;
import com.airlock.iam.core.misc.util.ticket.KeyMultiValue;
import com.airlock.iam.core.misc.util.ticket.Ticket;
import com.airlock.iam.core.misc.util.ticket.TicketDecoder;
import com.airlock.iam.core.misc.util.ticket.TicketEncodingException;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;

public final class SsoTicketServiceImpl implements SsoTicketService {
   private static final ELogger LOG = ELoggerFactory.getLogger(SsoTicketServiceImpl.class);
   private final List<SsoTicketExtractor> ticketExtractors;
   private final TicketDecoder ticketDecoder;
   private final Optional<AcceptedSsoTicketService> acceptedSsoTicketService;
   private final List<SsoTicketRoleExtractor> roleExtractors;
   private final List<SsoTicketContextDataExtractor> contextDataExtractors;
   private final SsoTicketStepSettings settings;

   @Inject
   public SsoTicketServiceImpl(
      List<SsoTicketExtractor> ticketExtractors,
      TicketDecoder ticketDecoder,
      Optional<AcceptedSsoTicketService> acceptedSsoTicketService,
      List<SsoTicketRoleExtractor> roleExtractors,
      List<SsoTicketContextDataExtractor> contextDataExtractors,
      SsoTicketStepSettings settings
   ) {
      this.ticketExtractors = ticketExtractors;
      this.ticketDecoder = ticketDecoder;
      this.acceptedSsoTicketService = acceptedSsoTicketService;
      this.roleExtractors = roleExtractors;
      this.contextDataExtractors = contextDataExtractors;
      this.settings = settings;
   }

   @Override
   public Result<SsoTicketService.SsoTicket, SsoTicketService.SsoTicketError> extractSsoTicket() {
      Optional<String> encodedTicket = this.extractTicket();
      if (encodedTicket.isEmpty()) {
         LOG.info("No SSO ticket extracted.");
         return Result.failure(SsoTicketService.SsoTicketError.ticketError(SsoTicketService.SsoTicketError.SsoTicketErrorType.SSO_TOKEN_MISSING));
      }

      Optional<Ticket> ticket = this.decodeTicket(encodedTicket.get());
      if (!ticket.isEmpty() && !this.isExpired(ticket.get())) {
         if (this.acceptedSsoTicketService.isPresent()) {
            if (this.acceptedSsoTicketService.get().alreadyAccepted(ticket.get())) {
               LOG.info("Rejecting SSO ticket because it has already been consumed.");
               return Result.failure(
                  SsoTicketService.SsoTicketError.ticketError(SsoTicketService.SsoTicketError.SsoTicketErrorType.SSO_TOKEN_VERIFICATION_FAILED)
               );
            }

            this.acceptedSsoTicketService.get().accept(ticket.get());
         }

         Optional<ProvidedUsername> username = this.readUsernameFromTicket(ticket.get());
         if (username.isEmpty()) {
            LOG.info("Could not read username from ticket.");
            return Result.failure(SsoTicketService.SsoTicketError.ticketError(SsoTicketService.SsoTicketError.SsoTicketErrorType.SSO_TOKEN_VERIFICATION_FAILED));
         } else {
            Optional<RepresenterId> representerId = this.readRepresenterIdFromTicket(ticket.get());
            if (this.settings.isRepresentation() && representerId.isEmpty()) {
               LOG.info("Could not read representer ID from ticket.");
               return Result.failure(
                  SsoTicketService.SsoTicketError.ticketError(
                     SsoTicketService.SsoTicketError.SsoTicketErrorType.SSO_TOKEN_VERIFICATION_FAILED, toUserIdentity(username.get(), representerId)
                  )
               );
            } else if (!this.settings.isRepresentation() && representerId.isPresent()) {
               LOG.info("Non-representation SSO ticket must not contain representer ID.");
               return Result.failure(
                  SsoTicketService.SsoTicketError.ticketError(
                     SsoTicketService.SsoTicketError.SsoTicketErrorType.SSO_TOKEN_VERIFICATION_FAILED, toUserIdentity(username.get(), representerId)
                  )
               );
            } else {
               return Result.result(
                  new SsoTicketService.SsoTicket(
                     username.get(), representerId, this.extractRoles(ticket.get()), this.extractContextData(ticket.get()), ticket.get()
                  )
               );
            }
         }
      } else {
         return Result.failure(SsoTicketService.SsoTicketError.ticketError(SsoTicketService.SsoTicketError.SsoTicketErrorType.SSO_TOKEN_VERIFICATION_FAILED));
      }
   }

   private static UserIdentity toUserIdentity(ProvidedUsername username, Optional<RepresenterId> representerId) {
      return new UserIdentity(UserId.userId(username.getValue()), username, representerId);
   }

   private Optional<String> extractTicket() {
      for (SsoTicketExtractor extractor : this.ticketExtractors) {
         Optional<String> encodedTicket = extractor.extract();
         if (encodedTicket.isPresent()) {
            return encodedTicket;
         }
      }

      LOG.debug("None of the configured ticket extractors could provide a ticket.");
      return Optional.empty();
   }

   private Optional<Ticket> decodeTicket(String encodedTicket) {
      try {
         return Optional.ofNullable(this.ticketDecoder.decode(encodedTicket, this.settings.urlEncoding().map(Charset::toString).orElse(null)));
      } catch (TicketEncodingException e) {
         LOG.info("Failed to decode SSO ticket: " + e.getMessage() + " - " + e.getCause());
         return Optional.empty();
      }
   }

   private boolean isExpired(Ticket ticket) {
      if (ticket.hasTicketExpired()) {
         LOG.info(String.format("Rejected expired SSO ticket (valid until %s).", ticket.getExpiryDate()));
      }

      return ticket.hasTicketExpired();
   }

   private Optional<ProvidedUsername> readUsernameFromTicket(Ticket ticket) {
      return readValueFromTicket(ticket, this.settings.usernameTicketKey()).map(ProvidedUsername::providedUsernameOf);
   }

   private Optional<RepresenterId> readRepresenterIdFromTicket(Ticket ticket) {
      return readValueFromTicket(ticket, "representerId").map(RepresenterId::new);
   }

   private Set<Role> extractRoles(Ticket ticket) {
      return this.roleExtractors.stream().flatMap(extractor -> extractor.extract(ticket).stream()).collect(Collectors.toSet());
   }

   private Set<ContextDataEntry> extractContextData(Ticket ticket) {
      return Set.copyOf(
         this.contextDataExtractors
            .stream()
            .flatMap(extractor -> extractor.extract(ticket).stream())
            .collect(Collectors.toMap(ContextDataEntry::key, Function.identity(), (c1, c2) -> c2))
            .values()
      );
   }

   private static Optional<String> readValueFromTicket(Ticket ticket, String key) {
      KeyMultiValue value = ticket.getValueForName(key);
      return value != null && value.getNumberOfValues() == 1 ? Optional.ofNullable(value.getFirstValue()) : Optional.empty();
   }

   @Override
   public Optional<SsoTicketService.ExtractionResult> extractAuthentee(SsoTicketService.SsoTicket ssoTicket, UsernameTransformation usernameTransformation) {
      return this.extractAuthentee(
         ssoTicket, providedUsername -> Optional.of(TransformWithLogContextUpdate.transformAndUpdateLogContext(usernameTransformation, providedUsername))
      );
   }

   private Optional<SsoTicketService.ExtractionResult> extractAuthentee(
      SsoTicketService.SsoTicket ssoTicket, Function<ProvidedUsername, Optional<UserId>> usernameTransformation
   ) {
      Optional<ProvidedUsername> ticketUsername = ssoTicket.ticket()
         .getFirstValueForName(this.settings.usernameTicketKey())
         .<String>map(StringUtils::trimToNull)
         .map(ProvidedUsername::providedUsernameOf);
      if (ticketUsername.isEmpty()) {
         LOG.warn(
            LogTypes.AUTHENTICATION_FAILED_SAT,
            "Received valid SSO ticket without a username. Expected key for user name is \"" + this.settings.usernameTicketKey() + "\". Rejecting SSO ticket."
         );
         return Optional.empty();
      }

      Optional<UserIdentity> userIdentity = this.settings
         .providedUsernameKey()
         .map(puk -> userIdentityWithProvidedUsername(ticketUsername.get(), ssoTicket.ticket(), puk))
         .orElseGet(() -> userIdentityWithTransformation(ticketUsername.get(), usernameTransformation))
         .map(uid -> addRepresenterId(uid, ssoTicket.ticket()));
      if (userIdentity.isEmpty()) {
         return Optional.empty();
      }

      Optional<UserId> transformedUsername = usernameTransformation.apply(ssoTicket.username());
      if (transformedUsername.isEmpty()) {
         return Optional.empty();
      }

      ContextData contextData = transformContextDataEntriesToContextData(ssoTicket);
      Authentee authentee = new AuthenteeImpl(userIdentity.get(), ssoTicket.roles(), contextData);
      return Optional.of(SsoTicketService.ExtractionResult.successfulExtraction(authentee));
   }

   private static ContextDataImpl transformContextDataEntriesToContextData(SsoTicketService.SsoTicket ssoTicket) {
      return new ContextDataImpl(ssoTicket.contextData().stream().collect(Collectors.toMap(ContextDataEntry::key, ContextDataEntry::value)));
   }

   private static UserIdentity addRepresenterId(UserIdentity uid, Ticket ssoTicket) {
      return ssoTicket.getFirstValueForName("representerId").map(RepresenterId::new).map(uid::withRepresenter).orElse(uid);
   }

   private static Optional<UserIdentity> userIdentityWithProvidedUsername(ProvidedUsername ticketUsername, Ticket ssoTicket, String puk) {
      return Optional.of(
         new UserIdentity(
            UserId.userId(ticketUsername.getValue()), ssoTicket.getFirstValueForName(puk).map(ProvidedUsername::providedUsernameOf).orElse(ticketUsername)
         )
      );
   }

   private static Optional<UserIdentity> userIdentityWithTransformation(
      ProvidedUsername ticketUsername, Function<ProvidedUsername, Optional<UserId>> usernameTransformation
   ) {
      return usernameTransformation.apply(ticketUsername).map(tu -> new UserIdentity(tu, ticketUsername));
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public static class SsoTicketServiceImplBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private List<SsoTicketExtractor> ticketExtractors;
      @SuppressFBWarnings(justification = "generated code")
      private TicketDecoder ticketDecoder;
      @SuppressFBWarnings(justification = "generated code")
      private Optional<AcceptedSsoTicketService> acceptedSsoTicketService;
      @SuppressFBWarnings(justification = "generated code")
      private List<SsoTicketRoleExtractor> roleExtractors;
      @SuppressFBWarnings(justification = "generated code")
      private List<SsoTicketContextDataExtractor> contextDataExtractors;
      @SuppressFBWarnings(justification = "generated code")
      private SsoTicketStepSettings settings;

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

      @SuppressFBWarnings(justification = "generated code")
      public SsoTicketServiceImpl.SsoTicketServiceImplBuilder ticketExtractors(List<SsoTicketExtractor> ticketExtractors) {
         this.ticketExtractors = ticketExtractors;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SsoTicketServiceImpl.SsoTicketServiceImplBuilder ticketDecoder(TicketDecoder ticketDecoder) {
         this.ticketDecoder = ticketDecoder;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SsoTicketServiceImpl.SsoTicketServiceImplBuilder acceptedSsoTicketService(Optional<AcceptedSsoTicketService> acceptedSsoTicketService) {
         this.acceptedSsoTicketService = acceptedSsoTicketService;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SsoTicketServiceImpl.SsoTicketServiceImplBuilder roleExtractors(List<SsoTicketRoleExtractor> roleExtractors) {
         this.roleExtractors = roleExtractors;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SsoTicketServiceImpl.SsoTicketServiceImplBuilder contextDataExtractors(List<SsoTicketContextDataExtractor> contextDataExtractors) {
         this.contextDataExtractors = contextDataExtractors;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SsoTicketServiceImpl.SsoTicketServiceImplBuilder settings(SsoTicketStepSettings settings) {
         this.settings = settings;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SsoTicketServiceImpl build() {
         return new SsoTicketServiceImpl(
            this.ticketExtractors, this.ticketDecoder, this.acceptedSsoTicketService, this.roleExtractors, this.contextDataExtractors, this.settings
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "SsoTicketServiceImpl.SsoTicketServiceImplBuilder(ticketExtractors="
            + this.ticketExtractors
            + ", ticketDecoder="
            + this.ticketDecoder
            + ", acceptedSsoTicketService="
            + this.acceptedSsoTicketService
            + ", roleExtractors="
            + this.roleExtractors
            + ", contextDataExtractors="
            + this.contextDataExtractors
            + ", settings="
            + this.settings
            + ")";
      }
   }
}
