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

namespace StellaOps.Feedser.Source.Ghsa.Internal;

internal static class GhsaMapper
{
    private static readonly HashSet<string> SemVerEcosystems = new(StringComparer.OrdinalIgnoreCase)
    {
        "npm",
        "maven",
        "pip",
        "rubygems",
        "composer",
        "nuget",
        "go",
        "cargo",
    };

    public static Advisory Map(GhsaRecordDto dto, DocumentRecord document, DateTimeOffset recordedAt)
    {
        ArgumentNullException.ThrowIfNull(dto);
        ArgumentNullException.ThrowIfNull(document);

        var fetchProvenance = new AdvisoryProvenance(GhsaConnectorPlugin.SourceName, "document", document.Uri, document.FetchedAt);
        var mapProvenance = new AdvisoryProvenance(GhsaConnectorPlugin.SourceName, "mapping", dto.GhsaId, recordedAt);

        var aliases = dto.Aliases
            .Where(static alias => !string.IsNullOrWhiteSpace(alias))
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToArray();

        var references = dto.References
            .Select(reference => CreateReference(reference, recordedAt))
            .Where(static reference => reference is not null)
            .Cast<AdvisoryReference>()
            .ToList();

        var affected = CreateAffectedPackages(dto, recordedAt);

        var severity = dto.Severity?.ToLowerInvariant();
        var summary = dto.Summary ?? dto.Description;

        return new Advisory(
            advisoryKey: dto.GhsaId,
            title: dto.Summary ?? dto.GhsaId,
            summary: summary,
            language: "en",
            published: dto.PublishedAt,
            modified: dto.UpdatedAt ?? dto.PublishedAt,
            severity: severity,
            exploitKnown: false,
            aliases: aliases,
            references: references,
            affectedPackages: affected,
            cvssMetrics: Array.Empty<CvssMetric>(),
            provenance: new[] { fetchProvenance, mapProvenance });
    }

    private static AdvisoryReference? CreateReference(GhsaReferenceDto reference, DateTimeOffset recordedAt)
    {
        if (string.IsNullOrWhiteSpace(reference.Url) || !Validation.LooksLikeHttpUrl(reference.Url))
        {
            return null;
        }

        var kind = reference.Type?.ToLowerInvariant();

        return new AdvisoryReference(
            reference.Url,
            kind,
            reference.Name,
            summary: null,
            provenance: new AdvisoryProvenance(
                GhsaConnectorPlugin.SourceName,
                "reference",
                reference.Url,
                recordedAt));
    }

    private static IReadOnlyList<AffectedPackage> CreateAffectedPackages(GhsaRecordDto dto, DateTimeOffset recordedAt)
    {
        if (dto.Affected.Count == 0)
        {
            return Array.Empty<AffectedPackage>();
        }

        var packages = new List<AffectedPackage>(dto.Affected.Count);
        foreach (var affected in dto.Affected)
        {
            var ecosystem = string.IsNullOrWhiteSpace(affected.Ecosystem) ? "unknown" : affected.Ecosystem.Trim();
            var packageName = string.IsNullOrWhiteSpace(affected.PackageName) ? "unknown-package" : affected.PackageName.Trim();
            var identifier = $"{ecosystem.ToLowerInvariant()}:{packageName}";

            var provenance = new[]
            {
                new AdvisoryProvenance(GhsaConnectorPlugin.SourceName, "affected", identifier, recordedAt),
            };

            var rangeKind = SemVerEcosystems.Contains(ecosystem) ? "semver" : "vendor";
            var packageType = SemVerEcosystems.Contains(ecosystem) ? AffectedPackageTypes.SemVer : AffectedPackageTypes.Vendor;

            var versionRanges = new List<AffectedVersionRange>();
            if (!string.IsNullOrWhiteSpace(affected.VulnerableRange) || !string.IsNullOrWhiteSpace(affected.PatchedVersion))
            {
                var primitives = new RangePrimitives(null, null, null, new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                {
                    ["ecosystem"] = ecosystem,
                    ["package"] = packageName,
                });

                versionRanges.Add(new AffectedVersionRange(
                    rangeKind,
                    introducedVersion: null,
                    fixedVersion: Validation.TrimToNull(affected.PatchedVersion),
                    lastAffectedVersion: null,
                    rangeExpression: Validation.TrimToNull(affected.VulnerableRange),
                    provenance: provenance[0],
                    primitives: primitives));
            }

            var statuses = new[]
            {
                new AffectedPackageStatus("affected", provenance[0]),
            };

            packages.Add(new AffectedPackage(
                packageType,
                identifier,
                platform: null,
                versionRanges: versionRanges,
                statuses: statuses,
                provenance: provenance));
        }

        return packages;
    }
}
