using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Schema;

namespace StellaOps.Feedser.Source.Jvn.Internal;

internal static class JvnDetailParser
{
    private static readonly XNamespace Vuldef = JvnConstants.VuldefNamespace;
    private static readonly XNamespace Status = JvnConstants.StatusNamespace;

    public static JvnDetailDto Parse(byte[] payload, string? documentUri)
    {
        ArgumentNullException.ThrowIfNull(payload);

        using var stream = new MemoryStream(payload, writable: false);
        var settings = new XmlReaderSettings
        {
            DtdProcessing = DtdProcessing.Prohibit,
            IgnoreComments = true,
            IgnoreProcessingInstructions = true,
            IgnoreWhitespace = true,
        };

        using var reader = XmlReader.Create(stream, settings);
        var document = XDocument.Load(reader, LoadOptions.None);
        Validate(document, documentUri);
        return Extract(document, documentUri);
    }

    private static void Validate(XDocument document, string? documentUri)
    {
        void Handler(object? sender, ValidationEventArgs args)
        {
            throw new JvnSchemaValidationException(
                $"JVN schema validation failed for {documentUri ?? "<unknown>"}: {args.Message}",
                args.Exception ?? new XmlSchemaValidationException(args.Message));
        }

        document.Validate(JvnSchemaProvider.SchemaSet, Handler, addSchemaInfo: true);
    }

    private static JvnDetailDto Extract(XDocument document, string? documentUri)
    {
        var root = document.Root ?? throw new InvalidOperationException("JVN VULDEF document missing root element.");

        var vulinfo = root.Element(Vuldef + "Vulinfo") ?? throw new InvalidOperationException("Vulinfo element missing.");
        var vulinfoId = Clean(vulinfo.Element(Vuldef + "VulinfoID")?.Value)
            ?? throw new InvalidOperationException("VulinfoID element missing.");

        var data = vulinfo.Element(Vuldef + "VulinfoData") ?? throw new InvalidOperationException("VulinfoData element missing.");
        var title = Clean(data.Element(Vuldef + "Title")?.Value) ?? vulinfoId;
        var overview = Clean(data.Element(Vuldef + "VulinfoDescription")?.Element(Vuldef + "Overview")?.Value);

        var dateFirstPublished = ParseDate(data.Element(Vuldef + "DateFirstPublished")?.Value);
        var dateLastUpdated = ParseDate(data.Element(Vuldef + "DateLastUpdated")?.Value);
        var datePublic = ParseDate(data.Element(Vuldef + "DatePublic")?.Value);

        var cvssEntries = ParseCvss(data.Element(Vuldef + "Impact"));
        var affected = ParseAffected(data.Element(Vuldef + "Affected"));
        var references = ParseReferences(data.Element(Vuldef + "Related"));
        var history = ParseHistory(data.Element(Vuldef + "History"));

        var cweIds = references.Where(r => string.Equals(r.Type, "cwe", StringComparison.OrdinalIgnoreCase))
            .Select(r => r.Id)
            .Where(static id => !string.IsNullOrWhiteSpace(id))
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .Select(static id => id!)
            .ToImmutableArray();

        var cveIds = references.Where(r => string.Equals(r.Type, "advisory", StringComparison.OrdinalIgnoreCase)
                && !string.IsNullOrWhiteSpace(r.Id)
                && r.Id.StartsWith("CVE-", StringComparison.OrdinalIgnoreCase))
            .Select(r => r.Id)
            .Where(static id => !string.IsNullOrWhiteSpace(id))
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .Select(static id => id!)
            .ToImmutableArray();

        var language = Clean(root.Attribute(XNamespace.Xml + "lang")?.Value);

        var statusElement = root.Element(Status + "Status");
        var jvnCategory = Clean(statusElement?.Attribute("category")?.Value);

        var vendorStatuses = affected
            .Select(a => a.Status)
            .Where(static status => !string.IsNullOrWhiteSpace(status))
            .Select(static status => status!.ToLowerInvariant())
            .Distinct(StringComparer.Ordinal)
            .ToImmutableArray();

        return new JvnDetailDto(
            vulinfoId,
            title,
            overview,
            language,
            dateFirstPublished,
            dateLastUpdated,
            datePublic,
            cvssEntries,
            affected,
            references,
            history,
            cweIds,
            cveIds,
            Clean(documentUri),
            jvnCategory,
            vendorStatuses);
    }

