using System.Collections.Immutable;
using System.Linq;
using System.Text.Json.Serialization;

namespace StellaOps.Feedser.Models;

/// <summary>
/// Canonical advisory document produced after merge. Collections are pre-sorted for deterministic serialization.
/// </summary>
public sealed record Advisory
{
    public static Advisory Empty { get; } = new(
        advisoryKey: "unknown",
        title: "",
        summary: null,
        language: null,
        published: null,
        modified: null,
        severity: null,
        exploitKnown: false,
        aliases: Array.Empty<string>(),
        credits: Array.Empty<AdvisoryCredit>(),
        references: Array.Empty<AdvisoryReference>(),
        affectedPackages: Array.Empty<AffectedPackage>(),
        cvssMetrics: Array.Empty<CvssMetric>(),
        provenance: Array.Empty<AdvisoryProvenance>(),
        description: null,
        cwes: Array.Empty<AdvisoryWeakness>(),
        canonicalMetricId: null);

    public Advisory(
        string advisoryKey,
        string title,
        string? summary,
        string? language,
        DateTimeOffset? published,
        DateTimeOffset? modified,
        string? severity,
        bool exploitKnown,
        IEnumerable<string>? aliases,
        IEnumerable<AdvisoryReference>? references,
        IEnumerable<AffectedPackage>? affectedPackages,
        IEnumerable<CvssMetric>? cvssMetrics,
        IEnumerable<AdvisoryProvenance>? provenance,
        string? description = null,
        IEnumerable<AdvisoryWeakness>? cwes = null,
        string? canonicalMetricId = null)
        : this(
            advisoryKey,
            title,
            summary,
            language,
            published,
            modified,
            severity,
            exploitKnown,
            aliases,
            Array.Empty<AdvisoryCredit>(),
            references,
            affectedPackages,
            cvssMetrics,
            provenance,
            description,
            cwes,
            canonicalMetricId)
    {
    }

    public Advisory(
        string advisoryKey,
        string title,
        string? summary,
        string? language,
        DateTimeOffset? published,
        DateTimeOffset? modified,
        string? severity,
        bool exploitKnown,
        IEnumerable<string>? aliases,
        IEnumerable<AdvisoryCredit>? credits,
        IEnumerable<AdvisoryReference>? references,
        IEnumerable<AffectedPackage>? affectedPackages,
        IEnumerable<CvssMetric>? cvssMetrics,
        IEnumerable<AdvisoryProvenance>? provenance,
        string? description = null,
        IEnumerable<AdvisoryWeakness>? cwes = null,
        string? canonicalMetricId = null)
    {
        AdvisoryKey = Validation.EnsureNotNullOrWhiteSpace(advisoryKey, nameof(advisoryKey));
        Title = Validation.EnsureNotNullOrWhiteSpace(title, nameof(title));
        Summary = Validation.TrimToNull(summary);
        Description = Validation.TrimToNull(description);
        Language = Validation.TrimToNull(language)?.ToLowerInvariant();
        Published = published?.ToUniversalTime();
        Modified = modified?.ToUniversalTime();
        Severity = SeverityNormalization.Normalize(severity);
        ExploitKnown = exploitKnown;

        Aliases = (aliases ?? Array.Empty<string>())
            .Select(static alias => Validation.TryNormalizeAlias(alias, out var normalized) ? normalized! : null)
            .Where(static alias => alias is not null)
            .Distinct(StringComparer.Ordinal)
            .OrderBy(static alias => alias, StringComparer.Ordinal)
            .Select(static alias => alias!)
            .ToImmutableArray();

        Credits = (credits ?? Array.Empty<AdvisoryCredit>())
            .Where(static credit => credit is not null)
            .OrderBy(static credit => credit.Role, StringComparer.Ordinal)
            .ThenBy(static credit => credit.DisplayName, StringComparer.Ordinal)
            .ToImmutableArray();

        References = (references ?? Array.Empty<AdvisoryReference>())
            .Where(static reference => reference is not null)
            .OrderBy(static reference => reference.Url, StringComparer.Ordinal)
            .ThenBy(static reference => reference.Kind, StringComparer.Ordinal)
            .ThenBy(static reference => reference.SourceTag, StringComparer.Ordinal)
            .ThenBy(static reference => reference.Provenance.RecordedAt)
            .ToImmutableArray();

        AffectedPackages = (affectedPackages ?? Array.Empty<AffectedPackage>())
            .Where(static package => package is not null)
            .OrderBy(static package => package.Type, StringComparer.Ordinal)
            .ThenBy(static package => package.Identifier, StringComparer.Ordinal)
            .ThenBy(static package => package.Platform, StringComparer.Ordinal)
            .ToImmutableArray();

        CvssMetrics = (cvssMetrics ?? Array.Empty<CvssMetric>())
            .Where(static metric => metric is not null)
            .OrderBy(static metric => metric.Version, StringComparer.Ordinal)
            .ThenBy(static metric => metric.Vector, StringComparer.Ordinal)
            .ToImmutableArray();

        Cwes = (cwes ?? Array.Empty<AdvisoryWeakness>())
            .Where(static weakness => weakness is not null)
            .OrderBy(static weakness => weakness.Taxonomy, StringComparer.Ordinal)
            .ThenBy(static weakness => weakness.Identifier, StringComparer.Ordinal)
            .ThenBy(static weakness => weakness.Name, StringComparer.Ordinal)
            .ToImmutableArray();

        CanonicalMetricId = Validation.TrimToNull(canonicalMetricId);

        Provenance = (provenance ?? Array.Empty<AdvisoryProvenance>())
            .Where(static p => p is not null)
            .OrderBy(static p => p.Source, StringComparer.Ordinal)
            .ThenBy(static p => p.Kind, StringComparer.Ordinal)
            .ThenBy(static p => p.RecordedAt)
            .ToImmutableArray();
    }

