using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Source.Distro.RedHat.Internal.Models;
using StellaOps.Feedser.Normalization.Cvss;
using StellaOps.Feedser.Normalization.Distro;
using StellaOps.Feedser.Normalization.Identifiers;
using StellaOps.Feedser.Normalization.Text;
using StellaOps.Feedser.Storage.Mongo.Documents;
using StellaOps.Feedser.Storage.Mongo.Dtos;

namespace StellaOps.Feedser.Source.Distro.RedHat.Internal;

internal static class RedHatMapper
{
    private static readonly JsonSerializerOptions SerializerOptions = new()
    {
        PropertyNameCaseInsensitive = true,
        NumberHandling = JsonNumberHandling.AllowReadingFromString,
    };

    public static Advisory? Map(string sourceName, DtoRecord dto, DocumentRecord document, JsonDocument payload)
    {
        ArgumentException.ThrowIfNullOrEmpty(sourceName);
        ArgumentNullException.ThrowIfNull(dto);
        ArgumentNullException.ThrowIfNull(document);
        ArgumentNullException.ThrowIfNull(payload);

        var csaf = JsonSerializer.Deserialize<RedHatCsafEnvelope>(payload.RootElement.GetRawText(), SerializerOptions);
        var documentSection = csaf?.Document;
        if (documentSection is null)
        {
            return null;
        }

        var tracking = documentSection.Tracking;
        var advisoryKey = NormalizeId(tracking?.Id)
            ?? NormalizeId(TryGetMetadata(document, "advisoryId"))
            ?? NormalizeId(document.Uri)
            ?? dto.DocumentId.ToString();

        var title = !string.IsNullOrWhiteSpace(documentSection.Title)
            ? DescriptionNormalizer.Normalize(new[] { new LocalizedText(documentSection.Title, documentSection.Lang) }).Text
            : string.Empty;
        if (string.IsNullOrEmpty(title))
        {
            title = advisoryKey;
        }

        var description = NormalizeSummary(documentSection);
        var summary = string.IsNullOrEmpty(description.Text) ? null : description.Text;
        var severity = NormalizeSeverity(documentSection.AggregateSeverity?.Text);
        var published = tracking?.InitialReleaseDate;
        var modified = tracking?.CurrentReleaseDate ?? published;
        var language = description.Language;

        var aliases = BuildAliases(advisoryKey, csaf);
        var references = BuildReferences(sourceName, dto.ValidatedAt, documentSection, csaf);
        var productIndex = RedHatProductIndex.Build(csaf.ProductTree);
        var affectedPackages = BuildAffectedPackages(sourceName, dto.ValidatedAt, csaf, productIndex);
        var cvssMetrics = BuildCvssMetrics(sourceName, dto.ValidatedAt, advisoryKey, csaf);

        var provenance = new[]
        {
            new AdvisoryProvenance(sourceName, "advisory", advisoryKey, dto.ValidatedAt),
        };

        return new Advisory(
            advisoryKey,
            title,
            summary,
            language,
            published,
            modified,
            severity,
            exploitKnown: false,
            aliases,
            references,
            affectedPackages,
            cvssMetrics,
            provenance);
    }

    private static IReadOnlyCollection<string> BuildAliases(string advisoryKey, RedHatCsafEnvelope csaf)
    {
        var aliases = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            advisoryKey,
        };

        if (csaf.Vulnerabilities is not null)
        {
            foreach (var vulnerability in csaf.Vulnerabilities)
            {
                if (!string.IsNullOrWhiteSpace(vulnerability?.Cve))
                {
                    aliases.Add(vulnerability!.Cve!.Trim());
                }
            }
        }

