using System.Collections.Generic;
using System.Globalization;
using System.Text.Json;

namespace StellaOps.Feedser.Source.Ghsa.Internal;

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

        var ghsaId = GetString(root, "ghsa_id") ?? throw new JsonException("ghsa_id missing");
        var summary = GetString(root, "summary");
        var description = GetString(root, "description");
        var severity = GetString(root, "severity");
        var publishedAt = GetDate(root, "published_at");
        var updatedAt = GetDate(root, "updated_at") ?? publishedAt;

        var aliases = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            ghsaId,
        };

        if (root.TryGetProperty("cve_ids", out var cveIds) && cveIds.ValueKind == JsonValueKind.Array)
        {
            foreach (var cve in cveIds.EnumerateArray())
            {
                if (cve.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(cve.GetString()))
                {
                    aliases.Add(cve.GetString()!);
                }
            }
        }

        var references = ParseReferences(root);
        var affected = ParseAffected(root);
        var credits = ParseCredits(root);
        var cwes = ParseCwes(root);
        var cvss = ParseCvss(root);

        return new GhsaRecordDto
        {
            GhsaId = ghsaId,
            Summary = summary,
            Description = description,
            Severity = severity,
            PublishedAt = publishedAt,
            UpdatedAt = updatedAt,
            Aliases = aliases.ToArray(),
            References = references,
            Affected = affected,
            Credits = credits,
            Cwes = cwes,
            Cvss = cvss,
        };
    }

    private static IReadOnlyList<GhsaReferenceDto> ParseReferences(JsonElement root)
    {
        if (!root.TryGetProperty("references", out var references) || references.ValueKind != JsonValueKind.Array)
        {
            return Array.Empty<GhsaReferenceDto>();
        }

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

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

            list.Add(new GhsaReferenceDto
            {
                Url = url,
                Type = GetString(reference, "type"),
                Name = GetString(reference, "name"),
            });
        }

        return list;
    }

    private static IReadOnlyList<GhsaAffectedDto> ParseAffected(JsonElement root)
    {
        if (!root.TryGetProperty("vulnerabilities", out var vulnerabilities) || vulnerabilities.ValueKind != JsonValueKind.Array)
        {
            return Array.Empty<GhsaAffectedDto>();
        }

        var list = new List<GhsaAffectedDto>(vulnerabilities.GetArrayLength());
        foreach (var entry in vulnerabilities.EnumerateArray())
        {
            if (entry.ValueKind != JsonValueKind.Object)
            {
                continue;
            }

            var package = entry.TryGetProperty("package", out var packageElement) && packageElement.ValueKind == JsonValueKind.Object
                ? packageElement
                : default;

            var packageName = GetString(package, "name") ?? "unknown-package";
            var ecosystem = GetString(package, "ecosystem") ?? "unknown";
            var vulnerableRange = GetString(entry, "vulnerable_version_range");

            string? patchedVersion = null;
            if (entry.TryGetProperty("first_patched_version", out var patchedElement) && patchedElement.ValueKind == JsonValueKind.Object)
            {
                patchedVersion = GetString(patchedElement, "identifier");
            }

            list.Add(new GhsaAffectedDto
            {
                PackageName = packageName,
                Ecosystem = ecosystem,
                VulnerableRange = vulnerableRange,
                PatchedVersion = patchedVersion,
            });
        }

        return list;
    }

    private static IReadOnlyList<GhsaCreditDto> ParseCredits(JsonElement root)
    {
        if (!root.TryGetProperty("credits", out var credits) || credits.ValueKind != JsonValueKind.Array)
        {
            return Array.Empty<GhsaCreditDto>();
        }

        var list = new List<GhsaCreditDto>(credits.GetArrayLength());
        foreach (var credit in credits.EnumerateArray())
        {
            if (credit.ValueKind != JsonValueKind.Object)
            {
                continue;
            }

            var type = GetString(credit, "type");
            var name = GetString(credit, "name");
            string? login = null;
            string? profile = null;

            if (credit.TryGetProperty("user", out var user) && user.ValueKind == JsonValueKind.Object)
            {
                login = GetString(user, "login");
                profile = GetString(user, "html_url") ?? GetString(user, "url");
                name ??= GetString(user, "name");
            }

            name ??= login;
            if (string.IsNullOrWhiteSpace(name))
            {
                continue;
            }

            list.Add(new GhsaCreditDto
            {
                Type = type,
                Name = name,
                Login = login,
                ProfileUrl = profile,
            });
        }

        return list;
    }

    private static IReadOnlyList<GhsaWeaknessDto> ParseCwes(JsonElement root)
    {
        if (!root.TryGetProperty("cwes", out var cwes) || cwes.ValueKind != JsonValueKind.Array)
        {
            return Array.Empty<GhsaWeaknessDto>();
        }

        var list = new List<GhsaWeaknessDto>(cwes.GetArrayLength());
        foreach (var entry in cwes.EnumerateArray())
        {
            if (entry.ValueKind != JsonValueKind.Object)
            {
                continue;
            }

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

            list.Add(new GhsaWeaknessDto
            {
                CweId = cweId,
                Name = GetString(entry, "name"),
            });
        }

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

    private static GhsaCvssDto? ParseCvss(JsonElement root)
    {
        if (!root.TryGetProperty("cvss", out var cvss) || cvss.ValueKind != JsonValueKind.Object)
        {
            return null;
        }

        double? score = null;
        if (cvss.TryGetProperty("score", out var scoreElement) && scoreElement.ValueKind == JsonValueKind.Number)
        {
            score = scoreElement.GetDouble();
        }

        var vector = GetString(cvss, "vector_string") ?? GetString(cvss, "vectorString");
        var severity = GetString(cvss, "severity");

        if (score is null && string.IsNullOrWhiteSpace(vector) && string.IsNullOrWhiteSpace(severity))
        {
            return null;
        }

        return new GhsaCvssDto
        {
            Score = score,
            VectorString = vector,
            Severity = severity,
        };
    }

    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(),
            _ => 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;
    }
}
