using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.RateLimiting;
using StellaOps.Authority.Plugins.Abstractions;
using StellaOps.Cryptography;

namespace StellaOps.Configuration;

/// <summary>
/// Strongly typed configuration for the StellaOps Authority service.
/// </summary>
public sealed class StellaOpsAuthorityOptions
{
    private readonly List<string> pluginDirectories = new();
    private readonly List<string> bypassNetworks = new();

    /// <summary>
    /// Schema version for downstream consumers to coordinate breaking changes.
    /// </summary>
    public int SchemaVersion { get; set; } = 1;

    /// <summary>
    /// Absolute issuer URI advertised to clients (e.g. https://authority.stella-ops.local).
    /// </summary>
    public Uri? Issuer { get; set; }

    /// <summary>
    /// Lifetime for OAuth access tokens issued by Authority.
    /// </summary>
    public TimeSpan AccessTokenLifetime { get; set; } = TimeSpan.FromMinutes(15);

    /// <summary>
    /// Lifetime for OAuth refresh tokens issued by Authority.
    /// </summary>
    public TimeSpan RefreshTokenLifetime { get; set; } = TimeSpan.FromDays(30);

    /// <summary>
    /// Lifetime for OpenID Connect identity tokens.
    /// </summary>
    public TimeSpan IdentityTokenLifetime { get; set; } = TimeSpan.FromMinutes(5);

    /// <summary>
    /// Lifetime for OAuth authorization codes.
    /// </summary>
    public TimeSpan AuthorizationCodeLifetime { get; set; } = TimeSpan.FromMinutes(5);

    /// <summary>
    /// Lifetime for OAuth device codes (device authorization flow).
    /// </summary>
    public TimeSpan DeviceCodeLifetime { get; set; } = TimeSpan.FromMinutes(15);

    /// <summary>
    /// Directories scanned for Authority plugins (absolute or relative to application base path).
    /// </summary>
    public IList<string> PluginDirectories => pluginDirectories;

    /// <summary>
    /// CIDR blocks permitted to bypass certain authentication policies (e.g. on-host cron).
    /// </summary>
    public IList<string> BypassNetworks => bypassNetworks;

    /// <summary>
    /// Configuration describing the Authority MongoDB storage.
    /// </summary>
    public AuthorityStorageOptions Storage { get; } = new();

    /// <summary>
    /// Bootstrap settings for initial administrative provisioning.
    /// </summary>
    public AuthorityBootstrapOptions Bootstrap { get; } = new();

    /// <summary>
    /// Configuration describing available Authority plugins and their manifests.
    /// </summary>
    public AuthorityPluginSettings Plugins { get; } = new();

    /// <summary>
    /// Security-related configuration for the Authority host.
    /// </summary>
    public AuthoritySecurityOptions Security { get; } = new();

    /// <summary>
    /// Signing options for Authority-generated artefacts (revocation bundles, JWKS).
    /// </summary>
    public AuthoritySigningOptions Signing { get; } = new();

    /// <summary>
    /// Validates configured values and normalises collections.
    /// </summary>
    /// <exception cref="InvalidOperationException">Thrown when configuration is invalid.</exception>
    public void Validate()
    {
        if (SchemaVersion <= 0)
        {
            throw new InvalidOperationException("Authority configuration requires a positive schemaVersion.");
        }

        if (Issuer is null)
        {
            throw new InvalidOperationException("Authority configuration requires an issuer URL.");
        }

        if (!Issuer.IsAbsoluteUri)
        {
            throw new InvalidOperationException("Authority issuer must be an absolute URI.");
        }

        if (string.Equals(Issuer.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) && !Issuer.IsLoopback)
        {
            throw new InvalidOperationException("Authority issuer must use HTTPS unless running on a loopback interface.");
        }

        ValidateLifetime(AccessTokenLifetime, nameof(AccessTokenLifetime), TimeSpan.FromHours(24));
        ValidateLifetime(RefreshTokenLifetime, nameof(RefreshTokenLifetime), TimeSpan.FromDays(365));
        ValidateLifetime(IdentityTokenLifetime, nameof(IdentityTokenLifetime), TimeSpan.FromHours(24));
        ValidateLifetime(AuthorizationCodeLifetime, nameof(AuthorizationCodeLifetime), TimeSpan.FromHours(1));
        ValidateLifetime(DeviceCodeLifetime, nameof(DeviceCodeLifetime), TimeSpan.FromHours(24));

        NormaliseList(pluginDirectories);
        NormaliseList(bypassNetworks);

        Security.Validate();
        Signing.Validate();
        Plugins.NormalizeAndValidate();
        Storage.Validate();
        Bootstrap.Validate();
    }

