using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Driver;
using MongoDB.Driver.GridFS;
using StellaOps.Feedser.Storage.Mongo.Documents;
using StellaOps.Feedser.Storage.Mongo.Dtos;

namespace StellaOps.Feedser.Storage.Mongo;

/// <summary>
/// Periodically purges expired raw documents, associated DTO payloads, and GridFS content.
/// Complements TTL indexes by ensuring deterministic cleanup before Mongo's background sweeper runs.
/// </summary>
internal sealed class RawDocumentRetentionService : BackgroundService
{
    private readonly IMongoCollection<DocumentDocument> _documents;
    private readonly IMongoCollection<DtoDocument> _dtos;
    private readonly GridFSBucket _bucket;
    private readonly MongoStorageOptions _options;
    private readonly ILogger<RawDocumentRetentionService> _logger;
    private readonly TimeProvider _timeProvider;

    public RawDocumentRetentionService(
        IMongoDatabase database,
        IOptions<MongoStorageOptions> options,
        ILogger<RawDocumentRetentionService> logger,
        TimeProvider? timeProvider = null)
    {
        ArgumentNullException.ThrowIfNull(database);
        ArgumentNullException.ThrowIfNull(options);
        ArgumentNullException.ThrowIfNull(logger);

        _documents = database.GetCollection<DocumentDocument>(MongoStorageDefaults.Collections.Document);
        _dtos = database.GetCollection<DtoDocument>(MongoStorageDefaults.Collections.Dto);
        _bucket = new GridFSBucket(database, new GridFSBucketOptions
        {
            BucketName = "documents",
            ReadConcern = database.Settings.ReadConcern,
            WriteConcern = database.Settings.WriteConcern,
        });

        _options = options.Value;
        _logger = logger;
        _timeProvider = timeProvider ?? TimeProvider.System;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        if (_options.RawDocumentRetention <= TimeSpan.Zero)
        {
            _logger.LogInformation("Raw document retention disabled; purge service idle.");
            return;
        }

        var sweepInterval = _options.RawDocumentRetentionSweepInterval > TimeSpan.Zero
            ? _options.RawDocumentRetentionSweepInterval
            : TimeSpan.FromHours(6);

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await SweepExpiredDocumentsAsync(stoppingToken).ConfigureAwait(false);
            }
            catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
            {
                break;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Raw document retention sweep failed");
            }

            try
            {
                await Task.Delay(sweepInterval, stoppingToken).ConfigureAwait(false);
            }
            catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
            {
                break;
            }
        }
    }

    internal async Task<int> SweepExpiredDocumentsAsync(CancellationToken cancellationToken)
    {
        var grace = _options.RawDocumentRetentionTtlGrace >= TimeSpan.Zero
            ? _options.RawDocumentRetentionTtlGrace
            : TimeSpan.Zero;
        var threshold = _timeProvider.GetUtcNow() + grace;

        var filterBuilder = Builders<DocumentDocument>.Filter;
        var filter = filterBuilder.And(
            filterBuilder.Ne(doc => doc.ExpiresAt, null),
            filterBuilder.Lte(doc => doc.ExpiresAt, threshold.UtcDateTime));

        var removed = 0;

        while (!cancellationToken.IsCancellationRequested)
        {
            var batch = await _documents
                .Find(filter)
                .SortBy(doc => doc.ExpiresAt)
                .Limit(200)
                .ToListAsync(cancellationToken)
                .ConfigureAwait(false);

            if (batch.Count == 0)
            {
                break;
            }

            foreach (var document in batch)
            {
                if (cancellationToken.IsCancellationRequested)
                {
                    break;
                }

                await PurgeDocumentAsync(document, cancellationToken).ConfigureAwait(false);
                removed++;
            }
        }

        if (removed > 0)
        {
            _logger.LogInformation("Purged {Count} expired raw documents (threshold <= {Threshold})", removed, threshold);
        }

        return removed;
    }

    private async Task PurgeDocumentAsync(DocumentDocument document, CancellationToken cancellationToken)
    {
        if (document.GridFsId.HasValue)
        {
            try
            {
                await _bucket.DeleteAsync(document.GridFsId.Value, cancellationToken).ConfigureAwait(false);
            }
            catch (GridFSFileNotFoundException)
            {
                // already removed or TTL swept
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Failed to delete GridFS payload {GridFsId} for document {DocumentId}", document.GridFsId, document.Id);
            }
        }

        await _dtos.DeleteManyAsync(x => x.DocumentId == document.Id, cancellationToken).ConfigureAwait(false);
        await _documents.DeleteOneAsync(x => x.Id == document.Id, cancellationToken).ConfigureAwait(false);
    }
}
