package com.airlock.iam.common.infrastructure.event;

import com.airlock.iam.base.api.domain.model.event.DomainEvent;
import com.airlock.iam.base.api.domain.model.event.DomainEventSubscriber;
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 com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.Collection;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.NonNull;

public final class EventSubscribersFactory {
   private static final ELogger LOG = ELoggerFactory.getLogger(EventSubscribersFactory.class);
   @NonNull
   private final ScannedEventSubscribers scannedEventSubscribers;
   @NonNull
   private final ConfiguredEventSubscribers configuredEventSubscribers;

   public static EventSubscribersFactory eventSubscribersFactory(
      ScannedEventSubscribers scannedEventSubscribers, ConfiguredEventSubscribers configuredEventSubscribers
   ) {
      return new EventSubscribersFactory(scannedEventSubscribers, configuredEventSubscribers);
   }

   public Collection<DomainEventSubscriber<? extends DomainEvent>> merge() {
      this.ensureScannedAndConfiguredSubscribersDisjoint();
      Set<DomainEventSubscriber<? extends DomainEvent>> subscribers = Stream.of(
            this.scannedEventSubscribers.subscribers(), this.configuredEventSubscribers.subscribers()
         )
         .flatMap(Collection::stream)
         .collect(Collectors.toUnmodifiableSet());
      log(subscribers);
      return subscribers;
   }

   private void ensureScannedAndConfiguredSubscribersDisjoint() {
      Set<String> scannedClasses = canonicalName(this.scannedEventSubscribers.subscribers());
      Set<String> configuredClasses = canonicalName(this.configuredEventSubscribers.subscribers());
      SetView<String> offendingClasses = Sets.intersection(scannedClasses, configuredClasses);
      if (!offendingClasses.isEmpty()) {
         throw new FailureException(message(offendingClasses));
      }
   }

   private static String message(SetView<String> intersection) {
      String offendingClasses = intersection.stream().sorted().collect(Collectors.joining(", ", "[", "]"));
      return "The following event subscriber classes were both detected via classpath scanning and configured in IAM: "
         + offendingClasses
         + ". This behavior is forbidden to prevent registering an event subscriber twice. Ensure that the logics of configured event subscribers reside outside the scanned packages configured in 'domainEventSubscribers'.";
   }

   private static Set<String> canonicalName(Collection<?> collection) {
      return collection.stream().map(o -> o.getClass().getCanonicalName()).collect(Collectors.toUnmodifiableSet());
   }

   private static void log(Collection<DomainEventSubscriber<? extends DomainEvent>> subscribers) {
      if (!subscribers.isEmpty()) {
         LOG.debug("Found " + subscribers.size() + " event subscribers: " + subscribers.stream().map(c -> c.getClass().getSimpleName()).sorted().toList());
      }
   }

   @SuppressFBWarnings(justification = "generated code")
   private EventSubscribersFactory(@NonNull ScannedEventSubscribers scannedEventSubscribers, @NonNull ConfiguredEventSubscribers configuredEventSubscribers) {
      if (scannedEventSubscribers == null) {
         throw new NullPointerException("scannedEventSubscribers is marked non-null but is null");
      }

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

      this.scannedEventSubscribers = scannedEventSubscribers;
      this.configuredEventSubscribers = configuredEventSubscribers;
   }
}
