using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;

namespace ZB.MOM.WW.OtOpcUa.Cluster;

/// <summary>
///     Fails the host at startup on a <see cref="ConfigSourceOptions"/> shape that would leave a
///     driver node unable to fetch its configuration.
/// </summary>
/// <remarks>
///     Like <see cref="MeshTransportOptionsValidator"/>, every fault caught here otherwise surfaces as
///     an <i>absence</i> — a deployment that never applies because the node has no endpoint to fetch
///     from or no key to authenticate with. An absence has no stack trace and no failing node to point
///     at; refusing to start is cheaper to diagnose.
/// </remarks>
public sealed class ConfigSourceOptionsValidator : IValidateOptions<ConfigSourceOptions>
{
    private readonly IConfiguration _configuration;

    /// <summary>
    ///     DI-constructed by <c>AddValidatedOptions</c> (a plain <c>AddSingleton</c>), so a
    ///     constructor dependency is safe here. <see cref="IConfiguration"/> — not
    ///     <c>IClusterRoleInfo</c> — is the source of this node's roles: <c>IClusterRoleInfo</c>'s
    ///     implementation needs the <c>ActorSystem</c>, which does not exist yet at
    ///     <c>ValidateOnStart</c> time.
    /// </summary>
    public ConfigSourceOptionsValidator(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    /// <inheritdoc />
    public ValidateOptionsResult Validate(string? name, ConfigSourceOptions options)
    {
        ArgumentNullException.ThrowIfNull(options);

        var errors = new List<string>();

        var isDirect = string.Equals(options.Mode, ConfigSourceOptions.ModeDirect, StringComparison.OrdinalIgnoreCase);
        var isFetch = string.Equals(
            options.Mode, ConfigSourceOptions.ModeFetchAndCache, StringComparison.OrdinalIgnoreCase);

        if (!isDirect && !isFetch)
        {
            errors.Add(
                $"{ConfigSourceOptions.SectionName}:{nameof(ConfigSourceOptions.Mode)} is "
                + $"'{options.Mode}'. Expected '{ConfigSourceOptions.ModeDirect}' or "
                + $"'{ConfigSourceOptions.ModeFetchAndCache}'.");
        }

        // The fetch surface is only load-bearing under FetchAndCache. Requiring it under the default
        // would make the section mandatory everywhere for a feature that is switched off — including on
        // admin-only nodes, which never fetch.
        if (isFetch)
        {
            if (options.CentralFetchEndpoints.Length == 0)
            {
                errors.Add(
                    $"{ConfigSourceOptions.SectionName}:{nameof(ConfigSourceOptions.CentralFetchEndpoints)} "
                    + "is empty. Under FetchAndCache a driver node cannot reach central to fetch its "
                    + "configuration, and every deployment would time out naming a node that is running "
                    + "perfectly.");
            }

            foreach (var endpoint in options.CentralFetchEndpoints)
            {
                if (!endpoint.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
                    && !endpoint.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
                {
                    errors.Add(
                        $"Central fetch endpoint '{endpoint}' must start with 'http://' (h2c) or "
                        + "'https://' — it is a gRPC base address (scheme://host:port), not a bare "
                        + "host:port.");
                }
            }

            if (string.IsNullOrEmpty(options.ApiKey))
            {
                errors.Add(
                    $"{ConfigSourceOptions.SectionName}:{nameof(ConfigSourceOptions.ApiKey)} is empty. "
                    + "Under FetchAndCache the shared bearer key is the whole authentication boundary to "
                    + "central's artifact service; without it every fetch is rejected. Supply it via the "
                    + "environment (ConfigSource__ApiKey).");
            }

            if (options.FetchTimeoutSeconds <= 0)
            {
                errors.Add(
                    $"{ConfigSourceOptions.SectionName}:{nameof(ConfigSourceOptions.FetchTimeoutSeconds)} "
                    + $"must be greater than 0 (was {options.FetchTimeoutSeconds}).");
            }
        }

        // Per-cluster mesh Phase 4: a driver-only node has no central ConfigDb connection to fall back
        // on, so Direct is only ever valid on a fused admin+driver node (admin holds the SQL
        // connection). Roles come from Cluster:Roles directly, not IClusterRoleInfo — see the ctor
        // remark.
        var roles = _configuration.GetSection("Cluster:Roles").Get<string[]>() ?? Array.Empty<string>();
        var isDriver = Array.IndexOf(roles, "driver") >= 0;
        var isAdmin = Array.IndexOf(roles, "admin") >= 0;

        if (isDriver && !isAdmin && isDirect)
        {
            errors.Add(
                "Cluster:Roles is a driver-only node (has 'driver', not 'admin') but "
                + $"{ConfigSourceOptions.SectionName}:{nameof(ConfigSourceOptions.Mode)} is 'Direct'. A "
                + "driver-only node has no central ConfigDb to read from — it must use 'FetchAndCache'. "
                + $"Set {ConfigSourceOptions.SectionName}:{nameof(ConfigSourceOptions.Mode)}="
                + $"{ConfigSourceOptions.ModeFetchAndCache} (see docs/Configuration.md → ConfigSource).");
        }

        return errors.Count == 0
            ? ValidateOptionsResult.Success
            : ValidateOptionsResult.Fail(string.Join(" ", errors));
    }
}
