using System.Globalization;
using System.Linq;
using System.Text.Json;
using StellaOps.Feedser.Normalization.Text;

namespace StellaOps.Feedser.Source.Cve.Internal;

internal static class CveRecordParser
{
    public static CveRecordDto Parse(ReadOnlySpan<byte> content)
    {
        using var document = JsonDocument.Parse(content.ToArray());
        var root = document.RootElement;

        var metadata = TryGetProperty(root, "cveMetadata");
        if (metadata.ValueKind != JsonValueKind.Object)
        {
            throw new JsonException("cveMetadata section missing.");
        }

        var containers = TryGetProperty(root, "containers");
        var cna = TryGetProperty(containers, "cna");

        var cveId = GetString(metadata, "cveId") ?? throw new JsonException("cveMetadata.cveId missing.");
        var state = GetString(metadata, "state") ?? "PUBLISHED";
        var published = GetDate(metadata, "datePublished");
        var modified = GetDate(metadata, "dateUpdated") ?? GetDate(metadata, "dateReserved");

        var description = ParseDescription(cna);

        var aliases = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            cveId,
        };
        foreach (var alias in ParseAliases(cna))
        {
            aliases.Add(alias);
        }

        var references = ParseReferences(cna);
        var affected = ParseAffected(cna);
        var metrics = ParseMetrics(cna);

