// Port of Go server/opts.go processConfigFileLine — maps parsed config dictionaries
// to NatsOptions. Reference: golang/nats-server/server/opts.go lines 1050-1400.

using System.Globalization;
using System.Text.RegularExpressions;
using NATS.Server.Auth;

namespace NATS.Server.Configuration;

/// <summary>
/// Maps a parsed NATS configuration dictionary (produced by <see cref="NatsConfParser"/>)
/// into a fully populated <see cref="NatsOptions"/> instance. Collects all validation
/// errors rather than failing on the first one.
/// </summary>
public static class ConfigProcessor
{
    /// <summary>
    /// Parses a configuration file and returns the populated options.
    /// </summary>
    public static NatsOptions ProcessConfigFile(string filePath)
    {
        var config = NatsConfParser.ParseFile(filePath);
        var opts = new NatsOptions { ConfigFile = filePath };
        ApplyConfig(config, opts);
        return opts;
    }

    /// <summary>
    /// Parses configuration text (not from a file) and returns the populated options.
    /// </summary>
    public static NatsOptions ProcessConfig(string configText)
    {
        var config = NatsConfParser.Parse(configText);
        var opts = new NatsOptions();
        ApplyConfig(config, opts);
        return opts;
    }

    /// <summary>
    /// Applies a parsed configuration dictionary to existing options.
    /// Throws <see cref="ConfigProcessorException"/> if any validation errors are collected.
    /// </summary>
    public static void ApplyConfig(Dictionary<string, object?> config, NatsOptions opts)
    {
        var errors = new List<string>();

        foreach (var (key, value) in config)
        {
            try
            {
                ProcessKey(key, value, opts, errors);
            }
            catch (Exception ex)
            {
                errors.Add($"Error processing '{key}': {ex.Message}");
            }
        }

        if (errors.Count > 0)
        {
            throw new ConfigProcessorException("Configuration errors", errors);
        }
    }

