using System;
using System.Collections.Generic;
using System.Linq;
using StellaOps.Auth.Abstractions;

namespace StellaOps.Auth.Client;

/// <summary>
/// Options controlling the StellaOps authentication client.
/// </summary>
public sealed class StellaOpsAuthClientOptions
{
    private static readonly TimeSpan[] DefaultRetryDelays =
    {
        TimeSpan.FromSeconds(1),
        TimeSpan.FromSeconds(2),
        TimeSpan.FromSeconds(5)
    };
    private static readonly TimeSpan DefaultOfflineTolerance = TimeSpan.FromMinutes(10);

    private readonly List<string> scopes = new();
    private readonly List<TimeSpan> retryDelays = new(DefaultRetryDelays);

    /// <summary>
    /// Authority (issuer) base URL.
    /// </summary>
    public string Authority { get; set; } = string.Empty;

    /// <summary>
    /// OAuth client identifier (optional for password flow).
    /// </summary>
    public string ClientId { get; set; } = string.Empty;

    /// <summary>
    /// OAuth client secret (optional for public clients).
    /// </summary>
    public string? ClientSecret { get; set; }

    /// <summary>
    /// Default scopes requested for flows that do not explicitly override them.
    /// </summary>
    public IList<string> DefaultScopes => scopes;

    /// <summary>
    /// Retry delays applied by HTTP retry policy (empty uses defaults).
    /// </summary>
    public IList<TimeSpan> RetryDelays => retryDelays;

    /// <summary>
    /// Gets or sets a value indicating whether HTTP retry policies are enabled.
    /// </summary>
    public bool EnableRetries { get; set; } = true;

    /// <summary>
    /// Timeout applied to discovery and token HTTP requests.
    /// </summary>
    public TimeSpan HttpTimeout { get; set; } = TimeSpan.FromSeconds(30);

    /// <summary>
    /// Lifetime of cached discovery metadata.
    /// </summary>
    public TimeSpan DiscoveryCacheLifetime { get; set; } = TimeSpan.FromMinutes(10);

    /// <summary>
    /// Lifetime of cached JWKS metadata.
    /// </summary>
    public TimeSpan JwksCacheLifetime { get; set; } = TimeSpan.FromMinutes(30);

    /// <summary>
    /// Buffer applied when determining cache expiration (default: 30 seconds).
    /// </summary>
    public TimeSpan ExpirationSkew { get; set; } = TimeSpan.FromSeconds(30);

    /// <summary>
    /// Gets or sets a value indicating whether cached discovery/JWKS responses may be served when the Authority is unreachable.
    /// </summary>
    public bool AllowOfflineCacheFallback { get; set; } = true;

    /// <summary>
    /// Additional tolerance window during which stale cache entries remain valid if offline fallback is allowed.
    /// </summary>
    public TimeSpan OfflineCacheTolerance { get; set; } = DefaultOfflineTolerance;

    /// <summary>
    /// Parsed Authority URI (populated after validation).
    /// </summary>
    public Uri AuthorityUri { get; private set; } = null!;

    /// <summary>
    /// Normalised scope list (populated after validation).
    /// </summary>
    public IReadOnlyList<string> NormalizedScopes { get; private set; } = Array.Empty<string>();

    /// <summary>
    /// Normalised retry delays (populated after validation).
    /// </summary>
    public IReadOnlyList<TimeSpan> NormalizedRetryDelays { get; private set; } = Array.Empty<TimeSpan>();

    /// <summary>
    /// Validates required values and normalises scope entries.
    /// </summary>
    public void Validate()
    {
        if (string.IsNullOrWhiteSpace(Authority))
        {
            throw new InvalidOperationException("Auth client requires an Authority URL.");
        }

        if (!Uri.TryCreate(Authority.Trim(), UriKind.Absolute, out var authorityUri))
        {
            throw new InvalidOperationException("Auth client Authority must be an absolute URI.");
        }

        if (HttpTimeout <= TimeSpan.Zero)
        {
            throw new InvalidOperationException("Auth client HTTP timeout must be greater than zero.");
        }

        if (DiscoveryCacheLifetime <= TimeSpan.Zero)
        {
            throw new InvalidOperationException("Discovery cache lifetime must be greater than zero.");
        }

        if (JwksCacheLifetime <= TimeSpan.Zero)
        {
            throw new InvalidOperationException("JWKS cache lifetime must be greater than zero.");
        }

        if (ExpirationSkew < TimeSpan.Zero || ExpirationSkew > TimeSpan.FromMinutes(5))
        {
            throw new InvalidOperationException("Expiration skew must be between 0 seconds and 5 minutes.");
        }

        if (OfflineCacheTolerance < TimeSpan.Zero)
        {
            throw new InvalidOperationException("Offline cache tolerance must be greater than or equal to zero.");
        }

        AuthorityUri = authorityUri;
        NormalizedScopes = NormalizeScopes(scopes);
        NormalizedRetryDelays = EnableRetries ? NormalizeRetryDelays(retryDelays) : Array.Empty<TimeSpan>();
    }

    private static IReadOnlyList<string> NormalizeScopes(IList<string> values)
    {
        if (values.Count == 0)
        {
            return Array.Empty<string>();
        }

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

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

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

            var normalized = StellaOpsScopes.Normalize(entry);
            if (normalized is null)
            {
                values.RemoveAt(index);
                continue;
            }

            if (!unique.Add(normalized))
            {
                values.RemoveAt(index);
                continue;
            }

            values[index] = normalized;
        }

        return values.Count == 0
            ? Array.Empty<string>()
            : values.OrderBy(static scope => scope, StringComparer.Ordinal).ToArray();
    }

    private static IReadOnlyList<TimeSpan> NormalizeRetryDelays(IList<TimeSpan> values)
    {
        for (var index = values.Count - 1; index >= 0; index--)
        {
            var delay = values[index];
            if (delay <= TimeSpan.Zero)
            {
                values.RemoveAt(index);
            }
        }

        if (values.Count == 0)
        {
            foreach (var delay in DefaultRetryDelays)
            {
                values.Add(delay);
            }
        }

        return values.ToArray();
    }
}
