package com.airlock.iam.admin.application.service.token.airlock2fa;

import com.airlock.iam.admin.api.domain.model.administrator.CurrentAdministrator;
import com.airlock.iam.admin.domain.service.token.airlock2fa.Airlock2FAAdminApiDeviceService;
import com.airlock.iam.admin.domain.service.token.airlock2fa.Airlock2FAAdminApiHardwareTokenService;
import com.airlock.iam.airlock2fa.api.domain.model.Airlock2FAAccountId;
import com.airlock.iam.airlock2fa.api.domain.model.Airlock2FADeviceId;
import com.airlock.iam.base.api.domain.model.user.UserId;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.elog.api.EDefaultLogType;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.factor.domain.model.airlock2fa.Airlock2FAAdminApiException;
import com.airlock.iam.factor.domain.model.airlock2fa.Airlock2FAUser;
import com.airlock.iam.factor.domain.model.airlock2fa.HardwareTokenArchiveError;
import com.airlock.iam.factor.domain.model.airlock2fa.HardwareTokenSynchronizeError;
import com.airlock.iam.factor.domain.model.airlock2fa.hardware.Airlock2FAAdminHardwareToken;
import com.airlock.iam.factor.domain.model.airlock2fa.hardware.Airlock2FAAdminHardwareTokenSynchronizationPasscodes;
import com.airlock.iam.factor.domain.model.airlock2fa.hardware.Airlock2FAHardwareTokenFilter;
import com.airlock.iam.factor.domain.model.airlock2fa.hardware.Airlock2FAHardwareTokenId;
import com.airlock.iam.factor.domain.service.airlock2fa.repository.Airlock2FAUserRepository;
import com.airlock.iam.log.api.helpdesk.HelpdeskLogger;
import com.airlock.iam.log.api.signing.SigningLogger;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;

public final class Airlock2FAHardwareTokenService {
   private static final ELogger LOG = ELoggerFactory.getLogger(Airlock2FAHardwareTokenService.class);
   private final Airlock2FAUserRepository accountRepository;
   private final Airlock2FAAdminApiHardwareTokenService hardwareTokenService;
   private final Airlock2FAAdminApiDeviceService deviceService;
   private final CurrentAdministrator administrator;
   private final HelpdeskLogger helpdeskLogger;
   private final SigningLogger signingLogger;
   private final Airlock2FAHardwareTokenSettings hardwareTokenSettings;

   @Inject
   public Airlock2FAHardwareTokenService(
      Airlock2FAUserRepository accountRepository,
      Airlock2FAAdminApiHardwareTokenService hardwareTokenService,
      Airlock2FAAdminApiDeviceService deviceService,
      CurrentAdministrator administrator,
      HelpdeskLogger helpdeskLogger,
      SigningLogger signingLogger,
      Airlock2FAHardwareTokenSettings hardwareTokenSettings
   ) {
      this.accountRepository = accountRepository;
      this.hardwareTokenService = hardwareTokenService;
      this.deviceService = deviceService;
      this.administrator = administrator;
      this.helpdeskLogger = helpdeskLogger;
      this.signingLogger = signingLogger;
      this.hardwareTokenSettings = hardwareTokenSettings;
   }

   public Airlock2FAHardwareTokenService.Airlock2FAAssignmentOutcome assignTokensToUser(UserId userId, List<Airlock2FAHardwareTokenId> tokensToAssign) {
      Optional<Airlock2FAAccountId> accountId = this.findAccountId(userId);
      if (accountId.isEmpty()) {
         LOG.warn(EDefaultLogType.GENERIC_LOG_TYPE, String.format("Could not find Airlock 2FA account for user %s.", LogUtil.quote(userId)));
         return Airlock2FAHardwareTokenService.Airlock2FAAssignmentOutcome.builder()
            .accountNotFound(true)
            .newlyAddedAssignments(List.of())
            .rejectedAssignments(tokensToAssign)
            .build();
      } else {
         Airlock2FAHardwareTokenService.Airlock2FAAssignmentOutcome outcome = this.assignTokensToUser(accountId.get(), tokensToAssign);
         this.log(userId, outcome);
         return outcome;
      }
   }

