using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Normalization.Cvss;
using StellaOps.Feedser.Normalization.SemVer;
using StellaOps.Feedser.Storage.Mongo.Documents;

namespace StellaOps.Feedser.Source.Ru.Nkcki.Internal;

internal static class RuNkckiMapper
{
    private static readonly ImmutableDictionary<string, string> SeverityLookup = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
    {
        ["критический"] = "critical",
        ["высокий"] = "high",
        ["средний"] = "medium",
        ["умеренный"] = "medium",
        ["низкий"] = "low",
        ["информационный"] = "informational",
    }.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase);

    public static Advisory Map(RuNkckiVulnerabilityDto dto, DocumentRecord document, DateTimeOffset recordedAt)
    {
        ArgumentNullException.ThrowIfNull(dto);
        ArgumentNullException.ThrowIfNull(document);

        var advisoryProvenance = new AdvisoryProvenance(
            RuNkckiConnectorPlugin.SourceName,
            "advisory",
            dto.AdvisoryKey,
            recordedAt,
            new[] { ProvenanceFieldMasks.Advisory });

        var aliases = BuildAliases(dto);
        var references = BuildReferences(dto, document, recordedAt);
        var packages = BuildPackages(dto, recordedAt);
        var cvssMetrics = BuildCvssMetrics(dto, recordedAt, out var severityFromCvss);
        var severityFromRating = NormalizeSeverity(dto.CvssRating);
        var severity = severityFromRating ?? severityFromCvss;

        if (severityFromRating is not null && severityFromCvss is not null)
        {
            severity = ChooseMoreSevere(severityFromRating, severityFromCvss);
        }

        var exploitKnown = DetermineExploitKnown(dto);

        return new Advisory(
            advisoryKey: dto.AdvisoryKey,
            title: dto.Description ?? dto.AdvisoryKey,
            summary: dto.Description,
            language: "ru",
            published: dto.DatePublished,
            modified: dto.DateUpdated,
            severity: severity,
            exploitKnown: exploitKnown,
            aliases: aliases,
            references: references,
            affectedPackages: packages,
            cvssMetrics: cvssMetrics,
            provenance: new[] { advisoryProvenance });
    }

    private static IReadOnlyList<string> BuildAliases(RuNkckiVulnerabilityDto dto)
    {
        var aliases = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        if (!string.IsNullOrWhiteSpace(dto.FstecId))
        {
            aliases.Add(dto.FstecId!);
        }

        if (!string.IsNullOrWhiteSpace(dto.MitreId))
        {
            aliases.Add(dto.MitreId!);
        }

        return aliases.ToImmutableSortedSet(StringComparer.Ordinal).ToImmutableArray();
    }

    private static IReadOnlyList<AdvisoryReference> BuildReferences(RuNkckiVulnerabilityDto dto, DocumentRecord document, DateTimeOffset recordedAt)
    {
        var references = new List<AdvisoryReference>();
        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        void AddReference(string? url, string kind, string? sourceTag, string? summary)
        {
            if (string.IsNullOrWhiteSpace(url))
            {
                return;
            }

            var key = $"{kind}|{url}";
            if (!seen.Add(key))
            {
                return;
            }

            var provenance = new AdvisoryProvenance(
                RuNkckiConnectorPlugin.SourceName,
                "reference",
                url,
                recordedAt,
                new[] { ProvenanceFieldMasks.References });

            references.Add(new AdvisoryReference(url, kind, sourceTag, summary, provenance));
        }

        AddReference(document.Uri, "details", "ru-nkcki", null);

        if (!string.IsNullOrWhiteSpace(dto.FstecId))
        {
            var slug = dto.FstecId!.Contains(':', StringComparison.Ordinal)
                ? dto.FstecId[(dto.FstecId.IndexOf(':') + 1)..]
                : dto.FstecId;
            AddReference($"https://bdu.fstec.ru/vul/{slug}", "details", "bdu", null);
        }

        foreach (var url in dto.Urls)
        {
            var kind = url.Contains("cert.gov.ru", StringComparison.OrdinalIgnoreCase) ? "details" : "external";
            var sourceTag = url.Contains("siemens", StringComparison.OrdinalIgnoreCase) ? "vendor" : null;
            AddReference(url, kind, sourceTag, null);
        }

        if (dto.Cwe?.Number is int number)
        {
            AddReference(
                $"https://cwe.mitre.org/data/definitions/{number}.html",
                "cwe",
                "cwe",
                dto.Cwe.Description);
        }

        return references;
    }

    private static IReadOnlyList<AffectedPackage> BuildPackages(RuNkckiVulnerabilityDto dto, DateTimeOffset recordedAt)
    {
        if (!dto.VulnerableSoftwareEntries.IsDefaultOrEmpty && dto.VulnerableSoftwareEntries.Length > 0)
        {
            return CreatePackages(dto.VulnerableSoftwareEntries, dto, recordedAt);
        }

        if (!string.IsNullOrWhiteSpace(dto.VulnerableSoftwareText))
        {
            var fallbackEntry = new RuNkckiSoftwareEntry(
                dto.VulnerableSoftwareText!,
                dto.VulnerableSoftwareText!,
                ImmutableArray<string>.Empty);
            return CreatePackages(new[] { fallbackEntry }, dto, recordedAt);
        }

        return Array.Empty<AffectedPackage>();
    }

    private static IReadOnlyList<AffectedPackage> CreatePackages(IEnumerable<RuNkckiSoftwareEntry> entries, RuNkckiVulnerabilityDto dto, DateTimeOffset recordedAt)
    {
        var type = DeterminePackageType(dto);
        var platform = dto.ProductCategories.IsDefaultOrEmpty || dto.ProductCategories.Length == 0
            ? null
            : string.Join(", ", dto.ProductCategories);

        var packages = new List<AffectedPackage>();

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

            var packageProvenance = new AdvisoryProvenance(
                RuNkckiConnectorPlugin.SourceName,
                "package",
                entry.Evidence,
                recordedAt,
                new[] { ProvenanceFieldMasks.AffectedPackages });

            var status = new AffectedPackageStatus(
                dto.PatchAvailable == true ? AffectedPackageStatusCatalog.Fixed : AffectedPackageStatusCatalog.Affected,
                new AdvisoryProvenance(
                    RuNkckiConnectorPlugin.SourceName,
                    "package-status",
                    dto.PatchAvailable == true ? "patch_available" : "affected",
                    recordedAt,
                    new[] { ProvenanceFieldMasks.PackageStatuses }));

            var rangeMetadata = BuildRangeMetadata(entry, recordedAt);

            packages.Add(new AffectedPackage(
                type,
                entry.Identifier,
                platform,
                rangeMetadata.Ranges,
                new[] { status },
                new[] { packageProvenance },
                rangeMetadata.Normalized));
        }

        return packages;
    }

    private static IReadOnlyList<CvssMetric> BuildCvssMetrics(RuNkckiVulnerabilityDto dto, DateTimeOffset recordedAt, out string? severity)
    {
        severity = null;
        var metrics = new List<CvssMetric>();

        if (!string.IsNullOrWhiteSpace(dto.CvssVector) && CvssMetricNormalizer.TryNormalize(null, dto.CvssVector, dto.CvssScore, null, out var normalized))
        {
            var provenance = new AdvisoryProvenance(
                RuNkckiConnectorPlugin.SourceName,
                "cvss",
                normalized.Vector,
                recordedAt,
                new[] { ProvenanceFieldMasks.CvssMetrics });
            var metric = normalized.ToModel(provenance);
            metrics.Add(metric);
            severity ??= metric.BaseSeverity;
        }

        if (!string.IsNullOrWhiteSpace(dto.CvssVectorV4))
        {
            var vector = dto.CvssVectorV4.StartsWith("CVSS:", StringComparison.OrdinalIgnoreCase)
                ? dto.CvssVectorV4
                : $"CVSS:4.0/{dto.CvssVectorV4}";
            var score = dto.CvssScoreV4.HasValue
                ? Math.Round(dto.CvssScoreV4.Value, 1, MidpointRounding.AwayFromZero)
                : 0.0;
            var severityV4 = DetermineCvss4Severity(score);

            var provenance = new AdvisoryProvenance(
                RuNkckiConnectorPlugin.SourceName,
                "cvss",
                vector,
                recordedAt,
                new[] { ProvenanceFieldMasks.CvssMetrics });

            metrics.Add(new CvssMetric("4.0", vector, score, severityV4, provenance));
            severity ??= severityV4;
        }

        return metrics;
    }

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

        var normalized = rating.Trim().ToLowerInvariant();

        if (SeverityLookup.TryGetValue(normalized, out var mapped))
        {
            return mapped;
        }

        if (normalized.StartsWith("крит", StringComparison.Ordinal))
        {
            return "critical";
        }

        if (normalized.StartsWith("высок", StringComparison.Ordinal))
        {
            return "high";
        }

        if (normalized.StartsWith("сред", StringComparison.Ordinal) || normalized.StartsWith("умер", StringComparison.Ordinal))
        {
            return "medium";
        }

        if (normalized.StartsWith("низк", StringComparison.Ordinal))
        {
            return "low";
        }

        if (normalized.StartsWith("информ", StringComparison.Ordinal))
        {
            return "informational";
        }

        return null;
    }

    private static string ChooseMoreSevere(string first, string second)
    {
        var order = new[] { "critical", "high", "medium", "low", "informational" };

        static int IndexOf(ReadOnlySpan<string> levels, string value)
        {
            for (var i = 0; i < levels.Length; i++)
            {
                if (string.Equals(levels[i], value, StringComparison.OrdinalIgnoreCase))
                {
                    return i;
                }
            }

            return -1;
        }

        var firstIndex = IndexOf(order.AsSpan(), first);
        var secondIndex = IndexOf(order.AsSpan(), second);

        if (firstIndex == -1 && secondIndex == -1)
        {
            return first;
        }

        if (firstIndex == -1)
        {
            return second;
        }

        if (secondIndex == -1)
        {
            return first;
        }

        return firstIndex <= secondIndex ? first : second;
    }

    private static bool DetermineExploitKnown(RuNkckiVulnerabilityDto dto)
    {
        if (!string.IsNullOrWhiteSpace(dto.MethodOfExploitation))
        {
            return true;
        }

        if (!string.IsNullOrWhiteSpace(dto.Impact))
        {
            var impact = dto.Impact.Trim().ToUpperInvariant();
            if (impact is "ACE" or "RCE" or "LPE")
            {
                return true;
            }
        }

        return false;
    }

    private static string DeterminePackageType(RuNkckiVulnerabilityDto dto)
    {
        if (dto.VulnerableSoftwareHasCpe == true)
        {
            return AffectedPackageTypes.Cpe;
        }

        if (!dto.ProductCategories.IsDefault && dto.ProductCategories.Any(static category =>
                category.Contains("ics", StringComparison.OrdinalIgnoreCase)
                || category.Contains("scada", StringComparison.OrdinalIgnoreCase)))
        {
            return AffectedPackageTypes.IcsVendor;
        }

        return AffectedPackageTypes.Vendor;
    }

    private static (IReadOnlyList<AffectedVersionRange> Ranges, IReadOnlyList<NormalizedVersionRule> Normalized) BuildRangeMetadata(
        RuNkckiSoftwareEntry entry,
        DateTimeOffset recordedAt)
    {
        if (entry.RangeExpressions.IsDefaultOrEmpty || entry.RangeExpressions.Length == 0)
        {
            return (Array.Empty<AffectedVersionRange>(), Array.Empty<NormalizedVersionRule>());
        }

        var ranges = new List<AffectedVersionRange>();
        var normalized = new List<NormalizedVersionRule>();
        var dedupe = new HashSet<string>(StringComparer.Ordinal);

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

            var results = SemVerRangeRuleBuilder.Build(expression, provenanceNote: entry.Evidence);
            if (results.Count == 0)
            {
                continue;
            }

            foreach (var result in results)
            {
                var key = $"{result.Primitive.Introduced}|{result.Primitive.Fixed}|{result.Primitive.LastAffected}|{result.Expression}";
                if (!dedupe.Add(key))
                {
                    continue;
                }

                var provenance = new AdvisoryProvenance(
                    RuNkckiConnectorPlugin.SourceName,
                    "package-range",
                    entry.Evidence,
                    recordedAt,
                    new[] { ProvenanceFieldMasks.VersionRanges });

                ranges.Add(new AffectedVersionRange(
                    NormalizedVersionSchemes.SemVer,
                    result.Primitive.Introduced,
                    result.Primitive.Fixed,
                    result.Primitive.LastAffected,
                    result.Expression,
                    provenance,
                    new RangePrimitives(result.Primitive, null, null, null)));

                normalized.Add(result.NormalizedRule);
            }
        }

        return (
            ranges.Count == 0 ? Array.Empty<AffectedVersionRange>() : ranges,
            normalized.Count == 0 ? Array.Empty<NormalizedVersionRule>() : normalized);
    }
    private static string DetermineCvss4Severity(double score)
    {
        if (score <= 0.0)
        {
            return "none";
        }

        if (score < 4.0)
        {
            return "low";
        }

        if (score < 7.0)
        {
            return "medium";
        }

        if (score < 9.0)
        {
            return "high";
        }

        return "critical";
    }
}
