using ZB.MOM.WW.Configuration;

namespace ZB.MOM.WW.ScadaBridge.Host;

/// <summary>
/// Validates required configuration before Akka.NET actor system creation.
/// Runs early in startup to fail fast with clear error messages.
/// </summary>
public static class StartupValidator
{
    /// <summary>Validates required configuration values and throws <see cref="InvalidOperationException"/> listing all errors if any are found.</summary>
    /// <param name="configuration">The application configuration to validate.</param>
    public static void Validate(IConfiguration configuration)
    {
        // Resolve the same locals the original imperative validator used, so the
        // cross-field predicates below can close over them. ConfigPreflight.Require
        // passes config[key] to each predicate, but the cross-field rules ignore that
        // argument and read these resolved values instead — preserving the exact
        // conditions (and therefore the byte-identical failure messages and ordering)
        // of the original StartupValidator.
        var nodeSection = configuration.GetSection("ScadaBridge:Node");
        var role = nodeSection["Role"];

        var portStr = nodeSection["RemotingPort"];
        bool portValid = int.TryParse(portStr, out var port) && port >= 1 && port <= 65535;

        var seedNodes = configuration.GetSection("ScadaBridge:Cluster:SeedNodes").Get<List<string>>();

        // GrpcPort: default 8083 when absent; only fails the range rule when the key is
        // present AND invalid. The out-param assignment mirrors the original so the
        // resolved grpcPort feeds the cross-field rules even on a parse failure.
        var grpcPortStr = nodeSection["GrpcPort"];
        int grpcPort = 8083; // NodeOptions default when the key is absent
        bool grpcValid = !(grpcPortStr != null && (!int.TryParse(grpcPortStr, out grpcPort) || grpcPort < 1 || grpcPort > 65535));

        // MetricsPort: default 8084 when absent; same parse-or-default contract as GrpcPort.
        var metricsPortStr = nodeSection["MetricsPort"];
        int metricsPort = 8084; // NodeOptions default when the key is absent
        bool metricsValid = !(metricsPortStr != null && (!int.TryParse(metricsPortStr, out metricsPort) || metricsPort < 1 || metricsPort > 65535));

        ConfigPreflight.For(configuration)
            // Role / NodeHostname / RemotingPort (unconditional)
            .Require("ScadaBridge:Node:Role",
                _ => !(string.IsNullOrEmpty(role) || (role != "Central" && role != "Site")),
                "must be 'Central' or 'Site'")
            .Require("ScadaBridge:Node:NodeHostname",
                _ => !string.IsNullOrEmpty(nodeSection["NodeHostname"]),
                "is required")
            .Require("ScadaBridge:Node:RemotingPort",
                _ => portValid,
                "must be 1-65535")
            // SiteId (Site only) — note: OUTSIDE the big Site block in the original,
            // so it must run before the unconditional SeedNodes-count rule.
            .When(role == "Site", p => p
                .Require("ScadaBridge:Node:SiteId",
                    _ => !string.IsNullOrEmpty(nodeSection["SiteId"]),
                    "is required for Site nodes"))
            // Central-only database/security rules.
            .When(role == "Central", p => p
                .Require("ScadaBridge:Database:ConfigurationDb",
                    _ => !string.IsNullOrEmpty(configuration.GetSection("ScadaBridge:Database")["ConfigurationDb"]),
                    "connection string required for Central")
                // Task 1.4: the LDAP server key moved into the nested Security:Ldap
                // sub-section (bound to the shared LdapOptions). Validate the nested key so
                // the pre-host preflight still fails fast on a missing LDAP server for
                // Central. The full LDAP option set (SearchBase / ServiceAccountDn /
                // transport) is additionally validated post-host by the shared
                // LdapOptionsValidator (registered with ValidateOnStart by AddZbLdapAuth).
                .Require("ScadaBridge:Security:Ldap:Server",
                    _ => !string.IsNullOrEmpty(configuration.GetSection("ScadaBridge:Security:Ldap")["Server"]),
                    "required for Central")
                .Require("ScadaBridge:Security:JwtSigningKey",
                    _ => !string.IsNullOrEmpty(configuration.GetSection("ScadaBridge:Security")["JwtSigningKey"]),
                    "required for Central")
                // Review #4 (fail-fast pepper validation): the inbound API-key pepper
                // backs the peppered-HMAC secret compare in the shared
                // ZB.MOM.WW.Auth.ApiKeys verifier (wired by AddZbApiKeyAuth at the
                // Central composition root). A missing or too-short pepper does not
                // fault at boot — the verifier just fails every secret compare, so the
                // inbound API silently serves 401s to otherwise-valid keys. Validate it
                // here (Central-only, pre-host) so a misconfigured pepper fails fast at
                // startup with a clear message instead of as a runtime auth blackout.
                // The Require predicate receives config[key] directly; the >=16-char
                // floor matches the test pepper's minimum and the secret-strength
                // baseline used elsewhere.
                .Require("ScadaBridge:InboundApi:ApiKeyPepper",
                    value => !string.IsNullOrEmpty(value) && value.Length >= 16,
                    "is required and must be at least 16 characters for Central (backs the inbound API-key peppered-HMAC verifier)"))
            // SeedNodes count (unconditional, after SiteId).
            .Require("ScadaBridge:Cluster:SeedNodes",
                _ => seedNodes != null && seedNodes.Count >= 2,
                "must have at least 2 entries")
            // The big Site-only block: GrpcPort/MetricsPort validity + cross-field
            // collisions + SiteDbPath + seed-node-port loop, in the original order.
            .When(role == "Site", p =>
            {
                // Host-007 / REQ-HOST-4: GrpcPort range, then GrpcPort vs RemotingPort.
                p.Require("ScadaBridge:Node:GrpcPort", _ => grpcValid, "must be 1-65535");
                // Identical GrpcPort/RemotingPort make Kestrel and Akka.Remote contend
                // for the same TCP port. Uses the resolved GrpcPort, including 8083.
                p.Require("ScadaBridge:Node:GrpcPort", _ => port != grpcPort, "must differ from RemotingPort");

                // Host-007 / REQ-HOST-4: MetricsPort range, then MetricsPort vs both ports.
                p.Require("ScadaBridge:Node:MetricsPort", _ => metricsValid, "must be 1-65535");
                // The Kestrel metrics (HTTP/1.1) listener port must differ from BOTH the
                // Akka remoting port and the gRPC port. Uses the resolved MetricsPort (8084 default).
                p.Require("ScadaBridge:Node:MetricsPort", _ => metricsPort != port, "must differ from RemotingPort");
                p.Require("ScadaBridge:Node:MetricsPort", _ => metricsPort != grpcPort, "must differ from GrpcPort");

                p.Require("ScadaBridge:Database:SiteDbPath",
                    _ => !string.IsNullOrEmpty(configuration.GetSection("ScadaBridge:Database")["SiteDbPath"]),
                    "required for Site nodes");

                // Host-004: a seed node must reference an Akka.Remote endpoint, never the
                // Kestrel HTTP/2 gRPC port. A seed entry whose port equals this node's
                // GrpcPort would make a joining node attempt an Akka.Remote TCP
                // association against the gRPC listener and fail.
                foreach (var seed in seedNodes ?? Enumerable.Empty<string>())
                {
                    p.Require("ScadaBridge:Cluster:SeedNodes",
                        _ => SeedNodePort(seed) != grpcPort,
                        $"entry '{seed}' must not target the gRPC port " +
                        $"({grpcPort}); seed nodes must reference Akka remoting ports");
                }
            })
            .ThrowIfInvalid();
    }

    /// <summary>
    /// Extracts the TCP port from an Akka seed-node address of the form
    /// <c>akka.tcp://system@host:port</c>. Returns <c>-1</c> when no port can be parsed.
    /// </summary>
    private static int SeedNodePort(string seedNode)
    {
        if (string.IsNullOrWhiteSpace(seedNode))
            return -1;

        var lastColon = seedNode.LastIndexOf(':');
        if (lastColon < 0 || lastColon == seedNode.Length - 1)
            return -1;

        return int.TryParse(seedNode[(lastColon + 1)..], out var port) ? port : -1;
    }
}
