package com.airlock.iam.common.domain.service.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.domain.model.event.SubscribedEvent;
import com.airlock.iam.base.domain.service.uri.UrlUtils;
import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.common.api.domain.model.async.AsynchronousExecutor;
import com.airlock.iam.common.domain.model.encoder.ValueMapEncoder;
import com.airlock.iam.common.domain.model.encoder.ValueMapStringEncoder;
import com.airlock.iam.common.domain.model.event.RemoteEventSubscriberSettings;
import com.airlock.iam.core.domain.model.http.HttpRequest;
import com.airlock.iam.core.domain.model.http.HttpRequestBody;
import com.airlock.iam.core.domain.model.http.HttpRequestHeader;
import com.airlock.iam.core.domain.model.http.HttpRequestHeaders;
import com.airlock.iam.core.domain.model.http.HttpRequestMethod;
import com.airlock.iam.core.domain.model.http.HttpRequestUri;
import com.airlock.iam.core.domain.model.http.HttpResponse;
import com.airlock.iam.core.domain.model.http.client.HttpClient;
import com.airlock.iam.elog.api.EDefaultLogType;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import lombok.NonNull;

public final class RemoteEventSubscriber implements DomainEventSubscriber<DomainEvent> {
   private static final ELogger LOG = ELoggerFactory.getLogger(RemoteEventSubscriber.class);
   private final Provider<SubscribedEvent> subscribedEvent;
   private final Provider<UserEventStringTemplateResolver> templateProvider;
   private final Provider<RemoteEventSubscriberSettings> settingsProvider;
   private final Provider<HttpClient> client;
   private final Provider<AsynchronousExecutor> executor;

   @Inject
   RemoteEventSubscriber(
      @NonNull Provider<SubscribedEvent> subscribedEvent,
      @NonNull Provider<UserEventStringTemplateResolver> templateProvider,
      @NonNull Provider<RemoteEventSubscriberSettings> settings,
      @NonNull Provider<HttpClient> client,
      @NonNull Provider<AsynchronousExecutor> executor
   ) {
      if (subscribedEvent == null) {
         throw new NullPointerException("subscribedEvent is marked non-null but is null");
      }

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

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

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

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

      this.subscribedEvent = subscribedEvent;
      this.templateProvider = templateProvider;
      this.settingsProvider = settings;
      this.client = client;
      this.executor = executor;
   }

   @Override
   public void handleEvent(DomainEvent event) {
      if (((SubscribedEvent)this.subscribedEvent.get()).canHandle(event)) {
         RemoteEventSubscriberSettings settings = (RemoteEventSubscriberSettings)this.settingsProvider.get();
         String url = ((UserEventStringTemplateResolver)this.templateProvider.get()).resolveTemplate(event, settings.urlTemplate(), urlValueEncoder());
         if (!isValidUri(url)) {
            LOG.warn(EDefaultLogType.GENERIC_LOG_TYPE, LogUtil.logSafeString(url) + " is not a valid URI - not performing HTTP request.");
            return;
         }

         String body = settings.bodyTemplate()
            .map(
               template -> ((UserEventStringTemplateResolver)this.templateProvider.get())
                  .resolveTemplate(event, template, settings.valueEncoding().getEncoder())
            )
            .orElse("");
         ((AsynchronousExecutor)this.executor.get())
            .execute(new RemoteEventSubscriber.AsyncRunnable(event, settings, url, body, (HttpClient)this.client.get()));
      }
   }

   private static ValueMapEncoder urlValueEncoder() {
      return new ValueMapStringEncoder(UrlUtils::urlEncodeUrlParameter);
   }

   private static boolean isValidUri(String uri) {
      try {
         new URI(uri);
         return true;
      } catch (URISyntaxException e) {
         return false;
      }
   }

   @Override
   public Class<DomainEvent> forType() {
      return DomainEvent.class;
   }

   @Override
   public String toString() {
      return this.getClass().getSimpleName() + "[" + this.subscribedEvent + "]";
   }

   @SuppressFBWarnings(justification = "generated code")
   public static RemoteEventSubscriber.RemoteEventSubscriberBuilder builder() {
      return new RemoteEventSubscriber.RemoteEventSubscriberBuilder();
   }

   private static final class AsyncRunnable implements Runnable {
      private final DomainEvent event;
      private final RemoteEventSubscriberSettings settings;
      private final String url;
      private final String body;
      private final HttpClient client;