   private Airlock2FAHardwareTokenService.Airlock2FAAssignmentOutcome assignTokensToUser(
      Airlock2FAAccountId accountId, List<Airlock2FAHardwareTokenId> tokensToAssign
   ) {
      List<Airlock2FAAdminHardwareToken> assignedTokens = this.hardwareTokenService
         .searchTokens(Airlock2FAHardwareTokenFilter.builder().assigned(true).build());
      List<Airlock2FAHardwareTokenId> rejected = new ArrayList<>();
      List<Airlock2FAHardwareTokenService.Airlock2FAAssignment> added = new ArrayList<>();

      for (Airlock2FAHardwareTokenId tokenId : tokensToAssign) {
         try {
            Optional<Airlock2FAAdminHardwareToken> alreadyExistingAssignment = assignedTokens.stream().filter(t -> t.getId().equals(tokenId)).findAny();
            if (alreadyExistingAssignment.isPresent()) {
               this.handleAlreadyAssignedToken(accountId, rejected, added, alreadyExistingAssignment.get());
            } else {
               this.assignTokenToAccount(accountId, added, tokenId);
            }
         } catch (Airlock2FAAdminApiException e) {
            rejected.add(tokenId);
         }
      }

      return Airlock2FAHardwareTokenService.Airlock2FAAssignmentOutcome.builder()
         .accountNotFound(false)
         .newlyAddedAssignments(added)
         .rejectedAssignments(rejected)
         .build();
   }

   private void handleAlreadyAssignedToken(
      Airlock2FAAccountId accountId,
      List<Airlock2FAHardwareTokenId> rejected,
      List<Airlock2FAHardwareTokenService.Airlock2FAAssignment> added,
      Airlock2FAAdminHardwareToken assignedToken
   ) {
      boolean alreadyAssignedToGivenAccountId = assignedToken.getEnrolledDeviceIds()
         .stream()
         .map(this.deviceService::getDeviceAssignee)
         .anyMatch(owner -> accountId.getId().equals(owner));
      if (!alreadyAssignedToGivenAccountId) {
         if (this.hardwareTokenSettings.shareHardwareTokensAmongUsers()) {
            this.assignTokenToAccount(accountId, added, assignedToken.getTokenId());
         } else {
            rejected.add(assignedToken.getTokenId());
         }
      }
   }

   private void assignTokenToAccount(
      Airlock2FAAccountId accountId, List<Airlock2FAHardwareTokenService.Airlock2FAAssignment> added, Airlock2FAHardwareTokenId tokenId
   ) {
      Airlock2FADeviceId deviceId = this.hardwareTokenService.assignTokenToAccount(accountId, tokenId);
      added.add(Airlock2FAHardwareTokenService.Airlock2FAAssignment.of(tokenId, deviceId));
   }

   public Optional<HardwareTokenArchiveError> archiveToken(Airlock2FAHardwareTokenId tokenId) {
      Optional<HardwareTokenArchiveError> archiveError = this.hardwareTokenService.archiveToken(tokenId);
      if (archiveError.isEmpty()) {
         this.signingLogger
            .log(
               String.format("%s has successfully archived hardware token %s.", LogUtil.quote(this.administrator.getName()), LogUtil.quote(tokenId.asString()))
            );
      } else {
         LOG.info(
            EDefaultLogType.GENERIC_LOG_TYPE, String.format("Archiving of hardware token %s failed due to error %s.", tokenId.asString(), archiveError.get())
         );
      }

      return archiveError;
   }

   public Optional<HardwareTokenSynchronizeError> synchronizeToken(
      Airlock2FAHardwareTokenId tokenId, Airlock2FAAdminHardwareTokenSynchronizationPasscodes passcodes
   ) {
      Optional<HardwareTokenSynchronizeError> synchronizationError = this.hardwareTokenService.synchronizeToken(tokenId, passcodes);
      if (synchronizationError.isEmpty()) {
         this.signingLogger
            .log(
               String.format(
                  "%s has successfully synchronized hardware token %s.", LogUtil.quote(this.administrator.getName()), LogUtil.quote(tokenId.asString())
               )
            );
      } else {
         LOG.info(
            EDefaultLogType.GENERIC_LOG_TYPE,
            String.format("Synchronization of hardware token %s failed due to error %s.", tokenId.asString(), synchronizationError.get())
         );
      }

      return synchronizationError;
   }

   public List<Airlock2FAAdminHardwareToken> searchTokens(Airlock2FAHardwareTokenFilter filter) throws Airlock2FAAdminApiException {
      return this.hardwareTokenService.searchTokens(filter);
   }

   private Optional<Airlock2FAAccountId> findAccountId(UserId userId) {
      return this.accountRepository.findBy(userId).map(Airlock2FAUser::getAccountId);
   }

