using System.Text.Json.Serialization;

namespace StellaOps.Feedser.Models;

/// <summary>
/// Describes a contiguous range of versions impacted by an advisory.
/// </summary>
public sealed record AffectedVersionRange
{
    [JsonConstructor]
    public AffectedVersionRange(
        string rangeKind,
        string? introducedVersion,
        string? fixedVersion,
        string? lastAffectedVersion,
        string? rangeExpression,
        AdvisoryProvenance provenance,
        RangePrimitives? primitives = null)
    {
        RangeKind = Validation.EnsureNotNullOrWhiteSpace(rangeKind, nameof(rangeKind)).ToLowerInvariant();
        IntroducedVersion = Validation.TrimToNull(introducedVersion);
        FixedVersion = Validation.TrimToNull(fixedVersion);
        LastAffectedVersion = Validation.TrimToNull(lastAffectedVersion);
        RangeExpression = Validation.TrimToNull(rangeExpression);
        Provenance = provenance ?? AdvisoryProvenance.Empty;
        Primitives = primitives;
    }

    /// <summary>
    /// Semantic kind of the range (e.g., semver, nevra, evr).
    /// </summary>
    public string RangeKind { get; }

    /// <summary>
    /// Inclusive version where impact begins.
    /// </summary>
    public string? IntroducedVersion { get; }

    /// <summary>
    /// Exclusive version where impact ends due to a fix.
    /// </summary>
    public string? FixedVersion { get; }

    /// <summary>
    /// Inclusive upper bound where the vendor reports exposure (when no fix available).
    /// </summary>
    public string? LastAffectedVersion { get; }

    /// <summary>
    /// Normalized textual representation of the range (fallback).
    /// </summary>
    public string? RangeExpression { get; }

    public AdvisoryProvenance Provenance { get; }

    public RangePrimitives? Primitives { get; }

    public string CreateDeterministicKey()
        => string.Join('|', RangeKind, IntroducedVersion ?? string.Empty, FixedVersion ?? string.Empty, LastAffectedVersion ?? string.Empty, RangeExpression ?? string.Empty);
}

/// <summary>
/// Deterministic comparer for version ranges. Orders by introduced, fixed, last affected, expression, kind.
/// </summary>
public sealed class AffectedVersionRangeComparer : IComparer<AffectedVersionRange>
{
    public static AffectedVersionRangeComparer Instance { get; } = new();

    private static readonly StringComparer Comparer = StringComparer.Ordinal;

    public int Compare(AffectedVersionRange? x, AffectedVersionRange? y)
    {
        if (ReferenceEquals(x, y))
        {
            return 0;
        }

        if (x is null)
        {
            return -1;
        }

        if (y is null)
        {
            return 1;
        }

        var compare = Comparer.Compare(x.IntroducedVersion, y.IntroducedVersion);
        if (compare != 0)
        {
            return compare;
        }

        compare = Comparer.Compare(x.FixedVersion, y.FixedVersion);
        if (compare != 0)
        {
            return compare;
        }

        compare = Comparer.Compare(x.LastAffectedVersion, y.LastAffectedVersion);
        if (compare != 0)
        {
            return compare;
        }

        compare = Comparer.Compare(x.RangeExpression, y.RangeExpression);
        if (compare != 0)
        {
            return compare;
        }

        return Comparer.Compare(x.RangeKind, y.RangeKind);
    }
}

/// <summary>
/// Equality comparer that ignores provenance differences.
/// </summary>
public sealed class AffectedVersionRangeEqualityComparer : IEqualityComparer<AffectedVersionRange>
{
    public static AffectedVersionRangeEqualityComparer Instance { get; } = new();

    public bool Equals(AffectedVersionRange? x, AffectedVersionRange? y)
    {
        if (ReferenceEquals(x, y))
        {
            return true;
        }

        if (x is null || y is null)
        {
            return false;
        }

        return string.Equals(x.RangeKind, y.RangeKind, StringComparison.Ordinal)
            && string.Equals(x.IntroducedVersion, y.IntroducedVersion, StringComparison.Ordinal)
            && string.Equals(x.FixedVersion, y.FixedVersion, StringComparison.Ordinal)
            && string.Equals(x.LastAffectedVersion, y.LastAffectedVersion, StringComparison.Ordinal)
            && string.Equals(x.RangeExpression, y.RangeExpression, StringComparison.Ordinal);
    }

    public int GetHashCode(AffectedVersionRange obj)
        => HashCode.Combine(
            obj.RangeKind,
            obj.IntroducedVersion,
            obj.FixedVersion,
            obj.LastAffectedVersion,
            obj.RangeExpression);
}
