using System;
using System.Buffers;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using Markdig;
using StellaOps.Feedser.Source.Common.Html;
using StellaOps.Feedser.Source.Common.Url;

namespace StellaOps.Feedser.Source.CertCc.Internal;

internal static class CertCcNoteParser
{
    private static readonly MarkdownPipeline MarkdownPipeline = new MarkdownPipelineBuilder()
        .UseAdvancedExtensions()
        .UseSoftlineBreakAsHardlineBreak()
        .DisableHtml()
        .Build();

    private static readonly HtmlContentSanitizer HtmlSanitizer = new();
    private static readonly Regex HtmlTagRegex = new("<[^>]+>", RegexOptions.Compiled | RegexOptions.CultureInvariant);

    public static CertCcNoteDto Parse(
        ReadOnlySpan<byte> noteJson,
        ReadOnlySpan<byte> vendorsJson,
        ReadOnlySpan<byte> vulnerabilitiesJson,
        ReadOnlySpan<byte> vendorStatusesJson)
    {
        using var noteDocument = JsonDocument.Parse(noteJson.ToArray());
        var (metadata, detailUri) = ParseNoteMetadata(noteDocument.RootElement);

        using var vendorsDocument = JsonDocument.Parse(vendorsJson.ToArray());
        var vendors = ParseVendors(vendorsDocument.RootElement, detailUri);

        using var vulnerabilitiesDocument = JsonDocument.Parse(vulnerabilitiesJson.ToArray());
        var vulnerabilities = ParseVulnerabilities(vulnerabilitiesDocument.RootElement);

        using var statusesDocument = JsonDocument.Parse(vendorStatusesJson.ToArray());
        var statuses = ParseVendorStatuses(statusesDocument.RootElement);

        return new CertCcNoteDto(metadata, vendors, statuses, vulnerabilities);
    }

    public static CertCcNoteDto ParseNote(ReadOnlySpan<byte> noteJson)
    {
        using var noteDocument = JsonDocument.Parse(noteJson.ToArray());
        var (metadata, _) = ParseNoteMetadata(noteDocument.RootElement);
        return new CertCcNoteDto(metadata, Array.Empty<CertCcVendorDto>(), Array.Empty<CertCcVendorStatusDto>(), Array.Empty<CertCcVulnerabilityDto>());
    }

    private static (CertCcNoteMetadata Metadata, Uri DetailUri) ParseNoteMetadata(JsonElement root)
    {
        if (root.ValueKind != JsonValueKind.Object)
        {
            throw new JsonException("CERT/CC note payload must be a JSON object.");
        }

        var vuId = GetString(root, "vuid");
        var idNumber = GetString(root, "idnumber") ?? throw new JsonException("CERT/CC note missing idnumber.");
        var title = GetString(root, "name") ?? throw new JsonException("CERT/CC note missing name.");
        var detailUri = BuildDetailUri(idNumber);

        var overview = NormalizeMarkdownToPlainText(root, "overview", detailUri);
        var summary = NormalizeMarkdownToPlainText(root, "clean_desc", detailUri);
        if (string.IsNullOrWhiteSpace(summary))
        {
            summary = NormalizeMarkdownToPlainText(root, "impact", detailUri);
        }

        var published = ParseDate(root, "publicdate") ?? ParseDate(root, "datefirstpublished");
        var updated = ParseDate(root, "dateupdated");
        var created = ParseDate(root, "datecreated");
        var revision = ParseInt(root, "revision");

        var cveIds = ExtractCveIds(root, "cveids");
        var references = ExtractReferenceList(root, "public", detailUri);

        var metadata = new CertCcNoteMetadata(
            VuId: string.IsNullOrWhiteSpace(vuId) ? null : vuId.Trim(),
            IdNumber: idNumber.Trim(),
            Title: title.Trim(),
            Overview: overview,
            Summary: summary,
            Published: published?.ToUniversalTime(),
            Updated: updated?.ToUniversalTime(),
            Created: created?.ToUniversalTime(),
            Revision: revision,
            CveIds: cveIds,
            PublicUrls: references,
            PrimaryUrl: detailUri.ToString());

        return (metadata, detailUri);
    }

