using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Feedser.Storage.Mongo.Aliases;

namespace StellaOps.Feedser.Merge.Services;

public sealed class AliasGraphResolver
{
    private readonly IAliasStore _aliasStore;

    public AliasGraphResolver(IAliasStore aliasStore)
    {
        _aliasStore = aliasStore ?? throw new ArgumentNullException(nameof(aliasStore));
    }

    public async Task<AliasIdentityResult> ResolveAsync(string advisoryKey, CancellationToken cancellationToken)
    {
        ArgumentException.ThrowIfNullOrEmpty(advisoryKey);
        var aliases = await _aliasStore.GetByAdvisoryAsync(advisoryKey, cancellationToken).ConfigureAwait(false);
        var collisions = new List<AliasCollision>();

        foreach (var alias in aliases)
        {
            var candidates = await _aliasStore.GetByAliasAsync(alias.Scheme, alias.Value, cancellationToken).ConfigureAwait(false);
            var advisoryKeys = candidates
                .Select(static candidate => candidate.AdvisoryKey)
                .Where(static key => !string.IsNullOrWhiteSpace(key))
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .ToArray();

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

            collisions.Add(new AliasCollision(alias.Scheme, alias.Value, advisoryKeys));
        }

        var unique = new Dictionary<string, AliasCollision>(StringComparer.Ordinal);
        foreach (var collision in collisions)
        {
            var key = $"{collision.Scheme}\u0001{collision.Value}";
            if (!unique.ContainsKey(key))
            {
                unique[key] = collision;
            }
        }

        var distinctCollisions = unique.Values.ToArray();

        return new AliasIdentityResult(advisoryKey, aliases, distinctCollisions);
    }

    public async Task<AliasComponent> BuildComponentAsync(string advisoryKey, CancellationToken cancellationToken)
    {
        ArgumentException.ThrowIfNullOrEmpty(advisoryKey);

        var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        var queue = new Queue<string>();
        var collisionMap = new Dictionary<string, AliasCollision>(StringComparer.Ordinal);

        var aliasCache = new Dictionary<string, IReadOnlyList<AliasRecord>>(StringComparer.OrdinalIgnoreCase);
        queue.Enqueue(advisoryKey);

        while (queue.Count > 0)
        {
            cancellationToken.ThrowIfCancellationRequested();
            var current = queue.Dequeue();
            if (!visited.Add(current))
            {
                continue;
            }

            var aliases = await GetAliasesAsync(current, cancellationToken, aliasCache).ConfigureAwait(false);
            aliasCache[current] = aliases;
            foreach (var alias in aliases)
            {
                var aliasRecords = await GetAdvisoriesForAliasAsync(alias.Scheme, alias.Value, cancellationToken).ConfigureAwait(false);
                var advisoryKeys = aliasRecords
                    .Select(static record => record.AdvisoryKey)
                    .Where(static key => !string.IsNullOrWhiteSpace(key))
                    .Distinct(StringComparer.OrdinalIgnoreCase)
                    .ToArray();

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

                foreach (var candidate in advisoryKeys)
                {
                    if (!visited.Contains(candidate))
                    {
                        queue.Enqueue(candidate);
                    }
                }

                var collision = new AliasCollision(alias.Scheme, alias.Value, advisoryKeys);
                var key = $"{collision.Scheme}\u0001{collision.Value}";
                collisionMap.TryAdd(key, collision);
            }
        }

        var aliasMap = new Dictionary<string, IReadOnlyList<AliasRecord>>(aliasCache, StringComparer.OrdinalIgnoreCase);
        return new AliasComponent(advisoryKey, visited.ToArray(), collisionMap.Values.ToArray(), aliasMap);
    }

    private async Task<IReadOnlyList<AliasRecord>> GetAliasesAsync(
        string advisoryKey,
        CancellationToken cancellationToken,
        IDictionary<string, IReadOnlyList<AliasRecord>> cache)
    {
        if (cache.TryGetValue(advisoryKey, out var cached))
        {
            return cached;
        }

        var aliases = await _aliasStore.GetByAdvisoryAsync(advisoryKey, cancellationToken).ConfigureAwait(false);
        cache[advisoryKey] = aliases;
        return aliases;
    }

    private Task<IReadOnlyList<AliasRecord>> GetAdvisoriesForAliasAsync(
        string scheme,
        string value,
        CancellationToken cancellationToken)
        => _aliasStore.GetByAliasAsync(scheme, value, cancellationToken);
}

public sealed record AliasIdentityResult(string AdvisoryKey, IReadOnlyList<AliasRecord> Aliases, IReadOnlyList<AliasCollision> Collisions);

public sealed record AliasComponent(
    string SeedAdvisoryKey,
    IReadOnlyList<string> AdvisoryKeys,
    IReadOnlyList<AliasCollision> Collisions,
    IReadOnlyDictionary<string, IReadOnlyList<AliasRecord>> AliasMap);
