using System;
using System.Collections.Generic;
using System.Linq;
using StellaOps.Auth.Abstractions;
namespace StellaOps.Auth.Client;
///
/// Options controlling the StellaOps authentication client.
///
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 scopes = new();
private readonly List retryDelays = new(DefaultRetryDelays);
///
/// Authority (issuer) base URL.
///
public string Authority { get; set; } = string.Empty;
///
/// OAuth client identifier (optional for password flow).
///
public string ClientId { get; set; } = string.Empty;
///
/// OAuth client secret (optional for public clients).
///
public string? ClientSecret { get; set; }
///
/// Default scopes requested for flows that do not explicitly override them.
///
public IList DefaultScopes => scopes;
///
/// Retry delays applied by HTTP retry policy (empty uses defaults).
///
public IList RetryDelays => retryDelays;
///
/// Gets or sets a value indicating whether HTTP retry policies are enabled.
///
public bool EnableRetries { get; set; } = true;
///
/// Timeout applied to discovery and token HTTP requests.
///
public TimeSpan HttpTimeout { get; set; } = TimeSpan.FromSeconds(30);
///
/// Lifetime of cached discovery metadata.
///
public TimeSpan DiscoveryCacheLifetime { get; set; } = TimeSpan.FromMinutes(10);
///
/// Lifetime of cached JWKS metadata.
///
public TimeSpan JwksCacheLifetime { get; set; } = TimeSpan.FromMinutes(30);
///
/// Buffer applied when determining cache expiration (default: 30 seconds).
///
public TimeSpan ExpirationSkew { get; set; } = TimeSpan.FromSeconds(30);
///
/// Gets or sets a value indicating whether cached discovery/JWKS responses may be served when the Authority is unreachable.
///
public bool AllowOfflineCacheFallback { get; set; } = true;
///
/// Additional tolerance window during which stale cache entries remain valid if offline fallback is allowed.
///
public TimeSpan OfflineCacheTolerance { get; set; } = DefaultOfflineTolerance;
///
/// Parsed Authority URI (populated after validation).
///
public Uri AuthorityUri { get; private set; } = null!;
///
/// Normalised scope list (populated after validation).
///
public IReadOnlyList NormalizedScopes { get; private set; } = Array.Empty();
///
/// Normalised retry delays (populated after validation).
///
public IReadOnlyList NormalizedRetryDelays { get; private set; } = Array.Empty();
///
/// Validates required values and normalises scope entries.
///
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();
}
private static IReadOnlyList NormalizeScopes(IList values)
{
if (values.Count == 0)
{
return Array.Empty();
}
var unique = new HashSet(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()
: values.OrderBy(static scope => scope, StringComparer.Ordinal).ToArray();
}
private static IReadOnlyList NormalizeRetryDelays(IList 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();
}
}