    private static IReadOnlyList<CertCcVendorDto> ParseVendors(JsonElement root, Uri baseUri)
    {
        if (root.ValueKind != JsonValueKind.Array || root.GetArrayLength() == 0)
        {
            return Array.Empty<CertCcVendorDto>();
        }

        var parsed = new List<CertCcVendorDto>(root.GetArrayLength());
        foreach (var element in root.EnumerateArray())
        {
            if (element.ValueKind != JsonValueKind.Object)
            {
                continue;
            }

            var vendor = GetString(element, "vendor");
            if (string.IsNullOrWhiteSpace(vendor))
            {
                continue;
            }

            var statement = NormalizeFreeformText(GetString(element, "statement"));
            var addendum = NormalizeFreeformText(GetString(element, "addendum"));
            var references = ExtractReferenceStringList(GetString(element, "references"), baseUri);

            parsed.Add(new CertCcVendorDto(
                vendor.Trim(),
                ContactDate: ParseDate(element, "contact_date"),
                StatementDate: ParseDate(element, "statement_date"),
                Updated: ParseDate(element, "dateupdated"),
                Statement: statement,
                Addendum: addendum,
                References: references));
        }

        if (parsed.Count == 0)
        {
            return Array.Empty<CertCcVendorDto>();
        }

        return parsed
            .OrderBy(static vendor => vendor.Vendor, StringComparer.OrdinalIgnoreCase)
            .ToArray();
    }

    private static IReadOnlyList<CertCcVulnerabilityDto> ParseVulnerabilities(JsonElement root)
    {
        if (root.ValueKind != JsonValueKind.Array || root.GetArrayLength() == 0)
        {
            return Array.Empty<CertCcVulnerabilityDto>();
        }

        var parsed = new List<CertCcVulnerabilityDto>(root.GetArrayLength());
        foreach (var element in root.EnumerateArray())
        {
            if (element.ValueKind != JsonValueKind.Object)
            {
                continue;
            }

            var cve = GetString(element, "cve");
            if (string.IsNullOrWhiteSpace(cve))
            {
                continue;
            }

            parsed.Add(new CertCcVulnerabilityDto(
                NormalizeCve(cve),
                Description: NormalizeFreeformText(GetString(element, "description")),
                DateAdded: ParseDate(element, "date_added"),
                DateUpdated: ParseDate(element, "dateupdated")));
        }

        if (parsed.Count == 0)
        {
            return Array.Empty<CertCcVulnerabilityDto>();
        }

        return parsed
            .OrderBy(static vuln => vuln.CveId, StringComparer.OrdinalIgnoreCase)
            .ToArray();
    }

    private static IReadOnlyList<CertCcVendorStatusDto> ParseVendorStatuses(JsonElement root)
    {
        if (root.ValueKind != JsonValueKind.Array || root.GetArrayLength() == 0)
        {
            return Array.Empty<CertCcVendorStatusDto>();
        }

        var parsed = new List<CertCcVendorStatusDto>(root.GetArrayLength());
        foreach (var element in root.EnumerateArray())
        {
            if (element.ValueKind != JsonValueKind.Object)
            {
                continue;
            }

            var vendor = GetString(element, "vendor");
            var cve = GetString(element, "vul");
            var status = GetString(element, "status");
            if (string.IsNullOrWhiteSpace(vendor) || string.IsNullOrWhiteSpace(cve) || string.IsNullOrWhiteSpace(status))
            {
                continue;
            }

            var references = ExtractReferenceStringList(GetString(element, "references"), baseUri: null);
            parsed.Add(new CertCcVendorStatusDto(
                vendor.Trim(),
                NormalizeCve(cve),
                status.Trim(),
                NormalizeFreeformText(GetString(element, "statement")),
                references,
                DateAdded: ParseDate(element, "date_added"),
                DateUpdated: ParseDate(element, "dateupdated")));
        }

        if (parsed.Count == 0)
        {
            return Array.Empty<CertCcVendorStatusDto>();
        }

        return parsed
            .OrderBy(static entry => entry.CveId, StringComparer.OrdinalIgnoreCase)
            .ThenBy(static entry => entry.Vendor, StringComparer.OrdinalIgnoreCase)
            .ToArray();
    }

