package com.airlock.iam.login.application.service.session;

import com.airlock.iam.base.misc.json.JsonCoding;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.core.misc.util.ticket.KeyMultiValue;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.elog.api.LogTypes;
import com.google.common.annotations.VisibleForTesting;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class ContextDataSerializer {
   private static final ELogger LOG = ELoggerFactory.getLogger(ContextDataSerializer.class);
   private static final DateTimeFormatter LOCALDATE_FORMATTER = DateTimeFormatter.BASIC_ISO_DATE;
   private static final ContextDataSerializer.Serializer<String> STRING = new ContextDataSerializer.Serializer<>(
      String.class, "STRING", String::toString, Object::toString
   );
   private static final ContextDataSerializer.Serializer<Integer> INTEGER = new ContextDataSerializer.Serializer<>(
      Integer.class, "INTEGER", Object::toString, Integer::parseInt
   );
   private static final ContextDataSerializer.Serializer<Long> LONG = new ContextDataSerializer.Serializer<>(
      Long.class, "LONG", Object::toString, Long::parseLong
   );
   private static final ContextDataSerializer.Serializer<Boolean> BOOLEAN = new ContextDataSerializer.Serializer<>(
      Boolean.class, "BOOLEAN", Object::toString, Boolean::parseBoolean
   );
   private static final ContextDataSerializer.Serializer<Date> DATE = new ContextDataSerializer.Serializer<>(
      Date.class, "DATE", d -> Long.toString(d.getTime()), s -> new Date(Long.parseLong(s))
   );
   private static final ContextDataSerializer.Serializer<LocalDate> LOCALDATE = new ContextDataSerializer.Serializer<>(
      LocalDate.class, "LOCALDATE", LOCALDATE_FORMATTER::format, s -> LocalDate.parse(s, LOCALDATE_FORMATTER)
   );
   private static final List<ContextDataSerializer.Serializer<?>> SERIALIZERS = List.of(STRING, INTEGER, LONG, BOOLEAN, DATE, LOCALDATE);

   public Optional<KeyMultiValue> serializeContextData(String kmvKey, Map<String, Object> contextDataMap, Predicate<String> contextDataKeyMatcher) {
      List<String> values = contextDataMap.entrySet()
         .stream()
         .filter(e -> contextDataKeyMatcher.test(e.getKey()))
         .map(e -> this.serializeValue(e.getKey(), e.getValue()))
         .filter(Optional::isPresent)
         .map(Optional::get)
         .collect(Collectors.toList());
      return values.isEmpty() ? Optional.empty() : Optional.of(new KeyMultiValue(kmvKey, values));
   }

   public Map<String, Object> deserializeContextData(KeyMultiValue kmv, Predicate<String> contextDataKeyMatchers) {
      return Arrays.stream(kmv.getValues())
         .map(v -> this.deserializeValue(v, contextDataKeyMatchers))
         .filter(Optional::isPresent)
         .map(Optional::get)
         .collect(Collectors.toMap(Entry::getKey, Entry::getValue));
   }

   @VisibleForTesting
   Optional<String> serializeValue(String key, Object value) {
      if (value == null) {
         return Optional.empty();
      }

      for (ContextDataSerializer.Serializer<?> serializer : SERIALIZERS) {
         if (serializer.canSerialize(value)) {
            return this.serializeUsing(serializer, key, value);
         }
      }

      LOG.debug("Cannot store context data with key " + LogUtil.quote(key) + " in session ticket. Unable to serialize type " + value.getClass().getName());
      return Optional.empty();
   }

   private Optional<String> serializeUsing(ContextDataSerializer.Serializer<?> serializer, String key, Object value) {
      String serializedValue;
      try {
         serializedValue = serializer.serialize(value);
      } catch (RuntimeException e) {
         LOG.warn(
            LogTypes.ILLEGAL_ARGUMENT,
            "Unable to serialize context data value with key " + LogUtil.quote(key) + " and type " + value.getClass().getName() + ": " + e.getMessage()
         );
         return Optional.empty();
      }

      Optional<String> json = JsonCoding.tryToJson(new ContextDataSerializer.SerializedEntry(key, serializer.getIdentifier(), serializedValue));
      if (json.isEmpty()) {
         LOG.warn(LogTypes.ILLEGAL_ARGUMENT, "Unable to serialize context data with key " + LogUtil.quote(key) + "; ignoring this entry.");
      }

      return json;
   }

   @VisibleForTesting
   Optional<Entry<String, Object>> deserializeValue(String string, Predicate<String> contextDataKeyMatchers) {
      Optional<ContextDataSerializer.SerializedEntry> optionalSerializedEntry = JsonCoding.tryFromJsonTo(string, ContextDataSerializer.SerializedEntry.class);
      if (optionalSerializedEntry.isEmpty()) {
         LOG.warn(LogTypes.ILLEGAL_ARGUMENT, "Error while deserializing context data. Ignoring this entry.");
         LOG.debug("Error while deserializing context data; ignoring this entry: " + LogUtil.logSafeQuote(string));
         return Optional.empty();
      }

      ContextDataSerializer.SerializedEntry serializedEntry = optionalSerializedEntry.get();
      if (serializedEntry.getKey() != null && serializedEntry.getType() != null && serializedEntry.getValue() != null) {
         if (!contextDataKeyMatchers.test(serializedEntry.getKey())) {
            LOG.debug(
               "Ignoring context-data entry for " + LogUtil.logSafeQuote(serializedEntry.getKey()) + " because it does not match any of the configured fields."
            );
            return Optional.empty();
         }

         for (ContextDataSerializer.Serializer<?> serializer : SERIALIZERS) {
            if (serializer.canDeserialize(serializedEntry.getType())) {
               return this.deserializeUsing(serializer, serializedEntry);
            }
         }

         LOG.warn(LogTypes.ILLEGAL_ARGUMENT, "Unable to read context data value from ticket; unknown data type; ignoring.");
         LOG.debug("Unable to read context data value from ticket; unknown data type; ignoring: " + LogUtil.logSafeQuote(string));
         return Optional.empty();
      } else {
         LOG.warn(
            LogTypes.ILLEGAL_ARGUMENT,
            String.format(
               "Missing attribute while deserializing context data; ignoring entry with key %s, type %s and value %s",
               LogUtil.quote(serializedEntry.getKey()),
               LogUtil.quote(serializedEntry.getType()),
               nullOrNotNull(serializedEntry.getValue())
            )
         );
         return Optional.empty();
      }
   }

   private Optional<Entry<String, Object>> deserializeUsing(
      ContextDataSerializer.Serializer<?> serializer, ContextDataSerializer.SerializedEntry serializedEntry
   ) {
      try {
         Object deserializedValue = serializer.deserialize(serializedEntry.getValue());
         return Optional.of(Map.entry(serializedEntry.getKey(), deserializedValue));
      } catch (RuntimeException e) {
         LOG.warn(
            LogTypes.ILLEGAL_ARGUMENT,
            "Unable to deserialize context data value from ticket of type " + serializer.getSerializableClass().getName() + " : " + e.getMessage()
         );
         LOG.debug(
            "Unable to deserialize context data value from ticket of type "
               + serializer.getSerializableClass().getName()
               + " with value "
               + LogUtil.quote(serializedEntry.getValue())
               + ": "
               + e.getMessage()
         );
         return Optional.empty();
      }
   }

   private static String nullOrNotNull(String value) {
      return value == null ? LogUtil.quote("null") : LogUtil.quote("non-null");
   }

   private static class SerializedEntry {
      private String key;
      private String type;
      private String value;

      @SuppressFBWarnings(justification = "generated code")
      public String getKey() {
         return this.key;
      }

      @SuppressFBWarnings(justification = "generated code")
      public String getType() {
         return this.type;
      }

      @SuppressFBWarnings(justification = "generated code")
      public String getValue() {
         return this.value;
      }

      @SuppressFBWarnings(justification = "generated code")
      public void setKey(String key) {
         this.key = key;
      }

      @SuppressFBWarnings(justification = "generated code")
      public void setType(String type) {
         this.type = type;
      }

      @SuppressFBWarnings(justification = "generated code")
      public void setValue(String value) {
         this.value = value;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SerializedEntry(String key, String type, String value) {
         this.key = key;
         this.type = type;
         this.value = value;
      }

      @SuppressFBWarnings(justification = "generated code")
      public SerializedEntry() {
      }
   }

   private static class Serializer<T> {
      private final Class<T> serializableClass;
      private final String identifier;
      private final Function<T, String> serializer;
      private final Function<String, T> deserializer;

      public boolean canSerialize(Object o) {
         return this.serializableClass.isAssignableFrom(o.getClass());
      }

      public boolean canDeserialize(String id) {
         return this.identifier.equals(id);
      }

      public String serialize(Object o) throws IllegalArgumentException {
         return this.serializer.apply(this.serializableClass.cast(o));
      }

      public T deserialize(String s) throws IllegalArgumentException {
         return this.deserializer.apply(s);
      }

      @SuppressFBWarnings(justification = "generated code")
      public Serializer(Class<T> serializableClass, String identifier, Function<T, String> serializer, Function<String, T> deserializer) {
         this.serializableClass = serializableClass;
         this.identifier = identifier;
         this.serializer = serializer;
         this.deserializer = deserializer;
      }

      @SuppressFBWarnings(justification = "generated code")
      public Class<T> getSerializableClass() {
         return this.serializableClass;
      }

      @SuppressFBWarnings(justification = "generated code")
      public String getIdentifier() {
         return this.identifier;
      }
   }
}
