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

import com.airlock.iam.admin.api.domain.model.administrator.CurrentAdministrator;
import com.airlock.iam.admin.domain.model.administrator.permissions.AdminappAction;
import com.airlock.iam.admin.domain.service.access.AdminappAccessController;
import com.airlock.iam.base.api.domain.model.exception.EntityNotFoundException;
import com.airlock.iam.base.api.domain.model.exception.EntityNotUniqueException;
import com.airlock.iam.base.api.domain.model.exception.ViolationException;
import com.airlock.iam.base.api.domain.model.store.exception.StoreFailureException;
import com.airlock.iam.base.api.domain.model.user.UserId;
import com.airlock.iam.base.api.domain.model.validation.GenericError;
import com.airlock.iam.base.api.domain.model.validation.ValidationResult;
import com.airlock.iam.base.api.domain.model.validation.ValidatorError;
import com.airlock.iam.base.infrastructure.validation.ConstraintViolationType;
import com.airlock.iam.base.misc.data.OptionalValue;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.api.domain.model.persistency.exception.PersisterException;
import com.airlock.iam.common.application.service.validation.DateRangeValidator;
import com.airlock.iam.common.domain.model.cronto.ExternalCrontoDeviceId;
import com.airlock.iam.common.domain.model.cronto.event.CrontoEventFactory;
import com.airlock.iam.common.domain.service.exception.FeatureDisabledException;
import com.airlock.iam.core.misc.cronto.CrontoHandler;
import com.airlock.iam.core.misc.cronto.Cryptogram;
import com.airlock.iam.core.misc.cronto.CrontoHandler.PlatformType;
import com.airlock.iam.core.misc.impl.cronto.pushnotification.PushDevicesHandler;
import com.airlock.iam.core.misc.impl.token.cronto.CrontoAccountToken;
import com.airlock.iam.core.misc.impl.token.cronto.CrontoDeviceToken;
import com.airlock.iam.core.misc.impl.token.cronto.CrontoLetterToken;
import com.airlock.iam.core.misc.impl.token.cronto.CrontoLetterTokenData;
import com.airlock.iam.core.misc.impl.token.cronto.CrontoToken;
import com.airlock.iam.core.misc.util.DateRange;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.log.api.signing.SigningLogger;
import com.google.common.collect.Sets;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;

public final class CrontoAdminService {
   private static final ELogger LOG = ELoggerFactory.getLogger(CrontoAdminService.class);
   private final Provider<CrontoHandler> crontoHandler;
   private final Provider<CurrentAdministrator> administrator;
   private final Provider<AdminappAccessController> adminappAccessController;
   private final Provider<CrontoEventFactory> eventFactory;
   private final Provider<SigningLogger> signingLogger;

   @Inject
   public CrontoAdminService(
      Provider<CrontoHandler> crontoHandler,
      Provider<CurrentAdministrator> administrator,
      Provider<AdminappAccessController> adminappAccessController,
      Provider<CrontoEventFactory> eventFactory,
      Provider<SigningLogger> signingLogger
   ) {
      this.crontoHandler = crontoHandler;
      this.administrator = administrator;
      this.adminappAccessController = adminappAccessController;
      this.eventFactory = eventFactory;
      this.signingLogger = signingLogger;
   }

   public boolean addCrontoAccount(UserId userId) {
      if (this.hasCrontoAccount(userId)) {
         throw new EntityNotUniqueException("User already has a Cronto account");
      }

      boolean success = this.crontoHandler().createNewAccountForUser(userId);
      if (success) {
         this.signingLogger().log("Added new Cronto account to user " + LogUtil.quote(userId));
      }

      return success;
   }

   public void deleteCrontoAccount(UserId userId) {
      this.ensureHasAccount(userId);

      try {
         CrontoAccountToken account = (CrontoAccountToken)this.crontoHandler().getAccountForUser(userId).orElseThrow();
         this.crontoHandler().deleteAccount(account);
         this.signingLogger().log("Deleted Cronto account of user " + LogUtil.quote(userId));
      } catch (PersisterException e) {
         throw new StoreFailureException(e);
      }
   }

