using ZB.MOM.WW.Configuration;

namespace ZB.MOM.WW.Overview.Registry;

/// <summary>
/// Validates the <c>Overview</c> registry. Registered through
/// <c>AddValidatedOptions&lt;OverviewOptions, OverviewOptionsValidator&gt;</c>, so a malformed
/// registry fails the host at startup rather than surfacing as a silently-empty dashboard or a
/// first-poll exception.
/// </summary>
public sealed class OverviewOptionsValidator : OptionsValidatorBase<OverviewOptions>
{
    /// <inheritdoc />
    protected override void Validate(ValidationBuilder builder, OverviewOptions options)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(options);

        RequirePositive(builder, options.PollIntervalSeconds, "Overview:PollIntervalSeconds");
        RequirePositive(builder, options.TimeoutSeconds, "Overview:TimeoutSeconds");
        RequirePositive(builder, options.StaleAfterSeconds, "Overview:StaleAfterSeconds");

        builder.MinCount(options.Applications as IReadOnlyCollection<ApplicationEntry>, 1, "Overview:Applications");

        var seenApplications = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        for (var a = 0; a < options.Applications.Count; a++)
        {
            var application = options.Applications[a];
            var appPath = $"Overview:Applications:{a}";

            builder.Required(application.Name, $"{appPath}:Name");
            if (!string.IsNullOrWhiteSpace(application.Name) && !seenApplications.Add(application.Name))
                builder.Add($"{appPath}:Name '{application.Name}' is duplicated; application names must be unique");

            RequirePositiveIfSet(builder, application.PollIntervalSeconds, $"{appPath}:PollIntervalSeconds");
            RequirePositiveIfSet(builder, application.TimeoutSeconds, $"{appPath}:TimeoutSeconds");
            RequirePositiveIfSet(builder, application.StaleAfterSeconds, $"{appPath}:StaleAfterSeconds");

            builder.MinCount(
                application.Instances as IReadOnlyCollection<InstanceEntry>, 1, $"{appPath}:Instances");

            var seenInstances = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

            for (var i = 0; i < application.Instances.Count; i++)
            {
                var instance = application.Instances[i];
                var path = $"{appPath}:Instances:{i}";

                builder.Required(instance.Name, $"{path}:Name");
                if (!string.IsNullOrWhiteSpace(instance.Name) && !seenInstances.Add(instance.Name))
                {
                    builder.Add(
                        $"{path}:Name '{instance.Name}' is duplicated within application "
                        + $"'{application.Name}'; instance names must be unique per application");
                }

                RequireAbsoluteHttpUrl(builder, instance.BaseUrl, $"{path}:BaseUrl", required: true);
                RequireAbsoluteHttpUrl(builder, instance.ManagementUrl, $"{path}:ManagementUrl", required: false);

                RequirePositiveIfSet(builder, instance.PollIntervalSeconds, $"{path}:PollIntervalSeconds");
                RequirePositiveIfSet(builder, instance.TimeoutSeconds, $"{path}:TimeoutSeconds");
                RequirePositiveIfSet(builder, instance.StaleAfterSeconds, $"{path}:StaleAfterSeconds");

                // Checked on the EFFECTIVE values, not the globals: an override at either level can
                // invert the relationship on one instance while the global pair stays sane, and an
                // instance whose stale window is inside its poll interval would render Stale
                // between two perfectly healthy polls.
                var timings = EffectiveTimings.Resolve(options, application, instance);
                if (timings.StaleAfter <= timings.PollInterval)
                {
                    builder.Add(
                        $"{path}: effective StaleAfterSeconds ({timings.StaleAfter.TotalSeconds:0.##}) must be "
                        + $"greater than effective PollIntervalSeconds ({timings.PollInterval.TotalSeconds:0.##}) "
                        + "— otherwise this instance renders Stale between two healthy polls");
                }
            }
        }
    }

    private static void RequirePositive(ValidationBuilder builder, int value, string field) =>
        builder.RequireThat(value > 0, $"{field} must be greater than 0");

    private static void RequirePositiveIfSet(ValidationBuilder builder, int? value, string field)
    {
        if (value is { } set)
            RequirePositive(builder, set, field);
    }

    private static void RequireAbsoluteHttpUrl(
        ValidationBuilder builder, string? value, string field, bool required)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            if (required)
                builder.Add($"{field} is required");
            return;
        }

        // Absolute + http(s) explicitly: a relative URL binds without complaint and only fails at
        // the first poll, as an opaque HttpClient error against a dashboard that has no base
        // address of its own.
        if (!Uri.TryCreate(value, UriKind.Absolute, out var uri)
            || (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
        {
            builder.Add($"{field} must be an absolute http:// or https:// URL (was '{value}')");
        }
    }
}
