using ZB.MOM.WW.Auth.Abstractions.Ldap;
using ZB.MOM.WW.Configuration;
using ZB.MOM.WW.MxGateway.Contracts;

namespace ZB.MOM.WW.MxGateway.Server.Configuration;

public sealed class GatewayOptionsValidator : OptionsValidatorBase<GatewayOptions>
{
    private const int MinimumMaxMessageBytes = 1024;
    private const int MaximumMaxMessageBytes = 256 * 1024 * 1024;

    /// <summary>
    /// Validates gateway configuration options.
    /// </summary>
    /// <param name="builder">The accumulator to record failures on.</param>
    /// <param name="options">Gateway options to validate.</param>
    protected override void Validate(ValidationBuilder builder, GatewayOptions options)
    {
        ValidateAuthentication(options.Authentication, builder);
        ValidateLdap(options.Ldap, builder);
        ValidateWorker(options.Worker, builder);
        ValidateSessions(options.Sessions, builder);
        ValidateEvents(options.Events, builder);
        ValidateDashboard(options.Dashboard, builder);
        ValidateProtocol(options.Protocol, builder);
        ValidateAlarms(options.Alarms, builder);
        ValidateTls(options.Tls, builder);
    }

    private static void ValidateAuthentication(AuthenticationOptions options, ValidationBuilder builder)
    {
        if (!Enum.IsDefined(options.Mode))
        {
            builder.Add("MxGateway:Authentication:Mode must be a supported authentication mode.");
            return;
        }

        if (options.Mode == AuthenticationMode.ApiKey)
        {
            AddIfBlank(
                options.SqlitePath,
                "MxGateway:Authentication:SqlitePath is required when API-key authentication is enabled.",
                builder);
            AddIfInvalidPath(
                options.SqlitePath,
                "MxGateway:Authentication:SqlitePath must be a valid filesystem path.",
                builder);
            AddIfBlank(
                options.PepperSecretName,
                "MxGateway:Authentication:PepperSecretName is required when API-key authentication is enabled.",
                builder);
        }
    }

    private static void ValidateLdap(LdapOptions options, ValidationBuilder builder)
    {
        if (!options.Enabled)
        {
            return;
        }

        AddIfBlank(options.Server, "MxGateway:Ldap:Server is required when LDAP login is enabled.", builder);
        AddIfBlank(options.SearchBase, "MxGateway:Ldap:SearchBase is required when LDAP login is enabled.", builder);
        AddIfBlank(
            options.ServiceAccountDn,
            "MxGateway:Ldap:ServiceAccountDn is required when LDAP login is enabled.",
            builder);
        AddIfBlank(
            options.ServiceAccountPassword,
            "MxGateway:Ldap:ServiceAccountPassword is required when LDAP login is enabled.",
            builder);
        AddIfBlank(
            options.UserNameAttribute,
            "MxGateway:Ldap:UserNameAttribute is required when LDAP login is enabled.",
            builder);
        AddIfBlank(
            options.DisplayNameAttribute,
            "MxGateway:Ldap:DisplayNameAttribute is required when LDAP login is enabled.",
            builder);
        AddIfBlank(
            options.GroupAttribute,
            "MxGateway:Ldap:GroupAttribute is required when LDAP login is enabled.",
            builder);
        builder.Port(options.Port, "MxGateway:Ldap:Port");

        if (options.Transport == LdapTransport.None && !options.AllowInsecure)
        {
            builder.Add("MxGateway:Ldap:AllowInsecure must be true when Transport is None (plaintext).");
        }
    }

