package com.airlock.iam.flow.api.domain.model.step;

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.stream.Stream;

public final class AdditionalAttributes {
   private static final ELogger LOG = ELoggerFactory.getLogger(AdditionalAttributes.class);
   public static final String CUSTOM_ATTRIBUTES_KEY = "customAttributes";
   private final Map<String, Object> attributes;

   public AdditionalAttributes() {
      this.attributes = new HashMap<>();
   }

   public AdditionalAttributes(Map<String, Object> attributes) {
      requireValidAttributes(attributes);
      this.attributes = new HashMap<>(attributes);
   }

   public Object get(String attribute) {
      return this.attributes.get(attribute);
   }

   public Object put(String attribute, Object value) {
      if (attribute.equals("customAttributes")) {
         return this.attributes.put("customAttributes", this.mergeCustomAttributes(value));
      }

      if (this.attributes.containsKey(attribute) && Objects.equals(this.attributes.get(attribute), value)) {
         LOG.debug("Overwriting additional attribute \"" + attribute + "\" from previous step result.");
      }

      return this.attributes.put(attribute, value);
   }

   private Map<String, Object> mergeCustomAttributes(Object newCustomAttributes) {
      requireValidCustomAttributes(newCustomAttributes);
      Map<String, Object> mergedAttributes = new HashMap<>(this.getCustomAttributes());
      mergedAttributes.putAll((Map<? extends String, ? extends Object>)newCustomAttributes);
      return mergedAttributes;
   }

   private Map<String, Object> getCustomAttributes() {
      return (Map<String, Object>)Optional.ofNullable((Map)this.attributes.get("customAttributes")).orElse(Collections.emptyMap());
   }

   public void putCustomAttribute(String attribute, Object value) {
      this.put("customAttributes", Map.of(attribute, value));
   }

   public void forEach(BiConsumer<? super String, ? super Object> action) {
      this.attributes.forEach(action);
   }

   public AdditionalAttributes putAll(AdditionalAttributes other) {
      other.forEach(this::put);
      return this;
   }

   public AdditionalAttributes copy() {
      return new AdditionalAttributes(this.asMap());
   }

   public Map<String, Object> asMap() {
      return new HashMap<>(this.attributes);
   }

   public Set<String> keySet() {
      return this.attributes.keySet();
   }

   public boolean isEmpty() {
      return this.attributes.isEmpty();
   }

   public String toLoggable() {
      return Stream.concat(
            this.attributes.keySet().stream().filter(key -> !key.equals("customAttributes")),
            this.getCustomAttributes().keySet().stream().map(key -> "customAttributes." + key)
         )
         .toList()
         .toString();
   }

   @Override
   public String toString() {
      return this.attributes.toString();
   }

   private static void requireValidAttributes(Map<String, Object> attributes) {
      Objects.requireNonNull(attributes);
      Optional.ofNullable(attributes.get("customAttributes")).ifPresent(AdditionalAttributes::requireValidCustomAttributes);
   }

   private static void requireValidCustomAttributes(Object customAttributes) {
      if (!(customAttributes instanceof Map)) {
         throwReservedKeyException();
      }

      ((Map)customAttributes).keySet().forEach(key -> {
         if (!(key instanceof String)) {
            throwReservedKeyException();
         }
      });
   }

   private static void throwReservedKeyException() {
      throw new FailureException(String.format("Key \"%s\" is reserved for Custom Additional Attributes", "customAttributes"));
   }

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

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