using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Claims;

namespace StellaOps.Auth.Abstractions;

/// <summary>
/// Fluent helper used to construct <see cref="ClaimsPrincipal"/> instances that follow StellaOps conventions.
/// </summary>
public sealed class StellaOpsPrincipalBuilder
{
    private readonly Dictionary<string, Claim> singleClaims = new(StringComparer.Ordinal);
    private readonly List<Claim> additionalClaims = new();
    private readonly HashSet<string> scopes = new(StringComparer.OrdinalIgnoreCase);
    private readonly HashSet<string> audiences = new(StringComparer.OrdinalIgnoreCase);

    private string authenticationType = StellaOpsAuthenticationDefaults.AuthenticationType;
    private string nameClaimType = ClaimTypes.Name;
    private string roleClaimType = ClaimTypes.Role;

    private string[]? cachedScopes;
    private string[]? cachedAudiences;

    /// <summary>
    /// Adds or replaces the canonical subject identifier.
    /// </summary>
    public StellaOpsPrincipalBuilder WithSubject(string subject)
        => SetSingleClaim(StellaOpsClaimTypes.Subject, subject);

    /// <summary>
    /// Adds or replaces the canonical client identifier.
    /// </summary>
    public StellaOpsPrincipalBuilder WithClientId(string clientId)
        => SetSingleClaim(StellaOpsClaimTypes.ClientId, clientId);

    /// <summary>
    /// Adds or replaces the tenant identifier claim.
    /// </summary>
    public StellaOpsPrincipalBuilder WithTenant(string tenant)
        => SetSingleClaim(StellaOpsClaimTypes.Tenant, tenant);