    private static void ValidateLifetime(TimeSpan value, string propertyName, TimeSpan maximum)
    {
        if (value <= TimeSpan.Zero)
        {
            throw new InvalidOperationException($"Authority configuration requires {propertyName} to be greater than zero.");
        }

        if (value > maximum)
        {
            throw new InvalidOperationException($"Authority configuration requires {propertyName} to be less than or equal to {maximum}.");
        }
    }

    private static void NormaliseList(IList<string> values)
    {
        if (values.Count == 0)
        {
            return;
        }

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

        for (var index = values.Count - 1; index >= 0; index--)
        {
            var entry = values[index];

            if (string.IsNullOrWhiteSpace(entry))
            {
                values.RemoveAt(index);
                continue;
            }

            var trimmed = entry.Trim();
            if (!unique.Add(trimmed))
            {
                values.RemoveAt(index);
                continue;
            }

            values[index] = trimmed;
        }
    }
}

public sealed class AuthoritySecurityOptions
{
    /// <summary>
    /// Rate limiting configuration applied to Authority endpoints.
    /// </summary>
    public AuthorityRateLimitingOptions RateLimiting { get; } = new();

    /// <summary>
    /// Default password hashing parameters advertised to Authority plug-ins.
    /// </summary>
    public PasswordHashOptions PasswordHashing { get; } = new();

    internal void Validate()
    {
        RateLimiting.Validate();
        PasswordHashing.Validate();
    }
}

public sealed class AuthorityRateLimitingOptions
{
    public AuthorityRateLimitingOptions()
    {
        Token = new AuthorityEndpointRateLimitOptions
        {
            PermitLimit = 30,
            Window = TimeSpan.FromMinutes(1),
            QueueLimit = 0
        };

        Authorize = new AuthorityEndpointRateLimitOptions
        {
            PermitLimit = 60,
            Window = TimeSpan.FromMinutes(1),
            QueueLimit = 10
        };

        Internal = new AuthorityEndpointRateLimitOptions
        {
            Enabled = false,
            PermitLimit = 5,
            Window = TimeSpan.FromMinutes(1),
            QueueLimit = 0
        };
    }

    /// <summary>
    /// Rate limiting configuration applied to the /token endpoint.
    /// </summary>
    public AuthorityEndpointRateLimitOptions Token { get; }

    /// <summary>
    /// Rate limiting configuration applied to the /authorize endpoint.
    /// </summary>
    public AuthorityEndpointRateLimitOptions Authorize { get; }

    /// <summary>
    /// Rate limiting configuration applied to /internal endpoints.
    /// </summary>
    public AuthorityEndpointRateLimitOptions Internal { get; }

    internal void Validate()
    {
        Token.Validate(nameof(Token));
        Authorize.Validate(nameof(Authorize));
        Internal.Validate(nameof(Internal));
    }
}

public sealed class AuthorityEndpointRateLimitOptions
{
    /// <summary>
    /// Gets or sets a value indicating whether rate limiting is enabled for the endpoint.
    /// </summary>
    public bool Enabled { get; set; } = true;

    /// <summary>
    /// Maximum number of requests allowed within the configured window.
    /// </summary>
    public int PermitLimit { get; set; } = 60;

    /// <summary>
    /// Size of the fixed window applied to the rate limiter.
    /// </summary>
    public TimeSpan Window { get; set; } = TimeSpan.FromMinutes(1);

    /// <summary>
    /// Maximum number of queued requests awaiting permits.
    /// </summary>
    public int QueueLimit { get; set; } = 0;

    /// <summary>
    /// Ordering strategy for queued requests.
    /// </summary>
    public QueueProcessingOrder QueueProcessingOrder { get; set; } = QueueProcessingOrder.OldestFirst;