      @Override
      public void run() {
         RemoteEventSubscriber.LOG
            .debug(String.format("Executing HTTP request to %s with body %s", LogUtil.logSafeString(this.url), LogUtil.logSafeString(this.body)));
         HttpResponse response = this.client.execute(createRequest(this.url, this.body, this.settings));
         if (response.hasSuccessStatus()) {
            RemoteEventSubscriber.LOG.info("Successfully sent HTTP request for " + DomainEventUtils.describeEvent(this.event));
         } else {
            RemoteEventSubscriber.LOG
               .warn(
                  EDefaultLogType.GENERIC_LOG_TYPE,
                  String.format("HTTP request for %s failed with status %s", DomainEventUtils.describeEvent(this.event), response.status())
               );
         }
      }

      private static HttpRequest createRequest(String url, String body, RemoteEventSubscriberSettings settings) {
         HttpRequest.HttpRequestBuilder requestBuilder = HttpRequest.builder()
            .uri(HttpRequestUri.httpRequestUri(url))
            .headers(
               HttpRequestHeaders.httpRequestHeaders(Collections.singletonList(HttpRequestHeader.httpRequestHeader("Content-Type", settings.contentType())))
            )
            .body(HttpRequestBody.httpRequestBody(body.getBytes(StandardCharsets.UTF_8)));

         return switch (settings.httpMethod()) {
            case GET -> requestBuilder.method(HttpRequestMethod.GET).build();
            case POST -> requestBuilder.method(HttpRequestMethod.POST).build();
            case PUT -> requestBuilder.method(HttpRequestMethod.PUT).build();
            case DELETE -> requestBuilder.method(HttpRequestMethod.DELETE).build();
         };
      }

      @SuppressFBWarnings(justification = "generated code")
      public AsyncRunnable(DomainEvent event, RemoteEventSubscriberSettings settings, String url, String body, HttpClient client) {
         this.event = event;
         this.settings = settings;
         this.url = url;
         this.body = body;
         this.client = client;
      }
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class RemoteEventSubscriberBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private Provider<SubscribedEvent> subscribedEvent;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<UserEventStringTemplateResolver> templateProvider;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<RemoteEventSubscriberSettings> settings;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<HttpClient> client;
      @SuppressFBWarnings(justification = "generated code")
      private Provider<AsynchronousExecutor> executor;

      @SuppressFBWarnings(justification = "generated code")
      RemoteEventSubscriberBuilder() {
      }

      @SuppressFBWarnings(justification = "generated code")
      public RemoteEventSubscriber.RemoteEventSubscriberBuilder subscribedEvent(@NonNull Provider<SubscribedEvent> subscribedEvent) {
         if (subscribedEvent == null) {
            throw new NullPointerException("subscribedEvent is marked non-null but is null");
         }

         this.subscribedEvent = subscribedEvent;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public RemoteEventSubscriber.RemoteEventSubscriberBuilder templateProvider(@NonNull Provider<UserEventStringTemplateResolver> templateProvider) {
         if (templateProvider == null) {
            throw new NullPointerException("templateProvider is marked non-null but is null");
         }

         this.templateProvider = templateProvider;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public RemoteEventSubscriber.RemoteEventSubscriberBuilder settings(@NonNull Provider<RemoteEventSubscriberSettings> settings) {
         if (settings == null) {
            throw new NullPointerException("settings is marked non-null but is null");
         }

         this.settings = settings;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public RemoteEventSubscriber.RemoteEventSubscriberBuilder client(@NonNull Provider<HttpClient> client) {
         if (client == null) {
            throw new NullPointerException("client is marked non-null but is null");
         }

         this.client = client;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public RemoteEventSubscriber.RemoteEventSubscriberBuilder executor(@NonNull Provider<AsynchronousExecutor> executor) {
         if (executor == null) {
            throw new NullPointerException("executor is marked non-null but is null");
         }

         this.executor = executor;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public RemoteEventSubscriber build() {
         return new RemoteEventSubscriber(this.subscribedEvent, this.templateProvider, this.settings, this.client, this.executor);
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "RemoteEventSubscriber.RemoteEventSubscriberBuilder(subscribedEvent="
            + this.subscribedEvent
            + ", templateProvider="
            + this.templateProvider
            + ", settings="
            + this.settings
            + ", client="
            + this.client
            + ", executor="
            + this.executor
            + ")";
      }
   }
}