    private static ImmutableArray<JvnCvssDto> ParseCvss(XElement? impactElement)
    {
        if (impactElement is null)
        {
            return ImmutableArray<JvnCvssDto>.Empty;
        }

        var results = new List<JvnCvssDto>();
        foreach (var cvssElement in impactElement.Elements(Vuldef + "Cvss"))
        {
            var version = Clean(cvssElement.Attribute("version")?.Value) ?? "";
            var severityElement = cvssElement.Element(Vuldef + "Severity");
            var severity = Clean(severityElement?.Value) ?? Clean(cvssElement.Attribute("severity")?.Value) ?? string.Empty;
            var type = Clean(severityElement?.Attribute("type")?.Value) ?? Clean(cvssElement.Attribute("type")?.Value) ?? "base";
            var scoreText = Clean(cvssElement.Element(Vuldef + "Base")?.Value)
                ?? Clean(cvssElement.Attribute("score")?.Value)
                ?? "0";
            if (!double.TryParse(scoreText, NumberStyles.Float, CultureInfo.InvariantCulture, out var score))
            {
                score = 0d;
            }

            var vector = Clean(cvssElement.Element(Vuldef + "Vector")?.Value)
                ?? Clean(cvssElement.Attribute("vector")?.Value);

            results.Add(new JvnCvssDto(
                version,
                type,
                severity,
                score,
                vector));
        }

        return results.ToImmutableArray();
    }

    private static ImmutableArray<JvnAffectedProductDto> ParseAffected(XElement? affectedElement)
    {
        if (affectedElement is null)
        {
            return ImmutableArray<JvnAffectedProductDto>.Empty;
        }

        var results = new List<JvnAffectedProductDto>();
        foreach (var item in affectedElement.Elements(Vuldef + "AffectedItem"))
        {
            var vendor = Clean(item.Element(Vuldef + "Name")?.Value);
            var product = Clean(item.Element(Vuldef + "ProductName")?.Value);
            var cpeElement = item.Element(Vuldef + "Cpe");
            var cpe = Clean(cpeElement?.Value);
            var cpeVendor = Clean(cpeElement?.Attribute("vendor")?.Value);
            var cpeProduct = Clean(cpeElement?.Attribute("product")?.Value);
            var version = Clean(ReadConcatenated(item.Elements(Vuldef + "VersionNumber")));
            var build = Clean(ReadConcatenated(item.Elements(Vuldef + "BuildNumber")));
            var description = Clean(ReadConcatenated(item.Elements(Vuldef + "Description")));
            var status = Clean(item.Attribute("affectedstatus")?.Value);

            results.Add(new JvnAffectedProductDto(vendor, product, cpe, cpeVendor, cpeProduct, version, build, description, status));
        }

        return results.ToImmutableArray();
    }

    private static ImmutableArray<JvnReferenceDto> ParseReferences(XElement? relatedElement)
    {
        if (relatedElement is null)
        {
            return ImmutableArray<JvnReferenceDto>.Empty;
        }

        var results = new List<JvnReferenceDto>();
        foreach (var item in relatedElement.Elements(Vuldef + "RelatedItem"))
        {
            var type = Clean(item.Attribute("type")?.Value) ?? string.Empty;
            var id = Clean(item.Element(Vuldef + "VulinfoID")?.Value) ?? string.Empty;
            var name = Clean(item.Element(Vuldef + "Name")?.Value);
            var url = Clean(item.Element(Vuldef + "URL")?.Value);

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

            if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || (uri.Scheme is not "http" and not "https"))
            {
                continue;
            }

            results.Add(new JvnReferenceDto(type, id, name, uri.ToString()));
        }

        return results.ToImmutableArray();
    }

    private static ImmutableArray<JvnHistoryEntryDto> ParseHistory(XElement? historyElement)
    {
        if (historyElement is null)
        {
            return ImmutableArray<JvnHistoryEntryDto>.Empty;
        }

        var results = new List<JvnHistoryEntryDto>();
        foreach (var item in historyElement.Elements(Vuldef + "HistoryItem"))
        {
            var number = Clean(item.Element(Vuldef + "HistoryNo")?.Value);
            var timestamp = ParseDate(item.Element(Vuldef + "DateTime")?.Value);
            var description = Clean(item.Element(Vuldef + "Description")?.Value);
            results.Add(new JvnHistoryEntryDto(number, timestamp, description));
        }

        return results.ToImmutableArray();
    }

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

        return DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var parsed)
            ? parsed.ToUniversalTime()
            : null;
    }

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

        return value.Trim();
    }

    private static string? ReadConcatenated(IEnumerable<XElement> elements)
    {
        var builder = new List<string>();
        foreach (var element in elements)
        {
            var text = element?.Value;
            if (string.IsNullOrWhiteSpace(text))
            {
                continue;
            }

            builder.Add(text.Trim());
        }

        return builder.Count == 0 ? null : string.Join("; ", builder);
    }
}
