using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Text.Json;
using NuGet.Versioning;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Normalization.Identifiers;
using StellaOps.Feedser.Normalization.Cvss;
using StellaOps.Feedser.Normalization.Text;
using StellaOps.Feedser.Storage.Mongo.Documents;

namespace StellaOps.Feedser.Source.Nvd.Internal;

internal static class NvdMapper
{
    public static IReadOnlyList<Advisory> Map(JsonDocument document, DocumentRecord sourceDocument, DateTimeOffset recordedAt)
    {
        ArgumentNullException.ThrowIfNull(document);
        ArgumentNullException.ThrowIfNull(sourceDocument);

        if (!document.RootElement.TryGetProperty("vulnerabilities", out var vulnerabilities) || vulnerabilities.ValueKind != JsonValueKind.Array)
        {
            return Array.Empty<Advisory>();
        }

        var advisories = new List<Advisory>(vulnerabilities.GetArrayLength());
        var index = 0;
        foreach (var vulnerability in vulnerabilities.EnumerateArray())
        {
            if (!vulnerability.TryGetProperty("cve", out var cve) || cve.ValueKind != JsonValueKind.Object)
            {
                index++;
                continue;
            }

            if (!cve.TryGetProperty("id", out var idElement) || idElement.ValueKind != JsonValueKind.String)
            {
                index++;
                continue;
            }

            var cveId = idElement.GetString();
            var advisoryKey = string.IsNullOrWhiteSpace(cveId)
                ? $"nvd:{sourceDocument.Id:N}:{index}"
                : cveId;

            var published = TryGetDateTime(cve, "published");
            var modified = TryGetDateTime(cve, "lastModified");
            var description = GetNormalizedDescription(cve);

            var weaknessMetadata = GetWeaknessMetadata(cve);
            var references = GetReferences(cve, sourceDocument, recordedAt, weaknessMetadata);
            var affectedPackages = GetAffectedPackages(cve, cveId, sourceDocument, recordedAt);
            var cvssMetrics = GetCvssMetrics(cve, sourceDocument, recordedAt, out var severity);
            var weaknesses = BuildWeaknesses(weaknessMetadata, recordedAt);
            var canonicalMetricId = cvssMetrics.Count > 0
                ? $"{cvssMetrics[0].Version}|{cvssMetrics[0].Vector}"
                : null;

            var provenance = new[]
            {
                new AdvisoryProvenance(
                    NvdConnectorPlugin.SourceName,
                    "document",
                    sourceDocument.Uri,
                    sourceDocument.FetchedAt,
                    new[] { ProvenanceFieldMasks.Advisory }),
                new AdvisoryProvenance(
                    NvdConnectorPlugin.SourceName,
                    "mapping",
                    string.IsNullOrWhiteSpace(cveId) ? advisoryKey : cveId,
                    recordedAt,
                    new[] { ProvenanceFieldMasks.Advisory }),
            };

            var title = string.IsNullOrWhiteSpace(cveId) ? advisoryKey : cveId;

            var aliasCandidates = new List<string>(capacity: 2);
            if (!string.IsNullOrWhiteSpace(cveId))
            {
                aliasCandidates.Add(cveId);
            }

            aliasCandidates.Add(advisoryKey);

            var advisory = new Advisory(
                advisoryKey: advisoryKey,
                title: title,
                summary: string.IsNullOrEmpty(description.Text) ? null : description.Text,
                language: description.Language,
                published: published,
                modified: modified,
                severity: severity,
                exploitKnown: false,
                aliases: aliasCandidates,
                references: references,
                affectedPackages: affectedPackages,
                cvssMetrics: cvssMetrics,
                provenance: provenance,
                description: string.IsNullOrEmpty(description.Text) ? null : description.Text,
                cwes: weaknesses,
                canonicalMetricId: canonicalMetricId);

            advisories.Add(advisory);
            index++;
        }

        return advisories;
    }

