using System.Globalization;
using System.Text;
using System.Xml.Linq;
using AngleSharp.Dom;
using AngleSharp.Html.Parser;
using System.Security.Cryptography;
using StellaOps.Feedser.Source.Common.Html;

namespace StellaOps.Feedser.Source.Acsc.Internal;

internal static class AcscFeedParser
{
    private static readonly XNamespace AtomNamespace = "http://www.w3.org/2005/Atom";
    private static readonly XNamespace ContentNamespace = "http://purl.org/rss/1.0/modules/content/";
    public static AcscFeedDto Parse(byte[] payload, string feedSlug, DateTimeOffset parsedAt, HtmlContentSanitizer sanitizer)
    {
        ArgumentNullException.ThrowIfNull(payload);
        ArgumentNullException.ThrowIfNull(sanitizer);

        if (payload.Length == 0)
        {
            return AcscFeedDto.Empty with
            {
                FeedSlug = feedSlug ?? string.Empty,
                ParsedAt = parsedAt,
                Entries = Array.Empty<AcscEntryDto>(),
            };
        }

        var xml = XDocument.Parse(Encoding.UTF8.GetString(payload));

        var (feedTitle, feedLink, feedUpdated) = ExtractFeedMetadata(xml);
        var items = ExtractEntries(xml).ToArray();

        var entries = new List<AcscEntryDto>(items.Length);
        foreach (var item in items)
        {
            var entryId = ExtractEntryId(item);
            if (string.IsNullOrWhiteSpace(entryId))
            {
                // Fall back to hash of title + link to avoid duplicates.
                entryId = GenerateFallbackId(item);
            }

            var title = ExtractTitle(item);
            var link = ExtractLink(item);
            var published = ExtractDate(item, "pubDate") ?? ExtractAtomDate(item, "published") ?? ExtractDcDate(item);
            var updated = ExtractAtomDate(item, "updated");

            var rawHtml = ExtractContent(item);
            var baseUri = TryCreateUri(link);
            var sanitizedHtml = sanitizer.Sanitize(rawHtml, baseUri);
            var htmlFragment = ParseHtmlFragment(sanitizedHtml);

            var summary = BuildSummary(htmlFragment) ?? string.Empty;
            var contentText = NormalizeWhitespace(htmlFragment?.TextContent ?? string.Empty);

            var references = ExtractReferences(htmlFragment);
            var fields = ExtractFields(htmlFragment, out var serialNumber, out var advisoryType);
            var aliases = BuildAliases(serialNumber, advisoryType);

            var entry = new AcscEntryDto(
                EntryId: entryId,
                Title: title,
                Link: link,
                FeedSlug: feedSlug ?? string.Empty,
                Published: published,
                Updated: updated,
                Summary: summary,
                ContentHtml: sanitizedHtml,
                ContentText: contentText,
                References: references,
                Aliases: aliases,
                Fields: fields);

            entries.Add(entry);
        }

        return new AcscFeedDto(
            FeedSlug: feedSlug ?? string.Empty,
            FeedTitle: feedTitle,
            FeedLink: feedLink,
            FeedUpdated: feedUpdated,
            ParsedAt: parsedAt,
            Entries: entries);
    }

    private static (string? Title, string? Link, DateTimeOffset? Updated) ExtractFeedMetadata(XDocument xml)
    {
        var root = xml.Root;
        if (root is null)
        {
            return (null, null, null);
        }

        if (string.Equals(root.Name.LocalName, "rss", StringComparison.OrdinalIgnoreCase))
        {
            var channel = root.Element("channel");
            var title = channel?.Element("title")?.Value?.Trim();
            var link = channel?.Element("link")?.Value?.Trim();
            var updated = TryParseDate(channel?.Element("lastBuildDate")?.Value);
            return (title, link, updated);
        }

        if (root.Name == AtomNamespace + "feed")
        {
            var title = root.Element(AtomNamespace + "title")?.Value?.Trim();
            var link = root.Elements(AtomNamespace + "link")
                .FirstOrDefault(static element =>
                    string.Equals(element.Attribute("rel")?.Value, "alternate", StringComparison.OrdinalIgnoreCase))
                ?.Attribute("href")?.Value?.Trim()
                ?? root.Element(AtomNamespace + "link")?.Attribute("href")?.Value?.Trim();
            var updated = TryParseDate(root.Element(AtomNamespace + "updated")?.Value);
            return (title, link, updated);
        }

        return (null, null, null);
    }

    private static IEnumerable<XElement> ExtractEntries(XDocument xml)
    {
        var root = xml.Root;
        if (root is null)
        {
            yield break;
        }

        if (string.Equals(root.Name.LocalName, "rss", StringComparison.OrdinalIgnoreCase))
        {
            var channel = root.Element("channel");
            if (channel is null)
            {
                yield break;
            }

            foreach (var item in channel.Elements("item"))
            {
                yield return item;
            }
            yield break;
        }

        if (root.Name == AtomNamespace + "feed")
        {
            foreach (var entry in root.Elements(AtomNamespace + "entry"))
            {
                yield return entry;
            }
        }
    }

