using System.Collections.Immutable;
using System.Runtime.Serialization;

namespace StellaOps.Vexer.Core;

public sealed record VexConsensus
{
    public VexConsensus(
        string vulnerabilityId,
        VexProduct product,
        VexConsensusStatus status,
        DateTimeOffset calculatedAt,
        IEnumerable<VexConsensusSource> sources,
        IEnumerable<VexConsensusConflict>? conflicts = null,
        string? policyVersion = null,
        string? summary = null,
        string? policyRevisionId = null,
        string? policyDigest = null)
    {
        if (string.IsNullOrWhiteSpace(vulnerabilityId))
        {
            throw new ArgumentException("Vulnerability id must be provided.", nameof(vulnerabilityId));
        }

        VulnerabilityId = vulnerabilityId.Trim();
        Product = product ?? throw new ArgumentNullException(nameof(product));
        Status = status;
        CalculatedAt = calculatedAt;
        Sources = NormalizeSources(sources);
        Conflicts = NormalizeConflicts(conflicts);
        PolicyVersion = string.IsNullOrWhiteSpace(policyVersion) ? null : policyVersion.Trim();
        Summary = string.IsNullOrWhiteSpace(summary) ? null : summary.Trim();
        PolicyRevisionId = string.IsNullOrWhiteSpace(policyRevisionId) ? null : policyRevisionId.Trim();
        PolicyDigest = string.IsNullOrWhiteSpace(policyDigest) ? null : policyDigest.Trim();
    }

    public string VulnerabilityId { get; }

    public VexProduct Product { get; }

    public VexConsensusStatus Status { get; }

    public DateTimeOffset CalculatedAt { get; }

    public ImmutableArray<VexConsensusSource> Sources { get; }

    public ImmutableArray<VexConsensusConflict> Conflicts { get; }

    public string? PolicyVersion { get; }

    public string? Summary { get; }

    public string? PolicyRevisionId { get; }

    public string? PolicyDigest { get; }

    private static ImmutableArray<VexConsensusSource> NormalizeSources(IEnumerable<VexConsensusSource> sources)
    {
        if (sources is null)
        {
            throw new ArgumentNullException(nameof(sources));
        }

        var builder = ImmutableArray.CreateBuilder<VexConsensusSource>();
        builder.AddRange(sources);
        if (builder.Count == 0)
        {
            return ImmutableArray<VexConsensusSource>.Empty;
        }

        return builder
            .OrderBy(static x => x.ProviderId, StringComparer.Ordinal)
            .ThenBy(static x => x.DocumentDigest, StringComparer.Ordinal)
            .ToImmutableArray();
    }

    private static ImmutableArray<VexConsensusConflict> NormalizeConflicts(IEnumerable<VexConsensusConflict>? conflicts)
    {
        if (conflicts is null)
        {
            return ImmutableArray<VexConsensusConflict>.Empty;
        }

        var items = conflicts.ToArray();
        return items.Length == 0
            ? ImmutableArray<VexConsensusConflict>.Empty
            : items
                .OrderBy(static x => x.ProviderId, StringComparer.Ordinal)
                .ThenBy(static x => x.DocumentDigest, StringComparer.Ordinal)
                .ToImmutableArray();
    }
}

public sealed record VexConsensusSource
{
    public VexConsensusSource(
        string providerId,
        VexClaimStatus status,
        string documentDigest,
        double weight,
        VexJustification? justification = null,
        string? detail = null,
        VexConfidence? confidence = null)
    {
        if (string.IsNullOrWhiteSpace(providerId))
        {
            throw new ArgumentException("Provider id must be provided.", nameof(providerId));
        }

        if (string.IsNullOrWhiteSpace(documentDigest))
        {
            throw new ArgumentException("Document digest must be provided.", nameof(documentDigest));
        }

        if (double.IsNaN(weight) || double.IsInfinity(weight) || weight < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(weight), "Weight must be a finite, non-negative number.");
        }

        ProviderId = providerId.Trim();
        Status = status;
        DocumentDigest = documentDigest.Trim();
        Weight = weight;
        Justification = justification;
        Detail = string.IsNullOrWhiteSpace(detail) ? null : detail.Trim();
        Confidence = confidence;
    }

    public string ProviderId { get; }

    public VexClaimStatus Status { get; }

    public string DocumentDigest { get; }

    public double Weight { get; }

    public VexJustification? Justification { get; }

    public string? Detail { get; }

    public VexConfidence? Confidence { get; }
}

public sealed record VexConsensusConflict
{
    public VexConsensusConflict(
        string providerId,
        VexClaimStatus status,
        string documentDigest,
        VexJustification? justification = null,
        string? detail = null,
        string? reason = null)
    {
        if (string.IsNullOrWhiteSpace(providerId))
        {
            throw new ArgumentException("Provider id must be provided.", nameof(providerId));
        }

        if (string.IsNullOrWhiteSpace(documentDigest))
        {
            throw new ArgumentException("Document digest must be provided.", nameof(documentDigest));
        }

        ProviderId = providerId.Trim();
        Status = status;
        DocumentDigest = documentDigest.Trim();
        Justification = justification;
        Detail = string.IsNullOrWhiteSpace(detail) ? null : detail.Trim();
        Reason = string.IsNullOrWhiteSpace(reason) ? null : reason.Trim();
    }

    public string ProviderId { get; }

    public VexClaimStatus Status { get; }

    public string DocumentDigest { get; }

    public VexJustification? Justification { get; }

    public string? Detail { get; }

    public string? Reason { get; }
}

[DataContract]
public enum VexConsensusStatus
{
    [EnumMember(Value = "affected")]
    Affected,

    [EnumMember(Value = "not_affected")]
    NotAffected,

    [EnumMember(Value = "fixed")]
    Fixed,

    [EnumMember(Value = "under_investigation")]
    UnderInvestigation,

    [EnumMember(Value = "divergent")]
    Divergent,
}