        return aliases;
    }

    private static NormalizedDescription NormalizeSummary(RedHatDocumentSection documentSection)
    {
        var summaryNotes = new List<LocalizedText>();
        var otherNotes = new List<LocalizedText>();

        if (documentSection.Notes is not null)
        {
            foreach (var note in documentSection.Notes)
            {
                if (note is null || string.IsNullOrWhiteSpace(note.Text))
                {
                    continue;
                }

                var candidate = new LocalizedText(note.Text, documentSection.Lang);
                if (note.CategoryEquals("summary"))
                {
                    summaryNotes.Add(candidate);
                }
                else
                {
                    otherNotes.Add(candidate);
                }
            }
        }

        var combined = summaryNotes.Count > 0
            ? summaryNotes.Concat(otherNotes).ToList()
            : otherNotes;

        return DescriptionNormalizer.Normalize(combined);
    }

    private static IReadOnlyCollection<AdvisoryReference> BuildReferences(
        string sourceName,
        DateTimeOffset recordedAt,
        RedHatDocumentSection? documentSection,
        RedHatCsafEnvelope csaf)
    {
        var references = new List<AdvisoryReference>();
        if (documentSection is not null)
        {
            AppendReferences(sourceName, recordedAt, documentSection.References, references);
        }

        if (csaf.Vulnerabilities is not null)
        {
            foreach (var vulnerability in csaf.Vulnerabilities)
            {
                AppendReferences(sourceName, recordedAt, vulnerability?.References, references);
            }
        }

        return NormalizeReferences(references);
    }

    private static void AppendReferences(string sourceName, DateTimeOffset recordedAt, IReadOnlyList<RedHatReference>? items, ICollection<AdvisoryReference> references)
    {
        if (items is null)
        {
            return;
        }

        foreach (var reference in items)
        {
            if (reference is null || string.IsNullOrWhiteSpace(reference.Url))
            {
                continue;
            }

            var url = reference.Url.Trim();
            if (!Validation.LooksLikeHttpUrl(url))
            {
                continue;
            }

            var provenance = new AdvisoryProvenance(sourceName, "reference", url, recordedAt);
            references.Add(new AdvisoryReference(url, reference.Category, null, reference.Summary, provenance));
        }
    }

    private static IReadOnlyCollection<AdvisoryReference> NormalizeReferences(IReadOnlyCollection<AdvisoryReference> references)
    {
        if (references.Count == 0)
        {
            return Array.Empty<AdvisoryReference>();
        }

        var map = new Dictionary<string, AdvisoryReference>(StringComparer.OrdinalIgnoreCase);
        foreach (var reference in references)
        {
            if (!map.TryGetValue(reference.Url, out var existing))
            {
                map[reference.Url] = reference;
                continue;
            }

            map[reference.Url] = MergeReferences(existing, reference);
        }

        return map.Values
            .OrderBy(static r => r.Kind is null ? 1 : 0)
            .ThenBy(static r => r.Kind ?? string.Empty, StringComparer.OrdinalIgnoreCase)
            .ThenBy(static r => r.Url, StringComparer.OrdinalIgnoreCase)
            .ThenBy(static r => r.SourceTag ?? string.Empty, StringComparer.OrdinalIgnoreCase)
            .ToArray();
    }

    private static AdvisoryReference MergeReferences(AdvisoryReference existing, AdvisoryReference candidate)
    {
        var kind = existing.Kind ?? candidate.Kind;
        var sourceTag = existing.SourceTag ?? candidate.SourceTag;
        var summary = ChoosePreferredSummary(existing.Summary, candidate.Summary);
        var provenance = existing.Provenance.RecordedAt <= candidate.Provenance.RecordedAt
            ? existing.Provenance
            : candidate.Provenance;

        if (kind == existing.Kind
            && sourceTag == existing.SourceTag
            && summary == existing.Summary
            && provenance == existing.Provenance)
        {
            return existing;
        }

        if (kind == candidate.Kind
            && sourceTag == candidate.SourceTag
            && summary == candidate.Summary
            && provenance == candidate.Provenance)
        {
            return candidate;
        }

        return new AdvisoryReference(existing.Url, kind, sourceTag, summary, provenance);
    }

    private static string? ChoosePreferredSummary(string? left, string? right)
    {
        var leftValue = string.IsNullOrWhiteSpace(left) ? null : left;
        var rightValue = string.IsNullOrWhiteSpace(right) ? null : right;

        if (leftValue is null)
        {
            return rightValue;
        }

        if (rightValue is null)
        {
            return leftValue;
        }

        return leftValue.Length >= rightValue.Length ? leftValue : rightValue;
    }

    private static IReadOnlyCollection<AffectedPackage> BuildAffectedPackages(
        string sourceName,
        DateTimeOffset recordedAt,
        RedHatCsafEnvelope csaf,
        RedHatProductIndex productIndex)
    {
        var rpmPackages = new Dictionary<string, RedHatAffectedRpm>(StringComparer.OrdinalIgnoreCase);
        var baseProducts = new Dictionary<string, RedHatProductStatusEntry>(StringComparer.OrdinalIgnoreCase);
        var knownAffectedByBase = BuildKnownAffectedIndex(csaf);

        if (csaf.Vulnerabilities is not null)
        {
            foreach (var vulnerability in csaf.Vulnerabilities)
            {
                if (vulnerability?.ProductStatus is null)
                {
                    continue;
                }

                RegisterAll(vulnerability.ProductStatus.Fixed, RedHatProductStatuses.Fixed, productIndex, rpmPackages, baseProducts);
                RegisterAll(vulnerability.ProductStatus.FirstFixed, RedHatProductStatuses.FirstFixed, productIndex, rpmPackages, baseProducts);
                RegisterAll(vulnerability.ProductStatus.KnownAffected, RedHatProductStatuses.KnownAffected, productIndex, rpmPackages, baseProducts);
                RegisterAll(vulnerability.ProductStatus.KnownNotAffected, RedHatProductStatuses.KnownNotAffected, productIndex, rpmPackages, baseProducts);
                RegisterAll(vulnerability.ProductStatus.UnderInvestigation, RedHatProductStatuses.UnderInvestigation, productIndex, rpmPackages, baseProducts);
            }
        }

        var affected = new List<AffectedPackage>(rpmPackages.Count + baseProducts.Count);

        foreach (var rpm in rpmPackages.Values)
        {
            if (rpm.Statuses.Count == 0)
            {
                continue;
            }

            var ranges = new List<AffectedVersionRange>();
            var statuses = new List<AffectedPackageStatus>();
            var provenance = new AdvisoryProvenance(sourceName, "package.nevra", rpm.ProductId ?? rpm.Nevra, recordedAt);

            var lastKnownAffected = knownAffectedByBase.TryGetValue(rpm.BaseProductId, out var candidate)
                ? candidate
                : null;

            if (!string.IsNullOrWhiteSpace(lastKnownAffected)
                && string.Equals(lastKnownAffected, rpm.Nevra, StringComparison.OrdinalIgnoreCase))
            {
                lastKnownAffected = null;
            }

            if (rpm.Statuses.Contains(RedHatProductStatuses.Fixed) || rpm.Statuses.Contains(RedHatProductStatuses.FirstFixed))
            {
                ranges.Add(new AffectedVersionRange(
                    "nevra",
                    introducedVersion: null,
                    fixedVersion: rpm.Nevra,
                    lastAffectedVersion: lastKnownAffected,
                    rangeExpression: null,
                    provenance: provenance,
                    primitives: BuildNevraPrimitives(null, rpm.Nevra, lastKnownAffected)));
            }

            if (!rpm.Statuses.Contains(RedHatProductStatuses.Fixed)
                && !rpm.Statuses.Contains(RedHatProductStatuses.FirstFixed)
                && rpm.Statuses.Contains(RedHatProductStatuses.KnownAffected))
            {
                ranges.Add(new AffectedVersionRange(
                    "nevra",
                    introducedVersion: null,
                    fixedVersion: null,
                    lastAffectedVersion: rpm.Nevra,
                    rangeExpression: null,
                    provenance: provenance,
                    primitives: BuildNevraPrimitives(null, null, rpm.Nevra)));
            }

            if (rpm.Statuses.Contains(RedHatProductStatuses.KnownNotAffected))
            {
                statuses.Add(new AffectedPackageStatus(RedHatProductStatuses.KnownNotAffected, provenance));
            }

            if (rpm.Statuses.Contains(RedHatProductStatuses.UnderInvestigation))
            {
                statuses.Add(new AffectedPackageStatus(RedHatProductStatuses.UnderInvestigation, provenance));
            }

            if (ranges.Count == 0 && statuses.Count == 0)
            {
                continue;
            }

            affected.Add(new AffectedPackage(
                AffectedPackageTypes.Rpm,
                rpm.Nevra,
                rpm.Platform,
                ranges,
                statuses,
                new[] { provenance }));
        }

        foreach (var baseEntry in baseProducts.Values)
        {
            if (baseEntry.Statuses.Count == 0)
            {
                continue;
            }

            var node = baseEntry.Node;
            if (string.IsNullOrWhiteSpace(node.Cpe))
            {
                continue;
            }

            if (!IdentifierNormalizer.TryNormalizeCpe(node.Cpe, out var normalizedCpe))
            {
                continue;
            }

            var provenance = new AdvisoryProvenance(sourceName, "oval", node.ProductId, recordedAt);
            var statuses = new List<AffectedPackageStatus>();

            if (baseEntry.Statuses.Contains(RedHatProductStatuses.KnownAffected))
            {
                statuses.Add(new AffectedPackageStatus(RedHatProductStatuses.KnownAffected, provenance));
            }

            if (baseEntry.Statuses.Contains(RedHatProductStatuses.KnownNotAffected))
            {
                statuses.Add(new AffectedPackageStatus(RedHatProductStatuses.KnownNotAffected, provenance));
            }

            if (baseEntry.Statuses.Contains(RedHatProductStatuses.UnderInvestigation))
            {
                statuses.Add(new AffectedPackageStatus(RedHatProductStatuses.UnderInvestigation, provenance));
            }

            if (statuses.Count == 0)
            {
                continue;
            }

            affected.Add(new AffectedPackage(
                AffectedPackageTypes.Cpe,
                normalizedCpe!,
                node.Name,
                Array.Empty<AffectedVersionRange>(),
                statuses,
                new[] { provenance }));
        }

        return affected;
    }

    private static Dictionary<string, string> BuildKnownAffectedIndex(RedHatCsafEnvelope csaf)
    {
        var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        if (csaf.Vulnerabilities is null)
        {
            return map;
        }

        foreach (var vulnerability in csaf.Vulnerabilities)
        {
            var entries = vulnerability?.ProductStatus?.KnownAffected;
            if (entries is null)
            {
                continue;
            }

            foreach (var entry in entries)
            {
                if (string.IsNullOrWhiteSpace(entry))
                {
                    continue;
                }

                var colonIndex = entry.IndexOf(':');
                if (colonIndex <= 0)
                {
                    continue;
                }

                var baseId = entry[..colonIndex].Trim();
                if (string.IsNullOrEmpty(baseId))
                {
                    continue;
                }

                var candidate = NormalizeNevra(entry[(colonIndex + 1)..]);
                if (!string.IsNullOrEmpty(candidate))
                {
                    map[baseId] = candidate;
                }
            }
        }

        return map;
    }

    private static void RegisterAll(
        IReadOnlyList<string>? entries,
        string status,
        RedHatProductIndex productIndex,
        IDictionary<string, RedHatAffectedRpm> rpmPackages,
        IDictionary<string, RedHatProductStatusEntry> baseProducts)
    {
        if (entries is null)
        {
            return;
        }

        foreach (var entry in entries)
        {
            RegisterProductStatus(entry, status, productIndex, rpmPackages, baseProducts);
        }
    }

    private static void RegisterProductStatus(
        string? rawEntry,
        string status,
        RedHatProductIndex productIndex,
        IDictionary<string, RedHatAffectedRpm> rpmPackages,
        IDictionary<string, RedHatProductStatusEntry> baseProducts)
    {
        if (string.IsNullOrWhiteSpace(rawEntry) || !IsActionableStatus(status))
        {
            return;
        }

        var entry = rawEntry.Trim();
        var colonIndex = entry.IndexOf(':');
        if (colonIndex <= 0 || colonIndex == entry.Length - 1)
        {
            if (productIndex.TryGetValue(entry, out var baseOnly))
            {
                var aggregate = baseProducts.TryGetValue(baseOnly.ProductId, out var existing)
                    ? existing
                    : new RedHatProductStatusEntry(baseOnly);
                aggregate.Statuses.Add(status);
                baseProducts[baseOnly.ProductId] = aggregate;
            }

            return;
        }

        var baseId = entry[..colonIndex];
        var packageId = entry[(colonIndex + 1)..];

        if (productIndex.TryGetValue(baseId, out var baseNode))
        {
            var aggregate = baseProducts.TryGetValue(baseNode.ProductId, out var existing)
                ? existing
                : new RedHatProductStatusEntry(baseNode);
            aggregate.Statuses.Add(status);
            baseProducts[baseNode.ProductId] = aggregate;
        }

        if (!productIndex.TryGetValue(packageId, out var packageNode))
        {
            return;
        }

        var nevra = NormalizeNevra(packageNode.Name ?? packageNode.ProductId);
        if (string.IsNullOrEmpty(nevra))
        {
            return;
        }

        var platform = baseProducts.TryGetValue(baseId, out var baseEntry)
            ? baseEntry.Node.Name ?? baseId
            : baseId;

        var key = string.Join('|', nevra, platform ?? string.Empty);
        if (!rpmPackages.TryGetValue(key, out var rpm))
        {
            rpm = new RedHatAffectedRpm(nevra, baseId, platform, packageNode.ProductId);
            rpmPackages[key] = rpm;
        }

        rpm.Statuses.Add(status);
    }

    private static bool IsActionableStatus(string status)
    {
        return status.Equals(RedHatProductStatuses.Fixed, StringComparison.OrdinalIgnoreCase)
            || status.Equals(RedHatProductStatuses.FirstFixed, StringComparison.OrdinalIgnoreCase)
            || status.Equals(RedHatProductStatuses.KnownAffected, StringComparison.OrdinalIgnoreCase)
            || status.Equals(RedHatProductStatuses.KnownNotAffected, StringComparison.OrdinalIgnoreCase)
            || status.Equals(RedHatProductStatuses.UnderInvestigation, StringComparison.OrdinalIgnoreCase);
    }

    private static IReadOnlyCollection<CvssMetric> BuildCvssMetrics(
        string sourceName,
        DateTimeOffset recordedAt,
        string advisoryKey,
        RedHatCsafEnvelope csaf)
    {
        var metrics = new List<CvssMetric>();
        if (csaf.Vulnerabilities is null)
        {
            return metrics;
        }

        foreach (var vulnerability in csaf.Vulnerabilities)
        {
            if (vulnerability?.Scores is null)
            {
                continue;
            }

            foreach (var score in vulnerability.Scores)
            {
                var cvss = score?.CvssV3;
                if (cvss is null)
                {
                    continue;
                }

                if (!CvssMetricNormalizer.TryNormalize(cvss.Version, cvss.VectorString, cvss.BaseScore, cvss.BaseSeverity, out var normalized))
                {
                    continue;
                }

                var provenance = new AdvisoryProvenance(sourceName, "cvss", vulnerability.Cve ?? advisoryKey, recordedAt);
                metrics.Add(normalized.ToModel(provenance));
            }
        }

        return metrics;
    }

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

        return value.Trim().ToLowerInvariant() switch
        {
            "critical" => "critical",
            "important" => "high",
            "moderate" => "medium",
            "low" => "low",
            "none" => "none",
            _ => value.Trim().ToLowerInvariant(),
        };
    }

    private static string? TryGetMetadata(DocumentRecord document, string key)
    {
        if (document.Metadata is null)
        {
            return null;
        }

        return document.Metadata.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value)
            ? value.Trim()
            : null;
    }

    private static RangePrimitives BuildNevraPrimitives(string? introduced, string? fixedVersion, string? lastAffected)
    {
        var primitive = new NevraPrimitive(
            ParseNevraComponent(introduced),
            ParseNevraComponent(fixedVersion),
            ParseNevraComponent(lastAffected));

        return new RangePrimitives(null, primitive, null, null);
    }

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

        if (!Nevra.TryParse(value, out var parsed) || parsed is null)
        {
            return null;
        }

        return new NevraComponent(parsed.Name, parsed.Epoch, parsed.Version, parsed.Release, parsed.Architecture);
    }

    private static string? NormalizeId(string? value)
        => string.IsNullOrWhiteSpace(value) ? null : value.Trim();

    private static string NormalizeNevra(string? value)
    {
        return string.IsNullOrWhiteSpace(value)
            ? string.Empty
            : value.Trim();
    }
}

