using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Runtime.CompilerServices;
using StellaOps.Feedser.Models;

namespace StellaOps.Feedser.Merge.Identity;

/// <summary>
/// Builds an alias-driven identity graph that groups advisories referring to the same vulnerability.
/// </summary>
public sealed class AdvisoryIdentityResolver
{
    private static readonly string[] CanonicalAliasPriority =
    {
        AliasSchemes.Cve,
        AliasSchemes.Rhsa,
        AliasSchemes.Usn,
        AliasSchemes.Dsa,
        AliasSchemes.SuseSu,
        AliasSchemes.Msrc,
        AliasSchemes.CiscoSa,
        AliasSchemes.OracleCpu,
        AliasSchemes.Vmsa,
        AliasSchemes.Apsb,
        AliasSchemes.Apa,
        AliasSchemes.AppleHt,
        AliasSchemes.ChromiumPost,
        AliasSchemes.Icsa,
        AliasSchemes.Jvndb,
        AliasSchemes.Jvn,
        AliasSchemes.Bdu,
        AliasSchemes.Vu,
        AliasSchemes.Ghsa,
        AliasSchemes.OsV,
    };

    /// <summary>
    /// Groups the provided advisories into identity clusters using normalized aliases.
    /// </summary>
    public IReadOnlyList<AdvisoryIdentityCluster> Resolve(IEnumerable<Advisory> advisories)
    {
        ArgumentNullException.ThrowIfNull(advisories);

        var materialized = advisories
            .Where(static advisory => advisory is not null)
            .Distinct()
            .ToArray();

        if (materialized.Length == 0)
        {
            return Array.Empty<AdvisoryIdentityCluster>();
        }

        var aliasIndex = BuildAliasIndex(materialized);
        var visited = new HashSet<Advisory>();
        var clusters = new List<AdvisoryIdentityCluster>();

        foreach (var advisory in materialized)
        {
            if (!visited.Add(advisory))
            {
                continue;
            }

            var component = TraverseComponent(advisory, visited, aliasIndex);
            var key = DetermineCanonicalKey(component);
            var aliases = component
                .SelectMany(static entry => entry.Aliases)
                .Select(static alias => new AliasIdentity(alias.Normalized, alias.Scheme));
            clusters.Add(new AdvisoryIdentityCluster(key, component.Select(static entry => entry.Advisory), aliases));
        }

        return clusters
            .OrderBy(static cluster => cluster.AdvisoryKey, StringComparer.OrdinalIgnoreCase)
            .ToImmutableArray();
    }

    private static Dictionary<string, List<AdvisoryAliasEntry>> BuildAliasIndex(IEnumerable<Advisory> advisories)
    {
        var index = new Dictionary<string, List<AdvisoryAliasEntry>>(StringComparer.OrdinalIgnoreCase);

        foreach (var advisory in advisories)
        {
            foreach (var alias in ExtractAliases(advisory))
            {
                if (!index.TryGetValue(alias.Normalized, out var list))
                {
                    list = new List<AdvisoryAliasEntry>();
                    index[alias.Normalized] = list;
                }

                list.Add(new AdvisoryAliasEntry(advisory, alias.Normalized, alias.Scheme));
            }
        }

        return index;
    }

    private static IReadOnlyList<AliasBinding> TraverseComponent(
        Advisory root,
        HashSet<Advisory> visited,
        Dictionary<string, List<AdvisoryAliasEntry>> aliasIndex)
    {
        var stack = new Stack<Advisory>();
        stack.Push(root);

        var bindings = new Dictionary<Advisory, AliasBinding>(ReferenceEqualityComparer<Advisory>.Instance);

        while (stack.Count > 0)
        {
            var advisory = stack.Pop();

            if (!bindings.TryGetValue(advisory, out var binding))
            {
                binding = new AliasBinding(advisory);
                bindings[advisory] = binding;
            }

            foreach (var alias in ExtractAliases(advisory))
            {
                binding.AddAlias(alias.Normalized, alias.Scheme);

                if (!aliasIndex.TryGetValue(alias.Normalized, out var neighbors))
                {
                    continue;
                }

                foreach (var neighbor in neighbors.Select(static entry => entry.Advisory))
                {
                    if (visited.Add(neighbor))
                    {
                        stack.Push(neighbor);
                    }

                    if (!bindings.TryGetValue(neighbor, out var neighborBinding))
                    {
                        neighborBinding = new AliasBinding(neighbor);
                        bindings[neighbor] = neighborBinding;
                    }

                    neighborBinding.AddAlias(alias.Normalized, alias.Scheme);
                }
            }
        }

        return bindings.Values
            .OrderBy(static binding => binding.Advisory.AdvisoryKey, StringComparer.OrdinalIgnoreCase)
            .ToImmutableArray();
    }