    private static string ExtractTitle(XElement element)
    {
        var title = element.Element("title")?.Value
            ?? element.Element(AtomNamespace + "title")?.Value
            ?? string.Empty;
        return title.Trim();
    }

    private static string? ExtractLink(XElement element)
    {
        var linkValue = element.Element("link")?.Value;
        if (!string.IsNullOrWhiteSpace(linkValue))
        {
            return linkValue.Trim();
        }

        var atomLink = element.Elements(AtomNamespace + "link")
            .FirstOrDefault(static el =>
                string.Equals(el.Attribute("rel")?.Value, "alternate", StringComparison.OrdinalIgnoreCase))
            ?? element.Element(AtomNamespace + "link");

        if (atomLink is not null)
        {
            var href = atomLink.Attribute("href")?.Value;
            if (!string.IsNullOrWhiteSpace(href))
            {
                return href.Trim();
            }
        }

        return null;
    }

    private static string ExtractEntryId(XElement element)
    {
        var guid = element.Element("guid")?.Value;
        if (!string.IsNullOrWhiteSpace(guid))
        {
            return guid.Trim();
        }

        var atomId = element.Element(AtomNamespace + "id")?.Value;
        if (!string.IsNullOrWhiteSpace(atomId))
        {
            return atomId.Trim();
        }

        if (!string.IsNullOrWhiteSpace(element.Element("link")?.Value))
        {
            return element.Element("link")!.Value.Trim();
        }

        if (!string.IsNullOrWhiteSpace(element.Element("title")?.Value))
        {
            return GenerateStableKey(element.Element("title")!.Value);
        }

        return string.Empty;
    }

    private static string GenerateFallbackId(XElement element)
    {
        var builder = new StringBuilder();
        var title = element.Element("title")?.Value;
        if (!string.IsNullOrWhiteSpace(title))
        {
            builder.Append(title.Trim());
        }

        var link = ExtractLink(element);
        if (!string.IsNullOrWhiteSpace(link))
        {
            if (builder.Length > 0)
            {
                builder.Append("::");
            }
            builder.Append(link);
        }

        if (builder.Length == 0)
        {
            return Guid.NewGuid().ToString("n");
        }

        return GenerateStableKey(builder.ToString());
    }

    private static string GenerateStableKey(string value)
    {
        using var sha = SHA256.Create();
        var bytes = Encoding.UTF8.GetBytes(value);
        var hash = sha.ComputeHash(bytes);
        return Convert.ToHexString(hash).ToLowerInvariant();
    }

    private static string ExtractContent(XElement element)
    {
        var encoded = element.Element(ContentNamespace + "encoded")?.Value;
        if (!string.IsNullOrWhiteSpace(encoded))
        {
            return encoded;
        }

        var description = element.Element("description")?.Value;
        if (!string.IsNullOrWhiteSpace(description))
        {
            return description;
        }

        var summary = element.Element(AtomNamespace + "summary")?.Value;
        if (!string.IsNullOrWhiteSpace(summary))
        {
            return summary;
        }

        return string.Empty;
    }

    private static DateTimeOffset? ExtractDate(XElement element, string name)
    {
        var value = element.Element(name)?.Value;
        return TryParseDate(value);
    }

    private static DateTimeOffset? ExtractAtomDate(XElement element, string name)
    {
        var value = element.Element(AtomNamespace + name)?.Value;
        return TryParseDate(value);
    }

    private static DateTimeOffset? ExtractDcDate(XElement element)
    {
        var value = element.Element(XName.Get("date", "http://purl.org/dc/elements/1.1/"))?.Value;
        return TryParseDate(value);
    }

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