   public boolean hasCrontoAccount(UserId userId) {
      return this.crontoHandler().getAccountForUser(userId).isPresent();
   }

   public CrontoLetterToken createActivationCodeWithDefaultPrinting(UserId userId) {
      this.ensureHasAccount(userId);

      try {
         List<CrontoLetterToken> existingCodes = this.crontoHandler().getAllLettersForUser(userId);
         if (!existingCodes.isEmpty()) {
            throw new ViolationException("activation code limit reached");
         }

         CrontoLetterToken code = this.crontoHandler().createActivationLetter(userId);
         this.crontoHandler().orderLetterPrinting(code, UserId.userId(((CurrentAdministrator)this.administrator.get()).getName()));
         this.signingLogger().log("Created new Cronto activation code with default printing for user " + LogUtil.quote(userId.getValue()));
         ((CrontoEventFactory)this.eventFactory.get()).publishCrontoLetterOrderedEvent(userId);
         return code;
      } catch (PersisterException e) {
         throw new StoreFailureException(e);
      }
   }

   public List<CrontoLetterToken> getActivationCodesForUser(UserId userId) {
      this.ensureHasAccount(userId);
      return this.crontoHandler().getAllLettersForUser(userId);
   }

   public CrontoLetterToken getCodeById(UserId userId, String codeId) {
      this.ensureHasAccount(userId);
      CrontoToken token = this.crontoHandler().getTokenBySerialId(userId, codeId);
      if (token == null) {
         throw new EntityNotFoundException("Cronto activation code with ID " + codeId + " not found");
      } else if (!(token instanceof CrontoLetterToken)) {
         throw new EntityNotFoundException("Cronto activation code token not found (wrong type): " + codeId);
      } else {
         return (CrontoLetterToken)token;
      }
   }

   public void deleteCode(UserId userId, String codeId) {
      this.ensureHasAccount(userId);
      CrontoLetterToken code = this.getCodeById(userId, codeId);
      this.removeToken(code);
      this.signingLogger().log("Deleted Cronto activation code");
   }

   public ValidationResult validateActivationCodeUpdate(UserId userId, CrontoActivationCodeUpdateData updateData) {
      this.ensureHasAccount(userId);
      CrontoLetterToken code = this.getCodeById(userId, updateData.getCodeId());
      List<ValidatorError> errors = new ArrayList<>();
      if (updateData.getValidFrom().hasValue() || updateData.getValidTo().hasValue()) {
         this.validateActivationCodeValidity(errors, code, updateData.getValidFrom(), updateData.getValidTo());
      }

      if (updateData.getAllowedNumberOfActivations().hasValue()) {
         this.validateAllowedNumberOfActivations(errors, (Integer)updateData.getAllowedNumberOfActivations().value());
      }

      if (updateData.getAllowedPlatforms().hasValue()) {
         this.validateAllowedPlatforms(errors, (String[])updateData.getAllowedPlatforms().value(), code);
      }

      if (updateData.getPrintingOptions().hasValue()) {
         this.validatePrintingOptions(errors, (String[])updateData.getPrintingOptions().value());
      }

      return errors.isEmpty() ? ValidationResult.valid() : ValidationResult.invalid(errors);
   }

   private void validateActivationCodeValidity(List<ValidatorError> errors, CrontoLetterToken code, OptionalValue<Date> validFrom, OptionalValue<Date> validTo) {
      ValidationResult res = DateRangeValidator.builder()
         .fromFieldName("validFrom")
         .toFieldName("validTo")
         .build()
         .validate(this.updatedDateRange(code, validFrom, validTo));
      if (!res.isValid()) {
         errors.addAll(res.getErrors());
      }
   }

   private DateRange updatedDateRange(CrontoLetterToken code, OptionalValue<Date> validFrom, OptionalValue<Date> validTo) {
      Date newFromDate = (Date)validFrom.orElse(code.getValidityRange().getFromDate());
      Date newToDate = (Date)validTo.orElse(code.getValidityRange().getToDate());
      return new DateRange(newFromDate, newToDate);
   }

