using System;
using System.Collections.Generic;
using System.Linq;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Storage.Mongo.Documents;

namespace StellaOps.Feedser.Source.Vndr.Msrc.Internal;

internal static class MsrcMapper
{
    public static Advisory Map(MsrcAdvisoryDto dto, DocumentRecord document, DateTimeOffset recordedAt)
    {
        ArgumentNullException.ThrowIfNull(dto);
        ArgumentNullException.ThrowIfNull(document);

        var advisoryKey = dto.AdvisoryId;
        var aliases = BuildAliases(dto);
        var references = BuildReferences(dto, recordedAt);
        var affectedPackages = BuildPackages(dto, recordedAt);
        var cvssMetrics = BuildCvss(dto, recordedAt);

        var provenance = new AdvisoryProvenance(
            source: MsrcConnectorPlugin.SourceName,
            kind: "advisory",
            value: advisoryKey,
            recordedAt,
            new[] { ProvenanceFieldMasks.Advisory });

        return new Advisory(
            advisoryKey: advisoryKey,
            title: dto.Title,
            summary: dto.Description,
            language: "en",
            published: dto.ReleaseDate,
            modified: dto.LastModifiedDate,
            severity: NormalizeSeverity(dto.Severity),
            exploitKnown: false,
            aliases: aliases,
            references: references,
            affectedPackages: affectedPackages,
            cvssMetrics: cvssMetrics,
            provenance: new[] { provenance });
    }

    private static IReadOnlyList<string> BuildAliases(MsrcAdvisoryDto dto)
    {
        var aliases = new List<string> { dto.AdvisoryId };
        foreach (var cve in dto.CveIds)
        {
            if (!string.IsNullOrWhiteSpace(cve))
            {
                aliases.Add(cve);
            }
        }

        foreach (var kb in dto.KbIds)
        {
            if (!string.IsNullOrWhiteSpace(kb))
            {
                aliases.Add(kb.StartsWith("KB", StringComparison.OrdinalIgnoreCase) ? kb : $"KB{kb}");
            }
        }

        return aliases
            .Where(static alias => !string.IsNullOrWhiteSpace(alias))
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .OrderBy(static alias => alias, StringComparer.OrdinalIgnoreCase)
            .ToArray();
    }

    private static IReadOnlyList<AdvisoryReference> BuildReferences(MsrcAdvisoryDto dto, DateTimeOffset recordedAt)
    {
        var references = new List<AdvisoryReference>();

        if (!string.IsNullOrWhiteSpace(dto.ReleaseNoteUrl))
        {
            references.Add(CreateReference(dto.ReleaseNoteUrl!, "details", recordedAt));
        }

        if (!string.IsNullOrWhiteSpace(dto.CvrfUrl))
        {
            references.Add(CreateReference(dto.CvrfUrl!, "cvrf", recordedAt));
        }

        foreach (var remediation in dto.Remediations)
        {
            if (!string.IsNullOrWhiteSpace(remediation.Url))
            {
                references.Add(CreateReference(
                    remediation.Url!,
                    string.Equals(remediation.Type, "security update", StringComparison.OrdinalIgnoreCase) ? "remediation" : remediation.Type ?? "reference",
                    recordedAt,
                    remediation.Description));
            }
        }

        return references
            .DistinctBy(static reference => reference.Url, StringComparer.OrdinalIgnoreCase)
            .OrderBy(static reference => reference.Url, StringComparer.OrdinalIgnoreCase)
            .ToArray();
    }

    private static AdvisoryReference CreateReference(string url, string kind, DateTimeOffset recordedAt, string? summary = null)
        => new(
            url,
            kind: kind.ToLowerInvariant(),
            sourceTag: "msrc",
            summary: summary,
            provenance: new AdvisoryProvenance(
                MsrcConnectorPlugin.SourceName,
                "reference",
                url,
                recordedAt,
                new[] { ProvenanceFieldMasks.References }));

