using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;

namespace StellaOps.Feedser.Models;

/// <summary>
/// Compares OSV and GHSA advisory datasets to surface mismatches in coverage, severity, or presence.
/// </summary>
public static class OsvGhsaParityInspector
{
    public static OsvGhsaParityReport Compare(IEnumerable<Advisory> osvAdvisories, IEnumerable<Advisory> ghsaAdvisories)
    {
        ArgumentNullException.ThrowIfNull(osvAdvisories);
        ArgumentNullException.ThrowIfNull(ghsaAdvisories);

        var osvByGhsa = BuildOsvMap(osvAdvisories);
        var ghsaById = BuildGhsaMap(ghsaAdvisories);

        var union = osvByGhsa.Keys
            .Union(ghsaById.Keys, StringComparer.OrdinalIgnoreCase)
            .OrderBy(static key => key, StringComparer.OrdinalIgnoreCase)
            .ToArray();

        var issues = ImmutableArray.CreateBuilder<OsvGhsaParityIssue>();

        foreach (var ghsaId in union)
        {
            osvByGhsa.TryGetValue(ghsaId, out var osv);
            ghsaById.TryGetValue(ghsaId, out var ghsa);
            var normalizedId = ghsaId.ToUpperInvariant();

            if (osv is null)
            {
                issues.Add(new OsvGhsaParityIssue(
                    normalizedId,
                    "missing_osv",
                    "GHSA advisory missing from OSV dataset.",
                    ImmutableArray.Create(ProvenanceFieldMasks.AffectedPackages)));
                continue;
            }

            if (ghsa is null)
            {
                issues.Add(new OsvGhsaParityIssue(
                    normalizedId,
                    "missing_ghsa",
                    "OSV mapped GHSA alias without a matching GHSA advisory.",
                    ImmutableArray.Create(ProvenanceFieldMasks.AffectedPackages)));
                continue;
            }

            if (!SeverityMatches(osv, ghsa))
            {
                var detail = $"Severity mismatch: OSV={osv.Severity ?? "(null)"}, GHSA={ghsa.Severity ?? "(null)"}.";
                issues.Add(new OsvGhsaParityIssue(
                    normalizedId,
                    "severity_mismatch",
                    detail,
                    ImmutableArray.Create(ProvenanceFieldMasks.Advisory)));
            }

            if (!RangeCoverageMatches(osv, ghsa))
            {
                var detail = $"Range coverage mismatch: OSV ranges={CountRanges(osv)}, GHSA ranges={CountRanges(ghsa)}.";
                issues.Add(new OsvGhsaParityIssue(
                    normalizedId,
                    "range_mismatch",
                    detail,
                    ImmutableArray.Create(ProvenanceFieldMasks.VersionRanges)));
            }
        }

        return new OsvGhsaParityReport(union.Length, issues.ToImmutable());
    }

    private static IReadOnlyDictionary<string, Advisory> BuildOsvMap(IEnumerable<Advisory> advisories)
    {
        var comparer = StringComparer.OrdinalIgnoreCase;
        var map = new Dictionary<string, Advisory>(comparer);

        foreach (var advisory in advisories)
        {
            if (advisory is null)
            {
                continue;
            }

            foreach (var alias in advisory.Aliases)
            {
                if (alias.StartsWith("ghsa-", StringComparison.OrdinalIgnoreCase))
                {
                    map.TryAdd(alias, advisory);
                }
            }
        }

        return map;
    }

    private static IReadOnlyDictionary<string, Advisory> BuildGhsaMap(IEnumerable<Advisory> advisories)
    {
        var comparer = StringComparer.OrdinalIgnoreCase;
        var map = new Dictionary<string, Advisory>(comparer);

        foreach (var advisory in advisories)
        {
            if (advisory is null)
            {
                continue;
            }

            if (advisory.AdvisoryKey.StartsWith("ghsa-", StringComparison.OrdinalIgnoreCase))
            {
                map.TryAdd(advisory.AdvisoryKey, advisory);
                continue;
            }

            foreach (var alias in advisory.Aliases)
            {
                if (alias.StartsWith("ghsa-", StringComparison.OrdinalIgnoreCase))
                {
                    map.TryAdd(alias, advisory);
                }
            }
        }

        return map;
    }

    private static bool SeverityMatches(Advisory osv, Advisory ghsa)
        => string.Equals(osv.Severity, ghsa.Severity, StringComparison.OrdinalIgnoreCase);

    private static bool RangeCoverageMatches(Advisory osv, Advisory ghsa)
    {
        var osvRanges = CountRanges(osv);
        var ghsaRanges = CountRanges(ghsa);
        if (osvRanges == ghsaRanges)
        {
            return true;
        }

        // Consider zero-vs-nonzero mismatches as actionable even if raw counts differ.
        return osvRanges == 0 && ghsaRanges == 0;
    }

    private static int CountRanges(Advisory advisory)
    {
        if (advisory.AffectedPackages.IsDefaultOrEmpty)
        {
            return 0;
        }

        var count = 0;
        foreach (var package in advisory.AffectedPackages)
        {
            if (package.VersionRanges.IsDefaultOrEmpty)
            {
                continue;
            }

            count += package.VersionRanges.Length;
        }

        return count;
    }
}

public sealed record OsvGhsaParityIssue(
    string GhsaId,
    string IssueKind,
    string Detail,
    ImmutableArray<string> FieldMask);

public sealed record OsvGhsaParityReport(int TotalGhsaIds, ImmutableArray<OsvGhsaParityIssue> Issues)
{
    public bool HasIssues => !Issues.IsDefaultOrEmpty && Issues.Length > 0;

    public int MissingFromOsv => Issues.Count(issue => issue.IssueKind.Equals("missing_osv", StringComparison.OrdinalIgnoreCase));

    public int MissingFromGhsa => Issues.Count(issue => issue.IssueKind.Equals("missing_ghsa", StringComparison.OrdinalIgnoreCase));
}
