using System.Linq;
using AngleSharp.Dom;
using AngleSharp.Html.Parser;
using StellaOps.Feedser.Source.Common.Url;

namespace StellaOps.Feedser.Source.Common.Html;

/// <summary>
/// Sanitizes untrusted HTML fragments produced by upstream advisories.
/// Removes executable content, enforces an allowlist of elements, and normalizes anchor href values.
/// </summary>
public sealed class HtmlContentSanitizer
{
    private static readonly HashSet<string> AllowedElements = new(StringComparer.OrdinalIgnoreCase)
    {
        "a", "abbr", "article", "b", "body", "blockquote", "br", "code", "dd", "div", "dl", "dt",
        "em", "h1", "h2", "h3", "h4", "h5", "h6", "html", "i", "li", "ol", "p", "pre", "s",
        "section", "small", "span", "strong", "sub", "sup", "table", "tbody", "td", "th", "thead", "tr", "ul"
    };

    private static readonly HashSet<string> UrlAttributes = new(StringComparer.OrdinalIgnoreCase)
    {
        "href", "src",
    };

    private readonly HtmlParser _parser;

    public HtmlContentSanitizer()
    {
        _parser = new HtmlParser(new HtmlParserOptions
        {
            IsKeepingSourceReferences = false,
        });
    }

    /// <summary>
    /// Sanitizes <paramref name="html"/> and returns a safe fragment suitable for rendering.
    /// </summary>
    public string Sanitize(string? html, Uri? baseUri = null)
    {
        if (string.IsNullOrWhiteSpace(html))
        {
            return string.Empty;
        }

        var document = _parser.ParseDocument(html);
        if (document.Body is null)
        {
            return string.Empty;
        }

        foreach (var element in document.All.ToList())
        {
            if (IsDangerous(element))
            {
                element.Remove();
                continue;
            }

            if (!AllowedElements.Contains(element.LocalName))
            {
                var owner = element.Owner;
                if (owner is null)
                {
                    element.Remove();
                    continue;
                }

                var text = element.TextContent ?? string.Empty;
                element.Replace(owner.CreateTextNode(text));
                continue;
            }

            CleanAttributes(element, baseUri);
        }

        var body = document.Body ?? document.DocumentElement;
        if (body is null)
        {
            return string.Empty;
        }

        var innerHtml = body.InnerHtml;
        return string.IsNullOrWhiteSpace(innerHtml) ? string.Empty : innerHtml.Trim();
    }

    private static bool IsDangerous(IElement element)
    {
        if (string.Equals(element.LocalName, "script", StringComparison.OrdinalIgnoreCase)
            || string.Equals(element.LocalName, "style", StringComparison.OrdinalIgnoreCase)
            || string.Equals(element.LocalName, "iframe", StringComparison.OrdinalIgnoreCase)
            || string.Equals(element.LocalName, "object", StringComparison.OrdinalIgnoreCase)
            || string.Equals(element.LocalName, "embed", StringComparison.OrdinalIgnoreCase))
        {
            return true;
        }

        return false;
    }

    private static void CleanAttributes(IElement element, Uri? baseUri)
    {
        if (element.Attributes is null || element.Attributes.Length == 0)
        {
            return;
        }

        foreach (var attribute in element.Attributes.ToList())
        {
            if (attribute.Name.StartsWith("on", StringComparison.OrdinalIgnoreCase))
            {
                element.RemoveAttribute(attribute.Name);
                continue;
            }

            if (UrlAttributes.Contains(attribute.Name))
            {
                NormalizeUrlAttribute(element, attribute, baseUri);
                continue;
            }

            if (!IsAttributeAllowed(element.LocalName, attribute.Name))
            {
                element.RemoveAttribute(attribute.Name);
            }
        }
    }

    private static bool IsAttributeAllowed(string elementName, string attributeName)
    {
        if (string.Equals(attributeName, "title", StringComparison.OrdinalIgnoreCase))
        {
            return true;
        }

        if (string.Equals(elementName, "a", StringComparison.OrdinalIgnoreCase)
            && string.Equals(attributeName, "rel", StringComparison.OrdinalIgnoreCase))
        {
            return true;
        }

        if (string.Equals(elementName, "table", StringComparison.OrdinalIgnoreCase)
            && (string.Equals(attributeName, "border", StringComparison.OrdinalIgnoreCase)
                || string.Equals(attributeName, "cellpadding", StringComparison.OrdinalIgnoreCase)
                || string.Equals(attributeName, "cellspacing", StringComparison.OrdinalIgnoreCase)))
        {
            return true;
        }

        return false;
    }

    private static void NormalizeUrlAttribute(IElement element, IAttr attribute, Uri? baseUri)
    {
        if (string.IsNullOrWhiteSpace(attribute.Value))
        {
            element.RemoveAttribute(attribute.Name);
            return;
        }

        if (!UrlNormalizer.TryNormalize(attribute.Value, baseUri, out var normalized))
        {
            element.RemoveAttribute(attribute.Name);
            return;
        }

        if (string.Equals(element.LocalName, "a", StringComparison.OrdinalIgnoreCase))
        {
            element.SetAttribute("rel", "noopener nofollow noreferrer");
        }

        if (normalized is null)
        {
            element.RemoveAttribute(attribute.Name);
            return;
        }

        element.SetAttribute(attribute.Name, normalized.ToString());
    }
}
