using FluentValidation;
using SatelliteProvider.Common.DTO;

namespace SatelliteProvider.Api.Validators;

// AZ-809: FluentValidation rules for POST /api/satellite/route. Wired
// through ValidationEndpointFilter<CreateRouteRequest> at endpoint
// registration time (.WithValidation<CreateRouteRequest>() in Program.cs).
// Failures are converted to RFC 7807 ValidationProblemDetails per
// _docs/02_document/contracts/api/error-shape.md v1.0.0.
//
// Required-field detection is handled at the deserializer level via
// [JsonRequired] on CreateRouteRequest, RoutePoint, GeofencePolygon, and
// GeoPoint, plus JsonSerializerOptions.UnmappedMemberHandling.Disallow
// (AZ-795 global). This validator covers post-deserialization business
// rules: non-zero id, name + description length, range checks on size /
// zoom / points-count, per-point lat/lon ranges (via RoutePointValidator),
// per-polygon corner ranges + NW-of-SE invariant (via GeofencePolygonValidator),
// and the cross-field createTilesZip-implies-requestMaps rule.
public sealed class CreateRouteRequestValidator : AbstractValidator<CreateRouteRequest>
{
    private const double MinRegionSizeMeters = 100.0;
    private const double MaxRegionSizeMeters = 10000.0;
    private const int MinZoom = 0;
    private const int MaxZoom = 22;
    private const int MinPoints = 2;
    private const int MaxPoints = 500;
    private const int MaxNameLength = 200;
    private const int MaxDescriptionLength = 1000;
    // Geofences are axis-aligned bbox rectangles used for AOI restriction
    // during route planning (see route-creation.md). Realistic use is 1-10
    // polygons per route; cap at 50 to give 5x headroom while bounding the
    // validator's worst-case allocation. The global Kestrel body limit
    // (500 MiB, sized for the UAV upload endpoint) is not a useful gate
    // here because polygon JSON is small (~90 bytes per minimum-shape
    // polygon); without this cap a single authenticated request could
    // submit millions of polygons and saturate the LOH.
    private const int MaxPolygons = 50;

    public CreateRouteRequestValidator()
    {
        RuleFor(req => req.Id)
            .NotEmpty()
            .WithMessage("`id` must be a non-zero GUID (the caller's idempotency key).");

        RuleFor(req => req.Name)
            .NotEmpty()
            .WithMessage("`name` is required and must not be empty or whitespace.")
            .MaximumLength(MaxNameLength)
            .WithMessage($"`name` must be at most {MaxNameLength} characters.");

        RuleFor(req => req.Description)
            .MaximumLength(MaxDescriptionLength)
            .When(req => req.Description is not null)
            .WithMessage($"`description` must be at most {MaxDescriptionLength} characters.");

        RuleFor(req => req.RegionSizeMeters)
            .InclusiveBetween(MinRegionSizeMeters, MaxRegionSizeMeters)
            .WithMessage($"`regionSizeMeters` must be between {MinRegionSizeMeters} and {MaxRegionSizeMeters} meters.");

        RuleFor(req => req.ZoomLevel)
            .InclusiveBetween(MinZoom, MaxZoom)
            .WithMessage($"`zoomLevel` must be between {MinZoom} and {MaxZoom} (slippy-map range).");

        RuleFor(req => req.Points)
            .NotNull().WithMessage("`points` is required.")
            .Must(p => p is null || p.Count >= MinPoints)
                .WithMessage($"`points` must contain at least {MinPoints} entries.")
            .Must(p => p is null || p.Count <= MaxPoints)
                .WithMessage($"`points` must contain at most {MaxPoints} entries.");

        RuleForEach(req => req.Points)
            .SetValidator(new RoutePointValidator());

        // Geofences are optional; per-polygon rules apply only when present.
        // FluentValidation's default property-name policy drops the parent
        // chain on deep expressions like `req.Geofences!.Polygons` — it emits
        // only the leaf `polygons`. We OverridePropertyName explicitly so the
        // wire-format error keys match the JSON path callers actually post:
        // `errors["geofences.polygons"]` and `errors["geofences.polygons[i].…"]`.
        When(req => req.Geofences is not null, () =>
        {
            RuleFor(req => req.Geofences!.Polygons)
                .NotNull().WithMessage("`geofences.polygons` is required when `geofences` is present.")
                .NotEmpty().WithMessage("`geofences.polygons` must contain at least 1 polygon when `geofences` is present.")
                .Must(polygons => polygons is null || polygons.Count <= MaxPolygons)
                    .WithMessage($"`geofences.polygons` must contain at most {MaxPolygons} polygons.")
                .OverridePropertyName("geofences.polygons");

            RuleForEach(req => req.Geofences!.Polygons)
                .SetValidator(new GeofencePolygonValidator())
                .OverridePropertyName("geofences.polygons");
        });

        // Cross-field invariant: cannot zip what wasn't downloaded.
        RuleFor(req => req)
            .Must(req => !(req.CreateTilesZip && !req.RequestMaps))
            .WithName("createTilesZip")
            .WithMessage("`createTilesZip` requires `requestMaps` to be true (can't zip what wasn't downloaded).");
    }
}
