using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Normalization.Cvss;
using StellaOps.Feedser.Normalization.SemVer;
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,
            new[] { ProvenanceFieldMasks.Advisory });
        var mapProvenance = new AdvisoryProvenance(
            GhsaConnectorPlugin.SourceName,
            "mapping",
            dto.GhsaId,
            recordedAt,
            new[] { ProvenanceFieldMasks.Advisory });

        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 credits = CreateCredits(dto.Credits, recordedAt);
        var weaknesses = CreateWeaknesses(dto.Cwes, recordedAt);
        var cvssMetrics = CreateCvssMetrics(dto.Cvss, recordedAt, out var cvssSeverity, out var canonicalMetricId);

        var severityHint = SeverityNormalization.Normalize(dto.Severity);
        var cvssSeverityHint = SeverityNormalization.Normalize(dto.Cvss?.Severity);
        var severity = severityHint ?? cvssSeverity ?? cvssSeverityHint;

        if (canonicalMetricId is null)
        {
            var fallbackSeverity = severityHint ?? cvssSeverityHint ?? cvssSeverity;
            if (!string.IsNullOrWhiteSpace(fallbackSeverity))
            {
                canonicalMetricId = BuildSeverityCanonicalMetricId(fallbackSeverity);
            }
        }

        var summary = dto.Summary ?? dto.Description;
        var description = Validation.TrimToNull(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,
            credits: credits,
            references: references,
            affectedPackages: affected,
            cvssMetrics: cvssMetrics,
            provenance: new[] { fetchProvenance, mapProvenance },
            description: description,
            cwes: weaknesses,
            canonicalMetricId: canonicalMetricId);
    }

    private static string BuildSeverityCanonicalMetricId(string severity)
        => $"{GhsaConnectorPlugin.SourceName}:severity/{severity}";

    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,
                new[] { ProvenanceFieldMasks.References }));
    }

    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,
                    new[] { ProvenanceFieldMasks.AffectedPackages }),
            };

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

            var (ranges, normalizedVersions) = SemVerEcosystems.Contains(ecosystem)
                ? CreateSemVerVersionArtifacts(affected, identifier, ecosystem, packageName, recordedAt)
                : CreateVendorVersionArtifacts(affected, rangeKind, identifier, ecosystem, packageName, recordedAt);

            var statuses = new[]
            {
                new AffectedPackageStatus(
                    "affected",
                    new AdvisoryProvenance(
                        GhsaConnectorPlugin.SourceName,
                        "affected-status",
                        identifier,
                        recordedAt,
                        new[] { ProvenanceFieldMasks.PackageStatuses })),
            };

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

        return packages;
    }

    private static IReadOnlyList<AdvisoryCredit> CreateCredits(IReadOnlyList<GhsaCreditDto> credits, DateTimeOffset recordedAt)
    {
        if (credits.Count == 0)
        {
            return Array.Empty<AdvisoryCredit>();
        }

        var results = new List<AdvisoryCredit>(credits.Count);
        foreach (var credit in credits)
        {
            var displayName = Validation.TrimToNull(credit.Name) ?? Validation.TrimToNull(credit.Login);
            if (displayName is null)
            {
                continue;
            }

            var contacts = new List<string>();
            if (!string.IsNullOrWhiteSpace(credit.ProfileUrl) && Validation.LooksLikeHttpUrl(credit.ProfileUrl))
            {
                contacts.Add(credit.ProfileUrl.Trim());
            }
            else if (!string.IsNullOrWhiteSpace(credit.Login))
            {
                contacts.Add($"https://github.com/{credit.Login.Trim()}");
            }

            var provenance = new AdvisoryProvenance(
                GhsaConnectorPlugin.SourceName,
                "credit",
                displayName,
                recordedAt,
                new[] { ProvenanceFieldMasks.Credits });

            results.Add(new AdvisoryCredit(displayName, credit.Type, contacts, provenance));
        }

        return results.Count == 0 ? Array.Empty<AdvisoryCredit>() : results;
    }

    private static IReadOnlyList<AdvisoryWeakness> CreateWeaknesses(IReadOnlyList<GhsaWeaknessDto> cwes, DateTimeOffset recordedAt)
    {
        if (cwes.Count == 0)
        {
            return Array.Empty<AdvisoryWeakness>();
        }

        var list = new List<AdvisoryWeakness>(cwes.Count);
        foreach (var cwe in cwes)
        {
            if (cwe is null || string.IsNullOrWhiteSpace(cwe.CweId))
            {
                continue;
            }

            var identifier = cwe.CweId.Trim();
            var provenance = new AdvisoryProvenance(
                GhsaConnectorPlugin.SourceName,
                "weakness",
                identifier,
                recordedAt,
                new[] { ProvenanceFieldMasks.Weaknesses });

            var provenanceArray = ImmutableArray.Create(provenance);
            list.Add(new AdvisoryWeakness(
                taxonomy: "cwe",
                identifier: identifier,
                name: Validation.TrimToNull(cwe.Name),
                uri: BuildCweUrl(identifier),
                provenance: provenanceArray));
        }

        return list.Count == 0 ? Array.Empty<AdvisoryWeakness>() : list;
    }

    private static IReadOnlyList<CvssMetric> CreateCvssMetrics(GhsaCvssDto? cvss, DateTimeOffset recordedAt, out string? severity, out string? canonicalMetricId)
    {
        severity = null;
        canonicalMetricId = null;

        if (cvss is null)
        {
            return Array.Empty<CvssMetric>();
        }

        var vector = Validation.TrimToNull(cvss.VectorString);
        if (!CvssMetricNormalizer.TryNormalize(null, vector, cvss.Score, cvss.Severity, out var normalized))
        {
            return Array.Empty<CvssMetric>();
        }

        severity = normalized.BaseSeverity;
        canonicalMetricId = $"{normalized.Version}|{normalized.Vector}";

        var provenance = new AdvisoryProvenance(
            GhsaConnectorPlugin.SourceName,
            "cvss",
            normalized.Vector,
            recordedAt,
            new[] { ProvenanceFieldMasks.CvssMetrics });

        return new[]
        {
            normalized.ToModel(provenance),
        };
    }

    private static string? BuildCweUrl(string? cweId)
    {
        if (string.IsNullOrWhiteSpace(cweId))
        {
            return null;
        }

        var trimmed = cweId.Trim();
        var dashIndex = trimmed.IndexOf('-');
        if (dashIndex < 0 || dashIndex == trimmed.Length - 1)
        {
            return null;
        }

        var digits = new StringBuilder();
        for (var i = dashIndex + 1; i < trimmed.Length; i++)
        {
            var ch = trimmed[i];
            if (char.IsDigit(ch))
            {
                digits.Append(ch);
            }
        }

        return digits.Length == 0 ? null : $"https://cwe.mitre.org/data/definitions/{digits}.html";
    }

    private static (IReadOnlyList<AffectedVersionRange> Ranges, IReadOnlyList<NormalizedVersionRule> Normalized) CreateSemVerVersionArtifacts(
        GhsaAffectedDto affected,
        string identifier,
        string ecosystem,
        string packageName,
        DateTimeOffset recordedAt)
    {
        var note = BuildNormalizedNote(identifier);
        var results = SemVerRangeRuleBuilder.Build(affected.VulnerableRange, affected.PatchedVersion, note);

        if (results.Count > 0)
        {
            var ranges = new List<AffectedVersionRange>(results.Count);
            var normalized = new List<NormalizedVersionRule>(results.Count);

            foreach (var result in results)
            {
                var primitive = result.Primitive;
                var rangeExpression = ResolveRangeExpression(result.Expression, primitive.ConstraintExpression, affected.VulnerableRange);

                ranges.Add(new AffectedVersionRange(
                    rangeKind: "semver",
                    introducedVersion: Validation.TrimToNull(primitive.Introduced),
                    fixedVersion: Validation.TrimToNull(primitive.Fixed),
                    lastAffectedVersion: Validation.TrimToNull(primitive.LastAffected),
                    rangeExpression: rangeExpression,
                    provenance: CreateRangeProvenance(identifier, recordedAt),
                    primitives: new RangePrimitives(
                        SemVer: primitive,
                        Nevra: null,
                        Evr: null,
                        VendorExtensions: CreateVendorExtensions(ecosystem, packageName))));

                normalized.Add(result.NormalizedRule);
            }

            return (ranges.ToArray(), normalized.ToArray());
        }

        var fallbackRange = CreateFallbackRange("semver", affected, identifier, ecosystem, packageName, recordedAt);
        if (fallbackRange is null)
        {
            return (Array.Empty<AffectedVersionRange>(), Array.Empty<NormalizedVersionRule>());
        }

        var fallbackRule = fallbackRange.ToNormalizedVersionRule(note);
        var normalizedFallback = fallbackRule is null
            ? Array.Empty<NormalizedVersionRule>()
            : new[] { fallbackRule };

        return (new[] { fallbackRange }, normalizedFallback);
    }

    private static (IReadOnlyList<AffectedVersionRange> Ranges, IReadOnlyList<NormalizedVersionRule> Normalized) CreateVendorVersionArtifacts(
        GhsaAffectedDto affected,
        string rangeKind,
        string identifier,
        string ecosystem,
        string packageName,
        DateTimeOffset recordedAt)
    {
        var range = CreateFallbackRange(rangeKind, affected, identifier, ecosystem, packageName, recordedAt);
        if (range is null)
        {
            return (Array.Empty<AffectedVersionRange>(), Array.Empty<NormalizedVersionRule>());
        }

        return (new[] { range }, Array.Empty<NormalizedVersionRule>());
    }

    private static AffectedVersionRange? CreateFallbackRange(
        string rangeKind,
        GhsaAffectedDto affected,
        string identifier,
        string ecosystem,
        string packageName,
        DateTimeOffset recordedAt)
    {
        var fixedVersion = Validation.TrimToNull(affected.PatchedVersion);
        var rangeExpression = Validation.TrimToNull(affected.VulnerableRange);

        if (fixedVersion is null && rangeExpression is null)
        {
            return null;
        }

        return new AffectedVersionRange(
            rangeKind,
            introducedVersion: null,
            fixedVersion: fixedVersion,
            lastAffectedVersion: null,
            rangeExpression: rangeExpression,
            provenance: CreateRangeProvenance(identifier, recordedAt),
            primitives: new RangePrimitives(
                SemVer: null,
                Nevra: null,
                Evr: null,
                VendorExtensions: CreateVendorExtensions(ecosystem, packageName)));
    }

    private static AdvisoryProvenance CreateRangeProvenance(string identifier, DateTimeOffset recordedAt)
        => new(
            GhsaConnectorPlugin.SourceName,
            "affected-range",
            identifier,
            recordedAt,
            new[] { ProvenanceFieldMasks.VersionRanges });

    private static IReadOnlyDictionary<string, string> CreateVendorExtensions(string ecosystem, string packageName)
        => new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
        {
            ["ecosystem"] = ecosystem,
            ["package"] = packageName,
        };

    private static string? BuildNormalizedNote(string identifier)
    {
        var trimmed = Validation.TrimToNull(identifier);
        return trimmed is null ? null : $"ghsa:{trimmed}";
    }

    private static string? ResolveRangeExpression(string? parsedExpression, string? constraintExpression, string? fallbackExpression)
    {
        var parsed = Validation.TrimToNull(parsedExpression);
        if (parsed is not null)
        {
            return parsed;
        }

        var constraint = Validation.TrimToNull(constraintExpression);
        if (constraint is not null)
        {
            return constraint;
        }

        return Validation.TrimToNull(fallbackExpression);
    }
}
