package auctiora;

import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Rate-limited HTTP client that enforces per-host request limits.
 *
 * Features:
 * - Per-host rate limiting (configurable max requests per second)
 * - Request counting and monitoring
 * - Thread-safe using semaphores
 * - Automatic host extraction from URLs
 *
 * This prevents overloading external services like Troostwijk and getting blocked.
 */
@ApplicationScoped
public class RateLimitedHttpClient {
   
   private static final Logger LOG = Logger.getLogger(RateLimitedHttpClient.class);
   
   private final HttpClient                httpClient;
   private final Map<String, RateLimiter>  rateLimiters;
   private final Map<String, RequestStats> requestStats;
   
   @ConfigProperty(name = "auction.http.rate-limit.default-max-rps", defaultValue = "2")
   int defaultMaxRequestsPerSecond;
   
   @ConfigProperty(name = "auction.http.rate-limit.troostwijk-max-rps", defaultValue = "1")
   int troostwijkMaxRequestsPerSecond;
   
   @ConfigProperty(name = "auction.http.timeout-seconds", defaultValue = "30")
   int timeoutSeconds;
   
   public RateLimitedHttpClient() {
      this.httpClient   = HttpClient.newBuilder()
                                    .connectTimeout(Duration.ofSeconds(30))
                                    .build();
      this.rateLimiters = new ConcurrentHashMap<>();
      this.requestStats = new ConcurrentHashMap<>();
   }
   
   /**
    * Sends a GET request with automatic rate limiting based on host.
    */
   public HttpResponse<String> sendGet(String url) throws IOException, InterruptedException {
      var request = HttpRequest.newBuilder()
                               .uri(URI.create(url))
                               .timeout(Duration.ofSeconds(timeoutSeconds))
                               .GET()
                               .build();
      
      return send(request, HttpResponse.BodyHandlers.ofString());
   }
   
   /**
    * Sends a request for binary data (like images) with rate limiting.
    */
   public HttpResponse<byte[]> sendGetBytes(String url) throws IOException, InterruptedException {
      var request = HttpRequest.newBuilder()
                               .uri(URI.create(url))
                               .timeout(Duration.ofSeconds(timeoutSeconds))
                               .GET()
                               .build();
      
      return send(request, HttpResponse.BodyHandlers.ofByteArray());
   }
   
   /**
    * Sends any HTTP request with automatic rate limiting.
    */
   public <T> HttpResponse<T> send(HttpRequest request, HttpResponse.BodyHandler<T> bodyHandler)
   throws IOException, InterruptedException {
      
      var host    = extractHost(request.uri());
      var limiter = getRateLimiter(host);
      var stats   = getRequestStats(host);
      
      // Enforce rate limit (blocks if necessary)
      limiter.acquire();
      
      // Track request
      stats.incrementTotal();
      var startTime = System.currentTimeMillis();
      
      try {
         var response = httpClient.send(request, bodyHandler);
         
         var duration = System.currentTimeMillis() - startTime;
         stats.recordSuccess(duration);
         
         LOG.debugf("HTTP %d %s %s (%dms)",
                    response.statusCode(), request.method(), host, duration);
         
         // Track rate limit violations (429 = Too Many Requests)
         if (response.statusCode() == 429) {
            stats.incrementRateLimited();
            LOG.warnf("⚠️  Rate limited by %s (HTTP 429)", host);
         }
         
         return response;
         
      } catch (IOException | InterruptedException e) {
         stats.incrementFailed();
         LOG.warnf("❌ HTTP request failed for %s: %s", host, e.getMessage());
         throw e;
      }
   }
   
   /**
    * Gets or creates a rate limiter for a specific host.
    */
   private RateLimiter getRateLimiter(String host) {
      return rateLimiters.computeIfAbsent(host, h -> {
         var maxRps = getMaxRequestsPerSecond(h);
         LOG.infof("Initializing rate limiter for %s: %d req/s", h, maxRps);
         return new RateLimiter(maxRps);
      });
   }
   
   /**
    * Gets or creates request stats for a specific host.
    */
   private RequestStats getRequestStats(String host) {
      return requestStats.computeIfAbsent(host, h -> new RequestStats(h));
   }
   
