using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using OpenIddict.Abstractions;
using OpenIddict.Server;
using StellaOps.Authority.RateLimiting;
using StellaOps.Cryptography.Audit;

namespace StellaOps.Authority.OpenIddict.Handlers;

internal static class ClientCredentialsAuditHelper
{
    internal static string EnsureCorrelationId(OpenIddictServerTransaction transaction)
    {
        ArgumentNullException.ThrowIfNull(transaction);

        if (transaction.Properties.TryGetValue(AuthorityOpenIddictConstants.AuditCorrelationProperty, out var value) &&
            value is string existing &&
            !string.IsNullOrWhiteSpace(existing))
        {
            return existing;
        }

        var correlation = Activity.Current?.TraceId.ToString() ??
                          Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);

        transaction.Properties[AuthorityOpenIddictConstants.AuditCorrelationProperty] = correlation;
        return correlation;
    }

    internal static AuthEventRecord CreateRecord(
        TimeProvider timeProvider,
        OpenIddictServerTransaction transaction,
        AuthorityRateLimiterMetadata? metadata,
        string? clientSecret,
        AuthEventOutcome outcome,
        string? reason,
        string? clientId,
        string? providerName,
        bool? confidential,
        IReadOnlyList<string> requestedScopes,
        IReadOnlyList<string> grantedScopes,
        string? invalidScope,
        IEnumerable<AuthEventProperty>? extraProperties = null,
        string? eventType = null)
    {
        ArgumentNullException.ThrowIfNull(timeProvider);
        ArgumentNullException.ThrowIfNull(transaction);

        var correlationId = EnsureCorrelationId(transaction);
        var client = BuildClient(clientId, providerName);
        var network = BuildNetwork(metadata);
        var normalizedGranted = NormalizeScopes(grantedScopes);
        var properties = BuildProperties(confidential, requestedScopes, invalidScope, extraProperties);

        return new AuthEventRecord
        {
            EventType = string.IsNullOrWhiteSpace(eventType) ? "authority.client_credentials.grant" : eventType,
            OccurredAt = timeProvider.GetUtcNow(),
            CorrelationId = correlationId,
            Outcome = outcome,
            Reason = Normalize(reason),
            Subject = null,
            Client = client,
            Scopes = normalizedGranted,
            Network = network,
            Properties = properties
        };
    }

    internal static AuthEventRecord CreateTamperRecord(
        TimeProvider timeProvider,
        OpenIddictServerTransaction transaction,
        AuthorityRateLimiterMetadata? metadata,
        string? clientId,
        string? providerName,
        bool? confidential,
        IEnumerable<string> unexpectedParameters)
    {
        var properties = new List<AuthEventProperty>
        {
            new()
            {
                Name = "request.tampered",
                Value = ClassifiedString.Public("true")
            }
        };

        if (confidential.HasValue)
        {
            properties.Add(new AuthEventProperty
            {
                Name = "client.confidential",
                Value = ClassifiedString.Public(confidential.Value ? "true" : "false")
            });
        }

        if (unexpectedParameters is not null)
        {
            foreach (var parameter in unexpectedParameters)
            {
                if (string.IsNullOrWhiteSpace(parameter))
                {
                    continue;
                }

                properties.Add(new AuthEventProperty
                {
                    Name = "request.unexpected_parameter",
                    Value = ClassifiedString.Public(parameter)
                });
            }
        }

        var reason = unexpectedParameters is null
            ? "Unexpected parameters supplied to client credentials request."
            : $"Unexpected parameters supplied to client credentials request: {string.Join(", ", unexpectedParameters)}.";

        return CreateRecord(
            timeProvider,
            transaction,
            metadata,
            clientSecret: null,
            outcome: AuthEventOutcome.Failure,
            reason: reason,
            clientId: clientId,
            providerName: providerName,
            confidential: confidential,
            requestedScopes: Array.Empty<string>(),
            grantedScopes: Array.Empty<string>(),
            invalidScope: null,
            extraProperties: properties,
            eventType: "authority.token.tamper");
    }

    private static AuthEventClient? BuildClient(string? clientId, string? providerName)
    {
        if (string.IsNullOrWhiteSpace(clientId) && string.IsNullOrWhiteSpace(providerName))
        {
            return null;
        }

        return new AuthEventClient
        {
            ClientId = ClassifiedString.Personal(Normalize(clientId)),
            Name = ClassifiedString.Empty,
            Provider = ClassifiedString.Public(Normalize(providerName))
        };
    }

    private static AuthEventNetwork? BuildNetwork(AuthorityRateLimiterMetadata? metadata)
    {
        var remote = Normalize(metadata?.RemoteIp);
        var forwarded = Normalize(metadata?.ForwardedFor);
        var userAgent = Normalize(metadata?.UserAgent);

        if (string.IsNullOrWhiteSpace(remote) && string.IsNullOrWhiteSpace(forwarded) && string.IsNullOrWhiteSpace(userAgent))
        {
            return null;
        }

        return new AuthEventNetwork
        {
            RemoteAddress = ClassifiedString.Personal(remote),
            ForwardedFor = ClassifiedString.Personal(forwarded),
            UserAgent = ClassifiedString.Personal(userAgent)
        };
    }

    private static IReadOnlyList<AuthEventProperty> BuildProperties(
        bool? confidential,
        IReadOnlyList<string> requestedScopes,
        string? invalidScope,
        IEnumerable<AuthEventProperty>? extraProperties)
    {
        var properties = new List<AuthEventProperty>();

        if (confidential.HasValue)
        {
            properties.Add(new AuthEventProperty
            {
                Name = "client.confidential",
                Value = ClassifiedString.Public(confidential.Value ? "true" : "false")
            });
        }

        var normalizedRequested = NormalizeScopes(requestedScopes);
        if (normalizedRequested is { Count: > 0 })
        {
            foreach (var scope in normalizedRequested)
            {
                if (string.IsNullOrWhiteSpace(scope))
                {
                    continue;
                }

                properties.Add(new AuthEventProperty
                {
                    Name = "scope.requested",
                    Value = ClassifiedString.Public(scope)
                });
            }
        }

        if (!string.IsNullOrWhiteSpace(invalidScope))
        {
            properties.Add(new AuthEventProperty
            {
                Name = "scope.invalid",
                Value = ClassifiedString.Public(invalidScope)
            });
        }

        if (extraProperties is not null)
        {
            foreach (var property in extraProperties)
            {
                if (property is null || string.IsNullOrWhiteSpace(property.Name))
                {
                    continue;
                }

                properties.Add(property);
            }
        }

        return properties.Count == 0 ? Array.Empty<AuthEventProperty>() : properties;
    }

    private static IReadOnlyList<string> NormalizeScopes(IReadOnlyList<string>? scopes)
    {
        if (scopes is null || scopes.Count == 0)
        {
            return Array.Empty<string>();
        }

        var normalized = scopes
            .Where(static scope => !string.IsNullOrWhiteSpace(scope))
            .Select(static scope => scope.Trim())
            .Where(static scope => scope.Length > 0)
            .Distinct(StringComparer.Ordinal)
            .OrderBy(static scope => scope, StringComparer.Ordinal)
            .ToArray();

        return normalized.Length == 0 ? Array.Empty<string>() : normalized;
    }

    private static string? Normalize(string? value)
        => string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}