   private void validateAllowedNumberOfActivations(List<ValidatorError> errors, Integer value) {
      if (value < 0) {
         errors.add(ValidatorError.validatorError("allowedNumberOfActivations", ConstraintViolationType.MIN));
      }
   }

   private void validateAllowedPlatforms(List<ValidatorError> errors, String[] values, CrontoLetterToken code) {
      Set<PlatformType> newPlatforms = new HashSet<>();

      for (String platformId : values) {
         Optional<PlatformType> platform = PlatformType.fromIdentifier(platformId);
         if (platform.isEmpty()) {
            LOG.info("Invalid Cronto platform type identifier: " + platformId);
            errors.add(ValidatorError.validatorError("allowedPlatforms", GenericError.INVALID_VALUE));
            return;
         }

         newPlatforms.add(platform.get());
      }

      Set<PlatformType> blacklistedNew = Sets.intersection(this.crontoHandler().getBlackListedPlatformTypes(), newPlatforms);
      Set<PlatformType> var11 = Sets.difference(blacklistedNew, code.getAllowedPlatforms());
      if (!var11.isEmpty()) {
         LOG.info("Requested platform types add blacklisted platform types: " + var11);
         errors.add(ValidatorError.validatorError("allowedPlatforms", GenericError.INVALID_VALUE));
      }
   }

   private void validatePrintingOptions(List<ValidatorError> errors, String[] values) {
      Set<String> requested = Sets.newHashSet(values);
      requested.removeAll(this.crontoHandler().getAvailablePrintingOptions());
      if (!requested.isEmpty()) {
         LOG.info("Requested order options contain invalid option: " + requested);
         errors.add(ValidatorError.validatorError("printingOptions", GenericError.INVALID_VALUE));
      }
   }

   public CrontoLetterToken updateActivationCode(UserId userId, CrontoActivationCodeUpdateData updateData) {
      this.ensureHasAccount(userId);
      ValidationResult validation = this.validateActivationCodeUpdate(userId, updateData);
      if (!validation.isValid()) {
         this.signingLogger().log("Update of Cronto activation code failed due to validation errors.");
         throw new ViolationException("The activation code changes are not valid: " + validation.getFailure());
      }

      String codeId = updateData.getCodeId();
      CrontoLetterToken code = this.getCodeById(userId, codeId);
      List<String> desc = new ArrayList<>();
      if (updateData.getValidFrom().hasValue() || updateData.getValidTo().hasValue()) {
         DateRange newRange = this.updatedDateRange(code, updateData.getValidFrom(), updateData.getValidTo());
         code.setValidityRange(newRange);
         desc.add("validity period to " + newRange);
      }

      if (updateData.getAllowedNumberOfActivations().hasValue()) {
         int value = (Integer)Objects.requireNonNull((Integer)updateData.getAllowedNumberOfActivations().value());
         code.setNumAllowedActivations(value);
         desc.add("number of allowed activations to " + value);
      }

      if (updateData.getAllowedPlatforms().hasValue()) {
         String[] value = Objects.requireNonNull((String[])updateData.getAllowedPlatforms().value());
         Set<PlatformType> updatedPlatforms = Arrays.stream(value).map(PlatformType::fromIdentifier).map(Optional::get).collect(Collectors.toSet());
         code.setAllowedPlatforms(updatedPlatforms);
         desc.add("allowed platform types to " + updatedPlatforms);
      }

      if (updateData.getPrintingOptions().hasValue()) {
         String[] value = Objects.requireNonNull((String[])updateData.getPrintingOptions().value());
         Set<String> newOptions = Sets.newHashSet(value);

         try {
            this.crontoHandler().orderLetterPrinting(code, UserId.userId(((CurrentAdministrator)this.administrator.get()).getName()), newOptions);
         } catch (PersisterException e) {
            throw new StoreFailureException(e);
         }

         if (CollectionUtils.isNotEmpty(newOptions)) {
            ((CrontoEventFactory)this.eventFactory.get()).publishCrontoLetterOrderedEvent(userId);
         }

         desc.add("order options to " + newOptions);
      }

      if (desc.isEmpty()) {
         this.signingLogger().log("Cronto activation code with ID " + LogUtil.quote(codeId) + " remains unchanged after update");
         return code;
      } else {
         this.persistToken(code);
         this.signingLogger().log("Updated Cronto activation code with ID " + LogUtil.quote(codeId) + ": " + StringUtils.join(desc, ", "));
         return this.getCodeById(userId, codeId);
      }
   }