    private static string? NormalizeMarkdownToPlainText(JsonElement element, string propertyName, Uri baseUri)
        => NormalizeMarkdownToPlainText(GetString(element, propertyName), baseUri);

    private static string? NormalizeMarkdownToPlainText(string? markdown, Uri baseUri)
    {
        if (string.IsNullOrWhiteSpace(markdown))
        {
            return null;
        }

        var normalized = NormalizeLineEndings(markdown.Trim());
        if (normalized.Length == 0)
        {
            return null;
        }

        var html = Markdig.Markdown.ToHtml(normalized, MarkdownPipeline);
        if (string.IsNullOrWhiteSpace(html))
        {
            return null;
        }

        var sanitized = HtmlSanitizer.Sanitize(html, baseUri);
        if (string.IsNullOrWhiteSpace(sanitized))
        {
            return null;
        }

        var plain = ConvertHtmlToPlainText(sanitized);
        return string.IsNullOrWhiteSpace(plain) ? null : plain;
    }

    private static string? NormalizeFreeformText(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return null;
        }

        var normalized = NormalizeLineEndings(value).Trim();
        if (normalized.Length == 0)
        {
            return null;
        }

        var lines = normalized
            .Split('\n')
            .Select(static line => line.TrimEnd())
            .ToArray();

