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;
using StellaOps.Feedser.Source.Common.Html;

namespace StellaOps.Feedser.Source.Cccs.Internal;

public sealed class CccsHtmlParser
{
    private static readonly Regex SerialRegex = new(@"(?:(Number|Num[eé]ro)\s*[:：]\s*)(?<id>[A-Z0-9\-\/]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
    private static readonly Regex DateRegex = new(@"(?:(Date|Date de publication)\s*[:：]\s*)(?<date>[A-Za-zÀ-ÿ0-9,\.\s\-]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
    private static readonly Regex CveRegex = new(@"CVE-\d{4}-\d{4,}", RegexOptions.IgnoreCase | RegexOptions.Compiled);
    private static readonly Regex CollapseWhitespaceRegex = new(@"\s+", RegexOptions.Compiled);

    private static readonly CultureInfo[] EnglishCultures =
    {
        CultureInfo.GetCultureInfo("en-CA"),
        CultureInfo.GetCultureInfo("en-US"),
        CultureInfo.InvariantCulture,
    };

    private static readonly CultureInfo[] FrenchCultures =
    {
        CultureInfo.GetCultureInfo("fr-CA"),
        CultureInfo.GetCultureInfo("fr-FR"),
        CultureInfo.InvariantCulture,
    };

    private static readonly string[] ProductHeadingKeywords =
    {
        "affected",
        "produit",
        "produits",
        "produits touch",
        "produits concern",
        "mesures recommand",
    };

    private static readonly string[] TrackingParameterPrefixes =
    {
        "utm_",
        "mc_",
        "mkt_",
        "elq",
    };

    private readonly HtmlContentSanitizer _sanitizer;
    private readonly HtmlParser _parser;

    public CccsHtmlParser(HtmlContentSanitizer sanitizer)
    {
        _sanitizer = sanitizer ?? throw new ArgumentNullException(nameof(sanitizer));
        _parser = new HtmlParser(new HtmlParserOptions
        {
            IsScripting = false,
            IsKeepingSourceReferences = false,
        });
    }

    internal CccsAdvisoryDto Parse(CccsRawAdvisoryDocument raw)
    {
        ArgumentNullException.ThrowIfNull(raw);

        var baseUri = TryCreateUri(raw.CanonicalUrl);
        var document = _parser.ParseDocument(raw.BodyHtml ?? string.Empty);
        var body = document.Body ?? document.DocumentElement;
        var sanitized = _sanitizer.Sanitize(body?.InnerHtml ?? raw.BodyHtml ?? string.Empty, baseUri);
        var contentRoot = body ?? document.DocumentElement;

        var serialNumber = !string.IsNullOrWhiteSpace(raw.SerialNumber)
            ? raw.SerialNumber!.Trim()
            : ExtractSerialNumber(document) ?? raw.SourceId;

        var published = raw.Published ?? ExtractDate(document, raw.Language) ?? raw.Modified;
        var references = ExtractReferences(contentRoot, baseUri, raw.Language);
        var products = ExtractProducts(contentRoot);
        var cveIds = ExtractCveIds(document);

        return new CccsAdvisoryDto
        {
            SourceId = raw.SourceId,
            SerialNumber = serialNumber,
            Language = raw.Language,
            Title = raw.Title,
            Summary = CollapseWhitespace(raw.Summary),
            CanonicalUrl = raw.CanonicalUrl,
            ContentHtml = sanitized,
            Published = published,
            Modified = raw.Modified ?? published,
            AlertType = raw.AlertType,
            Subject = raw.Subject,
            Products = products,
            References = references,
            CveIds = cveIds,
        };
    }

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

        return Uri.TryCreate(value, UriKind.Absolute, out var absolute) ? absolute : null;
    }

    private static string? ExtractSerialNumber(IDocument document)
    {
        if (document.Body is null)
        {
            return null;
        }

        foreach (var element in document.QuerySelectorAll("strong, p, div"))
        {
            var text = element.TextContent;
            if (string.IsNullOrWhiteSpace(text))
            {
                continue;
            }

            var match = SerialRegex.Match(text);
            if (match.Success && match.Groups["id"].Success)
            {
                var value = match.Groups["id"].Value.Trim();
                if (!string.IsNullOrWhiteSpace(value))
                {
                    return value;
                }
            }
        }

        var bodyText = document.Body.TextContent;
        var fallback = SerialRegex.Match(bodyText ?? string.Empty);
        return fallback.Success && fallback.Groups["id"].Success
            ? fallback.Groups["id"].Value.Trim()
            : null;
    }

    private static DateTimeOffset? ExtractDate(IDocument document, string language)
    {
        if (document.Body is null)
        {
            return null;
        }

        var textSegments = new List<string>();
        foreach (var element in document.QuerySelectorAll("strong, p, div"))
        {
            var text = element.TextContent;
            if (string.IsNullOrWhiteSpace(text))
            {
                continue;
            }

            var match = DateRegex.Match(text);
            if (match.Success && match.Groups["date"].Success)
            {
                textSegments.Add(match.Groups["date"].Value.Trim());
            }
        }

        if (textSegments.Count == 0 && !string.IsNullOrWhiteSpace(document.Body.TextContent))
        {
            textSegments.Add(document.Body.TextContent);
        }

        var cultures = language.StartsWith("fr", StringComparison.OrdinalIgnoreCase) ? FrenchCultures : EnglishCultures;

        foreach (var segment in textSegments)
        {
            foreach (var culture in cultures)
            {
                if (DateTime.TryParse(segment, culture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var parsed))
                {
                    return new DateTimeOffset(parsed.ToUniversalTime());
                }
            }
        }

        return null;
    }

    private static IReadOnlyList<string> ExtractProducts(IElement? root)
    {
        if (root is null)
        {
            return Array.Empty<string>();
        }

        var results = new List<string>();

        foreach (var heading in root.QuerySelectorAll("h1,h2,h3,h4,h5,h6"))
        {
            var text = heading.TextContent?.Trim();
            if (!IsProductHeading(text))
            {
                continue;
            }

            var sibling = heading.NextElementSibling;
            while (sibling is not null)
            {
                if (IsHeading(sibling))
                {
                    break;
                }

                if (IsListElement(sibling))
                {
                    AppendListItems(sibling, results);
                    if (results.Count > 0)
                    {
                        break;
                    }
                }
                else if (IsContentContainer(sibling))
                {
                    foreach (var list in sibling.QuerySelectorAll("ul,ol"))
                    {
                        AppendListItems(list, results);
                    }

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

                sibling = sibling.NextElementSibling;
            }

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

        if (results.Count == 0)
        {
            foreach (var li in root.QuerySelectorAll("ul li,ol li"))
            {
                var itemText = CollapseWhitespace(li.TextContent);
                if (!string.IsNullOrWhiteSpace(itemText))
                {
                    results.Add(itemText);
                }
            }
        }

        return results.Count == 0
            ? Array.Empty<string>()
            : results
                .Where(static value => !string.IsNullOrWhiteSpace(value))
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .OrderBy(static value => value, StringComparer.OrdinalIgnoreCase)
                .ToArray();
    }

    private static bool IsProductHeading(string? heading)
    {
        if (string.IsNullOrWhiteSpace(heading))
        {
            return false;
        }

        var lowered = heading.ToLowerInvariant();
        return ProductHeadingKeywords.Any(keyword => lowered.Contains(keyword, StringComparison.OrdinalIgnoreCase));
    }

    private static bool IsHeading(IElement element)
        => element.LocalName.Length == 2
            && element.LocalName[0] == 'h'
            && char.IsDigit(element.LocalName[1]);

    private static bool IsListElement(IElement element)
        => string.Equals(element.LocalName, "ul", StringComparison.OrdinalIgnoreCase)
            || string.Equals(element.LocalName, "ol", StringComparison.OrdinalIgnoreCase);

    private static bool IsContentContainer(IElement element)
        => string.Equals(element.LocalName, "div", StringComparison.OrdinalIgnoreCase)
            || string.Equals(element.LocalName, "section", StringComparison.OrdinalIgnoreCase)
            || string.Equals(element.LocalName, "article", StringComparison.OrdinalIgnoreCase);

    private static void AppendListItems(IElement listElement, ICollection<string> buffer)
    {
        foreach (var li in listElement.QuerySelectorAll("li"))
        {
            if (li is null)
            {
                continue;
            }

            var clone = li.Clone(true) as IElement;
            if (clone is null)
            {
                continue;
            }

            foreach (var nested in clone.QuerySelectorAll("ul,ol"))
            {
                nested.Remove();
            }

            var itemText = CollapseWhitespace(clone.TextContent);
            if (!string.IsNullOrWhiteSpace(itemText))
            {
                buffer.Add(itemText);
            }
        }
    }

    private static IReadOnlyList<CccsReferenceDto> ExtractReferences(IElement? root, Uri? baseUri, string language)
    {
        if (root is null)
        {
            return Array.Empty<CccsReferenceDto>();
        }

        var references = new List<CccsReferenceDto>();
        foreach (var anchor in root.QuerySelectorAll("a[href]"))
        {
            var href = anchor.GetAttribute("href");
            var normalized = NormalizeReferenceUrl(href, baseUri, language);
            if (normalized is null)
            {
                continue;
            }

            var label = CollapseWhitespace(anchor.TextContent);
            references.Add(new CccsReferenceDto(normalized, string.IsNullOrWhiteSpace(label) ? null : label));
        }

        return references.Count == 0
            ? Array.Empty<CccsReferenceDto>()
            : references
                .GroupBy(reference => reference.Url, StringComparer.Ordinal)
                .Select(group => group.First())
                .OrderBy(reference => reference.Url, StringComparer.Ordinal)
                .ToArray();
    }

    private static string? NormalizeReferenceUrl(string? href, Uri? baseUri, string language)
    {
        if (string.IsNullOrWhiteSpace(href))
        {
            return null;
        }

        if (!Uri.TryCreate(href, UriKind.Absolute, out var absolute))
        {
            if (baseUri is null || !Uri.TryCreate(baseUri, href, out absolute))
            {
                return null;
            }
        }

        var builder = new UriBuilder(absolute)
        {
            Fragment = string.Empty,
        };

        var filteredQuery = FilterTrackingParameters(builder.Query, builder.Uri, language);
        builder.Query = filteredQuery;

        return builder.Uri.ToString();
    }

    private static string FilterTrackingParameters(string query, Uri uri, string language)
    {
        if (string.IsNullOrWhiteSpace(query))
        {
            return string.Empty;
        }

        var trimmed = query.TrimStart('?');
        if (string.IsNullOrWhiteSpace(trimmed))
        {
            return string.Empty;
        }

        var parameters = trimmed.Split('&', StringSplitOptions.RemoveEmptyEntries);
        var kept = new List<string>();

        foreach (var parameter in parameters)
        {
            var separatorIndex = parameter.IndexOf('=');
            var key = separatorIndex >= 0 ? parameter[..separatorIndex] : parameter;
            if (TrackingParameterPrefixes.Any(prefix => key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)))
            {
                continue;
            }

            if (uri.Host.Contains("cyber.gc.ca", StringComparison.OrdinalIgnoreCase)
                && key.Equals("lang", StringComparison.OrdinalIgnoreCase))
            {
                kept.Add($"lang={language}");
                continue;
            }

            kept.Add(parameter);
        }

        if (uri.Host.Contains("cyber.gc.ca", StringComparison.OrdinalIgnoreCase)
            && kept.All(parameter => !parameter.StartsWith("lang=", StringComparison.OrdinalIgnoreCase)))
        {
            kept.Add($"lang={language}");
        }

        return kept.Count == 0 ? string.Empty : string.Join("&", kept);
    }

    private static IReadOnlyList<string> ExtractCveIds(IDocument document)
    {
        if (document.Body is null)
        {
            return Array.Empty<string>();
        }

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

        return matches
            .Select(match => match.Value.ToUpperInvariant())
            .Distinct(StringComparer.Ordinal)
            .OrderBy(value => value, StringComparer.Ordinal)
            .ToArray();
    }

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

        var collapsed = CollapseWhitespaceRegex.Replace(value, " ").Trim();
        return collapsed.Length == 0 ? null : collapsed;
    }
}
