using System;
using System.Collections.Generic;
using System.Linq;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Source.Common;
using StellaOps.Feedser.Normalization.Cvss;
using StellaOps.Feedser.Normalization.Identifiers;
using StellaOps.Feedser.Normalization.Text;
using StellaOps.Feedser.Storage.Mongo.Documents;
using StellaOps.Feedser.Storage.Mongo.Dtos;
using StellaOps.Feedser.Storage.Mongo.JpFlags;

namespace StellaOps.Feedser.Source.Jvn.Internal;

internal static class JvnAdvisoryMapper
{
    private static readonly string[] SeverityOrder = { "none", "low", "medium", "high", "critical" };

    public static (Advisory Advisory, JpFlagRecord Flag) Map(
        JvnDetailDto detail,
        DocumentRecord document,
        DtoRecord dtoRecord,
        TimeProvider timeProvider)
    {
        ArgumentNullException.ThrowIfNull(detail);
        ArgumentNullException.ThrowIfNull(document);
        ArgumentNullException.ThrowIfNull(dtoRecord);
        ArgumentNullException.ThrowIfNull(timeProvider);

        var recordedAt = dtoRecord.ValidatedAt;
        var fetchProvenance = new AdvisoryProvenance(JvnConnectorPlugin.SourceName, "document", document.Uri, document.FetchedAt);
        var mappingProvenance = new AdvisoryProvenance(JvnConnectorPlugin.SourceName, "mapping", detail.VulnerabilityId, recordedAt);

        var aliases = BuildAliases(detail);
        var references = BuildReferences(detail, recordedAt);
        var affectedPackages = BuildAffected(detail, recordedAt);
        var cvssMetrics = BuildCvss(detail, recordedAt, out var severity);

        var description = DescriptionNormalizer.Normalize(new[]
        {
            new LocalizedText(detail.Overview, detail.Language),
        });

        var language = description.Language;
        var summary = string.IsNullOrEmpty(description.Text) ? null : description.Text;

        var provenance = new[] { fetchProvenance, mappingProvenance };

        var advisory = new Advisory(
            detail.VulnerabilityId,
            detail.Title,
            summary,
            language,
            detail.DateFirstPublished,
            detail.DateLastUpdated,
            severity,
            exploitKnown: false,
            aliases,
            references,
            affectedPackages,
            cvssMetrics,
            provenance);

        var vendorStatus = detail.VendorStatuses.Length == 0
            ? null
            : string.Join(",", detail.VendorStatuses.OrderBy(static status => status, StringComparer.Ordinal));

        var flag = new JpFlagRecord(
            detail.VulnerabilityId,
            JvnConnectorPlugin.SourceName,
            detail.JvnCategory,
            vendorStatus,
            timeProvider.GetUtcNow());

        return (advisory, flag);
    }

    private static IEnumerable<string> BuildAliases(JvnDetailDto detail)
    {
        var aliases = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            detail.VulnerabilityId,
        };

        foreach (var cve in detail.CveIds)
        {
            if (!string.IsNullOrWhiteSpace(cve))
            {
                aliases.Add(cve);
            }
        }