    private static void ValidateWorker(WorkerOptions options, ValidationBuilder builder)
    {
        AddIfBlank(options.ExecutablePath, "MxGateway:Worker:ExecutablePath is required.", builder);
        AddIfInvalidPath(
            options.ExecutablePath,
            "MxGateway:Worker:ExecutablePath must be a valid filesystem path.",
            builder);

        if (!string.IsNullOrWhiteSpace(options.ExecutablePath)
            && !string.Equals(Path.GetExtension(options.ExecutablePath), ".exe", StringComparison.OrdinalIgnoreCase))
        {
            builder.Add("MxGateway:Worker:ExecutablePath must point to a .exe file.");
        }

        if (!string.IsNullOrWhiteSpace(options.WorkingDirectory))
        {
            AddIfInvalidPath(
                options.WorkingDirectory,
                "MxGateway:Worker:WorkingDirectory must be a valid filesystem path.",
                builder);
        }

        if (!Enum.IsDefined(options.RequiredArchitecture))
        {
            builder.Add("MxGateway:Worker:RequiredArchitecture must be a supported worker architecture.");
        }

        AddIfNotPositive(
            options.StartupTimeoutSeconds,
            "MxGateway:Worker:StartupTimeoutSeconds must be greater than zero.",
            builder);
        AddIfNotPositive(
            options.StartupProbeRetryAttempts,
            "MxGateway:Worker:StartupProbeRetryAttempts must be greater than zero.",
            builder);
        AddIfNotPositive(
            options.StartupProbeRetryDelayMilliseconds,
            "MxGateway:Worker:StartupProbeRetryDelayMilliseconds must be greater than zero.",
            builder);
        AddIfNotPositive(
            options.PipeConnectAttemptTimeoutMilliseconds,
            "MxGateway:Worker:PipeConnectAttemptTimeoutMilliseconds must be greater than zero.",
            builder);
        AddIfNotPositive(
            options.ShutdownTimeoutSeconds,
            "MxGateway:Worker:ShutdownTimeoutSeconds must be greater than zero.",
            builder);
        AddIfNotPositive(
            options.HeartbeatIntervalSeconds,
            "MxGateway:Worker:HeartbeatIntervalSeconds must be greater than zero.",
            builder);
        AddIfNotPositive(
            options.HeartbeatGraceSeconds,
            "MxGateway:Worker:HeartbeatGraceSeconds must be greater than zero.",
            builder);

        if (options.HeartbeatGraceSeconds < options.HeartbeatIntervalSeconds)
        {
            builder.Add(
                "MxGateway:Worker:HeartbeatGraceSeconds must be greater than or equal to HeartbeatIntervalSeconds.");
        }

        if (options.MaxMessageBytes is < MinimumMaxMessageBytes or > MaximumMaxMessageBytes)
        {
            builder.Add(
                $"MxGateway:Worker:MaxMessageBytes must be between {MinimumMaxMessageBytes} and {MaximumMaxMessageBytes}.");
        }
    }

    private static void ValidateSessions(SessionOptions options, ValidationBuilder builder)
    {
        AddIfNotPositive(
            options.DefaultCommandTimeoutSeconds,
            "MxGateway:Sessions:DefaultCommandTimeoutSeconds must be greater than zero.",
            builder);
        AddIfNotPositive(options.MaxSessions, "MxGateway:Sessions:MaxSessions must be greater than zero.", builder);
        AddIfNotPositive(
            options.MaxPendingCommandsPerSession,
            "MxGateway:Sessions:MaxPendingCommandsPerSession must be greater than zero.",
            builder);
        AddIfNotPositive(
            options.DefaultLeaseSeconds,
            "MxGateway:Sessions:DefaultLeaseSeconds must be greater than zero.",
            builder);
        AddIfNotPositive(
            options.LeaseSweepIntervalSeconds,
            "MxGateway:Sessions:LeaseSweepIntervalSeconds must be greater than zero.",
            builder);

        if (options.AllowMultipleEventSubscribers)
        {
            builder.Add(
                "MxGateway:Sessions:AllowMultipleEventSubscribers is not supported until event fan-out is implemented.");
        }
    }

