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

namespace StellaOps.Feedser.Source.CertBund.Internal;

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

        var aliases = BuildAliases(dto);
        var references = BuildReferences(dto, recordedAt);
        var packages = BuildPackages(dto, recordedAt);
        var provenance = new AdvisoryProvenance(
            CertBundConnectorPlugin.SourceName,
            "advisory",
            dto.AdvisoryId,
            recordedAt,
            new[] { ProvenanceFieldMasks.Advisory });

        return new Advisory(
            advisoryKey: dto.AdvisoryId,
            title: dto.Title,
            summary: dto.Summary,
            language: dto.Language?.ToLowerInvariant() ?? "de",
            published: dto.Published,
            modified: dto.Modified,
            severity: MapSeverity(dto.Severity),
            exploitKnown: false,
            aliases: aliases,
            references: references,
            affectedPackages: packages,
            cvssMetrics: Array.Empty<CvssMetric>(),
            provenance: new[] { provenance });
    }

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

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

    private static IReadOnlyList<AdvisoryReference> BuildReferences(CertBundAdvisoryDto dto, DateTimeOffset recordedAt)
    {
        var references = new List<AdvisoryReference>
        {
            new(dto.DetailUri.ToString(), "details", "cert-bund", null, new AdvisoryProvenance(
                CertBundConnectorPlugin.SourceName,
                "reference",
                dto.DetailUri.ToString(),
                recordedAt,
                new[] { ProvenanceFieldMasks.References }))
        };

        foreach (var reference in dto.References)
        {
            if (string.IsNullOrWhiteSpace(reference.Url))
            {
                continue;
            }

            references.Add(new AdvisoryReference(
                reference.Url,
                kind: "reference",
                sourceTag: "cert-bund",
                summary: reference.Label,
                provenance: new AdvisoryProvenance(
                    CertBundConnectorPlugin.SourceName,
                    "reference",
                    reference.Url,
                    recordedAt,
                    new[] { ProvenanceFieldMasks.References })));
        }

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

    private static IReadOnlyList<AffectedPackage> BuildPackages(CertBundAdvisoryDto 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 vendor = Validation.TrimToNull(product.Vendor) ?? "Unspecified";
            var name = Validation.TrimToNull(product.Name);
            var identifier = name is null ? vendor : $"{vendor} {name}";

            var provenance = new AdvisoryProvenance(
                CertBundConnectorPlugin.SourceName,
                "package",
                identifier,
                recordedAt,
                new[] { ProvenanceFieldMasks.AffectedPackages });

            var ranges = string.IsNullOrWhiteSpace(product.Versions)
                ? Array.Empty<AffectedVersionRange>()
                : new[]
                    {
                        new AffectedVersionRange(
                            rangeKind: "string",
                            introducedVersion: null,
                            fixedVersion: null,
                            lastAffectedVersion: null,
                            rangeExpression: product.Versions,
                            provenance: new AdvisoryProvenance(
                                CertBundConnectorPlugin.SourceName,
                                "package-range",
                                product.Versions,
                                recordedAt,
                                new[] { ProvenanceFieldMasks.VersionRanges }))
                    };

            packages.Add(new AffectedPackage(
                AffectedPackageTypes.Vendor,
                identifier,
                platform: null,
                versionRanges: ranges,
                statuses: Array.Empty<AffectedPackageStatus>(),
                provenance: new[] { provenance },
                normalizedVersions: Array.Empty<NormalizedVersionRule>()));
        }

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

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

        return severity.ToLowerInvariant() switch
        {
            "hoch" or "high" => "high",
            "mittel" or "medium" => "medium",
            "gering" or "low" => "low",
            _ => severity.ToLowerInvariant(),
        };
    }
}