    private static void ProcessKey(string key, object? value, NatsOptions opts, List<string> errors)
    {
        // Keys are already case-insensitive from the parser (OrdinalIgnoreCase dictionaries),
        // but we normalize here for the switch statement.
        switch (key.ToLowerInvariant())
        {
            case "listen":
                ParseListen(value, opts);
                break;
            case "port":
                opts.Port = ToInt(value);
                break;
            case "host" or "net":
                opts.Host = ToString(value);
                break;
            case "server_name":
                var name = ToString(value);
                if (name.Contains(' '))
                    errors.Add("server_name cannot contain spaces");
                else
                    opts.ServerName = name;
                break;
            case "client_advertise":
                opts.ClientAdvertise = ToString(value);
                break;

            // Logging
            case "debug":
                opts.Debug = ToBool(value);
                break;
            case "trace":
                opts.Trace = ToBool(value);
                break;
            case "trace_verbose":
                opts.TraceVerbose = ToBool(value);
                if (opts.TraceVerbose)
                    opts.Trace = true;
                break;
            case "logtime":
                opts.Logtime = ToBool(value);
                break;
            case "logtime_utc":
                opts.LogtimeUTC = ToBool(value);
                break;
            case "logfile" or "log_file":
                opts.LogFile = ToString(value);
                break;
            case "log_size_limit":
                opts.LogSizeLimit = ToLong(value);
                break;
            case "log_max_num":
                opts.LogMaxFiles = ToInt(value);
                break;
            case "syslog":
                opts.Syslog = ToBool(value);
                break;
            case "remote_syslog":
                opts.RemoteSyslog = ToString(value);
                break;

            // Limits
            case "max_payload":
                opts.MaxPayload = ToInt(value);
                break;
            case "max_control_line":
                opts.MaxControlLine = ToInt(value);
                break;
            case "max_connections" or "max_conn":
                opts.MaxConnections = ToInt(value);
                break;
            case "max_pending":
                opts.MaxPending = ToLong(value);
                break;
            case "max_subs" or "max_subscriptions":
                opts.MaxSubs = ToInt(value);
                break;
            case "max_sub_tokens" or "max_subscription_tokens":
                var tokens = ToInt(value);
                if (tokens > 256)
                    errors.Add("max_sub_tokens cannot exceed 256");
                else
                    opts.MaxSubTokens = tokens;
                break;
            case "max_traced_msg_len":
                opts.MaxTracedMsgLen = ToInt(value);
                break;
            case "max_closed_clients":
                opts.MaxClosedClients = ToInt(value);
                break;
            case "disable_sublist_cache" or "no_sublist_cache":
                opts.DisableSublistCache = ToBool(value);
                break;
            case "write_deadline":
                opts.WriteDeadline = ParseDuration(value);
                break;

            // Ping
            case "ping_interval":
                opts.PingInterval = ParseDuration(value);
                break;
            case "ping_max" or "ping_max_out":
                opts.MaxPingsOut = ToInt(value);
                break;

            // Monitoring
            case "http_port" or "monitor_port":
                opts.MonitorPort = ToInt(value);
                break;
            case "https_port":
                opts.MonitorHttpsPort = ToInt(value);
                break;
            case "http":
                ParseMonitorListen(value, opts, isHttps: false);
                break;
            case "https":
                ParseMonitorListen(value, opts, isHttps: true);
                break;
            case "http_base_path":
                opts.MonitorBasePath = ToString(value);
                break;

            // Lifecycle
            case "lame_duck_duration":
                opts.LameDuckDuration = ParseDuration(value);
                break;
            case "lame_duck_grace_period":
                opts.LameDuckGracePeriod = ParseDuration(value);
                break;

            // Files
            case "pidfile" or "pid_file":
                opts.PidFile = ToString(value);
                break;
            case "ports_file_dir":
                opts.PortsFileDir = ToString(value);
                break;

            // Auth
            case "authorization":
                if (value is Dictionary<string, object?> authDict)
                    ParseAuthorization(authDict, opts, errors);
                break;
            case "no_auth_user":
                opts.NoAuthUser = ToString(value);
                break;

            // TLS
            case "tls":
                if (value is Dictionary<string, object?> tlsDict)
                    ParseTls(tlsDict, opts, errors);
                break;
            case "allow_non_tls":
                opts.AllowNonTls = ToBool(value);
                break;

            // Cluster / inter-server / JetStream
            case "cluster":
                if (value is Dictionary<string, object?> clusterDict)
                    opts.Cluster = ParseCluster(clusterDict, errors);
                break;
            case "gateway":
                if (value is Dictionary<string, object?> gatewayDict)
                    opts.Gateway = ParseGateway(gatewayDict, errors);
                break;
            case "leaf":
            case "leafnode":
            case "leafnodes":
                if (value is Dictionary<string, object?> leafDict)
                    opts.LeafNode = ParseLeafNode(leafDict, errors);
                break;
            case "jetstream":
                if (value is Dictionary<string, object?> jsDict)
                    opts.JetStream = ParseJetStream(jsDict, errors);
                break;

            // Tags
            case "server_tags":
                if (value is Dictionary<string, object?> tagsDict)
                    ParseTags(tagsDict, opts);
                break;

            // Profiling
            case "prof_port":
                opts.ProfPort = ToInt(value);
                break;

            // System account
            case "system_account":
                opts.SystemAccount = ToString(value);
                break;
            case "no_system_account":
                opts.NoSystemAccount = ToBool(value);
                break;
            case "no_header_support":
                opts.NoHeaderSupport = ToBool(value);
                break;
            case "connect_error_reports":
                opts.ConnectErrorReports = ToInt(value);
                break;
            case "reconnect_error_reports":
                opts.ReconnectErrorReports = ToInt(value);
                break;

            // MQTT
            case "mqtt":
                if (value is Dictionary<string, object?> mqttDict)
                    ParseMqtt(mqttDict, opts, errors);
                break;

            // WebSocket
            case "websocket" or "ws":
                if (value is Dictionary<string, object?> wsDict)
                    ParseWebSocket(wsDict, opts, errors);
                break;

            // Unknown keys silently ignored (accounts, resolver, operator, etc.)
            default:
                break;
        }
    }

    // ─── Listen parsing ────────────────────────────────────────────

    /// <summary>
    /// Parses a "listen" value that can be:
    /// <list type="bullet">
    ///   <item><c>":4222"</c> — port only</item>
    ///   <item><c>"0.0.0.0:4222"</c> — host + port</item>
    ///   <item><c>"4222"</c> — bare number (port only)</item>
    ///   <item><c>4222</c> — integer (port only)</item>
    /// </list>
    /// </summary>
    private static void ParseListen(object? value, NatsOptions opts)
    {
        var (host, port) = ParseHostPort(value);
        if (host is not null)
            opts.Host = host;
        if (port is not null)
            opts.Port = port.Value;
    }

    /// <summary>
    /// Parses a monitor listen value. For "http" the port goes to MonitorPort;
    /// for "https" the port goes to MonitorHttpsPort.
    /// </summary>
    private static void ParseMonitorListen(object? value, NatsOptions opts, bool isHttps)
    {
        var (host, port) = ParseHostPort(value);
        if (host is not null)
            opts.MonitorHost = host;
        if (port is not null)
        {
            if (isHttps)
                opts.MonitorHttpsPort = port.Value;
            else
                opts.MonitorPort = port.Value;
        }
    }

    /// <summary>
    /// Shared host:port parsing logic.
    /// </summary>
    private static (string? Host, int? Port) ParseHostPort(object? value)
    {
        if (value is long l)
            return (null, (int)l);

        var str = ToString(value);

        // Try bare integer
        if (int.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out var barePort))
            return (null, barePort);