internal sealed class RedHatAffectedRpm
{
    public RedHatAffectedRpm(string nevra, string baseProductId, string? platform, string? productId)
    {
        Nevra = nevra;
        BaseProductId = baseProductId;
        Platform = platform;
        ProductId = productId;
        Statuses = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
    }

    public string Nevra { get; }

    public string BaseProductId { get; }

    public string? Platform { get; }

    public string? ProductId { get; }

    public HashSet<string> Statuses { get; }
}

internal sealed class RedHatProductStatusEntry
{
    public RedHatProductStatusEntry(RedHatProductNode node)
    {
        Node = node;
        Statuses = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
    }

    public RedHatProductNode Node { get; }

    public HashSet<string> Statuses { get; }
}

internal static class RedHatProductStatuses
{
    public const string Fixed = "fixed";
    public const string FirstFixed = "first_fixed";
    public const string KnownAffected = "known_affected";
    public const string KnownNotAffected = "known_not_affected";
    public const string UnderInvestigation = "under_investigation";
}

internal sealed class RedHatProductIndex
{
    private readonly Dictionary<string, RedHatProductNode> _products;

    private RedHatProductIndex(Dictionary<string, RedHatProductNode> products)
    {
        _products = products;
    }

    public static RedHatProductIndex Build(RedHatProductTree? tree)
    {
        var products = new Dictionary<string, RedHatProductNode>(StringComparer.OrdinalIgnoreCase);
        if (tree?.Branches is not null)
        {
            foreach (var branch in tree.Branches)
            {
                Traverse(branch, products);
            }
        }

        return new RedHatProductIndex(products);
    }

    public bool TryGetValue(string productId, out RedHatProductNode node)
        => _products.TryGetValue(productId, out node);

    private static void Traverse(RedHatProductBranch? branch, IDictionary<string, RedHatProductNode> products)
    {
        if (branch is null)
        {
            return;
        }

        if (branch.Product is not null && !string.IsNullOrWhiteSpace(branch.Product.ProductId))
        {
            var id = branch.Product.ProductId.Trim();
            products[id] = new RedHatProductNode(
                id,
                branch.Product.Name ?? branch.Name ?? id,
                branch.Product.ProductIdentificationHelper?.Cpe,
                branch.Product.ProductIdentificationHelper?.Purl);
        }

        if (branch.Branches is null)
        {
            return;
        }

        foreach (var child in branch.Branches)
        {
            Traverse(child, products);
        }
    }
}

internal sealed record RedHatProductNode(string ProductId, string? Name, string? Cpe, string? Purl);
