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

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

internal static class AdobeIndexParser
{
    private static readonly HtmlParser Parser = new();
    private static readonly Regex AdvisoryIdRegex = new("(APSB|APA)\\d{2}-\\d{2,}", RegexOptions.IgnoreCase | RegexOptions.Compiled);
    private static readonly string[] ExplicitFormats =
    {
        "MMMM d, yyyy",
        "MMM d, yyyy",
        "M/d/yyyy",
        "MM/dd/yyyy",
        "yyyy-MM-dd",
    };

    public static IReadOnlyCollection<AdobeIndexEntry> Parse(string html, Uri baseUri)
    {
        ArgumentNullException.ThrowIfNull(html);
        ArgumentNullException.ThrowIfNull(baseUri);

        var document = Parser.ParseDocument(html);
        var map = new Dictionary<string, AdobeIndexEntry>(StringComparer.OrdinalIgnoreCase);
        var anchors = document.QuerySelectorAll("a[href]");

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

            if (!href.Contains("/security/products/", StringComparison.OrdinalIgnoreCase))
            {
                continue;
            }

            if (!TryExtractAdvisoryId(anchor.TextContent, href, out var advisoryId))
            {
                continue;
            }

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

            var published = TryResolvePublished(anchor) ?? DateTimeOffset.UtcNow;
            var entry = new AdobeIndexEntry(advisoryId.ToUpperInvariant(), detailUri, published, anchor.TextContent?.Trim());
            map[entry.AdvisoryId] = entry;
        }

        return map.Values
            .OrderBy(static e => e.PublishedUtc)
            .ThenBy(static e => e.AdvisoryId, StringComparer.OrdinalIgnoreCase)
            .ToArray();
    }

    private static bool TryExtractAdvisoryId(string? text, string href, out string advisoryId)
    {
        if (!string.IsNullOrWhiteSpace(text))
        {
            var match = AdvisoryIdRegex.Match(text);
            if (match.Success)
            {
                advisoryId = match.Value.ToUpperInvariant();
                return true;
            }
        }

        var hrefMatch = AdvisoryIdRegex.Match(href);
        if (hrefMatch.Success)
        {
            advisoryId = hrefMatch.Value.ToUpperInvariant();
            return true;
        }

        advisoryId = string.Empty;
        return false;
    }

    private static DateTimeOffset? TryResolvePublished(IElement anchor)
    {
        var row = anchor.Closest("tr");
        if (row is not null)
        {
            var cells = row.GetElementsByTagName("td");
            if (cells.Length >= 2)
            {
                for (var idx = 1; idx < cells.Length; idx++)
                {
                    if (TryParseDate(cells[idx].TextContent, out var parsed))
                    {
                        return parsed;
                    }
                }
            }
        }

        var sibling = anchor.NextElementSibling;
        while (sibling is not null)
        {
            if (TryParseDate(sibling.TextContent, out var parsed))
            {
                return parsed;
            }

            sibling = sibling.NextElementSibling;
        }

        if (TryParseDate(anchor.ParentElement?.TextContent, out var parentDate))
        {
            return parentDate;
        }

        return null;
    }

    private static bool TryParseDate(string? value, out DateTimeOffset result)
    {
        result = default;
        if (string.IsNullOrWhiteSpace(value))
        {
            return false;
        }

        var trimmed = value.Trim();
        if (DateTimeOffset.TryParse(trimmed, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out result))
        {
            return Normalize(ref result);
        }

        foreach (var format in ExplicitFormats)
        {
            if (DateTime.TryParseExact(trimmed, format, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date))
            {
                result = new DateTimeOffset(date, TimeSpan.Zero);
                return Normalize(ref result);
            }
        }

        return false;
    }

    private static bool Normalize(ref DateTimeOffset value)
    {
        value = value.ToUniversalTime();
        value = new DateTimeOffset(value.Year, value.Month, value.Day, 0, 0, 0, TimeSpan.Zero);
        return true;
    }
}