    private static IReadOnlyList<AffectedPackage> BuildPackages(MsrcAdvisoryDto dto, DateTimeOffset recordedAt)
    {
        if (dto.Products.Count == 0)
        {
            return Array.Empty<AffectedPackage>();
        }

        var packages = new List<AffectedPackage>(dto.Products.Count);
        foreach (var product in dto.Products)
        {
            var identifier = string.IsNullOrWhiteSpace(product.Identifier) ? "Unknown Product" : product.Identifier;
            var provenance = new AdvisoryProvenance(
                MsrcConnectorPlugin.SourceName,
                "package",
                identifier,
                recordedAt,
                new[] { ProvenanceFieldMasks.AffectedPackages });

            var notes = new List<string>();
            if (!string.IsNullOrWhiteSpace(product.Platform))
            {
                notes.Add($"platform:{product.Platform}");
            }

            if (!string.IsNullOrWhiteSpace(product.Architecture))
            {
                notes.Add($"arch:{product.Architecture}");
            }

            if (!string.IsNullOrWhiteSpace(product.Cpe))
            {
                notes.Add($"cpe:{product.Cpe}");
            }

            var range = !string.IsNullOrWhiteSpace(product.BuildNumber)
                ? new[]
                {
                    new AffectedVersionRange(
                        rangeKind: "custom",
                        introducedVersion: null,
                        fixedVersion: null,
                        lastAffectedVersion: null,
                        rangeExpression: $"build:{product.BuildNumber}",
                        provenance: new AdvisoryProvenance(
                            MsrcConnectorPlugin.SourceName,
                            "package-range",
                            identifier,
                            recordedAt,
                            new[] { ProvenanceFieldMasks.VersionRanges })),
                }
                : Array.Empty<AffectedVersionRange>();

            var normalizedRules = !string.IsNullOrWhiteSpace(product.BuildNumber)
                ? new[]
                {
                    new NormalizedVersionRule(
                        scheme: "msrc.build",
                        type: NormalizedVersionRuleTypes.Exact,
                        value: product.BuildNumber,
                        notes: string.Join(";", notes.Where(static n => !string.IsNullOrWhiteSpace(n))))
                }
                : Array.Empty<NormalizedVersionRule>();

            packages.Add(new AffectedPackage(
                type: AffectedPackageTypes.Vendor,
                identifier: identifier,
                platform: product.Platform,
                versionRanges: range,
                statuses: Array.Empty<AffectedPackageStatus>(),
                provenance: new[] { provenance },
                normalizedVersions: normalizedRules));
        }

        return packages
            .DistinctBy(static package => package.Identifier, StringComparer.OrdinalIgnoreCase)
            .OrderBy(static package => package.Identifier, StringComparer.OrdinalIgnoreCase)
            .ToArray();
    }

    private static IReadOnlyList<CvssMetric> BuildCvss(MsrcAdvisoryDto dto, DateTimeOffset recordedAt)
    {
        if (dto.CvssBaseScore is null || string.IsNullOrWhiteSpace(dto.CvssVector))
        {
            return Array.Empty<CvssMetric>();
        }

        var severity = CvssSeverityFromScore(dto.CvssBaseScore.Value);

        return new[]
        {
            new CvssMetric(
                version: "3.1",
                vector: dto.CvssVector!,
                baseScore: dto.CvssBaseScore.Value,
                baseSeverity: severity,
                provenance: new AdvisoryProvenance(
                    MsrcConnectorPlugin.SourceName,
                    "cvss",
                    dto.AdvisoryId,
                    recordedAt,
                    new[] { ProvenanceFieldMasks.CvssMetrics })),
        };
    }

    private static string CvssSeverityFromScore(double score)
        => score switch
        {
            < 0 => "none",
            < 4 => "low",
            < 7 => "medium",
            < 9 => "high",
            _ => "critical",
        };

    private static string? NormalizeSeverity(string? severity)
    {
        if (string.IsNullOrWhiteSpace(severity))
        {
            return null;
        }

        return severity.Trim().ToLowerInvariant();
    }
}