    internal void Validate(string name)
    {
        if (!Enabled)
        {
            return;
        }

        if (PermitLimit <= 0)
        {
            throw new InvalidOperationException($"Authority rate limiting '{name}' requires permitLimit to be greater than zero.");
        }

        if (QueueLimit < 0)
        {
            throw new InvalidOperationException($"Authority rate limiting '{name}' queueLimit cannot be negative.");
        }

        if (Window <= TimeSpan.Zero || Window > TimeSpan.FromHours(1))
        {
            throw new InvalidOperationException($"Authority rate limiting '{name}' window must be greater than zero and no more than one hour.");
        }
    }
}

public sealed class AuthorityStorageOptions
{
    /// <summary>
    /// Mongo connection string used by Authority storage.
    /// </summary>
    public string ConnectionString { get; set; } = string.Empty;

    /// <summary>
    /// Optional explicit database name override.
    /// </summary>
    public string? DatabaseName { get; set; }

    /// <summary>
    /// Mongo command timeout.
    /// </summary>
    public TimeSpan CommandTimeout { get; set; } = TimeSpan.FromSeconds(30);

    internal void Validate()
    {
        if (string.IsNullOrWhiteSpace(ConnectionString))
        {
            throw new InvalidOperationException("Authority storage requires a Mongo connection string.");
        }

        if (CommandTimeout <= TimeSpan.Zero)
        {
            throw new InvalidOperationException("Authority storage command timeout must be greater than zero.");
        }
    }
}

public sealed class AuthorityBootstrapOptions
{
    /// <summary>
    /// Enables or disables bootstrap administrative APIs.
    /// </summary>
    public bool Enabled { get; set; } = false;

    /// <summary>
    /// API key required when invoking bootstrap endpoints.
    /// </summary>
    public string? ApiKey { get; set; } = string.Empty;

    /// <summary>
    /// Default identity provider used when none is specified in bootstrap requests.
    /// </summary>
    public string? DefaultIdentityProvider { get; set; } = "standard";

    internal void Validate()
    {
        if (!Enabled)
        {
            return;
        }

        if (string.IsNullOrWhiteSpace(ApiKey))
        {
            throw new InvalidOperationException("Authority bootstrap configuration requires an API key when enabled.");
        }

        if (string.IsNullOrWhiteSpace(DefaultIdentityProvider))
        {
            throw new InvalidOperationException("Authority bootstrap configuration requires a default identity provider name when enabled.");
        }
    }
}

public sealed class AuthorityPluginSettings
{
    private static readonly StringComparer OrdinalIgnoreCase = StringComparer.OrdinalIgnoreCase;

    /// <summary>
    /// Directory containing per-plugin configuration manifests (relative paths resolved against application base path).
    /// </summary>
    public string ConfigurationDirectory { get; set; } = "../etc/authority.plugins";

    /// <summary>
    /// Declarative descriptors for Authority plugins (keyed by logical plugin name).
    /// </summary>
    public IDictionary<string, AuthorityPluginDescriptorOptions> Descriptors { get; } = new Dictionary<string, AuthorityPluginDescriptorOptions>(OrdinalIgnoreCase);

    internal void NormalizeAndValidate()
    {
        if (Descriptors.Count == 0)
        {
            return;
        }

        foreach (var (name, descriptor) in Descriptors.ToArray())
        {
            if (descriptor is null)
            {
                throw new InvalidOperationException($"Authority plugin descriptor '{name}' is null.");
            }

            descriptor.Normalize(name);
            descriptor.Validate(name);
        }
    }
}

public sealed class AuthorityPluginDescriptorOptions
{
    private static readonly StringComparer OrdinalIgnoreCase = StringComparer.OrdinalIgnoreCase;

    private readonly List<string> capabilities = new();
    private readonly Dictionary<string, string?> metadata = new(OrdinalIgnoreCase);
    private static readonly HashSet<string> AllowedCapabilities = new(
        new[]
        {
            AuthorityPluginCapabilities.Password,
            AuthorityPluginCapabilities.Mfa,
            AuthorityPluginCapabilities.ClientProvisioning,
            AuthorityPluginCapabilities.Bootstrap
        },
        OrdinalIgnoreCase);