        return new CveRecordDto
        {
            CveId = cveId,
            Title = GetString(cna, "title") ?? cveId,
            Summary = description.Text,
            Language = description.Language,
            State = state,
            Published = published,
            Modified = modified,
            Aliases = aliases.ToArray(),
            References = references,
            Affected = affected,
            Metrics = metrics,
        };
    }

    private static NormalizedDescription ParseDescription(JsonElement element)
    {
        if (element.ValueKind != JsonValueKind.Object)
        {
            return DescriptionNormalizer.Normalize(Array.Empty<LocalizedText>());
        }

        if (!element.TryGetProperty("descriptions", out var descriptions) || descriptions.ValueKind != JsonValueKind.Array)
        {
            return DescriptionNormalizer.Normalize(Array.Empty<LocalizedText>());
        }

        var items = new List<LocalizedText>(descriptions.GetArrayLength());
        foreach (var entry in descriptions.EnumerateArray())
        {
            if (entry.ValueKind != JsonValueKind.Object)
            {
                continue;
            }

            var text = GetString(entry, "value");
            if (string.IsNullOrWhiteSpace(text))
            {
                continue;
            }

            var lang = GetString(entry, "lang");
            items.Add(new LocalizedText(text, lang));
        }

        return DescriptionNormalizer.Normalize(items);
    }

    private static IEnumerable<string> ParseAliases(JsonElement element)
    {
        if (element.ValueKind != JsonValueKind.Object)
        {
            yield break;
        }

        if (!element.TryGetProperty("aliases", out var aliases) || aliases.ValueKind != JsonValueKind.Array)
        {
            yield break;
        }

        foreach (var alias in aliases.EnumerateArray())
        {
            if (alias.ValueKind == JsonValueKind.String)
            {
                var value = alias.GetString();
                if (!string.IsNullOrWhiteSpace(value))
                {
                    yield return value;
                }
            }
        }
    }

    private static IReadOnlyList<CveReferenceDto> ParseReferences(JsonElement element)
    {
        if (element.ValueKind != JsonValueKind.Object)
        {
            return Array.Empty<CveReferenceDto>();
        }

        if (!element.TryGetProperty("references", out var references) || references.ValueKind != JsonValueKind.Array)
        {
            return Array.Empty<CveReferenceDto>();
        }

        var list = new List<CveReferenceDto>(references.GetArrayLength());
        foreach (var reference in references.EnumerateArray())
        {
            if (reference.ValueKind != JsonValueKind.Object)
            {
                continue;
            }

            var url = GetString(reference, "url");
            if (string.IsNullOrWhiteSpace(url))
            {
                continue;
            }

            var tags = Array.Empty<string>();
            if (reference.TryGetProperty("tags", out var tagsElement) && tagsElement.ValueKind == JsonValueKind.Array)
            {
                tags = tagsElement
                    .EnumerateArray()
                    .Where(static t => t.ValueKind == JsonValueKind.String)
                    .Select(static t => t.GetString()!)
                    .Where(static v => !string.IsNullOrWhiteSpace(v))
                    .ToArray();
            }

            var source = GetString(reference, "name") ?? GetString(reference, "source");
            list.Add(new CveReferenceDto
            {
                Url = url,
                Source = source,
                Tags = tags,
            });
        }

        return list;
    }

    private static IReadOnlyList<CveAffectedDto> ParseAffected(JsonElement element)
    {
        if (element.ValueKind != JsonValueKind.Object)
        {
            return Array.Empty<CveAffectedDto>();
        }

        if (!element.TryGetProperty("affected", out var affected) || affected.ValueKind != JsonValueKind.Array)
        {
            return Array.Empty<CveAffectedDto>();
        }

        var list = new List<CveAffectedDto>(affected.GetArrayLength());
        foreach (var item in affected.EnumerateArray())
        {
            if (item.ValueKind != JsonValueKind.Object)
            {
                continue;
            }

            var versions = new List<CveVersionDto>();
            if (item.TryGetProperty("versions", out var versionsElement) && versionsElement.ValueKind == JsonValueKind.Array)
            {
                foreach (var versionEntry in versionsElement.EnumerateArray())
                {
                    if (versionEntry.ValueKind != JsonValueKind.Object)
                    {
                        continue;
                    }

                    versions.Add(new CveVersionDto
                    {
                        Status = GetString(versionEntry, "status"),
                        Version = GetString(versionEntry, "version"),
                        LessThan = GetString(versionEntry, "lessThan"),
                        LessThanOrEqual = GetString(versionEntry, "lessThanOrEqual"),
                        VersionType = GetString(versionEntry, "versionType"),
                        Range = GetString(versionEntry, "versionRange"),
                    });
                }
            }

            list.Add(new CveAffectedDto
            {
                Vendor = GetString(item, "vendor") ?? GetString(item, "vendorName"),
                Product = GetString(item, "product") ?? GetString(item, "productName"),
                Platform = GetString(item, "platform"),
                DefaultStatus = GetString(item, "defaultStatus"),
                Versions = versions,
            });
        }

        return list;
    }

    private static IReadOnlyList<CveCvssMetricDto> ParseMetrics(JsonElement element)
    {
        if (element.ValueKind != JsonValueKind.Object)
        {
            return Array.Empty<CveCvssMetricDto>();
        }

        if (!element.TryGetProperty("metrics", out var metrics) || metrics.ValueKind != JsonValueKind.Array)
        {
            return Array.Empty<CveCvssMetricDto>();
        }

        var list = new List<CveCvssMetricDto>(metrics.GetArrayLength());
        foreach (var metric in metrics.EnumerateArray())
        {
            if (metric.ValueKind != JsonValueKind.Object)
            {
                continue;
            }

            if (metric.TryGetProperty("cvssV4_0", out var cvss40) && cvss40.ValueKind == JsonValueKind.Object)
            {
                list.Add(ParseCvss(cvss40, "4.0"));
            }
            else if (metric.TryGetProperty("cvssV3_1", out var cvss31) && cvss31.ValueKind == JsonValueKind.Object)
            {
                list.Add(ParseCvss(cvss31, "3.1"));
            }
            else if (metric.TryGetProperty("cvssV3", out var cvss3) && cvss3.ValueKind == JsonValueKind.Object)
            {
                list.Add(ParseCvss(cvss3, "3.0"));
            }
            else if (metric.TryGetProperty("cvssV2", out var cvss2) && cvss2.ValueKind == JsonValueKind.Object)
            {
                list.Add(ParseCvss(cvss2, "2.0"));
            }
        }

        return list;
    }

    private static CveCvssMetricDto ParseCvss(JsonElement element, string fallbackVersion)
    {
        var version = GetString(element, "version") ?? fallbackVersion;
        var vector = GetString(element, "vectorString") ?? GetString(element, "vector");
        var baseScore = GetDouble(element, "baseScore");
        var severity = GetString(element, "baseSeverity") ?? GetString(element, "severity");

        return new CveCvssMetricDto
        {
            Version = version,
            Vector = vector,
            BaseScore = baseScore,
            BaseSeverity = severity,
        };
    }

    private static JsonElement TryGetProperty(JsonElement element, string propertyName)
    {
        if (element.ValueKind == JsonValueKind.Object && element.TryGetProperty(propertyName, out var property))
        {
            return property;
        }

        return default;
    }

    private static string? GetString(JsonElement element, string propertyName)
    {
        if (element.ValueKind != JsonValueKind.Object)
        {
            return null;
        }

        if (!element.TryGetProperty(propertyName, out var property))
        {
            return null;
        }

        return property.ValueKind switch
        {
            JsonValueKind.String => property.GetString(),
            JsonValueKind.Number when property.TryGetDouble(out var number) => number.ToString(CultureInfo.InvariantCulture),
            _ => null,
        };
    }

    private static DateTimeOffset? GetDate(JsonElement element, string propertyName)
    {
        var value = GetString(element, propertyName);
        if (string.IsNullOrWhiteSpace(value))
        {
            return null;
        }

        return DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var parsed)
            ? parsed.ToUniversalTime()
            : null;
    }

    private static double? GetDouble(JsonElement element, string propertyName)
    {
        if (element.ValueKind != JsonValueKind.Object)
        {
            return null;
        }

        if (!element.TryGetProperty(propertyName, out var property))
        {
            return null;
        }

        if (property.ValueKind == JsonValueKind.Number && property.TryGetDouble(out var number))
        {
            return number;
        }

        if (property.ValueKind == JsonValueKind.String && double.TryParse(property.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed))
        {
            return parsed;
        }

        return null;
    }
}
