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

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

internal static class AdobeDetailParser
{
    private static readonly HtmlParser Parser = new();
    private static readonly Regex CveRegex = new("CVE-\\d{4}-\\d{4,}", RegexOptions.IgnoreCase | RegexOptions.Compiled);
    private static readonly string[] DateMarkers = { "date published", "release date", "published" };

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

        using var document = Parser.ParseDocument(html);
        var title = metadata.Title ?? document.QuerySelector("h1")?.TextContent?.Trim() ?? metadata.AdvisoryId;
        var summary = document.QuerySelector("p")?.TextContent?.Trim();

        var published = metadata.PublishedUtc ?? TryExtractPublished(document) ?? DateTimeOffset.UtcNow;

        var cves = ExtractCves(document.Body?.TextContent ?? string.Empty);
        var products = ExtractProductEntries(title, document);

        return AdobeBulletinDto.Create(
            metadata.AdvisoryId,
            title,
            published,
            products,
            cves,
            metadata.DetailUri,
            summary);
    }

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

        var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (Match match in CveRegex.Matches(text))
        {
            if (!string.IsNullOrWhiteSpace(match.Value))
            {
                set.Add(match.Value.ToUpperInvariant());
            }
        }

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

    private static IReadOnlyList<AdobeProductEntry> ExtractProductEntries(string title, IDocument document)
    {
        var builders = new Dictionary<AdobeProductKey, AdobeProductEntryBuilder>(AdobeProductKeyComparer.Instance);

        foreach (var builder in ParseAffectedTable(document))
        {
            builders[builder.Key] = builder;
        }

        foreach (var updated in ParseUpdatedTable(document))
        {
            if (builders.TryGetValue(updated.Key, out var builder))
            {
                builder.UpdatedVersion ??= updated.UpdatedVersion;
                builder.Priority ??= updated.Priority;
                builder.Availability ??= updated.Availability;
            }
            else
            {
                builders[updated.Key] = updated;
            }
        }

        if (builders.Count == 0 && !string.IsNullOrWhiteSpace(title))
        {
            var fallback = new AdobeProductEntryBuilder(
                NormalizeWhitespace(title),
                string.Empty,
                string.Empty)
            {
                AffectedVersion = null,
                UpdatedVersion = null,
                Priority = null,
                Availability = null
            };

            builders[fallback.Key] = fallback;
        }

        return builders.Values
            .Select(static builder => builder.ToEntry())
            .ToList();
    }

    private static IEnumerable<AdobeProductEntryBuilder> ParseAffectedTable(IDocument document)
    {
        var table = FindTableByHeader(document, "Affected Versions");
        if (table is null)
        {
            yield break;
        }

        foreach (var row in table.Rows.Skip(1))
        {
            var cells = row.Cells;
            if (cells.Length < 3)
            {
                continue;
            }

            var product = NormalizeWhitespace(cells[0]?.TextContent);
            var track = NormalizeWhitespace(cells.ElementAtOrDefault(1)?.TextContent);
            var platformText = NormalizeWhitespace(cells.ElementAtOrDefault(3)?.TextContent);

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

            var affectedCell = cells[2];
            foreach (var line in ExtractLines(affectedCell))
            {
                if (string.IsNullOrWhiteSpace(line))
                {
                    continue;
                }

                var (platform, versionText) = SplitPlatformLine(line, platformText);
                var builder = new AdobeProductEntryBuilder(product, track, platform)
                {
                    AffectedVersion = versionText
                };

                yield return builder;
            }
        }
    }

    private static IEnumerable<AdobeProductEntryBuilder> ParseUpdatedTable(IDocument document)
    {
        var table = FindTableByHeader(document, "Updated Versions");
        if (table is null)
        {
            yield break;
        }

        foreach (var row in table.Rows.Skip(1))
        {
            var cells = row.Cells;
            if (cells.Length < 3)
            {
                continue;
            }

            var product = NormalizeWhitespace(cells[0]?.TextContent);
            var track = NormalizeWhitespace(cells.ElementAtOrDefault(1)?.TextContent);
            var platformText = NormalizeWhitespace(cells.ElementAtOrDefault(3)?.TextContent);
            var priority = NormalizeWhitespace(cells.ElementAtOrDefault(4)?.TextContent);
            var availability = NormalizeWhitespace(cells.ElementAtOrDefault(5)?.TextContent);

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

            var updatedCell = cells[2];
            var lines = ExtractLines(updatedCell);
            if (lines.Count == 0)
            {
                lines.Add(updatedCell.TextContent ?? string.Empty);
            }

            foreach (var line in lines)
            {
                if (string.IsNullOrWhiteSpace(line))
                {
                    continue;
                }

                var (platform, versionText) = SplitPlatformLine(line, platformText);
                var builder = new AdobeProductEntryBuilder(product, track, platform)
                {
                    UpdatedVersion = versionText,
                    Priority = priority,
                    Availability = availability
                };

                yield return builder;
            }
        }
    }

    private static IHtmlTableElement? FindTableByHeader(IDocument document, string headerText)
    {
        return document
            .QuerySelectorAll("table")
            .OfType<IHtmlTableElement>()
            .FirstOrDefault(table => table.TextContent.Contains(headerText, StringComparison.OrdinalIgnoreCase));
    }

    private static List<string> ExtractLines(IElement? cell)
    {
        var lines = new List<string>();
        if (cell is null)
        {
            return lines;
        }

        var paragraphs = cell.QuerySelectorAll("p").Select(static p => p.TextContent).ToArray();
        if (paragraphs.Length > 0)
        {
            foreach (var paragraph in paragraphs)
            {
                var normalized = NormalizeWhitespace(paragraph);
                if (!string.IsNullOrWhiteSpace(normalized))
                {
                    lines.Add(normalized);
                }
            }

            return lines;
        }

        var items = cell.QuerySelectorAll("li").Select(static li => li.TextContent).ToArray();
        if (items.Length > 0)
        {
            foreach (var item in items)
            {
                var normalized = NormalizeWhitespace(item);
                if (!string.IsNullOrWhiteSpace(normalized))
                {
                    lines.Add(normalized);
                }
            }

            return lines;
        }

        var raw = NormalizeWhitespace(cell.TextContent);
        if (!string.IsNullOrWhiteSpace(raw))
        {
            lines.AddRange(raw.Split(new[] { '\n' }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries));
        }

        return lines;
    }

    private static (string Platform, string? Version) SplitPlatformLine(string line, string? fallbackPlatform)
    {
        var separatorIndex = line.IndexOf('-', StringComparison.Ordinal);
        if (separatorIndex > 0 && separatorIndex < line.Length - 1)
        {
            var prefix = line[..separatorIndex].Trim();
            var versionText = line[(separatorIndex + 1)..].Trim();
            return (NormalizePlatform(prefix) ?? NormalizePlatform(fallbackPlatform) ?? fallbackPlatform ?? string.Empty, versionText);
        }

        return (NormalizePlatform(fallbackPlatform) ?? fallbackPlatform ?? string.Empty, line.Trim());
    }

    private static string? NormalizePlatform(string? platform)
    {
        if (string.IsNullOrWhiteSpace(platform))
        {
            return null;
        }

        var trimmed = platform.Trim();
        return trimmed.ToLowerInvariant() switch
        {
            "win" or "windows" => "Windows",
            "mac" or "macos" or "mac os" => "macOS",
            "windows & macos" or "windows &  macos" => "Windows & macOS",
            _ => trimmed
        };
    }

    private static DateTimeOffset? TryExtractPublished(IDocument document)
    {
        var candidates = new List<string?>();
        candidates.Add(document.QuerySelector("time")?.GetAttribute("datetime"));
        candidates.Add(document.QuerySelector("time")?.TextContent);

        foreach (var marker in DateMarkers)
        {
            var element = document.All.FirstOrDefault(node => node.TextContent.Contains(marker, StringComparison.OrdinalIgnoreCase));
            if (element is not null)
            {
                candidates.Add(element.TextContent);
            }
        }

        foreach (var candidate in candidates)
        {
            if (TryParseDate(candidate, out var parsed))
            {
                return parsed;
            }
        }

        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))
        {
            result = result.ToUniversalTime();
            return true;
        }

        if (DateTime.TryParse(trimmed, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date))
        {
            result = new DateTimeOffset(date, TimeSpan.Zero).ToUniversalTime();
            return true;
        }

        return false;
    }

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

        var sanitized = value ?? string.Empty;
        return string.Join(" ", sanitized.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries));
    }

    private sealed record AdobeProductKey(string Product, string Track, string Platform);

    private sealed class AdobeProductKeyComparer : IEqualityComparer<AdobeProductKey>
    {
        public static AdobeProductKeyComparer Instance { get; } = new();

        public bool Equals(AdobeProductKey? x, AdobeProductKey? y)
        {
            if (ReferenceEquals(x, y))
            {
                return true;
            }

            if (x is null || y is null)
            {
                return false;
            }

            return string.Equals(x.Product, y.Product, StringComparison.OrdinalIgnoreCase)
                && string.Equals(x.Track, y.Track, StringComparison.OrdinalIgnoreCase)
                && string.Equals(x.Platform, y.Platform, StringComparison.OrdinalIgnoreCase);
        }

        public int GetHashCode(AdobeProductKey obj)
        {
            var hash = new HashCode();
            hash.Add(obj.Product, StringComparer.OrdinalIgnoreCase);
            hash.Add(obj.Track, StringComparer.OrdinalIgnoreCase);
            hash.Add(obj.Platform, StringComparer.OrdinalIgnoreCase);
            return hash.ToHashCode();
        }
    }

    private sealed class AdobeProductEntryBuilder
    {
        public AdobeProductEntryBuilder(string product, string track, string platform)
        {
            Product = NormalizeWhitespace(product);
            Track = NormalizeWhitespace(track);
            Platform = NormalizeWhitespace(platform);
        }

        public AdobeProductKey Key => new(Product, Track, Platform);

        public string Product { get; }
        public string Track { get; }
        public string Platform { get; }

        public string? AffectedVersion { get; set; }
        public string? UpdatedVersion { get; set; }
        public string? Priority { get; set; }
        public string? Availability { get; set; }

        public AdobeProductEntry ToEntry()
            => new(Product, Track, Platform, AffectedVersion, UpdatedVersion, Priority, Availability);
    }
}
