package cloud.nstart.terravault.ordinis.readapi.spatial;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * Минимальная structural валидация GeoJSON Polygon перед передачей в PostGIS.
 * Не пытаемся parse в JTS — это сделает {@code ST_GeomFromGeoJSON} на
 * DB-side. Цель этого парсера: отсечь явный мусор (не JSON, не Polygon,
 * слишком короткий ring) до того как попадём в repository.
 *
 * <p>Spec: <a href="https://datatracker.ietf.org/doc/html/rfc7946">RFC 7946</a>.
 *
 * <p>Принимаем только {@code {"type":"Polygon","coordinates":[[...]]}}.
 * MultiPolygon, Feature, FeatureCollection — не поддержано в v1 (consumer
 * пусть extract'нет первый geometry на своей стороне).
 */
public final class GeoJsonPolygon {

  private static final ObjectMapper MAPPER = new ObjectMapper();

  /** Лимит на размер JSON в bytes — защита от DoS. 1 MB достаточно для
   *  любого вменяемого polygon (1MB ≈ 50k coordinates). */
  static final int MAX_JSON_BYTES = 1_000_000;

  /** Лимит на coordinate count в exterior ring + holes. */
  static final int MAX_POINTS = 50_000;

  private GeoJsonPolygon() {}

  /**
   * Validate + canonicalize. Возвращает уже-канонический JSON string
   * (whitespace stripped, key order normalized) — этот вариант идёт в
   * Postgres ST_GeomFromGeoJSON.
   *
   * @throws IllegalArgumentException на любую структурную ошибку
   */
  public static String validateAndCanonicalize(String rawJson) {
    if (rawJson == null || rawJson.isBlank()) {
      throw new IllegalArgumentException("polygon GeoJSON is empty");
    }
    if (rawJson.length() > MAX_JSON_BYTES) {
      throw new IllegalArgumentException(
          "polygon GeoJSON exceeds " + MAX_JSON_BYTES + " bytes");
    }
    JsonNode root;
    try {
      root = MAPPER.readTree(rawJson);
    } catch (Exception e) {
      throw new IllegalArgumentException("polygon is not valid JSON: " + e.getMessage());
    }
    if (!root.isObject()) {
      throw new IllegalArgumentException("polygon GeoJSON must be an object");
    }
    JsonNode type = root.get("type");
    if (type == null || !"Polygon".equals(type.asText())) {
      throw new IllegalArgumentException(
          "polygon GeoJSON type must be \"Polygon\", got: " + (type == null ? "null" : type.asText()));
    }
    JsonNode coords = root.get("coordinates");
    if (coords == null || !coords.isArray() || coords.isEmpty()) {
      throw new IllegalArgumentException("polygon coordinates must be a non-empty array");
    }

    int totalPoints = 0;
    for (int i = 0; i < coords.size(); i++) {
      JsonNode ring = coords.get(i);
      if (!ring.isArray() || ring.size() < 4) {
        throw new IllegalArgumentException(
            "polygon ring #" + i + " must have >= 4 points (closed linear ring)");
      }
      // RFC 7946: first == last
      JsonNode first = ring.get(0);
      JsonNode last = ring.get(ring.size() - 1);
      if (!first.equals(last)) {
        throw new IllegalArgumentException(
            "polygon ring #" + i + " must be closed (first point == last point)");
      }
      for (int j = 0; j < ring.size(); j++) {
        JsonNode p = ring.get(j);
        if (!p.isArray() || p.size() < 2) {
          throw new IllegalArgumentException(
              "polygon ring #" + i + " point #" + j + " must be [longitude, latitude]");
        }
        double lon = p.get(0).asDouble();
        double lat = p.get(1).asDouble();
        if (lon < -180 || lon > 180) {
          throw new IllegalArgumentException(
              "longitude " + lon + " out of range [-180, 180] (ring #" + i + " point #" + j + ")");
        }
        if (lat < -90 || lat > 90) {
          throw new IllegalArgumentException(
              "latitude " + lat + " out of range [-90, 90] (ring #" + i + " point #" + j + ")");
        }
        totalPoints++;
        if (totalPoints > MAX_POINTS) {
          throw new IllegalArgumentException(
              "polygon has > " + MAX_POINTS + " points total");
        }
      }
    }

    // Canonical form: whitespace stripped, predictable encoding для
    // Postgres parsing.
    try {
      return MAPPER.writeValueAsString(root);
    } catch (Exception e) {
      throw new IllegalStateException("Failed to re-serialize validated GeoJSON", e);
    }
  }
}
