using System.Collections.Generic;
using System.Linq;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Normalization.Cvss;
using StellaOps.Feedser.Storage.Mongo.Documents;
using NuGet.Versioning;

namespace StellaOps.Feedser.Source.Cve.Internal;

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

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

        var fetchProvenance = new AdvisoryProvenance(CveConnectorPlugin.SourceName, "document", document.Uri, document.FetchedAt);
        var mapProvenance = new AdvisoryProvenance(CveConnectorPlugin.SourceName, "mapping", dto.CveId, recordedAt);

        var aliases = dto.Aliases
            .Append(dto.CveId)
            .Where(static alias => !string.IsNullOrWhiteSpace(alias))
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToArray();

        var references = dto.References
            .Select(reference => CreateReference(reference, recordedAt))
            .Where(static reference => reference is not null)
            .Cast<AdvisoryReference>()
            .ToList();

        var affected = CreateAffectedPackages(dto, recordedAt);
        var cvssMetrics = CreateCvssMetrics(dto, recordedAt, document.Uri);
        var severity = DetermineSeverity(cvssMetrics);

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

        var title = string.IsNullOrWhiteSpace(dto.Title) ? dto.CveId : dto.Title!;

        return new Advisory(
            advisoryKey: dto.CveId,
            title: title,
            summary: dto.Summary,
            language: dto.Language,
            published: dto.Published,
            modified: dto.Modified ?? dto.Published,
            severity: severity,
            exploitKnown: false,
            aliases: aliases,
            references: references,
            affectedPackages: affected,
            cvssMetrics: cvssMetrics,
            provenance: provenance);
    }

    private static AdvisoryReference? CreateReference(CveReferenceDto dto, DateTimeOffset recordedAt)
    {
        if (string.IsNullOrWhiteSpace(dto.Url) || !Validation.LooksLikeHttpUrl(dto.Url))
        {
            return null;
        }

        var kind = dto.Tags.FirstOrDefault();
        return new AdvisoryReference(
            dto.Url,
            kind,
            dto.Source,
            summary: null,
            provenance: new AdvisoryProvenance(CveConnectorPlugin.SourceName, "reference", dto.Url, recordedAt));
    }

    private static IReadOnlyList<AffectedPackage> CreateAffectedPackages(CveRecordDto dto, DateTimeOffset recordedAt)
    {
        if (dto.Affected.Count == 0)
        {
            return Array.Empty<AffectedPackage>();
        }

        var packages = new List<AffectedPackage>(dto.Affected.Count);
        foreach (var affected in dto.Affected)
        {
            var vendor = string.IsNullOrWhiteSpace(affected.Vendor) ? "unknown-vendor" : affected.Vendor!.Trim();
            var product = string.IsNullOrWhiteSpace(affected.Product) ? "unknown-product" : affected.Product!.Trim();
            var identifier = string.Equals(product, vendor, StringComparison.OrdinalIgnoreCase)
                ? vendor.ToLowerInvariant()
                : $"{vendor}:{product}".ToLowerInvariant();

            var provenance = new[]
            {
                new AdvisoryProvenance(CveConnectorPlugin.SourceName, "affected", identifier, recordedAt),
            };

            var vendorExtensions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            {
                ["vendor"] = vendor,
                ["product"] = product,
            };
            if (!string.IsNullOrWhiteSpace(affected.Platform))
            {
                vendorExtensions["platform"] = affected.Platform!;
            }

            var note = BuildNormalizedNote(dto.CveId, identifier);
            var (ranges, normalizedVersions) = CreateVersionArtifacts(affected, recordedAt, identifier, vendorExtensions, note);
            var statuses = CreateStatuses(affected, recordedAt, identifier);

            if (ranges.Count == 0)
            {
                var fallbackPrimitives = vendorExtensions.Count == 0
                    ? null
                    : new RangePrimitives(null, null, null, vendorExtensions);

                ranges.Add(new AffectedVersionRange(
                    rangeKind: "vendor",
                    introducedVersion: null,
                    fixedVersion: null,
                    lastAffectedVersion: null,
                    rangeExpression: null,
                    provenance: provenance[0],
                    primitives: fallbackPrimitives));
            }

            packages.Add(new AffectedPackage(
                type: AffectedPackageTypes.Vendor,
                identifier: identifier,
                platform: affected.Platform,
                versionRanges: ranges,
                statuses: statuses,
                provenance: provenance,
                normalizedVersions: normalizedVersions.Count == 0
                    ? Array.Empty<NormalizedVersionRule>()
                    : normalizedVersions.ToArray()));
        }

        return packages;
    }

    private static (List<AffectedVersionRange> Ranges, List<NormalizedVersionRule> Normalized) CreateVersionArtifacts(
        CveAffectedDto affected,
        DateTimeOffset recordedAt,
        string identifier,
        IReadOnlyDictionary<string, string> vendorExtensions,
        string normalizedNote)
    {
        var ranges = new List<AffectedVersionRange>();
        var normalized = new List<NormalizedVersionRule>();

        foreach (var version in affected.Versions)
        {
            var range = BuildVersionRange(version, recordedAt, identifier, vendorExtensions);
            if (range is null)
            {
                continue;
            }

            ranges.Add(range);

            var rule = range.ToNormalizedVersionRule(normalizedNote);
            if (rule is not null)
            {
                normalized.Add(rule);
            }
        }

        return (ranges, normalized);
    }

    private static AffectedVersionRange? BuildVersionRange(
        CveVersionDto version,
        DateTimeOffset recordedAt,
        string identifier,
        IReadOnlyDictionary<string, string> baseVendorExtensions)
    {
        var vendor = new Dictionary<string, string>(baseVendorExtensions, StringComparer.OrdinalIgnoreCase);

        void AddExtension(string key, string? value)
        {
            if (!string.IsNullOrWhiteSpace(value))
            {
                vendor[key] = value.Trim();
            }
        }

        AddExtension("version", version.Version);
        AddExtension("lessThan", version.LessThan);
        AddExtension("lessThanOrEqual", version.LessThanOrEqual);
        AddExtension("versionType", version.VersionType);
        AddExtension("versionRange", version.Range);

        var introduced = Normalize(version.Version);
        var fixedExclusive = Normalize(version.LessThan);
        var lastInclusive = Normalize(version.LessThanOrEqual);

        var rangeExpression = Normalize(string.IsNullOrWhiteSpace(version.Range)
            ? BuildRangeExpression(version)
            : version.Range);

        var provenance = new AdvisoryProvenance(
            CveConnectorPlugin.SourceName,
            "affected-range",
            identifier,
            recordedAt);

        var semVerPrimitive = TryBuildSemVerPrimitive(
            version.VersionType,
            introduced,
            fixedExclusive,
            lastInclusive,
            rangeExpression,
            out var primitive);

        if (semVerPrimitive)
        {
            introduced = primitive!.Introduced ?? introduced;
            fixedExclusive = primitive.Fixed ?? fixedExclusive;
            lastInclusive = primitive.LastAffected ?? lastInclusive;
        }

        if (introduced is null && fixedExclusive is null && lastInclusive is null && rangeExpression is null && primitive is null && vendor.Count == baseVendorExtensions.Count)
        {
            return null;
        }

        var rangeKind = primitive is not null
            ? NormalizedVersionSchemes.SemVer
            : (string.IsNullOrWhiteSpace(version.VersionType)
                ? "vendor"
                : version.VersionType!.Trim().ToLowerInvariant());

        var rangePrimitives = primitive is null && vendor.Count == 0
            ? null
            : new RangePrimitives(
                primitive,
                Nevra: null,
                Evr: null,
                VendorExtensions: vendor.Count == 0 ? null : vendor);

        return new AffectedVersionRange(
            rangeKind: rangeKind,
            introducedVersion: introduced,
            fixedVersion: fixedExclusive,
            lastAffectedVersion: lastInclusive,
            rangeExpression: rangeExpression,
            provenance: provenance,
            primitives: rangePrimitives);
    }

    private static List<AffectedPackageStatus> CreateStatuses(CveAffectedDto affected, DateTimeOffset recordedAt, string identifier)
    {
        var statuses = new List<AffectedPackageStatus>();

        void AddStatus(string? status)
        {
            if (string.IsNullOrWhiteSpace(status))
            {
                return;
            }

            statuses.Add(new AffectedPackageStatus(
                status,
                new AdvisoryProvenance(CveConnectorPlugin.SourceName, "affected-status", identifier, recordedAt)));
        }

        AddStatus(affected.DefaultStatus);

        foreach (var version in affected.Versions)
        {
            AddStatus(version.Status);
        }

        return statuses;
    }

    private static string? Normalize(string? value)
        => string.IsNullOrWhiteSpace(value) || value is "*" or "-" ? null : value.Trim();

    private static string? BuildRangeExpression(CveVersionDto version)
    {
        var builder = new List<string>();
        if (!string.IsNullOrWhiteSpace(version.Version))
        {
            builder.Add($"version={version.Version}");
        }

        if (!string.IsNullOrWhiteSpace(version.LessThan))
        {
            builder.Add($"< {version.LessThan}");
        }

        if (!string.IsNullOrWhiteSpace(version.LessThanOrEqual))
        {
            builder.Add($"<= {version.LessThanOrEqual}");
        }

        if (builder.Count == 0)
        {
            return null;
        }

        return string.Join(", ", builder);
    }

    private static string BuildNormalizedNote(string cveId, string identifier)
    {
        var baseId = string.IsNullOrWhiteSpace(cveId)
            ? "unknown"
            : cveId.Trim().ToLowerInvariant();
        return $"cve:{baseId}:{identifier}";
    }

    private static bool TryBuildSemVerPrimitive(
        string? versionType,
        string? introduced,
        string? fixedExclusive,
        string? lastInclusive,
        string? constraintExpression,
        out SemVerPrimitive? primitive)
    {
        primitive = null;

        if (!string.Equals(versionType, "semver", StringComparison.OrdinalIgnoreCase))
        {
            return false;
        }

        if (!TryNormalizeSemVer(introduced, out var normalizedIntroduced)
            || !TryNormalizeSemVer(fixedExclusive, out var normalizedFixed)
            || !TryNormalizeSemVer(lastInclusive, out var normalizedLast))
        {
            normalizedIntroduced = introduced;
            normalizedFixed = fixedExclusive;
            normalizedLast = lastInclusive;
            return false;
        }

        if (normalizedIntroduced is null && normalizedFixed is null && normalizedLast is null)
        {
            return false;
        }

        var introducedInclusive = true;
        var fixedInclusive = false;
        var lastInclusiveFlag = true;

        if (normalizedFixed is null && normalizedLast is null && normalizedIntroduced is not null)
        {
            // Exact version. Treat as introduced/fixed equality.
            normalizedFixed = normalizedIntroduced;
            normalizedLast = normalizedIntroduced;
        }

        primitive = new SemVerPrimitive(
            Introduced: normalizedIntroduced,
            IntroducedInclusive: introducedInclusive,
            Fixed: normalizedFixed,
            FixedInclusive: fixedInclusive,
            LastAffected: normalizedLast,
            LastAffectedInclusive: lastInclusiveFlag,
            ConstraintExpression: constraintExpression,
            ExactValue: normalizedFixed is not null && normalizedIntroduced is not null && normalizedFixed == normalizedIntroduced
                ? normalizedIntroduced
                : null);

        return true;
    }

    private static bool TryNormalizeSemVer(string? value, out string? normalized)
    {
        normalized = null;
        if (string.IsNullOrWhiteSpace(value))
        {
            return true;
        }

        var trimmed = value.Trim();
        if (trimmed.StartsWith("v", StringComparison.OrdinalIgnoreCase) && trimmed.Length > 1)
        {
            trimmed = trimmed[1..];
        }

        if (!NuGetVersion.TryParse(trimmed, out var parsed))
        {
            return false;
        }

        normalized = parsed.ToNormalizedString();
        return true;
    }

    private static IReadOnlyList<CvssMetric> CreateCvssMetrics(CveRecordDto dto, DateTimeOffset recordedAt, string sourceUri)
    {
        if (dto.Metrics.Count == 0)
        {
            return Array.Empty<CvssMetric>();
        }

        var provenance = new AdvisoryProvenance(CveConnectorPlugin.SourceName, "cvss", sourceUri, recordedAt);
        var metrics = new List<CvssMetric>(dto.Metrics.Count);
        foreach (var metric in dto.Metrics)
        {
            if (!CvssMetricNormalizer.TryNormalize(metric.Version, metric.Vector, metric.BaseScore, metric.BaseSeverity, out var normalized))
            {
                continue;
            }

            metrics.Add(new CvssMetric(
                normalized.Version,
                normalized.Vector,
                normalized.BaseScore,
                normalized.BaseSeverity,
                provenance));
        }

        return metrics;
    }

    private static string? DetermineSeverity(IReadOnlyList<CvssMetric> metrics)
    {
        if (metrics.Count == 0)
        {
            return null;
        }

        foreach (var level in SeverityOrder)
        {
            if (metrics.Any(metric => string.Equals(metric.BaseSeverity, level, StringComparison.OrdinalIgnoreCase)))
            {
                return level;
            }
        }

        return metrics
            .Select(metric => metric.BaseSeverity)
            .FirstOrDefault();
    }
}
