using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
using MongoDB.Bson;
using MongoDB.Driver;

namespace StellaOps.Feedser.Storage.Mongo.Aliases;

public sealed class AliasStore : IAliasStore
{
    private readonly IMongoCollection<AliasDocument> _collection;
    private readonly ILogger<AliasStore> _logger;

    public AliasStore(IMongoDatabase database, ILogger<AliasStore> logger)
    {
        _collection = (database ?? throw new ArgumentNullException(nameof(database)))
            .GetCollection<AliasDocument>(MongoStorageDefaults.Collections.Alias);
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async Task<AliasUpsertResult> ReplaceAsync(
        string advisoryKey,
        IEnumerable<AliasEntry> aliases,
        DateTimeOffset updatedAt,
        CancellationToken cancellationToken)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(advisoryKey);

        var aliasList = Normalize(aliases).ToArray();
        var deleteFilter = Builders<AliasDocument>.Filter.Eq(x => x.AdvisoryKey, advisoryKey);
        await _collection.DeleteManyAsync(deleteFilter, cancellationToken).ConfigureAwait(false);

        if (aliasList.Length > 0)
        {
            var documents = new List<AliasDocument>(aliasList.Length);
            var updatedAtUtc = updatedAt.ToUniversalTime().UtcDateTime;
            foreach (var alias in aliasList)
            {
                documents.Add(new AliasDocument
                {
                    Id = ObjectId.GenerateNewId(),
                    AdvisoryKey = advisoryKey,
                    Scheme = alias.Scheme,
                    Value = alias.Value,
                    UpdatedAt = updatedAtUtc,
                });
            }

            if (documents.Count > 0)
            {
                await _collection.InsertManyAsync(
                    documents,
                    new InsertManyOptions { IsOrdered = false },
                    cancellationToken).ConfigureAwait(false);
            }
        }

        if (aliasList.Length == 0)
        {
            return new AliasUpsertResult(advisoryKey, Array.Empty<AliasCollision>());
        }

        var collisions = new List<AliasCollision>();
        foreach (var alias in aliasList)
        {
            var filter = Builders<AliasDocument>.Filter.Eq(x => x.Scheme, alias.Scheme)
                & Builders<AliasDocument>.Filter.Eq(x => x.Value, alias.Value);

            using var cursor = await _collection.FindAsync(filter, cancellationToken: cancellationToken).ConfigureAwait(false);
            var advisoryKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            while (await cursor.MoveNextAsync(cancellationToken).ConfigureAwait(false))
            {
                foreach (var document in cursor.Current)
                {
                    advisoryKeys.Add(document.AdvisoryKey);
                }
            }

            if (advisoryKeys.Count <= 1)
            {
                continue;
            }

            var collision = new AliasCollision(alias.Scheme, alias.Value, advisoryKeys.ToArray());
            collisions.Add(collision);
            AliasStoreMetrics.RecordCollision(alias.Scheme, advisoryKeys.Count);
            _logger.LogWarning(
                "Alias collision detected for {Scheme}:{Value}; advisories: {Advisories}",
                alias.Scheme,
                alias.Value,
                string.Join(", ", advisoryKeys));
        }

        return new AliasUpsertResult(advisoryKey, collisions);
    }

    public async Task<IReadOnlyList<AliasRecord>> GetByAliasAsync(string scheme, string value, CancellationToken cancellationToken)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(scheme);
        ArgumentException.ThrowIfNullOrWhiteSpace(value);

        var normalizedScheme = NormalizeScheme(scheme);
        var normalizedValue = value.Trim();
        var filter = Builders<AliasDocument>.Filter.Eq(x => x.Scheme, normalizedScheme)
            & Builders<AliasDocument>.Filter.Eq(x => x.Value, normalizedValue);

        var documents = await _collection.Find(filter).ToListAsync(cancellationToken).ConfigureAwait(false);
        return documents.Select(static d => d.ToRecord()).ToArray();
    }

    public async Task<IReadOnlyList<AliasRecord>> GetByAdvisoryAsync(string advisoryKey, CancellationToken cancellationToken)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(advisoryKey);
        var filter = Builders<AliasDocument>.Filter.Eq(x => x.AdvisoryKey, advisoryKey);
        var documents = await _collection.Find(filter).ToListAsync(cancellationToken).ConfigureAwait(false);
        return documents.Select(static d => d.ToRecord()).ToArray();
    }

    private static IEnumerable<AliasEntry> Normalize(IEnumerable<AliasEntry> aliases)
    {
        if (aliases is null)
        {
            yield break;
        }

        var seen = new HashSet<string>(StringComparer.Ordinal);
        foreach (var alias in aliases)
        {
            if (alias is null)
            {
                continue;
            }

            var scheme = NormalizeScheme(alias.Scheme);
            var value = alias.Value?.Trim();
            if (string.IsNullOrEmpty(value))
            {
                continue;
            }

            var key = $"{scheme}\u0001{value}";
            if (!seen.Add(key))
            {
                continue;
            }

            yield return new AliasEntry(scheme, value);
        }
    }

    private static string NormalizeScheme(string scheme)
    {
        return string.IsNullOrWhiteSpace(scheme)
            ? AliasStoreConstants.UnscopedScheme
            : scheme.Trim().ToUpperInvariant();
    }
}
