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

namespace StellaOps.Authority.Bootstrap;

internal sealed class BootstrapInviteCleanupService : BackgroundService
{
    private readonly IAuthorityBootstrapInviteStore inviteStore;
    private readonly IAuthEventSink auditSink;
    private readonly TimeProvider timeProvider;
    private readonly ILogger<BootstrapInviteCleanupService> logger;
    private readonly TimeSpan interval;

    public BootstrapInviteCleanupService(
        IAuthorityBootstrapInviteStore inviteStore,
        IAuthEventSink auditSink,
        TimeProvider timeProvider,
        ILogger<BootstrapInviteCleanupService> logger)
    {
        this.inviteStore = inviteStore ?? throw new ArgumentNullException(nameof(inviteStore));
        this.auditSink = auditSink ?? throw new ArgumentNullException(nameof(auditSink));
        this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
        this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
        interval = TimeSpan.FromMinutes(5);
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var timer = new PeriodicTimer(interval);

        try
        {
            while (await timer.WaitForNextTickAsync(stoppingToken).ConfigureAwait(false))
            {
                await SweepExpiredInvitesAsync(stoppingToken).ConfigureAwait(false);
            }
        }
        catch (OperationCanceledException)
        {
            // Shutdown requested.
        }
        finally
        {
            timer.Dispose();
        }
    }

    internal async Task SweepExpiredInvitesAsync(CancellationToken cancellationToken)
    {
        var now = timeProvider.GetUtcNow();
        var expired = await inviteStore.ExpireAsync(now, cancellationToken).ConfigureAwait(false);
        if (expired.Count == 0)
        {
            return;
        }

        logger.LogInformation("Expired {Count} bootstrap invite(s).", expired.Count);

        foreach (var invite in expired)
        {
            var record = new AuthEventRecord
            {
                EventType = "authority.bootstrap.invite.expired",
                OccurredAt = now,
                CorrelationId = Guid.NewGuid().ToString("N"),
                Outcome = AuthEventOutcome.Success,
                Reason = "Invite expired before consumption.",
                Subject = null,
                Client = null,
                Scopes = Array.Empty<string>(),
                Network = null,
                Properties = BuildInviteProperties(invite)
            };

            await auditSink.WriteAsync(record, cancellationToken).ConfigureAwait(false);
        }
    }

    private static AuthEventProperty[] BuildInviteProperties(AuthorityBootstrapInviteDocument invite)
    {
        var properties = new List<AuthEventProperty>
        {
            new() { Name = "invite.token", Value = ClassifiedString.Public(invite.Token) },
            new() { Name = "invite.type", Value = ClassifiedString.Public(invite.Type) },
            new() { Name = "invite.expires_at", Value = ClassifiedString.Public(invite.ExpiresAt.ToString("O", CultureInfo.InvariantCulture)) }
        };

        if (!string.IsNullOrWhiteSpace(invite.Provider))
        {
            properties.Add(new AuthEventProperty { Name = "invite.provider", Value = ClassifiedString.Public(invite.Provider) });
        }

        if (!string.IsNullOrWhiteSpace(invite.Target))
        {
            properties.Add(new AuthEventProperty { Name = "invite.target", Value = ClassifiedString.Public(invite.Target) });
        }

        return properties.ToArray();
    }
}