        return aliases;
    }

    private static IEnumerable<AdvisoryReference> BuildReferences(JvnDetailDto detail, DateTimeOffset recordedAt)
    {
        var references = new List<AdvisoryReference>();

        foreach (var reference in detail.References)
        {
            if (string.IsNullOrWhiteSpace(reference.Url))
            {
                continue;
            }

            string? kind = reference.Type?.ToLowerInvariant() switch
            {
                "vendor" => "vendor",
                "advisory" => "advisory",
                "cwe" => "weakness",
                _ => null,
            };

            string? sourceTag = !string.IsNullOrWhiteSpace(reference.Id) ? reference.Id : reference.Type;
            string? summary = reference.Name;

            try
            {
                references.Add(new AdvisoryReference(
                    reference.Url,
                    kind,
                    sourceTag,
                    summary,
                    new AdvisoryProvenance(JvnConnectorPlugin.SourceName, "reference", reference.Url, recordedAt)));
            }
            catch (ArgumentException)
            {
                // Ignore malformed URLs that slipped through validation.
            }
        }

        if (references.Count == 0)
        {
            return references;
        }

        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);
        }

        var deduped = map.Values.ToList();
        deduped.Sort(CompareReferences);
        return deduped;
    }

    private static IEnumerable<AffectedPackage> BuildAffected(JvnDetailDto detail, DateTimeOffset recordedAt)
    {
        var packages = new List<AffectedPackage>();

        foreach (var product in detail.Affected)
        {
            if (string.IsNullOrWhiteSpace(product.Cpe))
            {
                continue;
            }

            if (!string.IsNullOrWhiteSpace(product.Status) && !product.Status.StartsWith("vulnerable", StringComparison.OrdinalIgnoreCase))
            {
                continue;
            }

            if (!IdentifierNormalizer.TryNormalizeCpe(product.Cpe, out var cpe))
            {
                continue;
            }

            var provenance = new List<AdvisoryProvenance>
            {
                new AdvisoryProvenance(JvnConnectorPlugin.SourceName, "affected", cpe!, recordedAt),
            };

            var attributeParts = new List<string>(capacity: 2);
            if (!string.IsNullOrWhiteSpace(product.CpeVendor))
            {
                attributeParts.Add($"vendor={product.CpeVendor}");
            }

            if (!string.IsNullOrWhiteSpace(product.CpeProduct))
            {
                attributeParts.Add($"product={product.CpeProduct}");
            }

            if (attributeParts.Count > 0)
            {
                provenance.Add(new AdvisoryProvenance(
                    JvnConnectorPlugin.SourceName,
                    "cpe-attributes",
                    string.Join(";", attributeParts),
                    recordedAt));
            }

            var platform = product.Vendor ?? product.CpeVendor;

            var versionRanges = BuildVersionRanges(product, recordedAt, provenance[0]);

            packages.Add(new AffectedPackage(
                AffectedPackageTypes.Cpe,
                cpe!,
                platform: platform,
                versionRanges: versionRanges,
                statuses: Array.Empty<AffectedPackageStatus>(),
                provenance: provenance.ToArray()));
        }

        return packages;
    }

    private static IReadOnlyList<AffectedVersionRange> BuildVersionRanges(JvnAffectedProductDto product, DateTimeOffset recordedAt, AdvisoryProvenance provenance)
    {
        var extensions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        if (!string.IsNullOrWhiteSpace(product.Version))
        {
            extensions["jvn.version"] = product.Version!;
        }

        if (!string.IsNullOrWhiteSpace(product.Build))
        {
            extensions["jvn.build"] = product.Build!;
        }

        if (!string.IsNullOrWhiteSpace(product.Description))
        {
            extensions["jvn.description"] = product.Description!;
        }

        if (!string.IsNullOrWhiteSpace(product.Status))
        {
            extensions["jvn.status"] = product.Status!;
        }

        if (extensions.Count == 0)
        {
            return Array.Empty<AffectedVersionRange>();
        }

        var primitives = new RangePrimitives(
            null,
            null,
            null,
            extensions);

        var expression = product.Version;
        var range = new AffectedVersionRange(
            rangeKind: "cpe",
            introducedVersion: null,
            fixedVersion: null,
            lastAffectedVersion: null,
            rangeExpression: string.IsNullOrWhiteSpace(expression) ? null : expression,
            provenance: provenance,
            primitives: primitives);

        return new[] { range };
    }

    private static IReadOnlyList<CvssMetric> BuildCvss(JvnDetailDto detail, DateTimeOffset recordedAt, out string? severity)
    {
        var metrics = new List<CvssMetric>();
        severity = null;
        var bestRank = -1;

        foreach (var cvss in detail.Cvss)
        {
            if (!CvssMetricNormalizer.TryNormalize(cvss.Version, cvss.Vector, cvss.Score, cvss.Severity, out var normalized))
            {
                continue;
            }

            var provenance = new AdvisoryProvenance(JvnConnectorPlugin.SourceName, "cvss", cvss.Type, recordedAt);
            metrics.Add(normalized.ToModel(provenance));

            var rank = Array.IndexOf(SeverityOrder, normalized.BaseSeverity);
            if (rank > bestRank)
            {
                bestRank = rank;
                severity = normalized.BaseSeverity;
            }
        }

        return metrics;
    }

    private static int CompareReferences(AdvisoryReference? left, AdvisoryReference? right)
    {
        if (ReferenceEquals(left, right))
        {
            return 0;
        }

        if (left is null)
        {
            return 1;
        }

        if (right is null)
        {
            return -1;
        }

        var compare = StringComparer.OrdinalIgnoreCase.Compare(left.Url, right.Url);
        if (compare != 0)
        {
            return compare;
        }

        compare = CompareNullable(left.Kind, right.Kind);
        if (compare != 0)
        {
            return compare;
        }

        compare = CompareNullable(left.SourceTag, right.SourceTag);
        if (compare != 0)
        {
            return compare;
        }

        compare = CompareNullable(left.Summary, right.Summary);
        if (compare != 0)
        {
            return compare;
        }

        compare = StringComparer.Ordinal.Compare(left.Provenance.Source, right.Provenance.Source);
        if (compare != 0)
        {
            return compare;
        }

        compare = StringComparer.Ordinal.Compare(left.Provenance.Kind, right.Provenance.Kind);
        if (compare != 0)
        {
            return compare;
        }

        compare = CompareNullable(left.Provenance.Value, right.Provenance.Value);
        if (compare != 0)
        {
            return compare;
        }

        return left.Provenance.RecordedAt.CompareTo(right.Provenance.RecordedAt);
    }

    private static int CompareNullable(string? left, string? right)
    {
        if (left is null && right is null)
        {
            return 0;
        }

        if (left is null)
        {
            return 1;
        }

        if (right is null)
        {
            return -1;
        }

        return StringComparer.Ordinal.Compare(left, right);
    }

    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;
    }
}