    private static void ValidateEvents(EventOptions options, ValidationBuilder builder)
    {
        AddIfNotPositive(options.QueueCapacity, "MxGateway:Events:QueueCapacity must be greater than zero.", builder);

        if (!Enum.IsDefined(options.BackpressurePolicy))
        {
            builder.Add("MxGateway:Events:BackpressurePolicy must be a supported backpressure policy.");
        }
    }

    private static void ValidateDashboard(DashboardOptions options, ValidationBuilder builder)
    {
        // GroupToRole shape is validated even when the dashboard is disabled so
        // misconfiguration surfaces at startup; emptiness is allowed, with the
        // consequence that no LDAP user can sign in (login returns "no roles
        // mapped"). Operators who disable the dashboard or want a closed
        // deployment can ship without a mapping.
        foreach (KeyValuePair<string, string> entry in options.GroupToRole)
        {
            if (string.IsNullOrWhiteSpace(entry.Key))
            {
                builder.Add("MxGateway:Dashboard:GroupToRole keys (LDAP group names) must be non-blank.");
            }

            if (!string.Equals(entry.Value, Dashboard.DashboardRoles.Admin, StringComparison.Ordinal)
                && !string.Equals(entry.Value, Dashboard.DashboardRoles.Viewer, StringComparison.Ordinal))
            {
                builder.Add(
                    $"MxGateway:Dashboard:GroupToRole['{entry.Key}'] must be '{Dashboard.DashboardRoles.Admin}' or '{Dashboard.DashboardRoles.Viewer}'.");
            }
        }

        AddIfNotPositive(
            options.SnapshotIntervalMilliseconds,
            "MxGateway:Dashboard:SnapshotIntervalMilliseconds must be greater than zero.",
            builder);
        AddIfNegative(
            options.RecentFaultLimit,
            "MxGateway:Dashboard:RecentFaultLimit must be greater than or equal to zero.",
            builder);
        AddIfNegative(
            options.RecentSessionLimit,
            "MxGateway:Dashboard:RecentSessionLimit must be greater than or equal to zero.",
            builder);
    }

    private static readonly string[] ValidAlarmFallbackModes = ["Auto", "ForceAlarmManager", "ForceSubtag"];

    private static void ValidateAlarms(AlarmsOptions options, ValidationBuilder builder)
    {
        if (!options.Enabled)
        {
            return;
        }

        // When the central alarm monitor is enabled, it needs either a canonical
        // SubscriptionExpression or a DefaultArea to compose one from. Validating
        // it at startup makes the misconfiguration fail-fast at boot, in line
        // with every other section.
        if (string.IsNullOrWhiteSpace(options.SubscriptionExpression)
            && string.IsNullOrWhiteSpace(options.DefaultArea))
        {
            builder.Add(
                "MxGateway:Alarms requires either a non-blank SubscriptionExpression or a non-blank DefaultArea when Enabled is true.");
        }

        if (!string.IsNullOrWhiteSpace(options.SubscriptionExpression)
            && !options.SubscriptionExpression.StartsWith(@"\\", StringComparison.Ordinal))
        {
            builder.Add(
                @"MxGateway:Alarms:SubscriptionExpression must start with '\\' (canonical \\<host>\Galaxy!<area> shape).");
        }

        ValidateAlarmFallback(options.Fallback, builder);
    }

