using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using AngleSharp.Html.Dom;
using AngleSharp.Html.Parser;

namespace StellaOps.Feedser.Source.Vndr.Oracle.Internal;

internal static class OracleParser
{
    private static readonly Regex AnchorRegex = new("<a[^>]+href=\"(?<url>https?://[^\"]+)\"", RegexOptions.IgnoreCase | RegexOptions.Compiled);
    private static readonly Regex TagRegex = new("<[^>]+>", RegexOptions.Compiled);
    private static readonly Regex WhitespaceRegex = new("\\s+", RegexOptions.Compiled);
    private static readonly Regex CveRegex = new("CVE-\\d{4}-\\d{3,7}", RegexOptions.IgnoreCase | RegexOptions.Compiled);
    private static readonly Regex UpdatedDateRegex = new("\"updatedDate\"\\s*:\\s*\"(?<value>[^\"]+)\"", RegexOptions.IgnoreCase | RegexOptions.Compiled);
    private static readonly string[] AllowedReferenceTokens =
    {
        "security-alerts",
        "/kb/",
        "/patches",
        "/rs",
        "/support/",
        "/mos/",
        "/technicalresources/",
        "/technetwork/"
    };

    public static OracleDto Parse(string html, OracleDocumentMetadata metadata)
    {
        ArgumentException.ThrowIfNullOrEmpty(html);
        ArgumentNullException.ThrowIfNull(metadata);

        var parser = new HtmlParser();
        var document = parser.ParseDocument(html);

        var published = ExtractPublishedDate(document) ?? metadata.Published;
        var content = Sanitize(html);
        var affected = ExtractAffectedEntries(document);
        var references = ExtractReferences(html);
        var patchDocuments = ExtractPatchDocuments(document, metadata.DetailUri);
        var cveIds = ExtractCveIds(document, content, affected);

        return new OracleDto(
            metadata.AdvisoryId,
            metadata.Title,
            metadata.DetailUri.ToString(),
            published,
            content,
            references,
            cveIds,
            affected,
            patchDocuments);
    }

    private static string Sanitize(string html)
    {
        var withoutTags = TagRegex.Replace(html, " ");
        var decoded = System.Net.WebUtility.HtmlDecode(withoutTags) ?? string.Empty;
        return WhitespaceRegex.Replace(decoded, " ").Trim();
    }

    private static IReadOnlyList<string> ExtractReferences(string html)
    {
        var references = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (Match match in AnchorRegex.Matches(html))
        {
            if (!match.Success)
            {
                continue;
            }

            var raw = match.Groups["url"].Value?.Trim();
            if (string.IsNullOrEmpty(raw))
            {
                continue;
            }

            var decoded = System.Net.WebUtility.HtmlDecode(raw) ?? raw;

            if (!Uri.TryCreate(decoded, UriKind.Absolute, out var uri))
            {
                continue;
            }

            if (!string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase)
                && !string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
            {
                continue;
            }

            if (!ShouldIncludeReference(uri))
            {
                continue;
            }

            references.Add(uri.ToString());
        }

        return references.Count == 0
            ? Array.Empty<string>()
            : references.OrderBy(url => url, StringComparer.OrdinalIgnoreCase).ToArray();
    }

    private static bool ShouldIncludeReference(Uri uri)
    {
        if (uri.Host.EndsWith("cve.org", StringComparison.OrdinalIgnoreCase))
        {
            return true;
        }

        if (!uri.Host.EndsWith("oracle.com", StringComparison.OrdinalIgnoreCase))
        {
            return false;
        }

        if (uri.Query.Contains("type=doc", StringComparison.OrdinalIgnoreCase))
        {
            return true;
        }

        var path = uri.AbsolutePath ?? string.Empty;
        return AllowedReferenceTokens.Any(token => path.Contains(token, StringComparison.OrdinalIgnoreCase));
    }