        if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AllowWhiteSpaces, out var result))
        {
            return result.ToUniversalTime();
        }

        if (DateTimeOffset.TryParse(value, CultureInfo.CurrentCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AllowWhiteSpaces, out result))
        {
            return result.ToUniversalTime();
        }

        return null;
    }

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

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

    private static IElement? ParseHtmlFragment(string html)
    {
        if (string.IsNullOrWhiteSpace(html))
        {
            return null;
        }

        var parser = new HtmlParser(new HtmlParserOptions
        {
            IsKeepingSourceReferences = false,
        });
        var document = parser.ParseDocument($"<body>{html}</body>");
        return document.Body;
    }

    private static string? BuildSummary(IElement? root)
    {
        if (root is null || !root.HasChildNodes)
        {
            return root?.TextContent is { Length: > 0 } text
                ? NormalizeWhitespace(text)
                : string.Empty;
        }

        var segments = new List<string>();
        foreach (var child in root.Children)
        {
            var text = NormalizeWhitespace(child.TextContent);
            if (string.IsNullOrEmpty(text))
            {
                continue;
            }

            if (string.Equals(child.NodeName, "LI", StringComparison.OrdinalIgnoreCase))
            {
                segments.Add($"- {text}");
                continue;
            }

            segments.Add(text);
        }

        if (segments.Count == 0)
        {
            var fallback = NormalizeWhitespace(root.TextContent);
            return fallback;
        }

        return string.Join("\n\n", segments);
    }

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

        var anchors = root.QuerySelectorAll("a");
        if (anchors.Length == 0)
        {
            return Array.Empty<AcscReferenceDto>();
        }

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

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

            if (!seen.Add(href))
            {
                continue;
            }

            var text = NormalizeWhitespace(anchor.TextContent);
            if (string.IsNullOrEmpty(text))
            {
                text = href;
            }

            references.Add(new AcscReferenceDto(text, href));
        }

        return references;
    }

    private static IReadOnlyDictionary<string, string> ExtractFields(IElement? root, out string? serialNumber, out string? advisoryType)
    {
        serialNumber = null;
        advisoryType = null;

        if (root is null)
        {
            return EmptyFields;
        }

        var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

        foreach (var element in root.QuerySelectorAll("strong"))
        {
            var labelRaw = NormalizeWhitespace(element.TextContent);
            if (string.IsNullOrEmpty(labelRaw))
            {
                continue;
            }

            var label = labelRaw.TrimEnd(':').Trim();
            if (string.IsNullOrEmpty(label))
            {
                continue;
            }

            var key = NormalizeFieldKey(label);
            if (string.IsNullOrEmpty(key))
            {
                continue;
            }

            var value = ExtractFieldValue(element);
            if (string.IsNullOrEmpty(value))
            {
                continue;
            }

            if (!map.ContainsKey(key))
            {
                map[key] = value;
            }

            if (string.Equals(key, "serialNumber", StringComparison.OrdinalIgnoreCase))
            {
                serialNumber ??= value;
            }
            else if (string.Equals(key, "advisoryType", StringComparison.OrdinalIgnoreCase))
            {
                advisoryType ??= value;
            }
        }

        return map.Count == 0
            ? EmptyFields
            : map;
    }

    private static string? ExtractFieldValue(IElement strongElement)
    {
        var builder = new StringBuilder();
        var node = strongElement.NextSibling;

        while (node is not null)
        {
            if (node.NodeType == NodeType.Text)
            {
                builder.Append(node.TextContent);
            }
            else if (node is IElement element)
            {
                builder.Append(element.TextContent);
            }

            node = node.NextSibling;
        }

        var value = builder.ToString();
        if (string.IsNullOrWhiteSpace(value))
        {
            var parent = strongElement.ParentElement;
            if (parent is not null)
            {
                var parentText = parent.TextContent ?? string.Empty;
                var trimmed = parentText.Replace(strongElement.TextContent ?? string.Empty, string.Empty, StringComparison.OrdinalIgnoreCase);
                value = trimmed;
            }
        }

        value = NormalizeWhitespace(value);
        if (string.IsNullOrEmpty(value))
        {
            return null;
        }

        value = value.TrimStart(':', '-', '–', '—', ' ');
        return value.Trim();
    }

    private static IReadOnlyList<string> BuildAliases(string? serialNumber, string? advisoryType)
    {
        var aliases = new List<string>(capacity: 2);
        if (!string.IsNullOrWhiteSpace(serialNumber))
        {
            aliases.Add(serialNumber.Trim());
        }

        if (!string.IsNullOrWhiteSpace(advisoryType))
        {
            aliases.Add(advisoryType.Trim());
        }

        return aliases.Count == 0 ? Array.Empty<string>() : aliases;
    }

    private static string NormalizeFieldKey(string label)
    {
        if (string.IsNullOrWhiteSpace(label))
        {
            return string.Empty;
        }

        var builder = new StringBuilder(label.Length);
        var upperNext = false;

        foreach (var c in label)
        {
            if (char.IsLetterOrDigit(c))
            {
                if (builder.Length == 0)
                {
                    builder.Append(char.ToLowerInvariant(c));
                }
                else if (upperNext)
                {
                    builder.Append(char.ToUpperInvariant(c));
                    upperNext = false;
                }
                else
                {
                    builder.Append(char.ToLowerInvariant(c));
                }
            }
            else
            {
                if (builder.Length > 0)
                {
                    upperNext = true;
                }
            }
        }

        return builder.Length == 0 ? label.Trim() : builder.ToString();
    }

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

        var builder = new StringBuilder(value.Length);
        var previousIsWhitespace = false;

        foreach (var ch in value)
        {
            if (char.IsWhiteSpace(ch))
            {
                if (!previousIsWhitespace)
                {
                    builder.Append(' ');
                    previousIsWhitespace = true;
                }
                continue;
            }

            builder.Append(ch);
            previousIsWhitespace = false;
        }

        return builder.ToString().Trim();
    }
    private static readonly IReadOnlyDictionary<string, string> EmptyFields = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
