using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StellaOps.Authority.Storage.Mongo.Documents;
using StellaOps.Authority.Storage.Mongo.Stores;
using StellaOps.Cryptography.Audit;

namespace StellaOps.Authority.Audit;

internal sealed class AuthorityAuditSink : IAuthEventSink
{
    private static readonly StringComparer OrdinalComparer = StringComparer.Ordinal;

    private readonly IAuthorityLoginAttemptStore loginAttemptStore;
    private readonly ILogger<AuthorityAuditSink> logger;

    public AuthorityAuditSink(
        IAuthorityLoginAttemptStore loginAttemptStore,
        ILogger<AuthorityAuditSink> logger)
    {
        this.loginAttemptStore = loginAttemptStore ?? throw new ArgumentNullException(nameof(loginAttemptStore));
        this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async ValueTask WriteAsync(AuthEventRecord record, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(record);

        var logState = BuildLogScope(record);
        using (logger.BeginScope(logState))
        {
            logger.LogInformation(
                "Authority audit event {EventType} emitted with outcome {Outcome}.",
                record.EventType,
                NormalizeOutcome(record.Outcome));
        }

        var document = MapToDocument(record);
        await loginAttemptStore.InsertAsync(document, cancellationToken).ConfigureAwait(false);
    }

    private static AuthorityLoginAttemptDocument MapToDocument(AuthEventRecord record)
    {
        var document = new AuthorityLoginAttemptDocument
        {
            EventType = record.EventType,
            Outcome = NormalizeOutcome(record.Outcome),
            CorrelationId = Normalize(record.CorrelationId),
            SubjectId = record.Subject?.SubjectId.Value,
            Username = record.Subject?.Username.Value,
            ClientId = record.Client?.ClientId.Value,
            Plugin = record.Client?.Provider.Value,
            Successful = record.Outcome == AuthEventOutcome.Success,
            Reason = Normalize(record.Reason),
            RemoteAddress = record.Network?.RemoteAddress.Value ?? record.Network?.ForwardedFor.Value,
            OccurredAt = record.OccurredAt
        };

        if (record.Scopes is { Count: > 0 })
        {
            document.Scopes = record.Scopes
                .Where(static scope => !string.IsNullOrWhiteSpace(scope))
                .Select(static scope => scope.Trim())
                .Where(static scope => scope.Length > 0)
                .Distinct(OrdinalComparer)
                .OrderBy(static scope => scope, OrdinalComparer)
                .ToList();
        }

        var properties = new List<AuthorityLoginAttemptPropertyDocument>();

        if (record.Subject is { } subject)
        {
            AddProperty(properties, "subject.display_name", subject.DisplayName);
            AddProperty(properties, "subject.realm", subject.Realm);

            if (subject.Attributes is { Count: > 0 })
            {
                foreach (var attribute in subject.Attributes)
                {
                    AddProperty(properties, $"subject.attr.{attribute.Name}", attribute.Value);
                }
            }
        }

        if (record.Client is { } client)
        {
            AddProperty(properties, "client.name", client.Name);
        }

        if (record.Network is { } network)
        {
            AddProperty(properties, "network.remote", network.RemoteAddress);
            AddProperty(properties, "network.forwarded_for", network.ForwardedFor);
            AddProperty(properties, "network.user_agent", network.UserAgent);
        }

        if (record.Properties is { Count: > 0 })
        {
            foreach (var property in record.Properties)
            {
                AddProperty(properties, property.Name, property.Value);
            }
        }

        if (properties.Count > 0)
        {
            document.Properties = properties;
        }

        return document;
    }

    private static IReadOnlyCollection<KeyValuePair<string, object?>> BuildLogScope(AuthEventRecord record)
    {
        var entries = new List<KeyValuePair<string, object?>>
        {
            new("audit.event_type", record.EventType),
            new("audit.outcome", NormalizeOutcome(record.Outcome)),
            new("audit.timestamp", record.OccurredAt.ToString("O", CultureInfo.InvariantCulture))
        };

        AddValue(entries, "audit.correlation_id", Normalize(record.CorrelationId));
        AddValue(entries, "audit.reason", Normalize(record.Reason));

        if (record.Subject is { } subject)
        {
            AddClassified(entries, "audit.subject.id", subject.SubjectId);
            AddClassified(entries, "audit.subject.username", subject.Username);
            AddClassified(entries, "audit.subject.display_name", subject.DisplayName);
            AddClassified(entries, "audit.subject.realm", subject.Realm);
        }

        if (record.Client is { } client)
        {
            AddClassified(entries, "audit.client.id", client.ClientId);
            AddClassified(entries, "audit.client.name", client.Name);
            AddClassified(entries, "audit.client.provider", client.Provider);
        }

        if (record.Network is { } network)
        {
            AddClassified(entries, "audit.network.remote", network.RemoteAddress);
            AddClassified(entries, "audit.network.forwarded_for", network.ForwardedFor);
            AddClassified(entries, "audit.network.user_agent", network.UserAgent);
        }

        if (record.Scopes is { Count: > 0 })
        {
            entries.Add(new KeyValuePair<string, object?>(
                "audit.scopes",
                record.Scopes.Where(static scope => !string.IsNullOrWhiteSpace(scope)).ToArray()));
        }

        if (record.Properties is { Count: > 0 })
        {
            foreach (var property in record.Properties)
            {
                AddClassified(entries, $"audit.property.{property.Name}", property.Value);
            }
        }

        return entries;
    }

    private static void AddProperty(ICollection<AuthorityLoginAttemptPropertyDocument> properties, string name, ClassifiedString value)
    {
        if (!value.HasValue || string.IsNullOrWhiteSpace(name))
        {
            return;
        }

        properties.Add(new AuthorityLoginAttemptPropertyDocument
        {
            Name = name,
            Value = value.Value,
            Classification = NormalizeClassification(value.Classification)
        });
    }

    private static void AddValue(ICollection<KeyValuePair<string, object?>> entries, string key, string? value)
    {
        if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(value))
        {
            return;
        }

        entries.Add(new KeyValuePair<string, object?>(key, value));
    }

    private static void AddClassified(ICollection<KeyValuePair<string, object?>> entries, string key, ClassifiedString value)
    {
        if (!value.HasValue || string.IsNullOrWhiteSpace(key))
        {
            return;
        }

        entries.Add(new KeyValuePair<string, object?>(key, new
        {
            value.Value,
            classification = NormalizeClassification(value.Classification)
        }));
    }

    private static string NormalizeOutcome(AuthEventOutcome outcome)
        => outcome switch
        {
            AuthEventOutcome.Success => "success",
            AuthEventOutcome.Failure => "failure",
            AuthEventOutcome.LockedOut => "locked_out",
            AuthEventOutcome.RateLimited => "rate_limited",
            AuthEventOutcome.Error => "error",
            _ => "unknown"
        };

    private static string NormalizeClassification(AuthEventDataClassification classification)
        => classification switch
        {
            AuthEventDataClassification.Personal => "personal",
            AuthEventDataClassification.Sensitive => "sensitive",
            _ => "none"
        };

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