    [JsonConstructor]
    public Advisory(
        string advisoryKey,
        string title,
        string? summary,
        string? language,
        DateTimeOffset? published,
        DateTimeOffset? modified,
        string? severity,
        bool exploitKnown,
        ImmutableArray<string> aliases,
        ImmutableArray<AdvisoryCredit> credits,
        ImmutableArray<AdvisoryReference> references,
        ImmutableArray<AffectedPackage> affectedPackages,
        ImmutableArray<CvssMetric> cvssMetrics,
        ImmutableArray<AdvisoryProvenance> provenance,
        string? description,
        ImmutableArray<AdvisoryWeakness> cwes,
        string? canonicalMetricId)
        : this(
            advisoryKey,
            title,
            summary,
            language,
            published,
            modified,
            severity,
            exploitKnown,
            aliases.IsDefault ? null : aliases.AsEnumerable(),
            credits.IsDefault ? null : credits.AsEnumerable(),
            references.IsDefault ? null : references.AsEnumerable(),
            affectedPackages.IsDefault ? null : affectedPackages.AsEnumerable(),
            cvssMetrics.IsDefault ? null : cvssMetrics.AsEnumerable(),
            provenance.IsDefault ? null : provenance.AsEnumerable(),
            description,
            cwes.IsDefault ? null : cwes.AsEnumerable(),
            canonicalMetricId)
    {
    }

    public string AdvisoryKey { get; }

    public string Title { get; }

    public string? Summary { get; }

    public string? Description { get; }

    public string? Language { get; }

    public DateTimeOffset? Published { get; }

    public DateTimeOffset? Modified { get; }

    public string? Severity { get; }

    public bool ExploitKnown { get; }

    public ImmutableArray<string> Aliases { get; }

    public ImmutableArray<AdvisoryCredit> Credits { get; }

    public ImmutableArray<AdvisoryReference> References { get; }

    public ImmutableArray<AffectedPackage> AffectedPackages { get; }

    public ImmutableArray<CvssMetric> CvssMetrics { get; }

    public ImmutableArray<AdvisoryWeakness> Cwes { get; }

    public string? CanonicalMetricId { get; }

    public ImmutableArray<AdvisoryProvenance> Provenance { get; }
}
