using System.Globalization;

namespace ZB.MOM.WW.Configuration;

/// <summary>
/// Internal rule primitives shared by <see cref="ValidationBuilder"/> (validates a bound options
/// object) and <see cref="ConfigPreflight"/> (validates raw <c>IConfiguration</c>). Each method
/// returns <c>null</c> when valid, or a formatted <c>"&lt;field&gt; &lt;reason&gt;"</c> message
/// otherwise. Centralizing them keeps wording identical across both front-ends.
/// </summary>
internal static class Checks
{
    internal static string? Required(string? value, string field) =>
        string.IsNullOrWhiteSpace(value) ? $"{field} is required" : null;

    internal static string? Port(int value, string field) =>
        value is < 1 or > 65535 ? $"{field} must be between 1 and 65535 (was {value})" : null;

    /// <summary>
    /// Validates a raw string as a TCP port (parse + range), returning <c>null</c> when valid.
    /// Centralizes the port wording for callers that hold the raw config value. Parsing is strict
    /// and culture-invariant (<see cref="NumberStyles.None"/>): a leading sign or surrounding
    /// whitespace is rejected. Both the parse-failure and range-failure messages quote the offending
    /// raw value so they read consistently.
    /// </summary>
    internal static string? PortValue(string? raw, string field) =>
        int.TryParse(raw, NumberStyles.None, CultureInfo.InvariantCulture, out var port) && port is >= 1 and <= 65535
            ? null
            : $"{field} must be between 1 and 65535 (was '{raw ?? "null"}')";

    /// <summary>
    /// Validates a non-bracketed <c>host:port</c> endpoint (port 1-65535). Bracketed IPv6
    /// literals (<c>[::1]:port</c>) are out of scope and are rejected.
    /// </summary>
    internal static string? HostPort(string? value, string field)
    {
        if (string.IsNullOrWhiteSpace(value)) return $"{field} is required";
        var idx = value.LastIndexOf(':');
        if (idx <= 0 || idx == value.Length - 1
            || value.AsSpan(0, idx).Contains(':')
            || !int.TryParse(value[(idx + 1)..], NumberStyles.None, CultureInfo.InvariantCulture, out var port)
            || port is < 1 or > 65535)
            return $"{field} must be 'host:port' with port 1-65535 (was '{value}')";
        return null;
    }

    internal static string? PositiveTimeSpan(TimeSpan value, string field) =>
        value <= TimeSpan.Zero ? $"{field} must be a positive duration (was {value})" : null;

    internal static string? OneOf(string? value, IReadOnlyCollection<string> allowed, string field) =>
        value is not null && allowed.Contains(value, StringComparer.OrdinalIgnoreCase)
            ? null
            : $"{field} must be one of [{string.Join(", ", allowed)}] (was '{value ?? "null"}')";

    internal static string? MinCount<T>(IReadOnlyCollection<T>? value, int min, string field) =>
        value is null || value.Count < min
            ? $"{field} must contain at least {min} item(s) (had {value?.Count ?? 0})"
            : null;
}
