package com.airlock.iam.flow.shared.domain.service.data;

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.misc.log.LogUtil;
import com.airlock.iam.common.api.domain.model.user.User;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.flow.shared.api.domain.model.item.info.UserDataItemInfo;
import com.airlock.iam.flow.shared.domain.model.item.ContextDataItemDefinitionBase;
import com.airlock.iam.flow.shared.domain.model.step.data.ContextDataItems;
import com.airlock.iam.flow.shared.domain.model.step.data.UserDataEditChangeDescription;
import com.airlock.iam.flow.shared.domain.service.user.EditableItemChangeServiceList;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.NonNull;

public class ContextDataItemsChangeServiceImpl implements EditableItemChangeServiceList {
   private static final ELogger LOG = ELoggerFactory.getLogger(ContextDataItemsChangeServiceImpl.class);
   @NonNull
   private final ContextDataItems contextDataItems;
   @NonNull
   private final UserDataEditChangeDescription change;
   @NonNull
   private final User user;

   @Inject
   public ContextDataItemsChangeServiceImpl(@NonNull ContextDataItems contextDataItems, @NonNull UserDataEditChangeDescription change, @NonNull User user) {
      if (contextDataItems == null) {
         throw new NullPointerException("contextDataItems is marked non-null but is null");
      }

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

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

      this.contextDataItems = contextDataItems;
      this.change = change;
      this.user = user;
   }

   @Override
   public boolean canHandle(String key) {
      return this.contextDataItems.hasItemNamed(key);
   }

   @Override
   public ValidationResult handleItemChange(String key, Optional<Object> value) {
      ContextDataItemDefinitionBase<?> dataItem = this.contextDataItems.findItemWithName(key).orElseThrow();
      return this.handleChange(dataItem, value);
   }

   private ValidationResult handleChange(ContextDataItemDefinitionBase<?> dataItem, Optional<Object> value) {
      return value.isEmpty() ? this.deleteContextDataValue(dataItem) : this.changeContextDataValue(dataItem, value.get());
   }

   private ValidationResult deleteContextDataValue(ContextDataItemDefinitionBase<?> item) {
      ValidationResult validationResult = item.validate(Optional.empty());
      if (!validationResult.isValid()) {
         LOG.debug("Deleting context-data " + LogUtil.quote(item.getName() + " not valid: " + validationResult.getFailure()));
         return validationResult;
      } else {
         this.change.removeValue(item.getName());
         return ValidationResult.valid();
      }
   }

   private <T> ValidationResult changeContextDataValue(ContextDataItemDefinitionBase<T> item, Object value) {
      Optional<T> typedValue = item.convert(value);
      if (typedValue.isEmpty()) {
         LOG.debug("Ignoring context data item with key " + LogUtil.quote(item.getName()) + " because it has unexpected type " + value.getClass());
         return ValidationResult.singleError(item.getName(), GenericError.WRONG_FORMAT);
      } else {
         ValidationResult validationResult = item.validate(typedValue);
         if (!validationResult.isValid()) {
            LOG.debug(
               "Changing context-data "
                  + LogUtil.logSafeQuote(item.getName())
                  + " to "
                  + LogUtil.logSafeQuote(typedValue.get())
                  + " not valid: "
                  + validationResult.getFailure()
            );
            return validationResult;
         } else {
            this.change.addChange(item.getName(), typedValue.get());
            return ValidationResult.valid();
         }
      }
   }

   @Override
   public ValidationResult validateAllData() {
      ValidationResult validationResult = ValidationResult.valid();
      this.contextDataItems.getDefinitions().forEach(item -> validationResult.mergeWith(this.validate((ContextDataItemDefinitionBase<?>)item)));
      return validationResult;
   }

   private <T> ValidationResult validate(ContextDataItemDefinitionBase<T> item) {
      Optional<T> currentValue = this.currentValue(item);
      ValidationResult result = item.validate(currentValue);
      if (!result.isValid()) {
         LOG.debug(
            "Context-data field "
               + LogUtil.logSafeQuote(item.getName())
               + " with current value "
               + currentValue.map(LogUtil::logSafeQuote).orElse(null)
               + " is not valid: "
               + result.getFailure()
         );
      }

      return result;
   }

   private <T> Optional<T> currentValue(ContextDataItemDefinitionBase<T> item) {
      if (this.change.getRemovals().contains(item.getName())) {
         return Optional.empty();
      } else {
         return this.change.getChanges().containsKey(item.getName())
            ? Optional.ofNullable(this.change.getChanges().get(item.getName())).flatMap(item::convert)
            : item.getValueFromUser(this.user);
      }
   }

   @Override
   public List<UserDataItemInfo<Object>> getItemInfos() {
      return this.contextDataItems.getDefinitions().stream().map(this::toUserDataItemInfo).collect(Collectors.toUnmodifiableList());
   }

   private <T> UserDataItemInfo<Object> toUserDataItemInfo(ContextDataItemDefinitionBase<T> item) {
      return item.getItemInfo(this.currentValue(item));
   }

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

   @SuppressFBWarnings(justification = "generated code")
   public static class ContextDataItemsChangeServiceImplBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private ContextDataItems contextDataItems;
      @SuppressFBWarnings(justification = "generated code")
      private UserDataEditChangeDescription change;
      @SuppressFBWarnings(justification = "generated code")
      private User user;

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

      @SuppressFBWarnings(justification = "generated code")
      public ContextDataItemsChangeServiceImpl.ContextDataItemsChangeServiceImplBuilder contextDataItems(@NonNull ContextDataItems contextDataItems) {
         if (contextDataItems == null) {
            throw new NullPointerException("contextDataItems is marked non-null but is null");
         }

         this.contextDataItems = contextDataItems;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ContextDataItemsChangeServiceImpl.ContextDataItemsChangeServiceImplBuilder change(@NonNull UserDataEditChangeDescription change) {
         if (change == null) {
            throw new NullPointerException("change is marked non-null but is null");
         }

         this.change = change;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ContextDataItemsChangeServiceImpl.ContextDataItemsChangeServiceImplBuilder user(@NonNull User user) {
         if (user == null) {
            throw new NullPointerException("user is marked non-null but is null");
         }

         this.user = user;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public ContextDataItemsChangeServiceImpl build() {
         return new ContextDataItemsChangeServiceImpl(this.contextDataItems, this.change, this.user);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "ContextDataItemsChangeServiceImpl.ContextDataItemsChangeServiceImplBuilder(contextDataItems="
            + this.contextDataItems
            + ", change="
            + this.change
            + ", user="
            + this.user
            + ")";
      }
   }
}