   private void log(UserId userId, Airlock2FAHardwareTokenService.Airlock2FAAssignmentOutcome outcome) {
      if (!outcome.getNewlyAddedAssignments().isEmpty()) {
         this.log(
            userId,
            String.format(
               "successfully assigned hardware tokens: %s.",
               asString(outcome.getNewlyAddedAssignments(), Airlock2FAHardwareTokenService.Airlock2FAAssignment::asString)
            )
         );
      }

      if (!outcome.getRejectedAssignments().isEmpty()) {
         this.log(
            userId, String.format("failed to assign hardware tokens: %s.", asString(outcome.getRejectedAssignments(), Airlock2FAHardwareTokenId::asString))
         );
      }
   }

   private void log(UserId userId, String action) {
      String message = "Administrator " + LogUtil.quote(this.administrator.getName()) + " " + action;
      this.helpdeskLogger.log(userId, message);
      String messageWithUser = String.format("%s (for user %s)", message, LogUtil.quote(userId));
      this.signingLogger.log(messageWithUser);
   }

   private static <T> String asString(List<T> tokenIds, Function<T, String> mapper) {
      return tokenIds.stream().map(mapper).<CharSequence>map(LogUtil::quote).collect(Collectors.joining(", "));
   }

   public static final class Airlock2FAAssignment {
      private final Airlock2FAHardwareTokenId tokenId;
      private final Airlock2FADeviceId deviceId;

      public static Airlock2FAHardwareTokenService.Airlock2FAAssignment of(Airlock2FAHardwareTokenId tokenId, Airlock2FADeviceId deviceId) {
         return new Airlock2FAHardwareTokenService.Airlock2FAAssignment(tokenId, deviceId);
      }