    private static void ValidateAlarmFallback(AlarmFallbackOptions fallback, ValidationBuilder builder)
    {
        // Validate Mode is one of the recognised values (case-insensitive).
        bool modeValid = Array.Exists(
            ValidAlarmFallbackModes,
            m => string.Equals(m, fallback.Mode, StringComparison.OrdinalIgnoreCase));

        if (!modeValid)
        {
            builder.Add(
                $"MxGateway:Alarms:Fallback:Mode must be one of: {string.Join(", ", ValidAlarmFallbackModes)} (was '{fallback.Mode}').");
        }

        // ForceSubtag requires either Galaxy Repository discovery or an explicit IncludeAttributes list.
        if (modeValid
            && string.Equals(fallback.Mode, "ForceSubtag", StringComparison.OrdinalIgnoreCase)
            && !fallback.Discovery.UseGalaxyRepository
            && fallback.Discovery.IncludeAttributes.Length == 0)
        {
            builder.Add(
                "MxGateway:Alarms:Fallback ForceSubtag requires Galaxy Repository discovery or a non-empty Discovery:IncludeAttributes list.");
        }

        // Floor validation: numeric thresholds must be at least 1.
        AddIfNotPositive(
            fallback.ConsecutiveFailureThreshold,
            "MxGateway:Alarms:Fallback:ConsecutiveFailureThreshold must be greater than zero.",
            builder);
        AddIfNotPositive(
            fallback.FailbackProbeIntervalSeconds,
            "MxGateway:Alarms:Fallback:FailbackProbeIntervalSeconds must be greater than zero.",
            builder);
        AddIfNotPositive(
            fallback.FailbackStableProbes,
            "MxGateway:Alarms:Fallback:FailbackStableProbes must be greater than zero.",
            builder);
    }

    private const int MinimumCertValidityYears = 1;
    private const int MaximumCertValidityYears = 100;

    private static void ValidateTls(TlsOptions options, ValidationBuilder builder)
    {
        if (options.ValidityYears is < MinimumCertValidityYears or > MaximumCertValidityYears)
        {
            builder.Add(
                $"MxGateway:Tls:ValidityYears must be between {MinimumCertValidityYears} and {MaximumCertValidityYears}.");
        }

        // The default is non-blank, so this only catches an explicitly-blanked path.
        AddIfBlank(
            options.SelfSignedCertPath,
            "MxGateway:Tls:SelfSignedCertPath must not be blank.",
            builder);
        AddIfInvalidPath(
            options.SelfSignedCertPath,
            "MxGateway:Tls:SelfSignedCertPath must be a valid filesystem path.",
            builder);

        foreach (string dns in options.AdditionalDnsNames)
        {
            if (string.IsNullOrWhiteSpace(dns))
            {
                builder.Add("MxGateway:Tls:AdditionalDnsNames entries must be non-blank.");
            }
        }
    }

    private static void ValidateProtocol(ProtocolOptions options, ValidationBuilder builder)
    {
        if (options.WorkerProtocolVersion != GatewayContractInfo.WorkerProtocolVersion)
        {
            builder.Add(
                $"MxGateway:Protocol:WorkerProtocolVersion must be {GatewayContractInfo.WorkerProtocolVersion}.");
        }

        if (options.MaxGrpcMessageBytes is < MinimumMaxMessageBytes or > MaximumMaxMessageBytes)
        {
            builder.Add(
                $"MxGateway:Protocol:MaxGrpcMessageBytes must be between {MinimumMaxMessageBytes} and {MaximumMaxMessageBytes}.");
        }
    }

    private static void AddIfBlank(string? value, string message, ValidationBuilder builder)
    {
        builder.RequireThat(!string.IsNullOrWhiteSpace(value), message);
    }

    private static void AddIfNotPositive(int value, string message, ValidationBuilder builder)
    {
        builder.RequireThat(value > 0, message);
    }

    private static void AddIfNegative(int value, string message, ValidationBuilder builder)
    {
        builder.RequireThat(value >= 0, message);
    }

    private static void AddIfInvalidPath(string? value, string message, ValidationBuilder builder)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return;
        }

        try
        {
            _ = Path.GetFullPath(value);
        }
        catch (ArgumentException)
        {
            builder.Add(message);
        }
        catch (NotSupportedException)
        {
            builder.Add(message);
        }
        catch (PathTooLongException)
        {
            builder.Add(message);
        }
        catch (IOException)
        {
            builder.Add(message);
        }
    }
}
