package com.airlock.iam.base.misc.json;

import com.airlock.iam.base.api.domain.model.exception.ViolationException;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.elog.api.LogTypes;
import com.fasterxml.jackson.jr.ob.JSON;
import com.fasterxml.jackson.jr.ob.JSON.Feature;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public final class JsonCoding {
   private static final ELogger LOG = ELoggerFactory.getLogger(JsonCoding.class);

   public static Map<String, Object> fromJson(Object json) {
      try {
         return JSON.std.mapFrom(json);
      } catch (IOException e) {
         throw new ViolationException(e);
      }
   }

   public static Map<String, Object> fromJson(String json) {
      return fromJson((Object)json);
   }

   public static <T> T fromJsonTo(String json, Class<T> type) {
      try {
         return (T)JSON.std.beanFrom(type, json);
      } catch (IOException e) {
         throw new ViolationException(e);
      }
   }

   public static <T> Optional<T> tryFromJsonTo(String json, Class<T> type) {
      try {
         return Optional.of((T)JSON.std.beanFrom(type, json));
      } catch (IOException e) {
         return Optional.empty();
      }
   }

   public static <T> List<T> fromJsonToListOf(String json, Class<T> type) {
      try {
         return JSON.std.listOfFrom(type, json);
      } catch (IOException e) {
         throw new ViolationException(e);
      }
   }

   public static <T> Map<String, T> fromJsonToMapOf(String json, Class<T> type) {
      try {
         return JSON.std.mapOfFrom(type, json);
      } catch (IOException e) {
         throw new ViolationException(e);
      }
   }

   public static <T> Optional<Map<String, T>> tryFromJsonToMapOf(String json, Class<T> type) {
      try {
         return Optional.of(JSON.std.mapOfFrom(type, json));
      } catch (IOException e) {
         return Optional.empty();
      }
   }

   public static Optional<Map<String, Object>> tryMapFromJson(Object jsonObject) {
      try {
         return Optional.of(JSON.std.mapFrom(jsonObject));
      } catch (IOException e) {
         return Optional.empty();
      }
   }

   public static Object fromJsonToObject(Object source) {
      try {
         return JSON.std.anyFrom(source);
      } catch (IOException e) {
         throw new ViolationException(e);
      }
   }

   public static Optional<Object> tryFromJsonToObject(Object source) {
      try {
         return Optional.of(JSON.std.anyFrom(source));
      } catch (IOException e) {
         return Optional.empty();
      }
   }

   public static String toJson(Object source) {
      try {
         return toJson(source, false, false);
      } catch (IOException e) {
         throw new ViolationException(e);
      }
   }

   public static String toJson(Object value, boolean includeNull, boolean prettyPrint) throws IOException {
      JSON mapper = JSON.std;
      if (includeNull) {
         mapper = mapper.with(new Feature[]{Feature.WRITE_NULL_PROPERTIES});
      }

      if (prettyPrint) {
         mapper = mapper.with(new Feature[]{Feature.PRETTY_PRINT_OUTPUT});
      }

      return mapper.asString(value);
   }

   public static Optional<String> tryToJson(Object source) {
      try {
         return Optional.of(JSON.std.asString(source));
      } catch (IOException e) {
         return Optional.empty();
      }
   }

   public static String toPrettyJson(Object source) {
      try {
         return toJson(source, false, true);
      } catch (IOException e) {
         throw new ViolationException(e);
      }
   }

   public static Map<String, Object> deepMerge(Map<String, Object> target, Map<String, Object> source) {
      return deepMerge(target, source, "");
   }

   private static Map<String, Object> deepMerge(Map<String, Object> target, Map<String, Object> source, String resourceKeyPrefix) {
      source.forEach(
         (key, value) -> {
            String keyPath = appendKey(resourceKeyPrefix, key);
            if (target.containsKey(key) && value.getClass() != target.get(key).getClass()) {
               LOG.warn(
                  LogTypes.CONFIGURATION_ERROR,
                  "Error while merging JSON structures: key \"" + keyPath + "\" is not of the same JSON type in source and target; ignoring."
               );
            } else {
               if (isJsonObject(value)) {
                  deepMergeJsonObject(target, key, value, keyPath);
               } else {
                  target.merge(key, value, (t, s) -> (Object)s);
               }
            }
         }
      );
      return target;
   }

   private static void deepMergeJsonObject(Map<String, Object> target, String key, Object value, String keyPath) {
      Object newTarget = target.get(key);
      if (newTarget != null) {
         deepMerge((Map<String, Object>)newTarget, (Map<String, Object>)value, keyPath);
      } else {
         target.put(key, value);
      }
   }

   private static boolean isJsonObject(Object value) {
      return value instanceof Map;
   }

   private static String appendKey(String resourceKeyPrefix, String key) {
      return resourceKeyPrefix.isEmpty() ? key : resourceKeyPrefix + "." + key;
   }

   private JsonCoding() {
      throw new AssertionError();
   }
}