        return string.Join('\n', lines).Trim();
    }

    private static string ConvertHtmlToPlainText(string html)
    {
        if (string.IsNullOrWhiteSpace(html))
        {
            return string.Empty;
        }

        var decoded = WebUtility.HtmlDecode(html);
        decoded = decoded
            .Replace("<br />", "\n", StringComparison.OrdinalIgnoreCase)
            .Replace("<br/>", "\n", StringComparison.OrdinalIgnoreCase)
            .Replace("<br>", "\n", StringComparison.OrdinalIgnoreCase);

        decoded = Regex.Replace(decoded, "</p>", "\n\n", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
        decoded = Regex.Replace(decoded, "</div>", "\n", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
        decoded = Regex.Replace(decoded, "<li>", "- ", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
        decoded = Regex.Replace(decoded, "</li>", "\n", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
        decoded = Regex.Replace(decoded, "</tr>", "\n", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
        decoded = Regex.Replace(decoded, "</td>", " \t", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);

        decoded = HtmlTagRegex.Replace(decoded, string.Empty);
        decoded = NormalizeLineEndings(decoded);

        var lines = decoded
            .Split('\n', StringSplitOptions.RemoveEmptyEntries)
            .Select(static line => line.Trim())
            .ToArray();

        return string.Join('\n', lines).Trim();
    }

    private static IReadOnlyList<string> ExtractReferenceList(JsonElement element, string propertyName, Uri baseUri)
    {
        if (!element.TryGetProperty(propertyName, out var raw) || raw.ValueKind != JsonValueKind.Array || raw.GetArrayLength() == 0)
        {
            return Array.Empty<string>();
        }

        var references = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (var candidate in raw.EnumerateArray())
        {
            if (candidate.ValueKind != JsonValueKind.String)
            {
                continue;
            }

            var text = candidate.GetString();
            if (UrlNormalizer.TryNormalize(text, baseUri, out var normalized, stripFragment: true, forceHttps: false) && normalized is not null)
            {
                references.Add(normalized.ToString());
            }
        }

        if (references.Count == 0)
        {
            return Array.Empty<string>();
        }

        return references
            .OrderBy(static url => url, StringComparer.OrdinalIgnoreCase)
            .ToArray();
    }

    private static IReadOnlyList<string> ExtractReferenceStringList(string? value, Uri? baseUri)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return Array.Empty<string>();
        }

        var buffer = ArrayPool<string>.Shared.Rent(16);
        try
        {
            var count = 0;
            var span = value.AsSpan();
            var start = 0;

            for (var index = 0; index < span.Length; index++)
            {
                var ch = span[index];
                if (ch == '\r' || ch == '\n')
                {
                    if (index > start)
                    {
                        AppendSegment(span, start, index - start, baseUri, buffer, ref count);
                    }

                    if (ch == '\r' && index + 1 < span.Length && span[index + 1] == '\n')
                    {
                        index++;
                    }

                    start = index + 1;
                }
            }

            if (start < span.Length)
            {
                AppendSegment(span, start, span.Length - start, baseUri, buffer, ref count);
            }

            if (count == 0)
            {
                return Array.Empty<string>();
            }

            return buffer.AsSpan(0, count)
                .ToArray()
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .OrderBy(static url => url, StringComparer.OrdinalIgnoreCase)
                .ToArray();
        }
        finally
        {
            ArrayPool<string>.Shared.Return(buffer, clearArray: true);
        }
    }

    private static void AppendSegment(ReadOnlySpan<char> span, int start, int length, Uri? baseUri, string[] buffer, ref int count)
    {
        var segment = span.Slice(start, length).ToString().Trim();
        if (segment.Length == 0)
        {
            return;
        }

        if (!UrlNormalizer.TryNormalize(segment, baseUri, out var normalized, stripFragment: true, forceHttps: false) || normalized is null)
        {
            return;
        }

        if (count >= buffer.Length)
        {
            return;
        }

        buffer[count++] = normalized.ToString();
    }

    private static IReadOnlyList<string> ExtractCveIds(JsonElement element, string propertyName)
    {
        if (!element.TryGetProperty(propertyName, out var raw) || raw.ValueKind != JsonValueKind.Array || raw.GetArrayLength() == 0)
        {
            return Array.Empty<string>();
        }

        var values = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (var entry in raw.EnumerateArray())
        {
            if (entry.ValueKind != JsonValueKind.String)
            {
                continue;
            }

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

            values.Add(NormalizeCve(text));
        }

        if (values.Count == 0)
        {
            return Array.Empty<string>();
        }

        return values
            .OrderBy(static id => id, StringComparer.OrdinalIgnoreCase)
            .ToArray();
    }

    private static string NormalizeCve(string value)
    {
        var trimmed = value.Trim();
        if (!trimmed.StartsWith("CVE-", StringComparison.OrdinalIgnoreCase))
        {
            trimmed = $"CVE-{trimmed}";
        }

        var builder = new StringBuilder(trimmed.Length);
        foreach (var ch in trimmed)
        {
            builder.Append(char.ToUpperInvariant(ch));
        }

        return builder.ToString();
    }

    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 => property.ToString(),
            _ => null,
        };
    }

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

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

    private static int? ParseInt(JsonElement element, string propertyName)
    {
        if (!element.TryGetProperty(propertyName, out var property))
        {
            return null;
        }

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

        var text = GetString(element, propertyName);
        if (string.IsNullOrWhiteSpace(text))
        {
            return null;
        }

        return int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) ? parsed : (int?)null;
    }

    private static Uri BuildDetailUri(string idNumber)
    {
        var sanitized = idNumber.Trim();
        return new Uri($"https://www.kb.cert.org/vuls/id/{sanitized}", UriKind.Absolute);
    }

    private static string NormalizeLineEndings(string value)
    {
        if (value.IndexOf('\r') < 0)
        {
            return value;
        }

        return value.Replace("\r\n", "\n", StringComparison.Ordinal).Replace('\r', '\n');
    }
}