    private static string DetermineCanonicalKey(IReadOnlyList<AliasBinding> component)
    {
        var aliases = component
            .SelectMany(static binding => binding.Aliases)
            .Where(static alias => !string.IsNullOrWhiteSpace(alias.Normalized))
            .ToArray();

        foreach (var scheme in CanonicalAliasPriority)
        {
            var candidate = aliases
                .Where(alias => string.Equals(alias.Scheme, scheme, StringComparison.OrdinalIgnoreCase))
                .Select(static alias => alias.Normalized)
                .OrderBy(static alias => alias, StringComparer.OrdinalIgnoreCase)
                .FirstOrDefault();

            if (candidate is not null)
            {
                return candidate;
            }
        }

        var fallbackAlias = aliases
            .Select(static alias => alias.Normalized)
            .OrderBy(static alias => alias, StringComparer.OrdinalIgnoreCase)
            .FirstOrDefault();

        if (!string.IsNullOrWhiteSpace(fallbackAlias))
        {
            return fallbackAlias;
        }

        var advisoryKey = component
            .Select(static binding => binding.Advisory.AdvisoryKey)
            .Where(static value => !string.IsNullOrWhiteSpace(value))
            .OrderBy(static value => value, StringComparer.OrdinalIgnoreCase)
            .FirstOrDefault();

        if (!string.IsNullOrWhiteSpace(advisoryKey))
        {
            return advisoryKey.Trim();
        }

        throw new InvalidOperationException("Unable to determine canonical advisory key for cluster.");
    }

    private static IEnumerable<AliasProjection> ExtractAliases(Advisory advisory)
    {
        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        foreach (var candidate in EnumerateAliasCandidates(advisory))
        {
            if (string.IsNullOrWhiteSpace(candidate))
            {
                continue;
            }

            var trimmed = candidate.Trim();
            if (!seen.Add(trimmed))
            {
                continue;
            }

            if (AliasSchemeRegistry.TryNormalize(trimmed, out var normalized, out var scheme) &&
                !string.IsNullOrWhiteSpace(normalized))
            {
                yield return new AliasProjection(normalized.Trim(), string.IsNullOrWhiteSpace(scheme) ? null : scheme);
            }
            else if (!string.IsNullOrWhiteSpace(normalized))
            {
                yield return new AliasProjection(normalized.Trim(), null);
            }
        }
    }

    private static IEnumerable<string> EnumerateAliasCandidates(Advisory advisory)
    {
        if (!string.IsNullOrWhiteSpace(advisory.AdvisoryKey))
        {
            yield return advisory.AdvisoryKey;
        }

        if (!advisory.Aliases.IsDefaultOrEmpty)
        {
            foreach (var alias in advisory.Aliases)
            {
                if (!string.IsNullOrWhiteSpace(alias))
                {
                    yield return alias;
                }
            }
        }
    }

    private readonly record struct AdvisoryAliasEntry(Advisory Advisory, string Normalized, string? Scheme);

    private readonly record struct AliasProjection(string Normalized, string? Scheme);

    private sealed class AliasBinding
    {
        private readonly HashSet<AliasProjection> _aliases = new(HashSetAliasComparer.Instance);

        public AliasBinding(Advisory advisory)
        {
            Advisory = advisory ?? throw new ArgumentNullException(nameof(advisory));
        }

        public Advisory Advisory { get; }

        public IReadOnlyCollection<AliasProjection> Aliases => _aliases;

        public void AddAlias(string normalized, string? scheme)
        {
            if (string.IsNullOrWhiteSpace(normalized))
            {
                return;
            }

            _aliases.Add(new AliasProjection(normalized.Trim(), scheme is null ? null : scheme.Trim()));
        }
    }

    private sealed class HashSetAliasComparer : IEqualityComparer<AliasProjection>
    {
        public static readonly HashSetAliasComparer Instance = new();

        public bool Equals(AliasProjection x, AliasProjection y)
            => string.Equals(x.Normalized, y.Normalized, StringComparison.OrdinalIgnoreCase)
               && string.Equals(x.Scheme, y.Scheme, StringComparison.OrdinalIgnoreCase);

        public int GetHashCode(AliasProjection obj)
        {
            var hash = StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Normalized);
            if (!string.IsNullOrWhiteSpace(obj.Scheme))
            {
                hash = HashCode.Combine(hash, StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Scheme));
            }

            return hash;
        }
    }

    private sealed class ReferenceEqualityComparer<T> : IEqualityComparer<T>
        where T : class
    {
        public static readonly ReferenceEqualityComparer<T> Instance = new();

        public bool Equals(T? x, T? y) => ReferenceEquals(x, y);

        public int GetHashCode(T obj) => RuntimeHelpers.GetHashCode(obj);
    }
}
