using FluentValidation;
using SatelliteProvider.Common.DTO;

namespace SatelliteProvider.Api.Validators;

// AZ-796: FluentValidation rules for POST /api/satellite/tiles/inventory.
// Wired through ValidationEndpointFilter<TileInventoryRequest> at endpoint
// registration time (`WithValidation<TileInventoryRequest>()` 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 (rules 5+) is partially handled at the deserializer
// level via `[JsonRequired]` on TileCoord.Z/X/Y plus
// `JsonSerializerOptions.UnmappedMemberHandling.Disallow` (AZ-795). This
// validator covers the non-deserializer-detectable rules: XOR populated,
// per-array entry caps, and slippy-map range constraints.
public sealed class InventoryRequestValidator : AbstractValidator<TileInventoryRequest>
{
    public InventoryRequestValidator()
    {
        RuleFor(req => req).Custom((req, ctx) =>
        {
            var hasTiles = req.Tiles is { Count: > 0 };
            var hasHashes = req.LocationHashes is { Count: > 0 };
            if (hasTiles == hasHashes)
            {
                ctx.AddFailure(
                    "$",
                    "Populate exactly one of `tiles` or `locationHashes` (sending both, or neither, is not allowed).");
            }
        });

        RuleFor(req => req.Tiles!.Count)
            .LessThanOrEqualTo(TileInventoryLimits.MaxEntriesPerRequest)
            .OverridePropertyName("tiles")
            .WithMessage($"`tiles` must contain at most {TileInventoryLimits.MaxEntriesPerRequest} entries.")
            .When(req => req.Tiles is not null);

        RuleFor(req => req.LocationHashes!.Count)
            .LessThanOrEqualTo(TileInventoryLimits.MaxEntriesPerRequest)
            .OverridePropertyName("locationHashes")
            .WithMessage($"`locationHashes` must contain at most {TileInventoryLimits.MaxEntriesPerRequest} entries.")
            .When(req => req.LocationHashes is not null);

        RuleForEach(req => req.Tiles)
            .SetValidator(new TileCoordValidator())
            .When(req => req.Tiles is not null);
    }
}

internal sealed class TileCoordValidator : AbstractValidator<TileCoord>
{
    private const int MaxZoom = 22;

    public TileCoordValidator()
    {
        RuleFor(c => c.Z)
            .InclusiveBetween(0, MaxZoom)
            .WithMessage($"`z` must be between 0 and {MaxZoom} (slippy-map zoom range).");

        RuleFor(c => c.X)
            .GreaterThanOrEqualTo(0)
            .WithMessage("`x` must be ≥ 0.")
            .Must((coord, x) => coord.Z >= 0 && coord.Z <= MaxZoom && x < (1L << coord.Z))
            .WithMessage(coord => $"`x` must be < 2^z = {(coord.Z >= 0 && coord.Z <= MaxZoom ? (1L << coord.Z).ToString() : "<invalid z>")} for z={coord.Z}.");

        RuleFor(c => c.Y)
            .GreaterThanOrEqualTo(0)
            .WithMessage("`y` must be ≥ 0.")
            .Must((coord, y) => coord.Z >= 0 && coord.Z <= MaxZoom && y < (1L << coord.Z))
            .WithMessage(coord => $"`y` must be < 2^z = {(coord.Z >= 0 && coord.Z <= MaxZoom ? (1L << coord.Z).ToString() : "<invalid z>")} for z={coord.Z}.");
    }
}