    private static DateTimeOffset? ExtractPublishedDate(IHtmlDocument document)
    {
        var meta = document.QuerySelectorAll("meta")
            .FirstOrDefault(static element => string.Equals(element.GetAttribute("name"), "Updated Date", StringComparison.OrdinalIgnoreCase));
        if (meta is not null && TryParseOracleDate(meta.GetAttribute("content"), out var parsed))
        {
            return parsed;
        }

        foreach (var script in document.Scripts)
        {
            var text = script.TextContent;
            if (string.IsNullOrWhiteSpace(text))
            {
                continue;
            }

            var match = UpdatedDateRegex.Match(text);
            if (!match.Success)
            {
                continue;
            }

            if (TryParseOracleDate(match.Groups["value"].Value, out var embedded))
            {
                return embedded;
            }
        }

        return null;
    }

    private static bool TryParseOracleDate(string? value, out DateTimeOffset result)
    {
        if (!string.IsNullOrWhiteSpace(value)
            && DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out result))
        {
            result = result.ToUniversalTime();
            return true;
        }

        result = default;
        return false;
    }

    private static IReadOnlyList<OracleAffectedEntry> ExtractAffectedEntries(IHtmlDocument document)
    {
        var entries = new List<OracleAffectedEntry>();

        foreach (var table in document.QuerySelectorAll("table"))
        {
            if (table is not IHtmlTableElement tableElement)
            {
                continue;
            }

            if (!IsRiskMatrixTable(tableElement))
            {
                continue;
            }

            var lastProduct = string.Empty;
            var lastComponent = string.Empty;
            var lastVersions = string.Empty;
            var lastNotes = string.Empty;
            IReadOnlyList<string> lastCves = Array.Empty<string>();

            foreach (var body in tableElement.Bodies)
            {
                foreach (var row in body.Rows)
                {
                    if (row is not IHtmlTableRowElement tableRow || tableRow.Cells.Length == 0)
                    {
                        continue;
                    }

                    var cveText = NormalizeCellText(GetCellText(tableRow, 0));
                    var cves = ExtractCvesFromText(cveText);
                    if (cves.Count == 0 && lastCves.Count > 0)
                    {
                        cves = lastCves;
                    }
                    else if (cves.Count > 0)
                    {
                        lastCves = cves;
                    }

                    var product = NormalizeCellText(GetCellText(tableRow, 1));
                    if (string.IsNullOrEmpty(product))
                    {
                        product = lastProduct;
                    }
                    else
                    {
                        lastProduct = product;
                    }

                    var component = NormalizeCellText(GetCellText(tableRow, 2));
                    if (string.IsNullOrEmpty(component))
                    {
                        component = lastComponent;
                    }
                    else
                    {
                        lastComponent = component;
                    }

                    var supportedVersions = NormalizeCellText(GetCellTextFromEnd(tableRow, 2));
                    if (string.IsNullOrEmpty(supportedVersions))
                    {
                        supportedVersions = lastVersions;
                    }
                    else
                    {
                        lastVersions = supportedVersions;
                    }

                    var notes = NormalizeCellText(GetCellTextFromEnd(tableRow, 1));
                    if (string.IsNullOrEmpty(notes))
                    {
                        notes = lastNotes;
                    }
                    else
                    {
                        lastNotes = notes;
                    }

                    if (string.IsNullOrEmpty(product) || cves.Count == 0)
                    {
                        continue;
                    }

                    entries.Add(new OracleAffectedEntry(
                        product,
                        string.IsNullOrEmpty(component) ? null : component,
                        string.IsNullOrEmpty(supportedVersions) ? null : supportedVersions,
                        string.IsNullOrEmpty(notes) ? null : notes,
                        cves));
                }
            }
        }

        return entries.Count == 0 ? Array.Empty<OracleAffectedEntry>() : entries;
    }

    private static IReadOnlyList<string> ExtractCveIds(IHtmlDocument document, string content, IReadOnlyList<OracleAffectedEntry> affectedEntries)
    {
        var cves = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        if (!string.IsNullOrWhiteSpace(content))
        {
            foreach (Match match in CveRegex.Matches(content))
            {
                cves.Add(match.Value.ToUpperInvariant());
            }
        }

        foreach (var entry in affectedEntries)
        {
            foreach (var cve in entry.CveIds)
            {
                if (!string.IsNullOrWhiteSpace(cve))
                {
                    cves.Add(cve.ToUpperInvariant());
                }
            }
        }

        var bodyText = document.Body?.TextContent;
        if (!string.IsNullOrWhiteSpace(bodyText))
        {
            foreach (Match match in CveRegex.Matches(bodyText))
            {
                cves.Add(match.Value.ToUpperInvariant());
            }
        }

        return cves.Count == 0
            ? Array.Empty<string>()
            : cves.OrderBy(static id => id, StringComparer.OrdinalIgnoreCase).ToArray();
    }

    private static IReadOnlyList<OraclePatchDocument> ExtractPatchDocuments(IHtmlDocument document, Uri detailUri)
    {
        var results = new List<OraclePatchDocument>();

        foreach (var table in document.QuerySelectorAll("table"))
        {
            if (table is not IHtmlTableElement tableElement)
            {
                continue;
            }

            if (!TableHasPatchHeader(tableElement))
            {
                continue;
            }

            foreach (var body in tableElement.Bodies)
            {
                foreach (var row in body.Rows)
                {
                    if (row is not IHtmlTableRowElement tableRow || tableRow.Cells.Length < 2)
                    {
                        continue;
                    }

                    var product = NormalizeCellText(tableRow.Cells[0]?.TextContent);
                    if (string.IsNullOrEmpty(product))
                    {
                        continue;
                    }

                    var anchor = tableRow.Cells[1]?.QuerySelector("a");
                    if (anchor is null)
                    {
                        continue;
                    }

            var href = anchor.GetAttribute("href");
            if (string.IsNullOrWhiteSpace(href))
            {
                continue;
            }

            var decoded = System.Net.WebUtility.HtmlDecode(href) ?? href;

            if (!Uri.TryCreate(detailUri, decoded, out var uri) || !uri.IsAbsoluteUri)
            {
                continue;
            }

                    if (!string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase)
                        && !string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
                    {
                        continue;
                    }

                    var title = NormalizeCellText(anchor.TextContent);
                    results.Add(new OraclePatchDocument(product, string.IsNullOrEmpty(title) ? null : title, uri.ToString()));
                }
            }
        }

        return results.Count == 0 ? Array.Empty<OraclePatchDocument>() : results;
    }

    private static bool IsRiskMatrixTable(IHtmlTableElement table)
    {
        var headerText = table.Head?.TextContent;
        if (string.IsNullOrWhiteSpace(headerText))
        {
            return false;
        }

        return headerText.Contains("CVE ID", StringComparison.OrdinalIgnoreCase)
            && headerText.Contains("Supported Versions", StringComparison.OrdinalIgnoreCase);
    }

    private static bool TableHasPatchHeader(IHtmlTableElement table)
    {
        var headerText = table.Head?.TextContent;
        if (string.IsNullOrWhiteSpace(headerText))
        {
            return false;
        }

        return headerText.Contains("Affected Products and Versions", StringComparison.OrdinalIgnoreCase)
            && headerText.Contains("Patch Availability Document", StringComparison.OrdinalIgnoreCase);
    }

    private static string? GetCellText(IHtmlTableRowElement row, int index)
    {
        if (index < 0 || index >= row.Cells.Length)
        {
            return null;
        }

        return row.Cells[index]?.TextContent;
    }

    private static string? GetCellTextFromEnd(IHtmlTableRowElement row, int offsetFromEnd)
    {
        if (offsetFromEnd <= 0)
        {
            return null;
        }

        var index = row.Cells.Length - offsetFromEnd;
        return index >= 0 ? row.Cells[index]?.TextContent : null;
    }

    private static IReadOnlyList<string> ExtractCvesFromText(string? text)
    {
        if (string.IsNullOrWhiteSpace(text))
        {
            return Array.Empty<string>();
        }

        var matches = CveRegex.Matches(text);
        if (matches.Count == 0)
        {
            return Array.Empty<string>();
        }

        var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (Match match in matches)
        {
            if (match.Success)
            {
                set.Add(match.Value.ToUpperInvariant());
            }
        }

        return set.Count == 0
            ? Array.Empty<string>()
            : set.OrderBy(static id => id, StringComparer.Ordinal).ToArray();
    }

    private static string NormalizeCellText(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return string.Empty;
        }

        var cleaned = value.Replace('\u00A0', ' ');
        cleaned = WhitespaceRegex.Replace(cleaned, " ");
        return cleaned.Trim();
    }
}