        // Check for host:port
        var colonIdx = str.LastIndexOf(':');
        if (colonIdx >= 0)
        {
            var hostPart = str[..colonIdx];
            var portPart = str[(colonIdx + 1)..];
            if (int.TryParse(portPart, NumberStyles.Integer, CultureInfo.InvariantCulture, out var p))
            {
                var host = hostPart.Length > 0 ? hostPart : null;
                return (host, p);
            }
        }

        throw new FormatException($"Cannot parse listen value: '{str}'");
    }

    // ─── Duration parsing ──────────────────────────────────────────

    /// <summary>
    /// Parses a duration value. Accepts:
    /// <list type="bullet">
    ///   <item>A string with unit suffix: "30s", "2m", "1h", "500ms"</item>
    ///   <item>A number (long/double) treated as seconds</item>
    /// </list>
    /// </summary>
    internal static TimeSpan ParseDuration(object? value)
    {
        return value switch
        {
            long seconds => TimeSpan.FromSeconds(seconds),
            double seconds => TimeSpan.FromSeconds(seconds),
            string s => ParseDurationString(s),
            _ => throw new FormatException($"Cannot parse duration from {value?.GetType().Name ?? "null"}"),
        };
    }

    private static readonly Regex DurationPattern = new(
        @"^(-?\d+(?:\.\d+)?)\s*(ms|s|m|h)$",
        RegexOptions.Compiled | RegexOptions.IgnoreCase);
    private static readonly Regex ByteSizePattern = new(
        @"^(\d+)\s*(b|kb|mb|gb|tb)?$",
        RegexOptions.Compiled | RegexOptions.IgnoreCase);

    private static TimeSpan ParseDurationString(string s)
    {
        var match = DurationPattern.Match(s);
        if (!match.Success)
            throw new FormatException($"Cannot parse duration: '{s}'");

        var amount = double.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture);
        var unit = match.Groups[2].Value.ToLowerInvariant();

        return unit switch
        {
            "ms" => TimeSpan.FromMilliseconds(amount),
            "s" => TimeSpan.FromSeconds(amount),
            "m" => TimeSpan.FromMinutes(amount),
            "h" => TimeSpan.FromHours(amount),
            _ => throw new FormatException($"Unknown duration unit: '{unit}'"),
        };
    }

    // ─── Cluster / gateway / leafnode / JetStream parsing ────────

    private static ClusterOptions ParseCluster(Dictionary<string, object?> dict, List<string> errors)
    {
        var options = new ClusterOptions();
        foreach (var (key, value) in dict)
        {
            switch (key.ToLowerInvariant())
            {
                case "name":
                    options.Name = ToString(value);
                    break;
                case "listen":
                    try
                    {
                        var (host, port) = ParseHostPort(value);
                        if (host is not null)
                            options.Host = host;
                        if (port is not null)
                            options.Port = port.Value;
                    }
                    catch (Exception ex)
                    {
                        errors.Add($"Invalid cluster.listen: {ex.Message}");
                    }

                    break;
                case "write_deadline":
                    try
                    {
                        options.WriteDeadline = ParseDuration(value);
                    }
                    catch (Exception ex)
                    {
                        errors.Add($"Invalid cluster.write_deadline: {ex.Message}");
                    }

                    break;
                case "routes":
                    if (value is List<object?> routeList)
                        options.Routes = ToStringList(routeList).ToList();
                    break;
                case "pool_size":
                    options.PoolSize = ToInt(value);
                    break;
                default:
                    break;
            }
        }

        return options;
    }

    private static GatewayOptions ParseGateway(Dictionary<string, object?> dict, List<string> errors)
    {
        var options = new GatewayOptions();
        foreach (var (key, value) in dict)
        {
            switch (key.ToLowerInvariant())
            {
                case "name":
                    options.Name = ToString(value);
                    break;
                case "host" or "net":
                    options.Host = ToString(value);
                    break;
                case "port":
                    options.Port = ToInt(value);
                    break;
                case "listen":
                    try
                    {
                        var (host, port) = ParseHostPort(value);
                        if (host is not null)
                            options.Host = host;
                        if (port is not null)
                            options.Port = port.Value;
                    }
                    catch (Exception ex)
                    {
                        errors.Add($"Invalid gateway.listen: {ex.Message}");
                    }

                    break;
                case "reject_unknown_cluster" or "reject_unknown":
                    options.RejectUnknown = ToBool(value);
                    break;
                case "advertise":
                    options.Advertise = ToString(value);
                    break;
                case "connect_retries":
                    options.ConnectRetries = ToInt(value);
                    break;
                case "connect_backoff":
                    options.ConnectBackoff = ToBool(value);
                    break;
                case "write_deadline":
                    try
                    {
                        options.WriteDeadline = ParseDuration(value);
                    }
                    catch (Exception ex)
                    {
                        errors.Add($"Invalid gateway.write_deadline: {ex.Message}");
                    }

                    break;
                case "authorization" or "authentication":
                    if (value is Dictionary<string, object?> authDict)
                        ParseGatewayAuthorization(authDict, options, errors);
                    break;
                case "gateways":
                    // Must be a list, not a map
                    if (value is List<object?> gwList)
                    {
                        foreach (var item in gwList)
                        {
                            if (item is Dictionary<string, object?> gwDict)
                                options.RemoteGateways.Add(ParseRemoteGateway(gwDict, errors));
                        }
                    }
                    else if (value is Dictionary<string, object?>)
                    {
                        errors.Add("gateway.gateways must be an array, not a map");
                    }

                    break;
                default:
                    break;
            }
        }

        return options;
    }

    private static void ParseGatewayAuthorization(Dictionary<string, object?> dict, GatewayOptions options, List<string> errors)
    {
        // Gateway authorization only supports a single user — users array is not allowed.
        // Go reference: opts.go parseGateway — "does not allow multiple users"
        foreach (var (key, value) in dict)
        {
            switch (key.ToLowerInvariant())
            {
                case "user" or "username":
                    options.Username = ToString(value);
                    break;
                case "pass" or "password":
                    options.Password = ToString(value);
                    break;
                case "token":
                    // Token-only auth
                    options.Username = ToString(value);
                    break;
                case "timeout":
                    options.AuthTimeout = ToDouble(value);
                    break;
                case "users":
                    // Not supported in gateway auth
                    errors.Add("gateway authorization does not allow multiple users");
                    break;
                default:
                    break;
            }
        }
    }

    private static RemoteGatewayOptions ParseRemoteGateway(Dictionary<string, object?> dict, List<string> errors)
    {
        var remote = new RemoteGatewayOptions();
        foreach (var (key, value) in dict)
        {
            switch (key.ToLowerInvariant())
            {
                case "name":
                    remote.Name = ToString(value);
                    break;
                case "url":
                    remote.Urls.Add(ToString(value));
                    break;
                case "urls":
                    if (value is List<object?> urlList)
                        remote.Urls.AddRange(ToStringList(urlList));
                    break;
                default:
                    break;
            }
        }

        return remote;
    }

    private static LeafNodeOptions ParseLeafNode(Dictionary<string, object?> dict, List<string> errors)
    {
        var options = new LeafNodeOptions();
        foreach (var (key, value) in dict)
        {
            switch (key.ToLowerInvariant())
            {
                case "host" or "net":
                    options.Host = ToString(value);
                    break;
                case "port":
                    options.Port = ToInt(value);
                    break;
                case "listen":
                    try
                    {
                        var (host, port) = ParseHostPort(value);
                        if (host is not null)
                            options.Host = host;
                        if (port is not null)
                            options.Port = port.Value;
                    }
                    catch (Exception ex)
                    {
                        errors.Add($"Invalid leafnode.listen: {ex.Message}");
                    }

                    break;
                case "advertise":
                    options.Advertise = ToString(value);
                    break;
                case "write_deadline":
                    try
                    {
                        options.WriteDeadline = ParseDuration(value);
                    }
                    catch (Exception ex)
                    {
                        errors.Add($"Invalid leafnode.write_deadline: {ex.Message}");
                    }

                    break;
                case "authorization" or "authentication":
                    if (value is Dictionary<string, object?> authDict)
                        ParseLeafNodeAuthorization(authDict, options, errors);
                    break;
                case "remotes":
                    if (value is List<object?> remoteList)
                    {
                        foreach (var item in remoteList)
                        {
                            if (item is Dictionary<string, object?> remoteDict)
                                options.RemoteLeaves.Add(ParseRemoteLeaf(remoteDict, errors));
                        }
                    }

                    break;
                case "no_advertise":
                case "compress":
                case "tls":
                case "deny_exports":
                case "deny_imports":
                    // Silently accepted fields (some are handled elsewhere)
                    break;
                default:
                    break;
            }
        }

        return options;
    }

    private static void ParseLeafNodeAuthorization(Dictionary<string, object?> dict, LeafNodeOptions options, List<string> errors)
    {
        // Go reference: opts.go parseLeafNode authorization block
        foreach (var (key, value) in dict)
        {
            switch (key.ToLowerInvariant())
            {
                case "user" or "username":
                    options.Username = ToString(value);
                    break;
                case "pass" or "password":
                    options.Password = ToString(value);
                    break;
                case "token":
                    options.Username = ToString(value);
                    break;
                case "timeout":
                    // Go stores leafnode auth_timeout as float64 seconds.
                    // Supports plain numbers and duration strings like "1m".
                    options.AuthTimeout = value switch
                    {
                        long l => (double)l,
                        double d => d,
                        string s => ParseDuration(s).TotalSeconds,
                        _ => throw new FormatException($"Invalid leafnode auth timeout: {value?.GetType().Name}"),
                    };
                    break;
                default:
                    break;
            }
        }
    }

    private static RemoteLeafOptions ParseRemoteLeaf(Dictionary<string, object?> dict, List<string> errors)
    {
        var remote = new RemoteLeafOptions();
        var urls = new List<string>();
        string? localAccount = null;
        string? credentials = null;
        var dontRandomize = false;

        foreach (var (key, value) in dict)
        {
            switch (key.ToLowerInvariant())
            {
                case "url":
                    urls.Add(ToString(value));
                    break;
                case "urls":
                    if (value is List<object?> urlList)
                        urls.AddRange(ToStringList(urlList));
                    break;
                case "account":
                    localAccount = ToString(value);
                    break;
                case "credentials" or "creds":
                    credentials = ToString(value);
                    break;
                case "dont_randomize" or "no_randomize":
                    dontRandomize = ToBool(value);
                    break;
                default:
                    break;
            }
        }

        return new RemoteLeafOptions
        {
            LocalAccount = localAccount,
            Credentials = credentials,
            Urls = urls,
            DontRandomize = dontRandomize,
        };
    }

    private static JetStreamOptions ParseJetStream(Dictionary<string, object?> dict, List<string> errors)
    {
        var options = new JetStreamOptions();
        foreach (var (key, value) in dict)
        {
            switch (key.ToLowerInvariant())
            {
                case "store_dir":
                    options.StoreDir = ToString(value);
                    break;
                case "max_mem_store":
                    try
                    {
                        options.MaxMemoryStore = ParseByteSize(value);
                    }
                    catch (Exception ex)
                    {
                        errors.Add($"Invalid jetstream.max_mem_store: {ex.Message}");
                    }

                    break;
                case "max_file_store":
                    try
                    {
                        options.MaxFileStore = ParseByteSize(value);
                    }
                    catch (Exception ex)
                    {
                        errors.Add($"Invalid jetstream.max_file_store: {ex.Message}");
                    }

                    break;
            }
        }

        return options;
    }

    // ─── Authorization parsing ─────────────────────────────────────

    private static void ParseAuthorization(Dictionary<string, object?> dict, NatsOptions opts, List<string> errors)
    {
        string? token = null;
        List<object?>? userList = null;

        foreach (var (key, value) in dict)
        {
            switch (key.ToLowerInvariant())
            {
                case "user" or "username":
                    opts.Username = ToString(value);
                    break;
                case "pass" or "password":
                    opts.Password = ToString(value);
                    break;
                case "token":
                    token = ToString(value);
                    opts.Authorization = token;
                    break;
                case "timeout":
                    opts.AuthTimeout = value switch
                    {
                        long l => TimeSpan.FromSeconds(l),
                        double d => TimeSpan.FromSeconds(d),
                        string s => ParseDuration(s),
                        _ => throw new FormatException($"Invalid auth timeout type: {value?.GetType().Name}"),
                    };
                    break;
                case "users":
                    if (value is List<object?> ul)
                        userList = ul;
                    break;
                default:
                    // Unknown auth keys silently ignored
                    break;
            }
        }

        // Validate: token cannot be combined with users array.
        // Go reference: opts.go — "cannot have a token with a users array"
        if (token is not null && userList is not null)
        {
            errors.Add("Cannot have a token with a users array");
            return;
        }

        if (userList is not null)
        {
            var (plainUsers, nkeyUsers) = ParseUsersAndNkeys(userList, errors);
            if (plainUsers.Count > 0)
                opts.Users = plainUsers;
            if (nkeyUsers.Count > 0)
                opts.NKeys = nkeyUsers;
        }
    }

    /// <summary>
    /// Splits a users array into plain users and NKey users.
    /// An entry with an "nkey" field is an NKey user; entries with "user" are plain users.
    /// Go reference: opts.go — parseUsers (lines ~2500-2700).
    /// </summary>
    private static (List<User> PlainUsers, List<Auth.NKeyUser> NkeyUsers) ParseUsersAndNkeys(List<object?> list, List<string> errors)
    {
        var plainUsers = new List<User>();
        var nkeyUsers = new List<Auth.NKeyUser>();

        foreach (var item in list)
        {
            if (item is not Dictionary<string, object?> userDict)
            {
                errors.Add("Expected user entry to be a map");
                continue;
            }

            string? username = null;
            string? password = null;
            string? nkey = null;
            string? account = null;
            Permissions? permissions = null;

            foreach (var (key, value) in userDict)
            {
                switch (key.ToLowerInvariant())
                {
                    case "user" or "username":
                        username = ToString(value);
                        break;
                    case "pass" or "password":
                        password = ToString(value);
                        break;
                    case "nkey":
                        nkey = ToString(value);
                        break;
                    case "account":
                        account = ToString(value);
                        break;
                    case "permissions" or "permission":
                        if (value is Dictionary<string, object?> permDict)
                            permissions = ParsePermissions(permDict, errors);
                        break;
                }
            }

            if (nkey is not null)
            {
                // NKey user: validate no password and valid NKey format
                if (!ValidateNkey(nkey, password is not null, errors))
                    continue;

                nkeyUsers.Add(new Auth.NKeyUser
                {
                    Nkey = nkey,
                    Permissions = permissions,
                    Account = account,
                });
                continue;
            }

            if (username is null)
            {
                errors.Add("User entry missing 'user' field");
                continue;
            }

            plainUsers.Add(new User
            {
                Username = username,
                Password = password ?? string.Empty,
                Account = account,
                Permissions = permissions,
            });
        }

        return (plainUsers, nkeyUsers);
    }

    /// <summary>
    /// Validates an NKey public key string.
    /// Go reference: opts.go — nkey must start with 'U' and be at least 56 chars.
    /// </summary>
    private const int NKeyMinLen = 56;

    private static bool ValidateNkey(string nkey, bool hasPassword, List<string> errors)
    {
        if (nkey.Length < NKeyMinLen || !nkey.StartsWith('U'))
        {
            errors.Add($"Not a valid public NKey: {nkey}");
            return false;
        }

        if (hasPassword)
        {
            errors.Add("NKey user entry cannot have a password");
            return false;
        }

        return true;
    }

    private static List<User> ParseUsers(List<object?> list, List<string> errors)
    {
        var (plainUsers, _) = ParseUsersAndNkeys(list, errors);
        return plainUsers;
    }

    private static Permissions ParsePermissions(Dictionary<string, object?> dict, List<string> errors)
    {
        SubjectPermission? publish = null;
        SubjectPermission? subscribe = null;
        ResponsePermission? response = null;

        foreach (var (key, value) in dict)
        {
            switch (key.ToLowerInvariant())
            {
                case "publish" or "pub":
                    publish = ParseSubjectPermission(value, errors);
                    break;
                case "subscribe" or "sub":
                    subscribe = ParseSubjectPermission(value, errors);
                    break;
                case "resp" or "response":
                    if (value is Dictionary<string, object?> respDict)
                        response = ParseResponsePermission(respDict);
                    break;
            }
        }

        return new Permissions
        {
            Publish = publish,
            Subscribe = subscribe,
            Response = response,
        };
    }

    private static SubjectPermission? ParseSubjectPermission(object? value, List<string> errors)
    {
        // Can be a simple list of strings (treated as allow) or a dict with allow/deny
        if (value is Dictionary<string, object?> dict)
        {
            IReadOnlyList<string>? allow = null;
            IReadOnlyList<string>? deny = null;

            foreach (var (key, v) in dict)
            {
                switch (key.ToLowerInvariant())
                {
                    case "allow":
                        allow = ToStringList(v);
                        break;
                    case "deny":
                        deny = ToStringList(v);
                        break;
                }
            }

            return new SubjectPermission { Allow = allow, Deny = deny };
        }

        if (value is List<object?> list)
        {
            return new SubjectPermission { Allow = ToStringList(list) };
        }

        if (value is string s)
        {
            return new SubjectPermission { Allow = [s] };
        }

        return null;
    }

    private static ResponsePermission ParseResponsePermission(Dictionary<string, object?> dict)
    {
        var maxMsgs = 0;
        var expires = TimeSpan.Zero;

        foreach (var (key, value) in dict)
        {
            switch (key.ToLowerInvariant())
            {
                case "max_msgs" or "max":
                    maxMsgs = ToInt(value);
                    break;
                case "expires" or "ttl":
                    expires = ParseDuration(value);
                    break;
            }
        }

        return new ResponsePermission { MaxMsgs = maxMsgs, Expires = expires };
    }

    // ─── TLS parsing ───────────────────────────────────────────────

    private static void ParseTls(Dictionary<string, object?> dict, NatsOptions opts, List<string> errors)
    {
        foreach (var (key, value) in dict)
        {
            switch (key.ToLowerInvariant())
            {
                case "cert_file":
                    opts.TlsCert = ToString(value);
                    break;
                case "key_file":
                    opts.TlsKey = ToString(value);
                    break;
                case "ca_file":
                    opts.TlsCaCert = ToString(value);
                    break;
                case "verify":
                    opts.TlsVerify = ToBool(value);
                    break;
                case "verify_and_map":
                    var map = ToBool(value);
                    opts.TlsMap = map;
                    if (map)
                        opts.TlsVerify = true;
                    break;
                case "timeout":
                    opts.TlsTimeout = value switch
                    {
                        long l => TimeSpan.FromSeconds(l),
                        double d => TimeSpan.FromSeconds(d),
                        string s => ParseDuration(s),
                        _ => throw new FormatException($"Invalid TLS timeout type: {value?.GetType().Name}"),
                    };
                    break;
                case "connection_rate_limit":
                    opts.TlsRateLimit = ToLong(value);
                    break;
                case "pinned_certs":
                    if (value is List<object?> pinnedList)
                    {
                        var certs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
                        foreach (var item in pinnedList)
                        {
                            if (item is string s)
                                certs.Add(s.ToLowerInvariant());
                        }

                        opts.TlsPinnedCerts = certs;
                    }

                    break;
                case "handshake_first" or "first" or "immediate":
                    opts.TlsHandshakeFirst = ToBool(value);
                    break;
                case "handshake_first_fallback":
                    opts.TlsHandshakeFirstFallback = ParseDuration(value);
                    break;
                default:
                    // Unknown TLS keys silently ignored
                    break;
            }
        }
    }

    // ─── Tags parsing ──────────────────────────────────────────────

    private static void ParseTags(Dictionary<string, object?> dict, NatsOptions opts)
    {
        var tags = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        foreach (var (key, value) in dict)
        {
            tags[key] = ToString(value);
        }

        opts.Tags = tags;
    }

    // ─── MQTT parsing ────────────────────────────────────────────────
    // Reference: Go server/opts.go parseMQTT (lines ~5443-5541)

    private static void ParseMqtt(Dictionary<string, object?> dict, NatsOptions opts, List<string> errors)
    {
        var mqtt = opts.Mqtt ?? new MqttOptions();

        foreach (var (key, value) in dict)
        {
            switch (key.ToLowerInvariant())
            {
                case "listen":
                    var (host, port) = ParseHostPort(value);
                    if (host is not null) mqtt.Host = host;
                    if (port is not null) mqtt.Port = port.Value;
                    break;
                case "port":
                    mqtt.Port = ToInt(value);
                    break;
                case "host" or "net":
                    mqtt.Host = ToString(value);
                    break;
                case "no_auth_user":
                    mqtt.NoAuthUser = ToString(value);
                    break;
                case "tls":
                    if (value is Dictionary<string, object?> tlsDict)
                        ParseMqttTls(tlsDict, mqtt, errors);
                    break;
                case "authorization" or "authentication":
                    if (value is Dictionary<string, object?> authDict)
                        ParseMqttAuth(authDict, mqtt, errors);
                    break;
                case "ack_wait" or "ackwait":
                    mqtt.AckWait = ParseDuration(value);
                    break;
                case "js_api_timeout" or "api_timeout":
                    mqtt.JsApiTimeout = ParseDuration(value);
                    break;
                case "max_ack_pending" or "max_pending" or "max_inflight":
                    var pending = ToInt(value);
                    if (pending < 0 || pending > 0xFFFF)
                        errors.Add($"mqtt max_ack_pending invalid value {pending}, should be in [0..{0xFFFF}] range");
                    else
                        mqtt.MaxAckPending = (ushort)pending;
                    break;
                case "js_domain":
                    mqtt.JsDomain = ToString(value);
                    break;
                case "stream_replicas":
                    mqtt.StreamReplicas = ToInt(value);
                    break;
                case "consumer_replicas":
                    mqtt.ConsumerReplicas = ToInt(value);
                    break;
                case "consumer_memory_storage":
                    mqtt.ConsumerMemoryStorage = ToBool(value);
                    break;
                case "consumer_inactive_threshold" or "consumer_auto_cleanup":
                    mqtt.ConsumerInactiveThreshold = ParseDuration(value);
                    break;
                default:
                    break;
            }
        }

        opts.Mqtt = mqtt;
    }

    private static void ParseMqttAuth(Dictionary<string, object?> dict, MqttOptions mqtt, List<string> errors)
    {
        foreach (var (key, value) in dict)
        {
            switch (key.ToLowerInvariant())
            {
                case "user" or "username":
                    mqtt.Username = ToString(value);
                    break;
                case "pass" or "password":
                    mqtt.Password = ToString(value);
                    break;
                case "token":
                    mqtt.Token = ToString(value);
                    break;
                case "timeout":
                    mqtt.AuthTimeout = ToDouble(value);
                    break;
                default:
                    break;
            }
        }
    }

    // ─── WebSocket parsing ────────────────────────────────────────────────────
    // Reference: Go server/opts.go parseWebsocket (lines ~5600-5700)

    private static void ParseWebSocket(Dictionary<string, object?> dict, NatsOptions opts, List<string> errors)
    {
        var ws = opts.WebSocket ?? new WebSocketOptions();

        foreach (var (key, value) in dict)
        {
            switch (key.ToLowerInvariant())
            {
                case "listen":
                    try
                    {
                        var (host, port) = ParseHostPort(value);
                        if (host is not null) ws.Host = host;
                        if (port is not null) ws.Port = port.Value;
                    }
                    catch (Exception ex)
                    {
                        errors.Add($"Invalid websocket.listen: {ex.Message}");
                    }

                    break;
                case "port":
                    ws.Port = ToInt(value);
                    break;
                case "host" or "net":
                    ws.Host = ToString(value);
                    break;
                case "advertise":
                    ws.Advertise = ToString(value);
                    break;
                case "no_auth_user":
                    ws.NoAuthUser = ToString(value);
                    break;
                case "no_tls":
                    ws.NoTls = ToBool(value);
                    break;
                case "same_origin":
                    ws.SameOrigin = ToBool(value);
                    break;
                case "compression":
                    ws.Compression = ToBool(value);
                    break;
                case "ping_interval":
                    try
                    {
                        ws.PingInterval = ParseDuration(value);
                    }
                    catch (Exception ex)
                    {
                        errors.Add($"Invalid websocket.ping_interval: {ex.Message}");
                    }

                    break;
                case "handshake_timeout":
                    try
                    {
                        ws.HandshakeTimeout = ParseDuration(value);
                    }
                    catch (Exception ex)
                    {
                        errors.Add($"Invalid websocket.handshake_timeout: {ex.Message}");
                    }

                    break;
                case "jwt_cookie":
                    ws.JwtCookie = ToString(value);
                    break;
                case "username_header" or "username_cookie":
                    ws.UsernameCookie = ToString(value);
                    break;
                case "token_cookie":
                    ws.TokenCookie = ToString(value);
                    break;
                default:
                    break;
            }
        }

        opts.WebSocket = ws;
    }

    private static void ParseMqttTls(Dictionary<string, object?> dict, MqttOptions mqtt, List<string> errors)
    {
        foreach (var (key, value) in dict)
        {
            switch (key.ToLowerInvariant())
            {
                case "cert_file":
                    mqtt.TlsCert = ToString(value);
                    break;
                case "key_file":
                    mqtt.TlsKey = ToString(value);
                    break;
                case "ca_file":
                    mqtt.TlsCaCert = ToString(value);
                    break;
                case "verify":
                    mqtt.TlsVerify = ToBool(value);
                    break;
                case "verify_and_map":
                    var map = ToBool(value);
                    mqtt.TlsMap = map;
                    if (map) mqtt.TlsVerify = true;
                    break;
                case "timeout":
                    mqtt.TlsTimeout = ToDouble(value);
                    break;
                case "pinned_certs":
                    if (value is List<object?> pinnedList)
                    {
                        var certs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
                        foreach (var item in pinnedList)
                        {
                            if (item is string s)
                                certs.Add(s.ToLowerInvariant());
                        }

                        mqtt.TlsPinnedCerts = certs;
                    }

                    break;
                default:
                    break;
            }
        }
    }

    // ─── Type conversion helpers ───────────────────────────────────

    private static int ToInt(object? value) => value switch
    {
        long l => (int)l,
        int i => i,
        double d => (int)d,
        string s when int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) => i,
        _ => throw new FormatException($"Cannot convert {value?.GetType().Name ?? "null"} to int"),
    };

    private static long ToLong(object? value) => value switch
    {
        long l => l,
        int i => i,
        double d => (long)d,
        string s when long.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var l) => l,
        _ => throw new FormatException($"Cannot convert {value?.GetType().Name ?? "null"} to long"),
    };

    private static long ParseByteSize(object? value)
    {
        if (value is long l)
            return l;
        if (value is int i)
            return i;
        if (value is double d)
            return (long)d;
        if (value is not string s)
            throw new FormatException($"Cannot parse byte size from {value?.GetType().Name ?? "null"}");

        var trimmed = s.Trim();
        var match = ByteSizePattern.Match(trimmed);
        if (!match.Success)
            throw new FormatException($"Cannot parse byte size: '{s}'");

        var amount = long.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture);
        var unit = match.Groups[2].Value.ToLowerInvariant();
        var multiplier = unit switch
        {
            "" or "b" => 1L,
            "kb" => 1024L,
            "mb" => 1024L * 1024L,
            "gb" => 1024L * 1024L * 1024L,
            "tb" => 1024L * 1024L * 1024L * 1024L,
            _ => throw new FormatException($"Unknown byte-size unit: '{unit}'"),
        };

        checked
        {
            return amount * multiplier;
        }
    }

    private static bool ToBool(object? value) => value switch
    {
        bool b => b,
        _ => throw new FormatException($"Cannot convert {value?.GetType().Name ?? "null"} to bool"),
    };

    private static string ToString(object? value) => value switch
    {
        string s => s,
        long l => l.ToString(CultureInfo.InvariantCulture),
        _ => throw new FormatException($"Cannot convert {value?.GetType().Name ?? "null"} to string"),
    };

    private static double ToDouble(object? value) => value switch
    {
        double d => d,
        long l => l,
        int i => i,
        string s when double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var d) => d,
        _ => throw new FormatException($"Cannot convert {value?.GetType().Name ?? "null"} to double"),
    };

    private static IReadOnlyList<string> ToStringList(object? value)
    {
        if (value is List<object?> list)
        {
            var result = new List<string>(list.Count);
            foreach (var item in list)
            {
                if (item is string s)
                    result.Add(s);
            }

            return result;
        }

        if (value is string str)
            return [str];

        return [];
    }
}

/// <summary>
/// Thrown when one or more configuration validation errors are detected.
/// All errors are collected rather than failing on the first one.
/// </summary>
public sealed class ConfigProcessorException(string message, List<string> errors)
    : Exception(message)
{
    public IReadOnlyList<string> Errors => errors;
}
