using System;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Driver.Core.Clusters;
using MongoDB.Driver.GridFS;
using StellaOps.Vexer.Core;

namespace StellaOps.Vexer.Storage.Mongo;

public sealed class MongoVexRawStore : IVexRawStore
{
    private readonly IMongoClient _client;
    private readonly IMongoCollection<VexRawDocumentRecord> _collection;
    private readonly GridFSBucket _bucket;
    private readonly VexMongoStorageOptions _options;

    public MongoVexRawStore(
        IMongoClient client,
        IMongoDatabase database,
        IOptions<VexMongoStorageOptions> options)
    {
        _client = client ?? throw new ArgumentNullException(nameof(client));
        ArgumentNullException.ThrowIfNull(database);
        ArgumentNullException.ThrowIfNull(options);

        _options = options.Value;
        Validator.ValidateObject(_options, new ValidationContext(_options), validateAllProperties: true);

        VexMongoMappingRegistry.Register();
        _collection = database.GetCollection<VexRawDocumentRecord>(VexMongoCollectionNames.Raw);
        _bucket = new GridFSBucket(database, new GridFSBucketOptions
        {
            BucketName = _options.RawBucketName,
            ReadConcern = database.Settings.ReadConcern,
            ReadPreference = database.Settings.ReadPreference,
            WriteConcern = database.Settings.WriteConcern,
        });
    }

    public async ValueTask StoreAsync(VexRawDocument document, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(document);

        var threshold = _options.GridFsInlineThresholdBytes;
        var useInline = threshold == 0 || document.Content.Length <= threshold;
        string? newGridId = null;
        string? oldGridIdToDelete = null;

        if (!useInline)
        {
            newGridId = await UploadToGridFsAsync(document, cancellationToken).ConfigureAwait(false);
        }

        using var session = await _client.StartSessionAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
        var supportsTransactions = session.Client.Cluster.Description.Type != ClusterType.Standalone;

        var startedTransaction = false;
        if (supportsTransactions)
        {
            try
            {
                session.StartTransaction();
                startedTransaction = true;
            }
            catch (NotSupportedException)
            {
                supportsTransactions = false;
            }
        }

        try
        {
            var filter = Builders<VexRawDocumentRecord>.Filter.Eq(x => x.Id, document.Digest);
            var existing = await _collection
                .Find(session, filter)
                .FirstOrDefaultAsync(cancellationToken)
                .ConfigureAwait(false);

            var record = VexRawDocumentRecord.FromDomain(document, includeContent: useInline);
            record.GridFsObjectId = useInline ? null : newGridId;

            await _collection
                .ReplaceOneAsync(
                    session,
                    filter,
                    record,
                    new ReplaceOptions { IsUpsert = true },
                    cancellationToken)
                .ConfigureAwait(false);

            if (existing?.GridFsObjectId is string oldGridId && !string.IsNullOrWhiteSpace(oldGridId))
            {
                if (useInline || !string.Equals(newGridId, oldGridId, StringComparison.Ordinal))
                {
                    oldGridIdToDelete = oldGridId;
                }
            }

            if (startedTransaction)
            {
                await session.CommitTransactionAsync(cancellationToken).ConfigureAwait(false);
            }
        }
        catch
        {
            if (startedTransaction && session.IsInTransaction)
            {
                await session.AbortTransactionAsync(cancellationToken).ConfigureAwait(false);
            }

            if (!useInline && !string.IsNullOrWhiteSpace(newGridId))
            {
                await DeleteFromGridFsAsync(newGridId, cancellationToken).ConfigureAwait(false);
            }

            throw;
        }

        if (!string.IsNullOrWhiteSpace(oldGridIdToDelete))
        {
            await DeleteFromGridFsAsync(oldGridIdToDelete!, cancellationToken).ConfigureAwait(false);
        }
    }

    public async ValueTask<VexRawDocument?> FindByDigestAsync(string digest, CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(digest))
        {
            throw new ArgumentException("Digest must be provided.", nameof(digest));
        }

        var trimmed = digest.Trim();
        var filter = Builders<VexRawDocumentRecord>.Filter.Eq(x => x.Id, trimmed);
        var record = await _collection.Find(filter).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
        if (record is null)
        {
            return null;
        }

        if (!string.IsNullOrWhiteSpace(record.GridFsObjectId))
        {
            var bytes = await DownloadFromGridFsAsync(record.GridFsObjectId, cancellationToken).ConfigureAwait(false);
            return record.ToDomain(new ReadOnlyMemory<byte>(bytes));
        }

        return record.ToDomain();
    }

    private async Task<string?> UploadToGridFsAsync(VexRawDocument document, CancellationToken cancellationToken)
    {
        using var stream = new MemoryStream(document.Content.ToArray(), writable: false);
        var metadata = new BsonDocument
        {
            { "providerId", document.ProviderId },
            { "format", document.Format.ToString().ToLowerInvariant() },
            { "sourceUri", document.SourceUri.ToString() },
            { "retrievedAt", document.RetrievedAt.UtcDateTime },
        };

        var options = new GridFSUploadOptions { Metadata = metadata };
        var objectId = await _bucket
            .UploadFromStreamAsync(document.Digest, stream, options, cancellationToken)
            .ConfigureAwait(false);

        return objectId.ToString();
    }

    private async Task DeleteFromGridFsAsync(string gridFsObjectId, CancellationToken cancellationToken)
    {
        if (!ObjectId.TryParse(gridFsObjectId, out var objectId))
        {
            return;
        }

        try
        {
            await _bucket.DeleteAsync(objectId, cancellationToken).ConfigureAwait(false);
        }
        catch (GridFSFileNotFoundException)
        {
            // file already removed by TTL or manual cleanup
        }
    }

    private async Task<byte[]> DownloadFromGridFsAsync(string gridFsObjectId, CancellationToken cancellationToken)
    {
        if (!ObjectId.TryParse(gridFsObjectId, out var objectId))
        {
            return Array.Empty<byte>();
        }

        return await _bucket.DownloadAsBytesAsync(objectId, cancellationToken: cancellationToken).ConfigureAwait(false);
    }
}