      public String asString() {
         return "hardware token ID: " + this.tokenId + ", device ID: " + this.deviceId;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FAAssignment(Airlock2FAHardwareTokenId tokenId, Airlock2FADeviceId deviceId) {
         this.tokenId = tokenId;
         this.deviceId = deviceId;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FAHardwareTokenId getTokenId() {
         return this.tokenId;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Airlock2FADeviceId getDeviceId() {
         return this.deviceId;
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public boolean equals(Object o) {
         if (o == this) {
            return true;
         } else if (!(o instanceof Airlock2FAHardwareTokenService.Airlock2FAAssignment other)) {
            return false;
         } else {
            Object this$tokenId = this.tokenId;
            Object other$tokenId = other.tokenId;
            if (this$tokenId == null ? other$tokenId == null : this$tokenId.equals(other$tokenId)) {
               Object this$deviceId = this.deviceId;
               Object other$deviceId = other.deviceId;
               return this$deviceId == null ? other$deviceId == null : this$deviceId.equals(other$deviceId);
            } else {
               return false;
            }
         }
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public int hashCode() {
         int PRIME = 59;
         int result = 1;
         Object $tokenId = this.tokenId;
         result = result * 59 + ($tokenId == null ? 43 : $tokenId.hashCode());
         Object $deviceId = this.deviceId;
         return result * 59 + ($deviceId == null ? 43 : $deviceId.hashCode());
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "Airlock2FAHardwareTokenService.Airlock2FAAssignment(tokenId=" + this.getTokenId() + ", deviceId=" + this.getDeviceId() + ")";
      }
   }

   public static final class Airlock2FAAssignmentOutcome {
      private final boolean accountNotFound;
      private final List<Airlock2FAHardwareTokenService.Airlock2FAAssignment> newlyAddedAssignments;
      private final List<Airlock2FAHardwareTokenId> rejectedAssignments;

      public boolean isOverallSuccess() {
         return !this.accountNotFound && this.rejectedAssignments.isEmpty();
      }

      @SuppressFBWarnings(justification = "generated code")
      Airlock2FAAssignmentOutcome(
         boolean accountNotFound,
         List<Airlock2FAHardwareTokenService.Airlock2FAAssignment> newlyAddedAssignments,
         List<Airlock2FAHardwareTokenId> rejectedAssignments
      ) {
         this.accountNotFound = accountNotFound;
         this.newlyAddedAssignments = newlyAddedAssignments;
         this.rejectedAssignments = rejectedAssignments;
      }

      @SuppressFBWarnings(justification = "generated code")
      public static Airlock2FAHardwareTokenService.Airlock2FAAssignmentOutcome.Airlock2FAAssignmentOutcomeBuilder builder() {
         return new Airlock2FAHardwareTokenService.Airlock2FAAssignmentOutcome.Airlock2FAAssignmentOutcomeBuilder();
      }

      @SuppressFBWarnings(justification = "generated code")
      public boolean isAccountNotFound() {
         return this.accountNotFound;
      }

      @SuppressFBWarnings(justification = "generated code")
      public List<Airlock2FAHardwareTokenService.Airlock2FAAssignment> getNewlyAddedAssignments() {
         return this.newlyAddedAssignments;
      }

      @SuppressFBWarnings(justification = "generated code")
      public List<Airlock2FAHardwareTokenId> getRejectedAssignments() {
         return this.rejectedAssignments;
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public boolean equals(Object o) {
         if (o == this) {
            return true;
         } else if (!(o instanceof Airlock2FAHardwareTokenService.Airlock2FAAssignmentOutcome other)) {
            return false;
         } else if (this.accountNotFound != other.accountNotFound) {
            return false;
         } else {
            Object this$newlyAddedAssignments = this.newlyAddedAssignments;
            Object other$newlyAddedAssignments = other.newlyAddedAssignments;
            if (this$newlyAddedAssignments == null ? other$newlyAddedAssignments == null : this$newlyAddedAssignments.equals(other$newlyAddedAssignments)) {
               Object this$rejectedAssignments = this.rejectedAssignments;
               Object other$rejectedAssignments = other.rejectedAssignments;
               return this$rejectedAssignments == null ? other$rejectedAssignments == null : this$rejectedAssignments.equals(other$rejectedAssignments);
            } else {
               return false;
            }
         }
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public int hashCode() {
         int PRIME = 59;
         int result = 1;
         result = result * 59 + (this.accountNotFound ? 79 : 97);
         Object $newlyAddedAssignments = this.newlyAddedAssignments;
         result = result * 59 + ($newlyAddedAssignments == null ? 43 : $newlyAddedAssignments.hashCode());
         Object $rejectedAssignments = this.rejectedAssignments;
         return result * 59 + ($rejectedAssignments == null ? 43 : $rejectedAssignments.hashCode());
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "Airlock2FAHardwareTokenService.Airlock2FAAssignmentOutcome(accountNotFound="
            + this.isAccountNotFound()
            + ", newlyAddedAssignments="
            + this.getNewlyAddedAssignments()
            + ", rejectedAssignments="
            + this.getRejectedAssignments()
            + ")";
      }

      @SuppressFBWarnings(justification = "generated code")
      public static class Airlock2FAAssignmentOutcomeBuilder {
         @SuppressFBWarnings(justification = "generated code")
         private boolean accountNotFound;
         @SuppressFBWarnings(justification = "generated code")
         private List<Airlock2FAHardwareTokenService.Airlock2FAAssignment> newlyAddedAssignments;
         @SuppressFBWarnings(justification = "generated code")
         private List<Airlock2FAHardwareTokenId> rejectedAssignments;

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

         @SuppressFBWarnings(justification = "generated code")
         public Airlock2FAHardwareTokenService.Airlock2FAAssignmentOutcome.Airlock2FAAssignmentOutcomeBuilder accountNotFound(boolean accountNotFound) {
            this.accountNotFound = accountNotFound;
            return this;
         }

         @SuppressFBWarnings(justification = "generated code")
         public Airlock2FAHardwareTokenService.Airlock2FAAssignmentOutcome.Airlock2FAAssignmentOutcomeBuilder newlyAddedAssignments(
            List<Airlock2FAHardwareTokenService.Airlock2FAAssignment> newlyAddedAssignments
         ) {
            this.newlyAddedAssignments = newlyAddedAssignments;
            return this;
         }

         @SuppressFBWarnings(justification = "generated code")
         public Airlock2FAHardwareTokenService.Airlock2FAAssignmentOutcome.Airlock2FAAssignmentOutcomeBuilder rejectedAssignments(
            List<Airlock2FAHardwareTokenId> rejectedAssignments
         ) {
            this.rejectedAssignments = rejectedAssignments;
            return this;
         }

         @SuppressFBWarnings(justification = "generated code")
         public Airlock2FAHardwareTokenService.Airlock2FAAssignmentOutcome build() {
            return new Airlock2FAHardwareTokenService.Airlock2FAAssignmentOutcome(this.accountNotFound, this.newlyAddedAssignments, this.rejectedAssignments);
         }

         @SuppressFBWarnings(justification = "generated code")
         @Override
         public String toString() {
            return "Airlock2FAHardwareTokenService.Airlock2FAAssignmentOutcome.Airlock2FAAssignmentOutcomeBuilder(accountNotFound="
               + this.accountNotFound
               + ", newlyAddedAssignments="
               + this.newlyAddedAssignments
               + ", rejectedAssignments="
               + this.rejectedAssignments
               + ")";
         }
      }
   }
}
