const GEOMETRY_TYPES = new Set(["Point", "LineString", "Polygon", "MultiPolygon"]);
const MAX_GEOMETRY_BYTES = 192 * 1024;
const MAX_GEOMETRY_VERTICES = 10_000;

/**
 * Validate the bounded GeoJSON geometry subset accepted by provider-neutral
 * Data Products. GeometryCollections and arbitrary feature properties are
 * deliberately excluded: a fact owns exactly one geometry and its attributes
 * stay under the fact field policy.
 */
export function validateGeoJsonGeometry(value, path, errors, { allowedTypes = GEOMETRY_TYPES } = {}) {
  if (!isPlainObject(value) || !allowedTypes.has(value.type) || !Array.isArray(value.coordinates)) {
    errors.push(`${path}_must_be_bounded_geojson_geometry`);
    return;
  }
  rejectUnknownKeys(value, new Set(["type", "coordinates"]), path, errors);
  if (serializedByteLength(value) > MAX_GEOMETRY_BYTES) {
    errors.push(`${path}_size_exceeded`);
    return;
  }

  const vertices = [];
  if (value.type === "Point") {
    validatePosition(value.coordinates, path, errors, vertices);
  } else if (value.type === "LineString") {
    validateLineString(value.coordinates, path, errors, vertices);
  } else if (value.type === "Polygon") {
    validatePolygon(value.coordinates, path, errors, vertices);
  } else if (value.type === "MultiPolygon") {
    if (!value.coordinates.length) errors.push(`${path}.coordinates_must_be_nonempty`);
    value.coordinates.forEach((polygon, index) => validatePolygon(polygon, `${path}.coordinates[${index}]`, errors, vertices));
  }
  if (vertices.length > MAX_GEOMETRY_VERTICES) errors.push(`${path}_vertex_limit_exceeded`);
}

export function isBoundedGeoJsonGeometry(value, allowedTypes = GEOMETRY_TYPES) {
  const errors = [];
  validateGeoJsonGeometry(value, "geometry", errors, { allowedTypes });
  return errors.length === 0;
}

function validatePolygon(value, path, errors, vertices) {
  if (!Array.isArray(value) || !value.length) {
    errors.push(`${path}_polygon_rings_required`);
    return;
  }
  value.forEach((ring, index) => {
    const ringPath = `${path}[${index}]`;
    if (!Array.isArray(ring) || ring.length < 4) {
      errors.push(`${ringPath}_linear_ring_requires_four_positions`);
      return;
    }
    ring.forEach((position, positionIndex) => validatePosition(position, `${ringPath}[${positionIndex}]`, errors, vertices));
    if (!samePosition(ring[0], ring.at(-1))) errors.push(`${ringPath}_linear_ring_must_be_closed`);
  });
}

function validateLineString(value, path, errors, vertices) {
  if (!Array.isArray(value) || value.length < 2) {
    errors.push(`${path}_line_string_requires_two_positions`);
    return;
  }
  value.forEach((position, index) => validatePosition(position, `${path}[${index}]`, errors, vertices));
}

function validatePosition(value, path, errors, vertices) {
  if (!Array.isArray(value) || value.length !== 2 || !value.every(Number.isFinite)) {
    errors.push(`${path}_position_invalid`);
    return;
  }
  const [longitude, latitude] = value;
  if (longitude < -180 || longitude > 180) errors.push(`${path}.longitude_out_of_range`);
  if (latitude < -90 || latitude > 90) errors.push(`${path}.latitude_out_of_range`);
  vertices.push(value);
}

function samePosition(left, right) {
  return Array.isArray(left) && Array.isArray(right)
    && left.length === 2 && right.length === 2
    && Object.is(left[0], right[0]) && Object.is(left[1], right[1]);
}

function rejectUnknownKeys(value, allowed, path, errors) {
  for (const key of Object.keys(value)) {
    if (!allowed.has(key)) errors.push(`${path}.${key}_not_allowed`);
  }
}

function serializedByteLength(value) {
  try {
    return Buffer.byteLength(JSON.stringify(value));
  } catch {
    return Number.POSITIVE_INFINITY;
  }
}

function isPlainObject(value) {
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}

export const DATA_PRODUCT_GEOMETRY_TYPES = Object.freeze([...GEOMETRY_TYPES]);
export const DATA_PRODUCT_MAX_GEOMETRY_BYTES = MAX_GEOMETRY_BYTES;
export const DATA_PRODUCT_MAX_GEOMETRY_VERTICES = MAX_GEOMETRY_VERTICES;
