package cloud.nstart.terravault.ordinis.outbox.webhook;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.InetAddress;
import java.net.URI;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Set;

/**
 * SSRF (Server-Side Request Forgery) guard для webhook URLs. Блокирует
 * delivery на private CIDR (10/8, 172.16/12, 192.168/16, 127/8, 169.254/16).
 *
 * <p>Без этой защиты пользователь с правом создать subscription мог бы
 * получить blind-SSRF против:
 * <ul>
 *   <li>cloud metadata service (169.254.169.254 — AWS, GCP, Yandex Cloud)
 *   <li>internal admin UI / Vault / etcd на 127.0.0.1
 *   <li>kubelet API на 10.x внутри pod network
 * </ul>
 *
 * <p>Allowlist hosts через config — для testing с echo-сервером в
 * ordinis-staging кластере.
 */
public final class SsrfGuard {

  private static final Logger log = LoggerFactory.getLogger(SsrfGuard.class);

  /** Hosts которые ВСЕГДА разрешены (даже если резолвятся в private CIDR). */
  private final Set<String> allowedHosts;

  public SsrfGuard(List<String> allowedHosts) {
    this.allowedHosts = allowedHosts == null ? Set.of() : Set.copyOf(allowedHosts);
  }

  /**
   * Проверить что URL безопасен для outbound webhook delivery.
   *
   * @return null если OK, иначе reason почему URL отклонён
   */
  public String validate(String urlString) {
    URI uri;
    try {
      uri = URI.create(urlString);
    } catch (IllegalArgumentException e) {
      return "Malformed URL: " + e.getMessage();
    }

    String scheme = uri.getScheme();
    if (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme)) {
      return "URL scheme must be http or https, got: " + scheme;
    }

    String host = uri.getHost();
    if (host == null || host.isBlank()) {
      return "URL must have a host";
    }

    // Allowlist override: внутренний echo-server, тестовые URLs
    if (allowedHosts.contains(host.toLowerCase())) {
      return null;
    }

    // DNS resolution: получаем все A-records, проверяем все на private CIDR.
    // Если хотя бы один в private — отказ (защита от DNS rebinding атаки
    // тоже минимизирована, т.к. HttpClient может приконнектиться на
    // другой адрес позже; это best-effort).
    InetAddress[] addresses;
    try {
      addresses = InetAddress.getAllByName(host);
    } catch (UnknownHostException e) {
      return "DNS resolution failed: " + e.getMessage();
    }

    for (InetAddress addr : addresses) {
      if (isPrivate(addr)) {
        log.warn("SSRF guard rejected URL {} → {} (private CIDR)", urlString, addr.getHostAddress());
        return "Address resolves to private/loopback range: " + addr.getHostAddress();
      }
    }

    return null;
  }

  /**
   * Private/special-use ranges per RFC 1918, RFC 3927, RFC 4193, RFC 5735.
   * Loopback, link-local, multicast, broadcast, IPv6 unique-local — все в bucket.
   */
  static boolean isPrivate(InetAddress addr) {
    if (addr.isAnyLocalAddress()) return true;
    if (addr.isLoopbackAddress()) return true;     // 127.0.0.0/8, ::1
    if (addr.isLinkLocalAddress()) return true;    // 169.254/16, fe80::/10
    if (addr.isSiteLocalAddress()) return true;    // 10/8, 172.16/12, 192.168/16
    if (addr.isMulticastAddress()) return true;    // 224/4
    return false;
  }
}
