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

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

internal static class AppleDetailParser
{
    private static readonly HtmlParser Parser = new();
    private static readonly Regex CveRegex = new(@"CVE-\d{4}-\d{4,7}", RegexOptions.Compiled | RegexOptions.IgnoreCase);

    public static AppleDetailDto Parse(string html, AppleIndexEntry entry)
    {
        if (string.IsNullOrWhiteSpace(html))
        {
            throw new ArgumentException("HTML content must not be empty.", nameof(html));
        }

        var document = Parser.ParseDocument(html);
        var title = ResolveTitle(document, entry.Title);
        var summary = ResolveSummary(document);
        var (published, updated) = ResolveTimestamps(document, entry.PostingDate);
        var cves = ExtractCves(document);
        var affected = NormalizeAffectedProducts(ExtractProducts(document));
        var references = ExtractReferences(document, entry.DetailUri);

        var dto = new AppleDetailDto(
            entry.ArticleId,
            entry.ArticleId,
            title,
            summary,
            published,
            updated,
            cves,
            affected,
            references,
            entry.IsRapidSecurityResponse);

        return dto.WithAffectedFallback(entry.Products);
    }

    private static IReadOnlyList<AppleAffectedProductDto> NormalizeAffectedProducts(IReadOnlyList<AppleAffectedProductDto> affected)
    {
        if (affected.Count <= 1)
        {
            return affected;
        }

        return affected
            .OrderBy(static product => NormalizeSortKey(product.Platform))
            .ThenBy(static product => NormalizeSortKey(product.Name))
            .ThenBy(static product => NormalizeSortKey(product.Version))
            .ThenBy(static product => NormalizeSortKey(product.Build))
            .ToArray();
    }

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

        var span = value.AsSpan();
        var buffer = new char[span.Length];
        var index = 0;

        foreach (var ch in span)
        {
            if (char.IsWhiteSpace(ch))
            {
                continue;
            }

            buffer[index++] = char.ToUpperInvariant(ch);
        }