    private static NormalizedDescription GetNormalizedDescription(JsonElement cve)
    {
        var candidates = new List<LocalizedText>();

        if (cve.TryGetProperty("descriptions", out var descriptions) && descriptions.ValueKind == JsonValueKind.Array)
        {
            foreach (var item in descriptions.EnumerateArray())
            {
                if (item.ValueKind != JsonValueKind.Object)
                {
                    continue;
                }

                var text = item.TryGetProperty("value", out var valueElement) && valueElement.ValueKind == JsonValueKind.String
                    ? valueElement.GetString()
                    : null;
                var lang = item.TryGetProperty("lang", out var langElement) && langElement.ValueKind == JsonValueKind.String
                    ? langElement.GetString()
                    : null;

                if (!string.IsNullOrWhiteSpace(text))
                {
                    candidates.Add(new LocalizedText(text, lang));
                }
            }
        }

        return DescriptionNormalizer.Normalize(candidates);
    }

    private static DateTimeOffset? TryGetDateTime(JsonElement element, string propertyName)
    {
        if (!element.TryGetProperty(propertyName, out var property) || property.ValueKind != JsonValueKind.String)
        {
            return null;
        }

        return DateTimeOffset.TryParse(property.GetString(), out var parsed) ? parsed : null;
    }

    private static IReadOnlyList<AdvisoryReference> GetReferences(
        JsonElement cve,
        DocumentRecord document,
        DateTimeOffset recordedAt,
        IReadOnlyList<WeaknessMetadata> weaknesses)
    {
        var references = new List<AdvisoryReference>();
        if (!cve.TryGetProperty("references", out var referencesElement) || referencesElement.ValueKind != JsonValueKind.Array)
        {
            AppendWeaknessReferences(references, weaknesses, recordedAt);
            return references;
        }

        foreach (var reference in referencesElement.EnumerateArray())
        {
            if (!reference.TryGetProperty("url", out var urlElement) || urlElement.ValueKind != JsonValueKind.String)
            {
                continue;
            }

            var url = urlElement.GetString();
            if (string.IsNullOrWhiteSpace(url) || !Validation.LooksLikeHttpUrl(url))
            {
                continue;
            }

            var sourceTag = reference.TryGetProperty("source", out var sourceElement) ? sourceElement.GetString() : null;
            string? kind = null;
            if (reference.TryGetProperty("tags", out var tagsElement) && tagsElement.ValueKind == JsonValueKind.Array)
            {
                kind = tagsElement.EnumerateArray().Select(static t => t.GetString()).FirstOrDefault(static tag => !string.IsNullOrWhiteSpace(tag))?.ToLowerInvariant();
            }

            references.Add(new AdvisoryReference(
                url: url,
                kind: kind,
                sourceTag: sourceTag,
                summary: null,
                provenance: new AdvisoryProvenance(
                    NvdConnectorPlugin.SourceName,
                    "reference",
                    url,
                    recordedAt,
                    new[] { ProvenanceFieldMasks.References })));
        }

        AppendWeaknessReferences(references, weaknesses, recordedAt);
        return references;
    }

