using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
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 references = GetReferences(cve, sourceDocument, recordedAt);
            var affectedPackages = GetAffectedPackages(cve, sourceDocument, recordedAt);
            var cvssMetrics = GetCvssMetrics(cve, sourceDocument, recordedAt, out var severity);

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

            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);

            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)
    {
        var references = new List<AdvisoryReference>();
        if (!cve.TryGetProperty("references", out var referencesElement) || referencesElement.ValueKind != JsonValueKind.Array)
        {
            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", document.Uri, recordedAt)));
        }

        return references;
    }

    private static IReadOnlyList<AffectedPackage> GetAffectedPackages(JsonElement cve, 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);
                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(static 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();

                return new AffectedPackage(
                    type: AffectedPackageTypes.Cpe,
                    identifier: kvp.Key,
                    platform: null,
                    versionRanges: ranges,
                    statuses: Array.Empty<AffectedPackageStatus>(),
                    provenance: provenance);
            })
            .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",
                    document.Uri,
                    recordedAt)));
            }

            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;
        var expressionParts = new List<string>();

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

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

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

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

        if (version is not null)
        {
            introduced ??= version;
            lastAffected ??= 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;
        var primitives = extensions is null ? null : new RangePrimitives(null, null, null, extensions);

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

    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 sealed class PackageAccumulator
    {
        public List<AffectedVersionRange> Ranges { get; } = new();

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