        return new string(buffer, 0, index);
    }

    private static string ResolveTitle(IHtmlDocument document, string fallback)
    {
        var title = document.QuerySelector("[data-testid='update-title']")?.TextContent
            ?? document.QuerySelector("h1, h2")?.TextContent
            ?? document.Title;

        title = title?.Trim();
        return string.IsNullOrEmpty(title) ? fallback : title;
    }

    private static string ResolveSummary(IHtmlDocument document)
    {
        var summary = document.QuerySelector("[data-testid='update-summary']")?.TextContent
            ?? document.QuerySelector("meta[name='description']")?.GetAttribute("content")
            ?? document.QuerySelector("p")?.TextContent
            ?? string.Empty;

        return CleanWhitespace(summary);
    }

    private static (DateTimeOffset Published, DateTimeOffset? Updated) ResolveTimestamps(IHtmlDocument document, DateTimeOffset postingFallback)
    {
        DateTimeOffset published = postingFallback;
        DateTimeOffset? updated = null;

        foreach (var time in document.QuerySelectorAll("time"))
        {
            var raw = time.GetAttribute("datetime") ?? time.TextContent;
            if (string.IsNullOrWhiteSpace(raw))
            {
                continue;
            }

            if (!DateTimeOffset.TryParse(raw, out var parsed))
            {
                continue;
            }

            parsed = parsed.ToUniversalTime();

            var itemProp = time.GetAttribute("itemprop") ?? string.Empty;
            var dataTestId = time.GetAttribute("data-testid") ?? string.Empty;

            if (itemProp.Equals("datePublished", StringComparison.OrdinalIgnoreCase)
                || dataTestId.Equals("published", StringComparison.OrdinalIgnoreCase))
            {
                published = parsed;
            }
            else if (itemProp.Equals("dateModified", StringComparison.OrdinalIgnoreCase)
                || dataTestId.Equals("updated", StringComparison.OrdinalIgnoreCase))
            {
                updated = parsed;
            }
            else if (updated is null && parsed > published)
            {
                updated = parsed;
            }
        }

        return (published, updated);
    }

    private static IReadOnlyList<string> ExtractCves(IHtmlDocument document)
    {
        var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (var node in document.All)
        {
            if (node.NodeType != NodeType.Text && node.NodeType != NodeType.Element)
            {
                continue;
            }

            var text = node.TextContent;
            if (string.IsNullOrWhiteSpace(text))
            {
                continue;
            }

            foreach (Match match in CveRegex.Matches(text))
            {
                if (match.Success)
                {
                    set.Add(match.Value.ToUpperInvariant());
                }
            }
        }

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

        var list = set.ToList();
        list.Sort(StringComparer.OrdinalIgnoreCase);
        return list;
    }

    private static IReadOnlyList<AppleAffectedProductDto> ExtractProducts(IHtmlDocument document)
    {
        var rows = new List<AppleAffectedProductDto>();

        foreach (var element in document.QuerySelectorAll("[data-testid='product-row']"))
        {
            var platform = element.GetAttribute("data-platform") ?? string.Empty;
            var name = element.GetAttribute("data-product") ?? platform;
            var version = element.GetAttribute("data-version") ?? string.Empty;
            var build = element.GetAttribute("data-build") ?? string.Empty;

            if (string.IsNullOrWhiteSpace(name) && element is IHtmlTableRowElement tableRow)
            {
                var cells = tableRow.Cells.Select(static cell => CleanWhitespace(cell.TextContent)).ToArray();
                if (cells.Length >= 1)
                {
                    name = cells[0];
                }

                if (cells.Length >= 2 && string.IsNullOrWhiteSpace(version))
                {
                    version = cells[1];
                }

                if (cells.Length >= 3 && string.IsNullOrWhiteSpace(build))
                {
                    build = cells[2];
                }
            }

            if (string.IsNullOrWhiteSpace(name))
            {
                continue;
            }

            rows.Add(new AppleAffectedProductDto(platform, name, version, build));
        }

        if (rows.Count > 0)
        {
            return rows;
        }

        // fallback for generic tables without data attributes
        foreach (var table in document.QuerySelectorAll("table"))
        {
            var headers = table.QuerySelectorAll("th").Select(static th => CleanWhitespace(th.TextContent)).ToArray();
            if (headers.Length == 0)
            {
                continue;
            }

            var nameIndex = Array.FindIndex(headers, static header => header.Contains("product", StringComparison.OrdinalIgnoreCase)
                || header.Contains("device", StringComparison.OrdinalIgnoreCase));
            var versionIndex = Array.FindIndex(headers, static header => header.Contains("version", StringComparison.OrdinalIgnoreCase));
            var buildIndex = Array.FindIndex(headers, static header => header.Contains("build", StringComparison.OrdinalIgnoreCase)
                || header.Contains("release", StringComparison.OrdinalIgnoreCase));

            if (nameIndex == -1 && versionIndex == -1 && buildIndex == -1)
            {
                continue;
            }

            foreach (var row in table.QuerySelectorAll("tr"))
            {
                var cells = row.QuerySelectorAll("td").Select(static cell => CleanWhitespace(cell.TextContent)).ToArray();
                if (cells.Length == 0)
                {
                    continue;
                }

                string name = nameIndex >= 0 && nameIndex < cells.Length ? cells[nameIndex] : cells[0];
                string version = versionIndex >= 0 && versionIndex < cells.Length ? cells[versionIndex] : string.Empty;
                string build = buildIndex >= 0 && buildIndex < cells.Length ? cells[buildIndex] : string.Empty;

                if (string.IsNullOrWhiteSpace(name))
                {
                    continue;
                }

                rows.Add(new AppleAffectedProductDto(string.Empty, name, version, build));
            }

            if (rows.Count > 0)
            {
                break;
            }
        }

        return rows.Count == 0 ? Array.Empty<AppleAffectedProductDto>() : rows;
    }

    private static IReadOnlyList<AppleReferenceDto> ExtractReferences(IHtmlDocument document, Uri detailUri)
    {
        var scope = document.QuerySelector("article")
            ?? document.QuerySelector("main")
            ?? (IElement?)document.Body;

        if (scope is null)
        {
            return Array.Empty<AppleReferenceDto>();
        }

        var anchors = scope.QuerySelectorAll("a[href]");
        if (anchors.Length == 0)
        {
            return Array.Empty<AppleReferenceDto>();
        }

        var references = new List<AppleReferenceDto>(anchors.Length);
        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        foreach (var element in anchors)
        {
            if (element is not IHtmlAnchorElement anchor)
            {
                continue;
            }

            if (anchor.HasAttribute("data-globalnav-item-name")
                || anchor.HasAttribute("data-analytics-title"))
            {
                continue;
            }

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

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

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

            var normalized = uri.ToString();
            if (!seen.Add(normalized))
            {
                continue;
            }

            var title = CleanWhitespace(anchor.TextContent);
            var kind = ResolveReferenceKind(uri);
            references.Add(new AppleReferenceDto(normalized, title, kind));
        }

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

        references.Sort(static (left, right) => StringComparer.OrdinalIgnoreCase.Compare(left.Url, right.Url));
        return references;
    }

    private static bool IsRelevantReference(Uri uri)
    {
        if (!uri.IsAbsoluteUri)
        {
            return false;
        }

        if (!string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase)
            && !string.Equals(uri.Scheme, "http", StringComparison.OrdinalIgnoreCase))
        {
            return false;
        }

        if (string.IsNullOrWhiteSpace(uri.Host))
        {
            return false;
        }

        if (uri.Host.Equals("www.apple.com", StringComparison.OrdinalIgnoreCase))
        {
            if (!uri.AbsolutePath.Contains("/support/", StringComparison.OrdinalIgnoreCase)
                && !uri.AbsolutePath.Contains("/security", StringComparison.OrdinalIgnoreCase)
                && !uri.AbsolutePath.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }
        }

        if (uri.Host.EndsWith(".apple.com", StringComparison.OrdinalIgnoreCase))
        {
            var supported =
                uri.Host.StartsWith("support.", StringComparison.OrdinalIgnoreCase)
                || uri.Host.StartsWith("developer.", StringComparison.OrdinalIgnoreCase)
                || uri.Host.StartsWith("download.", StringComparison.OrdinalIgnoreCase)
                || uri.Host.StartsWith("updates.", StringComparison.OrdinalIgnoreCase)
                || uri.Host.Equals("www.apple.com", StringComparison.OrdinalIgnoreCase);

            if (!supported)
            {
                return false;
            }

            if (uri.Host.StartsWith("support.", StringComparison.OrdinalIgnoreCase)
                && uri.AbsolutePath == "/"
                && (string.IsNullOrEmpty(uri.Query)
                    || uri.Query.Contains("cid=", StringComparison.OrdinalIgnoreCase)))
            {
                return false;
            }
        }

        return true;
    }

    private static string? ResolveReferenceKind(Uri uri)
    {
        if (uri.Host.Contains("apple.com", StringComparison.OrdinalIgnoreCase))
        {
            if (uri.AbsolutePath.Contains("download", StringComparison.OrdinalIgnoreCase))
            {
                return "download";
            }

            if (uri.AbsolutePath.Contains(".pdf", StringComparison.OrdinalIgnoreCase))
            {
                return "document";
            }

            return "advisory";
        }

        if (uri.Host.Contains("nvd.nist.gov", StringComparison.OrdinalIgnoreCase))
        {
            return "nvd";
        }

        if (uri.Host.Contains("support", StringComparison.OrdinalIgnoreCase))
        {
            return "kb";
        }

        return null;
    }

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

        var span = value.AsSpan();
        var buffer = new char[span.Length];
        var index = 0;
        var previousWhitespace = false;

        foreach (var ch in span)
        {
            if (char.IsWhiteSpace(ch))
            {
                if (previousWhitespace)
                {
                    continue;
                }

                buffer[index++] = ' ';
                previousWhitespace = true;
            }
            else
            {
                buffer[index++] = ch;
                previousWhitespace = false;
            }
        }

        return new string(buffer, 0, index).Trim();
    }
}
