package cloud.nstart.terravault.ordinis.restapi.service.webhook;

import cloud.nstart.terravault.ordinis.domain.webhook.WebhookSubscription;
import cloud.nstart.terravault.ordinis.outbox.webhook.HmacSigner;
import cloud.nstart.terravault.ordinis.outbox.webhook.SsrfGuard;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * Synchronous test ping для webhook subscription. Admin кликает кнопку в UI →
 * receiver получает synthetic event ровно с теми же headers/HMAC что и
 * production delivery, ответ возвращается сразу для inline feedback.
 *
 * <p>Use case: после создания subscription verify что:
 * <ul>
 *   <li>URL достижим (DNS, network, TLS)
 *   <li>Receiver не отвергает HMAC signature (admin раскопировал secret правильно)
 *   <li>Receiver обрабатывает payload без 5xx
 * </ul>
 *
 * <p>Bypass'ит outbox/dispatcher — sync request, immediate response. Не
 * порождает webhook_deliveries row (test event, не реальный business event).
 *
 * <p>SSRF guard validate'ит URL host против allowed-hosts list (override) +
 * private CIDR check. Тот же что у dispatcher — единая security policy.
 */
@Service
public class WebhookTestPingService {

  private static final Logger log = LoggerFactory.getLogger(WebhookTestPingService.class);
  private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(3);
  private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(10);

  private final ObjectMapper objectMapper;
  private final HttpClient httpClient;
  private final SsrfGuard ssrfGuard;

  /**
   * Spring constructor — собирает HttpClient с дефолтным connect timeout, читает
   * allowed-hosts CSV из config. Тестовый код использует package-private
   * конструктор {@link #WebhookTestPingService(ObjectMapper, List, HttpClient)}.
   *
   * <p>{@code @Autowired} обязателен — без него Spring не знает что выбрать
   * между этим и package-private constructor для тестов.
   */
  @Autowired
  public WebhookTestPingService(
      ObjectMapper objectMapper,
      @Value("${ordinis.webhook.allowed-hosts:}") String allowedHostsCsv) {
    this(
        objectMapper,
        parseAllowedHosts(allowedHostsCsv),
        HttpClient.newBuilder().connectTimeout(CONNECT_TIMEOUT).build());
  }

  /** Package-private constructor для unit-тестов: inject mock HttpClient. */
  WebhookTestPingService(ObjectMapper objectMapper, List<String> allowedHosts, HttpClient httpClient) {
    this.objectMapper = objectMapper;
    this.httpClient = httpClient;
    this.ssrfGuard = new SsrfGuard(allowedHosts);
  }

  private static List<String> parseAllowedHosts(String csv) {
    if (csv == null || csv.isBlank()) {
      return List.of();
    }
    return Arrays.stream(csv.split(",")).map(String::trim).toList();
  }

  public Result testPing(WebhookSubscription sub) {
    String ssrfReason = ssrfGuard.validate(sub.getUrl());
    if (ssrfReason != null) {
      return Result.rejected("SSRF guard rejected URL: " + ssrfReason);
    }

    Map<String, Object> payload = buildSyntheticPayload(sub);
    byte[] body;
    try {
      body = objectMapper.writeValueAsBytes(payload);
    } catch (JsonProcessingException e) {
      return Result.rejected("Failed to serialize test payload: " + e.getMessage());
    }
    String signature = HmacSigner.sign(sub.getHmacSecret(), body);

    HttpRequest req = HttpRequest.newBuilder()
        .uri(URI.create(sub.getUrl()))
        .header("Content-Type", "application/json")
        .header(HmacSigner.HEADER_NAME, signature)
        .header("X-Ordinis-Event-Id", "test-ping")
        .header("X-Ordinis-Event-Type", "test.ping")
        .header("X-Ordinis-Scope", "PUBLIC")
        .header("X-Ordinis-Test", "true")
        .timeout(REQUEST_TIMEOUT)
        .POST(BodyPublishers.ofByteArray(body))
        .build();

    long start = System.nanoTime();
    try {
      HttpResponse<String> resp = sendRequest(req);
      long ms = (System.nanoTime() - start) / 1_000_000L;
      int status = resp.statusCode();
      String preview = trim(resp.body(), 200);
      if (status >= 200 && status < 300) {
        return Result.success(status, ms, preview);
      }
      return Result.failure(status, ms, "HTTP " + status + " — " + preview);
    } catch (Exception e) {
      long ms = (System.nanoTime() - start) / 1_000_000L;
      log.warn("Test ping to {} failed", sub.getUrl(), e);
      return Result.failure(null, ms, e.getClass().getSimpleName() + ": " + e.getMessage());
    }
  }

  /**
   * Extension point для тестов. Mockito не может мокать {@link HttpClient}
   * на Java 17+ (sealed/abstract class), поэтому unit-тесты переопределяют
   * этот protected метод через анонимный subclass и возвращают заранее
   * заготовленные ответы.
   */
  protected HttpResponse<String> sendRequest(HttpRequest req) throws Exception {
    return httpClient.send(req, BodyHandlers.ofString(StandardCharsets.UTF_8));
  }

  private Map<String, Object> buildSyntheticPayload(WebhookSubscription sub) {
    Map<String, Object> p = new LinkedHashMap<>();
    p.put("eventId", "test-ping");
    p.put("eventType", "test.ping");
    p.put("scope", "PUBLIC");
    p.put("subscriptionId", sub.getId().toString());
    p.put("subscriptionName", sub.getName());
    p.put("timestamp", OffsetDateTime.now().toString());
    p.put("message", "Test event from Ordinis admin. Если receiver получил — он живой и валидирует HMAC.");
    return p;
  }

  private static String trim(String s, int max) {
    if (s == null) return "";
    return s.length() <= max ? s : s.substring(0, max) + "…";
  }

  /**
   * Result test ping'а. status:
   * <ul>
   *   <li>success — HTTP 2xx
   *   <li>failure — HTTP non-2xx ИЛИ network exception (timeout, connection refused)
   *   <li>rejected — SSRF guard / serialization error, request даже не отправлен
   * </ul>
   */
  public record Result(
      String status,
      Integer httpStatus,
      Long latencyMs,
      String message) {

    public static Result success(int httpStatus, long latencyMs, String body) {
      return new Result("success", httpStatus, latencyMs, body);
    }

    public static Result failure(Integer httpStatus, long latencyMs, String error) {
      return new Result("failure", httpStatus, latencyMs, error);
    }

    public static Result rejected(String reason) {
      return new Result("rejected", null, null, reason);
    }
  }
}
