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

namespace StellaOps.Auth.ServerIntegration;

/// <summary>
/// Options controlling StellaOps resource server authentication.
/// </summary>
public sealed class StellaOpsResourceServerOptions
{
    private readonly List<string> audiences = new();
    private readonly List<string> requiredScopes = new();
    private readonly List<string> bypassNetworks = new();

    /// <summary>
    /// Gets or sets the Authority (issuer) URL that exposes OpenID discovery.
    /// </summary>
    public string Authority { get; set; } = string.Empty;

    /// <summary>
    /// Optional explicit OpenID Connect metadata address.
    /// </summary>
    public string? MetadataAddress { get; set; }

    /// <summary>
    /// Audiences accepted by the resource server (validated against the <c>aud</c> claim).
    /// </summary>
    public IList<string> Audiences => audiences;

    /// <summary>
    /// Scopes enforced by default authorisation policies.
    /// </summary>
    public IList<string> RequiredScopes => requiredScopes;

    /// <summary>
    /// Networks permitted to bypass authentication (used for trusted on-host automation).
    /// </summary>
    public IList<string> BypassNetworks => bypassNetworks;

    /// <summary>
    /// Whether HTTPS metadata is required when communicating with Authority.
    /// </summary>
    public bool RequireHttpsMetadata { get; set; } = true;

    /// <summary>
    /// Back-channel timeout when fetching metadata/JWKS.
    /// </summary>
    public TimeSpan BackchannelTimeout { get; set; } = TimeSpan.FromSeconds(30);

    /// <summary>
    /// Clock skew tolerated when validating tokens.
    /// </summary>
    public TimeSpan TokenClockSkew { get; set; } = TimeSpan.FromSeconds(60);

    /// <summary>
    /// Gets the canonical Authority URI (populated during validation).
    /// </summary>
    public Uri AuthorityUri { get; private set; } = null!;

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

    /// <summary>
    /// Gets the network matcher used for bypass checks (populated during validation).
    /// </summary>
    public NetworkMaskMatcher BypassMatcher { get; private set; } = NetworkMaskMatcher.DenyAll;

    /// <summary>
    /// Validates provided configuration and normalises collections.
    /// </summary>
    public void Validate()
    {
        if (string.IsNullOrWhiteSpace(Authority))
        {
            throw new InvalidOperationException("Resource server authentication requires an Authority URL.");
        }

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

        if (RequireHttpsMetadata &&
            !authorityUri.IsLoopback &&
            !string.Equals(authorityUri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("Resource server Authority URL must use HTTPS when HTTPS metadata is required.");
        }

        if (BackchannelTimeout <= TimeSpan.Zero)
        {
            throw new InvalidOperationException("Resource server back-channel timeout must be greater than zero.");
        }

        if (TokenClockSkew < TimeSpan.Zero || TokenClockSkew > TimeSpan.FromMinutes(5))
        {
            throw new InvalidOperationException("Resource server token clock skew must be between 0 seconds and 5 minutes.");
        }

        AuthorityUri = authorityUri;

        NormalizeList(audiences, toLower: false);
        NormalizeList(requiredScopes, toLower: true);
        NormalizeList(bypassNetworks, toLower: false);

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

        BypassMatcher = bypassNetworks.Count == 0
            ? NetworkMaskMatcher.DenyAll
            : new NetworkMaskMatcher(bypassNetworks);
    }

    private static void NormalizeList(IList<string> values, bool toLower)
    {
        if (values.Count == 0)
        {
            return;
        }

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

        for (var index = values.Count - 1; index >= 0; index--)
        {
            var value = values[index];
            if (string.IsNullOrWhiteSpace(value))
            {
                values.RemoveAt(index);
                continue;
            }

            var trimmed = value.Trim();
            if (toLower)
            {
                trimmed = trimmed.ToLowerInvariant();
            }

            if (!seen.Add(trimmed))
            {
                values.RemoveAt(index);
                continue;
            }

            values[index] = trimmed;
        }
    }
}
