package com.airlock.iam.base.api.domain.model.mapification;

import com.airlock.iam.base.api.domain.model.exception.FailureException;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public final class MapifiedValues {
   private final Map<String, Object> values;

   public MapifiedValues(Map<String, Object> values) {
      this.values = Collections.unmodifiableMap(validateType("<root>", values));
   }

   public MapifiedValues() {
      this.values = Collections.emptyMap();
   }

   public boolean containsKey(String key) {
      return this.values.containsKey(key);
   }

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

   public Map<String, String> asStringStringMap() {
      return this.values.entrySet().stream().collect(Collectors.toMap(Entry::getKey, e -> (String)e.getValue()));
   }

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

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

   public MapifiedValues getValues(String key) {
      this.verifyKey(key);
      return new MapifiedValues((Map<String, Object>)this.values.get(key));
   }

   public <T> Optional<T> demapifyOptional(String key, Function<MapifiedValues, T> demapifier) {
      return this.values.containsKey(key) ? Optional.of(demapifier.apply(this.getValues(key))) : Optional.empty();
   }

   public <T> T demapifyValue(String key, Function<MapifiedValues, T> demapifier) {
      return demapifier.apply(this.getValues(key));
   }

   public <T> Set<T> demapifySet(String key, Function<MapifiedValues, T> demapifier) {
      return this.demapifyStream(key, demapifier).collect(Collectors.toSet());
   }

   public <T> List<T> demapifyList(String key, Function<MapifiedValues, T> demapifier) {
      return this.demapifyStream(key, demapifier).collect(Collectors.toList());
   }

   private <T> Stream<T> demapifyStream(String key, Function<MapifiedValues, T> demapifier) {
      return !this.values.containsKey(key) ? Stream.of((T[])(new Object[0])) : ((List)this.values.get(key)).stream().map(MapifiedValues::new).map(demapifier);
   }

   public <T> Set<T> demapifySimpleSet(String key, Function<Object, T> demapifier) {
      return this.demapifySimpleStream(key, demapifier).collect(Collectors.toSet());
   }

   private <T> Stream<T> demapifySimpleStream(String key, Function<Object, T> demapifier) {
      return !this.values.containsKey(key) ? Stream.of((T[])(new Object[0])) : ((List)this.values.get(key)).stream().map(demapifier);
   }

   public <S, T> T simpleValue(String key, Function<S, T> typer) {
      this.verifyKey(key);
      return typer.apply((S)this.values.get(key));
   }

   public <T> T get(String key) {
      this.verifyKey(key);
      return (T)this.values.get(key);
   }

   public boolean getBoolean(String key) {
      this.verifyKey(key);
      return (Boolean)this.values.get(key);
   }

   public <T> List<T> getList(String key) {
      if (!this.values.containsKey(key)) {
         return Collections.emptyList();
      }

      Collection<T> list = this.get(key);
      return list.stream().map(v -> (T)(v instanceof Map map ? new MapifiedValues(map) : v)).toList();
   }

   public <T> Set<T> getSet(String key) {
      if (!this.values.containsKey(key)) {
         return Collections.emptySet();
      }

      Collection<T> list = this.get(key);
      return list.stream().map(v -> (T)(v instanceof Map map ? new MapifiedValues(map) : v)).collect(Collectors.toSet());
   }

   public <T> Optional<T> getOptional(String key) {
      return !this.values.containsKey(key) ? Optional.empty() : Optional.of(this.get(key));
   }

   public Optional<MapifiedValues> getOptionalValues(String key) {
      return !this.values.containsKey(key) ? Optional.empty() : Optional.of(this.getValues(key));
   }

   public <S, T> Optional<T> transformOptional(String key, Function<S, T> tranformation) {
      return Optional.ofNullable(this.values.get(key)).map(tranformation);
   }

   private void verifyKey(String key) {
      if (!this.containsKey(key)) {
         throw new FailureException("Deserialization failed: expected key \"" + key + "\" not found.");
      }
   }

   private static <T> T validateType(String key, T value) {
      if (value == null) {
         throw new FailureException("Value in MapifiedValue must not be null (key=" + key + ")");
      }

      if (value instanceof Map map) {
         if (map.keySet().stream().anyMatch(k -> !(k instanceof String))) {
            throw new FailureException("Keys in MapifiedValue map must be Strings (key=" + key + ")");
         }

         map.forEach((k, v) -> validateType((String)k, (T)v));
      } else if (value instanceof List list) {
         list.forEach(v -> validateType(key, v));
      } else if (value instanceof Set set) {
         set.forEach(v -> validateType(key, v));
      } else if (Stream.of(String.class, Long.class, Integer.class, Boolean.class).noneMatch(value.getClass()::equals)) {
         throw new FailureException("Invalid type for MapifiedValue: " + value.getClass() + " (key=" + key + ")");
      }

      return value;
   }

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

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