    private static IReadOnlyList<WeaknessMetadata> GetWeaknessMetadata(JsonElement cve)
    {
        if (!cve.TryGetProperty("weaknesses", out var weaknesses) || weaknesses.ValueKind != JsonValueKind.Array)
        {
            return Array.Empty<WeaknessMetadata>();
        }

        var list = new List<WeaknessMetadata>(weaknesses.GetArrayLength());
        foreach (var weakness in weaknesses.EnumerateArray())
        {
            if (!weakness.TryGetProperty("description", out var descriptions) || descriptions.ValueKind != JsonValueKind.Array)
            {
                continue;
            }

            string? cweId = null;
            string? name = null;

            foreach (var description in descriptions.EnumerateArray())
            {
                if (description.ValueKind != JsonValueKind.Object)
                {
                    continue;
                }

                if (!description.TryGetProperty("value", out var valueElement) || valueElement.ValueKind != JsonValueKind.String)
                {
                    continue;
                }

                var value = valueElement.GetString();
                if (string.IsNullOrWhiteSpace(value))
                {
                    continue;
                }

                var trimmed = value.Trim();
                if (trimmed.StartsWith("CWE-", StringComparison.OrdinalIgnoreCase))
                {
                    cweId ??= trimmed.ToUpperInvariant();
                }
                else
                {
                    name ??= trimmed;
                }
            }

            if (string.IsNullOrWhiteSpace(cweId))
            {
                continue;
            }

            list.Add(new WeaknessMetadata(cweId, name));
        }

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

    private static IReadOnlyList<AdvisoryWeakness> BuildWeaknesses(IReadOnlyList<WeaknessMetadata> metadata, DateTimeOffset recordedAt)
    {
        if (metadata.Count == 0)
        {
            return Array.Empty<AdvisoryWeakness>();
        }

        var list = new List<AdvisoryWeakness>(metadata.Count);
        foreach (var entry in metadata)
        {
            var provenance = new AdvisoryProvenance(
                NvdConnectorPlugin.SourceName,
                "weakness",
                entry.CweId,
                recordedAt,
                new[] { ProvenanceFieldMasks.Weaknesses });

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

        return list;
    }

    private static void AppendWeaknessReferences(
        List<AdvisoryReference> references,
        IReadOnlyList<WeaknessMetadata> weaknesses,
        DateTimeOffset recordedAt)
    {
        if (weaknesses.Count == 0)
        {
            return;
        }

        var existing = new HashSet<string>(references.Select(reference => reference.Url), StringComparer.OrdinalIgnoreCase);

        foreach (var weakness in weaknesses)
        {
            var url = BuildCweUrl(weakness.CweId);
            if (url is null || existing.Contains(url))
            {
                continue;
            }

            var provenance = new AdvisoryProvenance(
                NvdConnectorPlugin.SourceName,
                "reference",
                url,
                recordedAt,
                new[] { ProvenanceFieldMasks.References });

            references.Add(new AdvisoryReference(url, "weakness", weakness.CweId, weakness.Name, provenance));
            existing.Add(url);
        }
    }

    private static IReadOnlyList<AffectedPackage> GetAffectedPackages(JsonElement cve, string? cveId, DocumentRecord document, DateTimeOffset recordedAt)
    {
        var packages = new Dictionary<string, PackageAccumulator>(StringComparer.Ordinal);
        if (!cve.TryGetProperty("configurations", out var configurations) || configurations.ValueKind != JsonValueKind.Object)
        {
            return Array.Empty<AffectedPackage>();
        }

        if (!configurations.TryGetProperty("nodes", out var nodes) || nodes.ValueKind != JsonValueKind.Array)
        {
            return Array.Empty<AffectedPackage>();
        }

        foreach (var node in nodes.EnumerateArray())
        {
            if (!node.TryGetProperty("cpeMatch", out var matches) || matches.ValueKind != JsonValueKind.Array)
            {
                continue;
            }

            foreach (var match in matches.EnumerateArray())
            {
                if (match.TryGetProperty("vulnerable", out var vulnerableElement) && vulnerableElement.ValueKind == JsonValueKind.False)
                {
                    continue;
                }

                if (!match.TryGetProperty("criteria", out var criteriaElement) || criteriaElement.ValueKind != JsonValueKind.String)
                {
                    continue;
                }

                var criteria = criteriaElement.GetString();
                if (string.IsNullOrWhiteSpace(criteria))
                {
                    continue;
                }

                var identifier = IdentifierNormalizer.TryNormalizeCpe(criteria, out var normalizedCpe) && !string.IsNullOrWhiteSpace(normalizedCpe)
                    ? normalizedCpe
                    : criteria.Trim();

                var provenance = new AdvisoryProvenance(
                    NvdConnectorPlugin.SourceName,
                    "cpe",
                    document.Uri,
                    recordedAt,
                    new[] { ProvenanceFieldMasks.AffectedPackages });
                if (!packages.TryGetValue(identifier, out var accumulator))
                {
                    accumulator = new PackageAccumulator();
                    packages[identifier] = accumulator;
                }

                var range = BuildVersionRange(match, criteria, provenance);
                if (range is not null)
                {
                    accumulator.Ranges.Add(range);
                }

                accumulator.Provenance.Add(provenance);
            }
        }

        if (packages.Count == 0)
        {
            return Array.Empty<AffectedPackage>();
        }

        return packages
            .OrderBy(static kvp => kvp.Key, StringComparer.Ordinal)
            .Select(kvp =>
            {
                var ranges = kvp.Value.Ranges.Count == 0
                    ? Array.Empty<AffectedVersionRange>()
                    : kvp.Value.Ranges
                        .OrderBy(static range => range, AffectedVersionRangeComparer.Instance)
                        .ToArray();

                var provenance = kvp.Value.Provenance
                    .OrderBy(static p => p.Source, StringComparer.Ordinal)
                    .ThenBy(static p => p.Kind, StringComparer.Ordinal)
                    .ThenBy(static p => p.Value, StringComparer.Ordinal)
                    .ThenBy(static p => p.RecordedAt.UtcDateTime)
                    .ToArray();

                var normalizedNote = string.IsNullOrWhiteSpace(cveId)
                    ? $"nvd:{document.Id:N}"
                    : $"nvd:{cveId}";

                var normalizedVersions = new List<NormalizedVersionRule>(ranges.Length);
                foreach (var range in ranges)
                {
                    var rule = range.ToNormalizedVersionRule(normalizedNote);
                    if (rule is not null)
                    {
                        normalizedVersions.Add(rule);
                    }
                }

                return new AffectedPackage(
                    type: AffectedPackageTypes.Cpe,
                    identifier: kvp.Key,
                    platform: null,
                    versionRanges: ranges,
                    statuses: Array.Empty<AffectedPackageStatus>(),
                    provenance: provenance,
                    normalizedVersions: normalizedVersions.Count == 0
                        ? Array.Empty<NormalizedVersionRule>()
                        : normalizedVersions.ToArray());
            })
            .ToArray();
    }

    private static IReadOnlyList<CvssMetric> GetCvssMetrics(JsonElement cve, DocumentRecord document, DateTimeOffset recordedAt, out string? severity)
    {
        severity = null;
        if (!cve.TryGetProperty("metrics", out var metrics) || metrics.ValueKind != JsonValueKind.Object)
        {
            return Array.Empty<CvssMetric>();
        }

        var sources = new[] { "cvssMetricV31", "cvssMetricV30", "cvssMetricV2" };
        foreach (var source in sources)
        {
            if (!metrics.TryGetProperty(source, out var array) || array.ValueKind != JsonValueKind.Array)
            {
                continue;
            }

            var list = new List<CvssMetric>();
            foreach (var item in array.EnumerateArray())
            {
                if (!item.TryGetProperty("cvssData", out var data) || data.ValueKind != JsonValueKind.Object)
                {
                    continue;
                }

                if (!data.TryGetProperty("vectorString", out var vectorElement) || vectorElement.ValueKind != JsonValueKind.String)
                {
                    continue;
                }

                if (!data.TryGetProperty("baseScore", out var scoreElement) || scoreElement.ValueKind != JsonValueKind.Number)
                {
                    continue;
                }

                if (!data.TryGetProperty("baseSeverity", out var severityElement) || severityElement.ValueKind != JsonValueKind.String)
                {
                    continue;
                }

                var vector = vectorElement.GetString() ?? string.Empty;
                var baseScore = scoreElement.GetDouble();
                var baseSeverity = severityElement.GetString();
                var versionToken = source switch
                {
                    "cvssMetricV30" => "3.0",
                    "cvssMetricV31" => "3.1",
                    _ => "2.0",
                };

                if (!CvssMetricNormalizer.TryNormalize(versionToken, vector, baseScore, baseSeverity, out var normalized))
                {
                    continue;
                }

                severity ??= normalized.BaseSeverity;

                list.Add(normalized.ToModel(new AdvisoryProvenance(
                    NvdConnectorPlugin.SourceName,
                    "cvss",
                    normalized.Vector,
                    recordedAt,
                    new[] { ProvenanceFieldMasks.CvssMetrics })));
            }

            if (list.Count > 0)
            {
                return list;
            }
        }

        return Array.Empty<CvssMetric>();
    }

    private static AffectedVersionRange? BuildVersionRange(JsonElement match, string criteria, AdvisoryProvenance provenance)
    {
        static string? ReadString(JsonElement parent, string property)
        {
            if (!parent.TryGetProperty(property, out var value) || value.ValueKind != JsonValueKind.String)
            {
                return null;
            }

            var text = value.GetString();
            return string.IsNullOrWhiteSpace(text) ? null : text.Trim();
        }

        var version = ReadString(match, "version");
        if (string.Equals(version, "*", StringComparison.Ordinal))
        {
            version = null;
        }

        version ??= TryExtractVersionFromCriteria(criteria);

        var versionStartIncluding = ReadString(match, "versionStartIncluding");
        var versionStartExcluding = ReadString(match, "versionStartExcluding");
        var versionEndIncluding = ReadString(match, "versionEndIncluding");
        var versionEndExcluding = ReadString(match, "versionEndExcluding");

        var vendorExtensions = new Dictionary<string, string>(StringComparer.Ordinal);
        if (versionStartIncluding is not null)
        {
            vendorExtensions["versionStartIncluding"] = versionStartIncluding;
        }

        if (versionStartExcluding is not null)
        {
            vendorExtensions["versionStartExcluding"] = versionStartExcluding;
        }

        if (versionEndIncluding is not null)
        {
            vendorExtensions["versionEndIncluding"] = versionEndIncluding;
        }

        if (versionEndExcluding is not null)
        {
            vendorExtensions["versionEndExcluding"] = versionEndExcluding;
        }

        if (version is not null)
        {
            vendorExtensions["version"] = version;
        }

        string? introduced = null;
        string? fixedVersion = null;
        string? lastAffected = null;
        string? exactVersion = null;
        var expressionParts = new List<string>();

        var introducedInclusive = true;
        var fixedInclusive = false;
        var lastInclusive = true;

        if (versionStartIncluding is not null)
        {
            introduced = versionStartIncluding;
            introducedInclusive = true;
            expressionParts.Add($">={versionStartIncluding}");
        }

        if (versionStartExcluding is not null)
        {
            if (introduced is null)
            {
                introduced = versionStartExcluding;
                introducedInclusive = false;
            }
            expressionParts.Add($">{versionStartExcluding}");
        }

        if (versionEndExcluding is not null)
        {
            fixedVersion = versionEndExcluding;
            fixedInclusive = false;
            expressionParts.Add($"<{versionEndExcluding}");
        }

        if (versionEndIncluding is not null)
        {
            lastAffected = versionEndIncluding;
            lastInclusive = true;
            expressionParts.Add($"<={versionEndIncluding}");
        }

        if (version is not null)
        {
            introduced = version;
            introducedInclusive = true;
            lastAffected = version;
            lastInclusive = true;
            exactVersion = version;
            expressionParts.Add($"=={version}");
        }

        if (introduced is null && fixedVersion is null && lastAffected is null && vendorExtensions.Count == 0)
        {
            return null;
        }

        var rangeExpression = expressionParts.Count > 0 ? string.Join(' ', expressionParts) : null;
        IReadOnlyDictionary<string, string>? extensions = vendorExtensions.Count == 0 ? null : vendorExtensions;

        SemVerPrimitive? semVerPrimitive = null;
        if (TryBuildSemVerPrimitive(
            introduced,
            introducedInclusive,
            fixedVersion,
            fixedInclusive,
            lastAffected,
            lastInclusive,
            exactVersion,
            rangeExpression,
            out var primitive))
        {
            semVerPrimitive = primitive;
        }

        var primitives = semVerPrimitive is null && extensions is null
            ? null
            : new RangePrimitives(semVerPrimitive, null, null, extensions);

        var provenanceValue = provenance.Value ?? criteria;
        var rangeProvenance = new AdvisoryProvenance(
            provenance.Source,
            provenance.Kind,
            provenanceValue,
            provenance.RecordedAt,
            new[] { ProvenanceFieldMasks.VersionRanges });

        return new AffectedVersionRange(
            rangeKind: "cpe",
            introducedVersion: introduced,
            fixedVersion: fixedVersion,
            lastAffectedVersion: lastAffected,
            rangeExpression: rangeExpression,
            provenance: rangeProvenance,
            primitives);
    }

    private static bool TryBuildSemVerPrimitive(
        string? introduced,
        bool introducedInclusive,
        string? fixedVersion,
        bool fixedInclusive,
        string? lastAffected,
        bool lastInclusive,
        string? exactVersion,
        string? constraintExpression,
        out SemVerPrimitive? primitive)
    {
        primitive = null;

        if (!TryNormalizeSemVer(introduced, out var normalizedIntroduced)
            || !TryNormalizeSemVer(fixedVersion, out var normalizedFixed)
            || !TryNormalizeSemVer(lastAffected, out var normalizedLast)
            || !TryNormalizeSemVer(exactVersion, out var normalizedExact))
        {
            return false;
        }

        if (normalizedIntroduced is null && normalizedFixed is null && normalizedLast is null && normalizedExact is null)
        {
            return false;
        }

        primitive = new SemVerPrimitive(
            Introduced: normalizedIntroduced,
            IntroducedInclusive: normalizedIntroduced is null ? true : introducedInclusive,
            Fixed: normalizedFixed,
            FixedInclusive: normalizedFixed is null ? false : fixedInclusive,
            LastAffected: normalizedLast,
            LastAffectedInclusive: normalizedLast is null ? false : lastInclusive,
            ConstraintExpression: constraintExpression,
            ExactValue: normalizedExact);

    return true;
    }

    private static bool TryNormalizeSemVer(string? value, out string? normalized)
    {
        normalized = null;
        if (string.IsNullOrWhiteSpace(value))
        {
            return true;
        }

        var trimmed = value.Trim();
        if (trimmed.StartsWith("v", StringComparison.OrdinalIgnoreCase) && trimmed.Length > 1)
        {
            trimmed = trimmed[1..];
        }

        if (!NuGetVersion.TryParse(trimmed, out var parsed))
        {
            return false;
        }

        normalized = parsed.ToNormalizedString();
        return true;
    }

    private static string? BuildCweUrl(string cweId)
    {
        var dashIndex = cweId.IndexOf('-');
        if (dashIndex < 0 || dashIndex == cweId.Length - 1)
        {
            return null;
        }

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

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

    private static string? TryExtractVersionFromCriteria(string criteria)
    {
        if (string.IsNullOrWhiteSpace(criteria))
        {
            return null;
        }

        var segments = criteria.Split(':');
        if (segments.Length < 6)
        {
            return null;
        }

        var version = segments[5];
        if (string.IsNullOrWhiteSpace(version))
        {
            return null;
        }

        if (string.Equals(version, "*", StringComparison.Ordinal) || string.Equals(version, "-", StringComparison.Ordinal))
        {
            return null;
        }

        return version;
    }

    private readonly record struct WeaknessMetadata(string CweId, string? Name);

    private sealed class PackageAccumulator
    {
        public List<AffectedVersionRange> Ranges { get; } = new();

        public List<AdvisoryProvenance> Provenance { get; } = new();
    }
}