    /// <summary>
    /// Logical type identifier for the plugin (e.g. standard, ldap).
    /// </summary>
    public string? Type { get; set; }

    /// <summary>
    /// Name of the plugin assembly (without file extension).
    /// </summary>
    public string? AssemblyName { get; set; }

    /// <summary>
    /// Optional explicit assembly path override; relative paths resolve against plugin directories.
    /// </summary>
    public string? AssemblyPath { get; set; }

    /// <summary>
    /// Indicates whether the plugin should be enabled.
    /// </summary>
    public bool Enabled { get; set; } = true;

    /// <summary>
    /// Plugin capability hints surfaced to the Authority host.
    /// </summary>
    public IList<string> Capabilities => capabilities;

    /// <summary>
    /// Optional metadata (string key/value) passed to plugin implementations.
    /// </summary>
    public IDictionary<string, string?> Metadata => metadata;

    /// <summary>
    /// Relative path to the plugin-specific configuration file (defaults to &lt;pluginName&gt;.yaml).
    /// </summary>
    public string? ConfigFile { get; set; }

    internal void Normalize(string pluginName)
    {
        if (string.IsNullOrWhiteSpace(ConfigFile))
        {
            ConfigFile = $"{pluginName}.yaml";
        }
        else
        {
            ConfigFile = ConfigFile.Trim();
        }

        Type = string.IsNullOrWhiteSpace(Type) ? pluginName : Type.Trim();

        if (!string.IsNullOrWhiteSpace(AssemblyName))
        {
            AssemblyName = AssemblyName.Trim();
        }

        if (!string.IsNullOrWhiteSpace(AssemblyPath))
        {
            AssemblyPath = AssemblyPath.Trim();
        }

        if (capabilities.Count > 0)
        {
            var seen = new HashSet<string>(OrdinalIgnoreCase);
            var unique = new List<string>(capabilities.Count);

            foreach (var entry in capabilities)
            {
                if (string.IsNullOrWhiteSpace(entry))
                {
                    continue;
                }

                var canonical = entry.Trim().ToLowerInvariant();
                if (seen.Add(canonical))
                {
                    unique.Add(canonical);
                }
            }

            unique.Sort(StringComparer.Ordinal);

            capabilities.Clear();
            capabilities.AddRange(unique);
        }
    }

    internal void Validate(string pluginName)
    {
        if (string.IsNullOrWhiteSpace(AssemblyName) && string.IsNullOrWhiteSpace(AssemblyPath))
        {
            throw new InvalidOperationException($"Authority plugin '{pluginName}' must define either assemblyName or assemblyPath.");
        }

        if (string.IsNullOrWhiteSpace(ConfigFile))
        {
            throw new InvalidOperationException($"Authority plugin '{pluginName}' must define a configFile.");
        }

        if (Path.GetFileName(ConfigFile) != ConfigFile && Path.IsPathRooted(ConfigFile) && !File.Exists(ConfigFile))
        {
            throw new InvalidOperationException($"Authority plugin '{pluginName}' specifies configFile '{ConfigFile}' which does not exist.");
        }

        foreach (var capability in capabilities)
        {
            if (!AllowedCapabilities.Contains(capability))
            {
                throw new InvalidOperationException($"Authority plugin '{pluginName}' declares unknown capability '{capability}'. Allowed values: password, mfa, clientProvisioning, bootstrap.");
            }
        }
    }

    internal AuthorityPluginManifest ToManifest(string name, string configPath)
    {
        var capabilitiesSnapshot = capabilities.Count == 0
            ? Array.Empty<string>()
            : capabilities.ToArray();

        var metadataSnapshot = metadata.Count == 0
            ? new Dictionary<string, string?>(OrdinalIgnoreCase)
            : new Dictionary<string, string?>(metadata, OrdinalIgnoreCase);

        return new AuthorityPluginManifest(
            name,
            Type ?? name,
            Enabled,
            AssemblyName,
            AssemblyPath,
            capabilitiesSnapshot,
            metadataSnapshot,
            configPath);
    }
}
