using System;
using System.Collections.Generic;
using System.Linq;
using StellaOps.Feedser.Models;

namespace StellaOps.Feedser.Storage.Mongo.Advisories;

internal static class NormalizedVersionDocumentFactory
{
    public static List<NormalizedVersionDocument>? Create(Advisory advisory)
    {
        if (advisory.AffectedPackages.IsDefaultOrEmpty || advisory.AffectedPackages.Length == 0)
        {
            return null;
        }

        var documents = new List<NormalizedVersionDocument>();
        var advisoryFallbackReason = advisory.Provenance.FirstOrDefault()?.DecisionReason;
        var advisoryFallbackSource = advisory.Provenance.FirstOrDefault()?.Source;
        var advisoryFallbackRecordedAt = advisory.Provenance.FirstOrDefault()?.RecordedAt;

        foreach (var package in advisory.AffectedPackages)
        {
            if (package.NormalizedVersions.IsDefaultOrEmpty || package.NormalizedVersions.Length == 0)
            {
                continue;
            }

            foreach (var rule in package.NormalizedVersions)
            {
                var matchingRange = FindMatchingRange(package, rule);
                var decisionReason = matchingRange?.Provenance.DecisionReason
                    ?? package.Provenance.FirstOrDefault()?.DecisionReason
                    ?? advisoryFallbackReason;

                var source = matchingRange?.Provenance.Source
                    ?? package.Provenance.FirstOrDefault()?.Source
                    ?? advisoryFallbackSource;

                var recordedAt = matchingRange?.Provenance.RecordedAt
                    ?? package.Provenance.FirstOrDefault()?.RecordedAt
                    ?? advisoryFallbackRecordedAt;

                var constraint = matchingRange?.Primitives?.SemVer?.ConstraintExpression
                    ?? matchingRange?.RangeExpression;

                var style = matchingRange?.Primitives?.SemVer?.Style ?? rule.Type;

                documents.Add(new NormalizedVersionDocument
                {
                    PackageId = package.Identifier ?? string.Empty,
                    PackageType = package.Type ?? string.Empty,
                    Scheme = rule.Scheme,
                    Type = rule.Type,
                    Style = style,
                    Min = rule.Min,
                    MinInclusive = rule.MinInclusive,
                    Max = rule.Max,
                    MaxInclusive = rule.MaxInclusive,
                    Value = rule.Value,
                    Notes = rule.Notes,
                    DecisionReason = decisionReason,
                    Constraint = constraint,
                    Source = source,
                    RecordedAtUtc = recordedAt?.UtcDateTime,
                });
            }
        }

        return documents.Count == 0 ? null : documents;
    }

    private static AffectedVersionRange? FindMatchingRange(AffectedPackage package, NormalizedVersionRule rule)
    {
        foreach (var range in package.VersionRanges)
        {
            var candidate = range.ToNormalizedVersionRule(rule.Notes);
            if (candidate is null)
            {
                continue;
            }

            if (NormalizedRulesEquivalent(candidate, rule))
            {
                return range;
            }
        }

        return null;
    }

    private static bool NormalizedRulesEquivalent(NormalizedVersionRule left, NormalizedVersionRule right)
        => string.Equals(left.Scheme, right.Scheme, StringComparison.Ordinal)
            && string.Equals(left.Type, right.Type, StringComparison.Ordinal)
            && string.Equals(left.Min, right.Min, StringComparison.Ordinal)
            && left.MinInclusive == right.MinInclusive
            && string.Equals(left.Max, right.Max, StringComparison.Ordinal)
            && left.MaxInclusive == right.MaxInclusive
            && string.Equals(left.Value, right.Value, StringComparison.Ordinal);
}