   public CrontoLetterTokenData getActivationLetterData(CrontoLetterToken letterToken) {
      Optional<Cryptogram> activationCryptogram = ((AdminappAccessController)this.adminappAccessController.get())
            .isActionAllowed(AdminappAction.VIEW_CRONTO_ACTIVATION_SECRET)
         ? Optional.of(this.crontoHandler().getCryptogramForPrinting(letterToken))
         : Optional.empty();
      return CrontoLetterTokenData.builder()
         .serialId(letterToken.getSerialId())
         .allowedPlatformTypes(letterToken.getAllowedPlatforms())
         .numberOfAllowedActivations(letterToken.getNumAllowedActivations())
         .validityRange(letterToken.getValidityRange())
         .generationDate(letterToken.getGenerationDate())
         .totalUsages(letterToken.getTotalUsages())
         .firstUsageDate(letterToken.getFirstUsageDate())
         .latestUsageDate(letterToken.getLatestUsageDate())
         .printingOptions(letterToken.getPrintingOptions())
         .lastPrintingDate(letterToken.getLastPrintingDate())
         .activationCryptogram(activationCryptogram)
         .build();
   }

   public List<CrontoDeviceToken> getDevicesForUser(UserId userId) {
      this.ensureHasAccount(userId);
      return this.crontoHandler().getAllDevicesForUser(userId);
   }

   public CrontoDeviceToken getDeviceById(UserId userId, ExternalCrontoDeviceId id) {
      this.ensureHasAccount(userId);
      CrontoToken token = this.crontoHandler().getTokenBySerialId(userId, id.getId());
      if (token == null) {
         throw new EntityNotFoundException("Cronto device with ID " + id + " not found");
      } else if (!(token instanceof CrontoDeviceToken)) {
         throw new EntityNotFoundException("Cronto device token not found (wrong type): " + id);
      } else {
         return (CrontoDeviceToken)token;
      }
   }

   public String getDeviceIdForLogging(UserId userId, ExternalCrontoDeviceId id) {
      return this.getDeviceById(userId, id).getDeviceIdForDisplay();
   }

   public void deleteDevice(UserId userId, ExternalCrontoDeviceId id) {
      this.ensureHasAccount(userId);
      CrontoDeviceToken device = this.getDeviceById(userId, id);

      try {
         this.crontoHandler().deleteDevice(userId, device.getDeviceId());
      } catch (PersisterException e) {
         throw new StoreFailureException(e);
      }

      ((CrontoEventFactory)this.eventFactory.get()).publishCrontoDeviceDeletedEvent(userId, device.getDeviceId());
      this.signingLogger().log("Deleted Cronto device with ID " + device.getDeviceIdForDisplay());
   }

   public void setDeviceEnabled(UserId userId, ExternalCrontoDeviceId id, boolean enabled) {
      this.ensureHasAccount(userId);
      CrontoDeviceToken device = this.getDeviceById(userId, id);
      if (device.isActive() == enabled) {
         LOG.debug("Not changing active flag of device " + LogUtil.quote(device.getDeviceId()) + " since it is already " + LogUtil.quote(enabled));
      } else {
         device.setActive(enabled);
         this.persistToken(device);
         this.signingLogger().log("Changed state of device " + LogUtil.quote(device.getDeviceId()) + " to " + LogUtil.quote(enabled ? "enabled" : "disabled"));
      }
   }

   public void disallowPushNotifications(UserId userId, ExternalCrontoDeviceId id) {
      this.ensureHasAccount(userId);
      this.setAllowPushNotifications(userId, id, false);
   }

   public void allowPushNotifications(UserId userId, ExternalCrontoDeviceId id) {
      this.ensureHasAccount(userId);
      this.setAllowPushNotifications(userId, id, true);
   }