   /**
    * Determines max requests per second for a given host.
    */
   private int getMaxRequestsPerSecond(String host) {
      if (host.contains("troostwijk")) {
         return troostwijkMaxRequestsPerSecond;
      }
      return defaultMaxRequestsPerSecond;
   }
   
   /**
    * Extracts host from URI (e.g., "api.troostwijkauctions.com").
    */
   private String extractHost(URI uri) {
      return uri.getHost() != null ? uri.getHost() : uri.toString();
   }
   
   /**
    * Gets statistics for all hosts.
    */
   public Map<String, RequestStats> getAllStats() {
      return Map.copyOf(requestStats);
   }
   
   /**
    * Gets statistics for a specific host.
    */
   public RequestStats getStats(String host) {
      return requestStats.get(host);
   }
   
   /**
    * Rate limiter implementation using token bucket algorithm.
    * Allows burst traffic up to maxRequestsPerSecond, then enforces steady rate.
    */
   private static class RateLimiter {
      
      private final Semaphore semaphore;
      private final int       maxRequestsPerSecond;
      private final long      intervalNanos;
      
      RateLimiter(int maxRequestsPerSecond) {
         this.maxRequestsPerSecond = maxRequestsPerSecond;
         this.intervalNanos        = TimeUnit.SECONDS.toNanos(1) / maxRequestsPerSecond;
         this.semaphore            = new Semaphore(maxRequestsPerSecond);
         
         // Refill tokens periodically
         startRefillThread();
      }
      
      void acquire() throws InterruptedException {
         semaphore.acquire();
         
         // Enforce minimum delay between requests
         var delayMillis = intervalNanos / 1_000_000;
         if (delayMillis > 0) {
            Thread.sleep(delayMillis);
         }
      }
      
      private void startRefillThread() {
         var refillThread = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
               try {
                  Thread.sleep(1000); // Refill every second
                  var toRelease = maxRequestsPerSecond - semaphore.availablePermits();
                  if (toRelease > 0) {
                     semaphore.release(toRelease);
                  }
               } catch (InterruptedException e) {
                  Thread.currentThread().interrupt();
                  break;
               }
            }
         }, "RateLimiter-Refill");
         refillThread.setDaemon(true);
         refillThread.start();
      }
   }
   
   /**
    * Statistics tracker for HTTP requests per host.
    */
   public static class RequestStats {
      
      private final String     host;
      private final AtomicLong totalRequests       = new AtomicLong(0);
      private final AtomicLong successfulRequests  = new AtomicLong(0);
      private final AtomicLong failedRequests      = new AtomicLong(0);
      private final AtomicLong rateLimitedRequests = new AtomicLong(0);
      private final AtomicLong totalDurationMs     = new AtomicLong(0);
      
      RequestStats(String host) {
         this.host = host;
      }
      
      void incrementTotal() {
         totalRequests.incrementAndGet();
      }
      
      void recordSuccess(long durationMs) {
         successfulRequests.incrementAndGet();
         totalDurationMs.addAndGet(durationMs);
      }
      
      void incrementFailed() {
         failedRequests.incrementAndGet();
      }
      
      void incrementRateLimited() {
         rateLimitedRequests.incrementAndGet();
      }
      
      // Getters
      public String getHost() { return host; }
      public long getTotalRequests()       { return totalRequests.get(); }
      public long getSuccessfulRequests()  { return successfulRequests.get(); }
      public long getFailedRequests()      { return failedRequests.get(); }
      public long getRateLimitedRequests() { return rateLimitedRequests.get(); }
      public long getAverageDurationMs() {
         var successful = successfulRequests.get();
         return successful > 0 ? totalDurationMs.get() / successful : 0;
      }
      
      @Override
      public String toString() {
         return String.format("%s: %d total, %d success, %d failed, %d rate-limited, avg %dms",
                              host, getTotalRequests(), getSuccessfulRequests(),
                              getFailedRequests(), getRateLimitedRequests(), getAverageDurationMs());
      }
   }
}