    /// <summary>
    /// Adds or replaces the user display name claim.
    /// </summary>
    public StellaOpsPrincipalBuilder WithName(string name)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(name);
        singleClaims[nameClaimType] = new Claim(nameClaimType, name.Trim(), ClaimValueTypes.String);
        return this;
    }

    /// <summary>
    /// Adds or replaces the identity provider claim.
    /// </summary>
    public StellaOpsPrincipalBuilder WithIdentityProvider(string identityProvider)
        => SetSingleClaim(StellaOpsClaimTypes.IdentityProvider, identityProvider);

    /// <summary>
    /// Adds or replaces the session identifier claim.
    /// </summary>
    public StellaOpsPrincipalBuilder WithSessionId(string sessionId)
        => SetSingleClaim(StellaOpsClaimTypes.SessionId, sessionId);

    /// <summary>
    /// Adds or replaces the token identifier claim.
    /// </summary>
    public StellaOpsPrincipalBuilder WithTokenId(string tokenId)
        => SetSingleClaim(StellaOpsClaimTypes.TokenId, tokenId);

    /// <summary>
    /// Adds or replaces the authentication method reference claim.
    /// </summary>
    public StellaOpsPrincipalBuilder WithAuthenticationMethod(string method)
        => SetSingleClaim(StellaOpsClaimTypes.AuthenticationMethod, method);

    /// <summary>
    /// Sets the name claim type appended when building the <see cref="ClaimsIdentity"/>.
    /// </summary>
    public StellaOpsPrincipalBuilder WithNameClaimType(string claimType)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(claimType);
        nameClaimType = claimType.Trim();
        return this;
    }

    /// <summary>
    /// Sets the role claim type appended when building the <see cref="ClaimsIdentity"/>.
    /// </summary>
    public StellaOpsPrincipalBuilder WithRoleClaimType(string claimType)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(claimType);
        roleClaimType = claimType.Trim();
        return this;
    }

    /// <summary>
    /// Sets the authentication type stamped on the <see cref="ClaimsIdentity"/>.
    /// </summary>
    public StellaOpsPrincipalBuilder WithAuthenticationType(string authenticationType)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(authenticationType);
        this.authenticationType = authenticationType.Trim();
        return this;
    }

    /// <summary>
    /// Registers the supplied scopes (normalised to lower-case, deduplicated, sorted).
    /// </summary>
    public StellaOpsPrincipalBuilder WithScopes(IEnumerable<string> scopes)
    {
        ArgumentNullException.ThrowIfNull(scopes);

        foreach (var scope in scopes)
        {
            var normalized = StellaOpsScopes.Normalize(scope);
            if (normalized is null)
            {
                continue;
            }

            if (this.scopes.Add(normalized))
            {
                cachedScopes = null;
            }
        }

        return this;
    }

    /// <summary>
    /// Registers the supplied audiences (trimmed, deduplicated, sorted).
    /// </summary>
    public StellaOpsPrincipalBuilder WithAudiences(IEnumerable<string> audiences)
    {
        ArgumentNullException.ThrowIfNull(audiences);

        foreach (var audience in audiences)
        {
            if (string.IsNullOrWhiteSpace(audience))
            {
                continue;
            }

            if (this.audiences.Add(audience.Trim()))
            {
                cachedAudiences = null;
            }
        }

        return this;
    }

    /// <summary>
    /// Adds a single audience.
    /// </summary>
    public StellaOpsPrincipalBuilder WithAudience(string audience)
        => WithAudiences(new[] { audience });

    /// <summary>
    /// Adds an arbitrary claim (no deduplication is performed).
    /// </summary>
    public StellaOpsPrincipalBuilder AddClaim(string type, string value, string valueType = ClaimValueTypes.String)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(type);
        ArgumentException.ThrowIfNullOrWhiteSpace(value);

        var trimmedType = type.Trim();
        var trimmedValue = value.Trim();

        additionalClaims.Add(new Claim(trimmedType, trimmedValue, valueType));
        return this;
    }

    /// <summary>
    /// Adds multiple claims (incoming claims are cloned to enforce value trimming).
    /// </summary>
    public StellaOpsPrincipalBuilder AddClaims(IEnumerable<Claim> claims)
    {
        ArgumentNullException.ThrowIfNull(claims);

        foreach (var claim in claims)
        {
            ArgumentNullException.ThrowIfNull(claim);
            AddClaim(claim.Type, claim.Value, claim.ValueType);
        }

        return this;
    }

    /// <summary>
    /// Adds an <c>iat</c> (issued at) claim using Unix time seconds.
    /// </summary>
    public StellaOpsPrincipalBuilder WithIssuedAt(DateTimeOffset issuedAt)
        => SetSingleClaim("iat", ToUnixTime(issuedAt));

    /// <summary>
    /// Adds an <c>nbf</c> (not before) claim using Unix time seconds.
    /// </summary>
    public StellaOpsPrincipalBuilder WithNotBefore(DateTimeOffset notBefore)
        => SetSingleClaim("nbf", ToUnixTime(notBefore));

    /// <summary>
    /// Adds an <c>exp</c> (expires) claim using Unix time seconds.
    /// </summary>
    public StellaOpsPrincipalBuilder WithExpires(DateTimeOffset expires)
        => SetSingleClaim("exp", ToUnixTime(expires));

    /// <summary>
    /// Returns the normalised scope list (deduplicated + sorted).
    /// </summary>
    public IReadOnlyCollection<string> NormalizedScopes
    {
        get
        {
            cachedScopes ??= scopes.Count == 0
                ? Array.Empty<string>()
                : scopes.OrderBy(static scope => scope, StringComparer.Ordinal).ToArray();

            return cachedScopes;
        }
    }

    /// <summary>
    /// Returns the normalised audience list (deduplicated + sorted).
    /// </summary>
    public IReadOnlyCollection<string> Audiences
    {
        get
        {
            cachedAudiences ??= audiences.Count == 0
                ? Array.Empty<string>()
                : audiences.OrderBy(static audience => audience, StringComparer.Ordinal).ToArray();

            return cachedAudiences;
        }
    }

    /// <summary>
    /// Builds the immutable <see cref="ClaimsPrincipal"/> instance based on the registered data.
    /// </summary>
    public ClaimsPrincipal Build()
    {
        var claims = new List<Claim>(
            singleClaims.Count +
            additionalClaims.Count +
            NormalizedScopes.Count * 2 +
            Audiences.Count);

        claims.AddRange(singleClaims.Values);
        claims.AddRange(additionalClaims);

        if (NormalizedScopes.Count > 0)
        {
            var joined = string.Join(' ', NormalizedScopes);
            claims.Add(new Claim(StellaOpsClaimTypes.Scope, joined, ClaimValueTypes.String));

            foreach (var scope in NormalizedScopes)
            {
                claims.Add(new Claim(StellaOpsClaimTypes.ScopeItem, scope, ClaimValueTypes.String));
            }
        }

        if (Audiences.Count > 0)
        {
            foreach (var audience in Audiences)
            {
                claims.Add(new Claim(StellaOpsClaimTypes.Audience, audience, ClaimValueTypes.String));
            }
        }

        var identity = new ClaimsIdentity(claims, authenticationType, nameClaimType, roleClaimType);
        return new ClaimsPrincipal(identity);
    }

    private StellaOpsPrincipalBuilder SetSingleClaim(string type, string value)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(value);
        var trimmedValue = value.Trim();
        singleClaims[type] = new Claim(type, trimmedValue, ClaimValueTypes.String);
        return this;
    }

    private static string ToUnixTime(DateTimeOffset value)
        => value.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture);
}