   private void setAllowPushNotifications(UserId userId, ExternalCrontoDeviceId id, boolean allowPush) {
      if (!this.crontoHandler().isPushNotificationEnabled()) {
         throw new FeatureDisabledException("Push notifications are globally disabled, ignoring request.");
      }

      CrontoDeviceToken device = this.getDeviceById(userId, id);
      if (allowPush == device.isPushNotificationAllowed()) {
         LOG.debug("Push notifications for device " + LogUtil.quote(device.getDeviceId()) + " are already " + (allowPush ? "enabled" : "disabled"));
      } else {
         this.setEnablePushNotificationsForDevice(device, allowPush);
      }
   }

   private void setEnablePushNotificationsForDevice(CrontoDeviceToken device, boolean allowPush) {
      if (allowPush) {
         this.pushDevicesHandler().allowPushNotificationFor(device);
         String logMessage = "Allowed push notifications for device " + LogUtil.quote(device.getDeviceIdForDisplay());
         this.signingLogger().log(logMessage);
         LOG.info(logMessage);
      } else {
         this.pushDevicesHandler().disallowPushNotificationFor(device);
         String logMessage = "Disallowed push notifications for device " + LogUtil.quote(device.getDeviceIdForDisplay());
         this.signingLogger().log(logMessage);
         LOG.info(logMessage);
      }
   }

   private PushDevicesHandler pushDevicesHandler() {
      return this.crontoHandler().getPushDevicesHandler();
   }

   private void persistToken(CrontoToken token) {
      try {
         this.crontoHandler().getTokenDataProvider().updateToken(token.getDelegate());
      } catch (PersisterException e) {
         throw new StoreFailureException(e);
      }
   }

   private void removeToken(CrontoToken token) {
      try {
         this.crontoHandler().getTokenDataProvider().removeToken(token.getDelegate());
      } catch (PersisterException e) {
         throw new StoreFailureException(e);
      }
   }

   private void ensureHasAccount(UserId userId) {
      if (!this.hasCrontoAccount(userId)) {
         throw new EntityNotFoundException("User " + LogUtil.quote(userId) + " does not have a Cronto account");
      }
   }

   private CrontoHandler crontoHandler() {
      return (CrontoHandler)this.crontoHandler.get();
   }

   private SigningLogger signingLogger() {
      return (SigningLogger)this.signingLogger.get();
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public static class CrontoAdminServiceBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private Provider<CrontoHandler> crontoHandler;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<CurrentAdministrator> administrator;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<AdminappAccessController> adminappAccessController;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<CrontoEventFactory> eventFactory;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<SigningLogger> signingLogger;

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

      @SuppressFBWarnings(justification = "generated code")
      public CrontoAdminService.CrontoAdminServiceBuilder crontoHandler(Provider<CrontoHandler> crontoHandler) {
         this.crontoHandler = crontoHandler;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public CrontoAdminService.CrontoAdminServiceBuilder administrator(Provider<CurrentAdministrator> administrator) {
         this.administrator = administrator;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public CrontoAdminService.CrontoAdminServiceBuilder adminappAccessController(Provider<AdminappAccessController> adminappAccessController) {
         this.adminappAccessController = adminappAccessController;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public CrontoAdminService.CrontoAdminServiceBuilder eventFactory(Provider<CrontoEventFactory> eventFactory) {
         this.eventFactory = eventFactory;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public CrontoAdminService.CrontoAdminServiceBuilder signingLogger(Provider<SigningLogger> signingLogger) {
         this.signingLogger = signingLogger;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public CrontoAdminService build() {
         return new CrontoAdminService(this.crontoHandler, this.administrator, this.adminappAccessController, this.eventFactory, this.signingLogger);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "CrontoAdminService.CrontoAdminServiceBuilder(crontoHandler="
            + this.crontoHandler
            + ", administrator="
            + this.administrator
            + ", adminappAccessController="
            + this.adminappAccessController
            + ", eventFactory="
            + this.eventFactory
            + ", signingLogger="
            + this.signingLogger
            + ")";
      }
   }
}
