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

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 primitivesDictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            {
                ["vendor"] = vendor,
                ["product"] = product,
            };
            if (!string.IsNullOrWhiteSpace(affected.Platform))
            {
                primitivesDictionary["platform"] = affected.Platform!;
            }

            var primitives = new RangePrimitives(null, null, null, primitivesDictionary);
            var ranges = CreateVersionRanges(affected, recordedAt, identifier, primitives);
            var statuses = CreateStatuses(affected, recordedAt, identifier);

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

            packages.Add(new AffectedPackage(
                type: AffectedPackageTypes.Vendor,
                identifier: identifier,
                platform: affected.Platform,
                versionRanges: ranges,
                statuses: statuses,
                provenance: provenance));
        }

        return packages;
    }

    private static List<AffectedVersionRange> CreateVersionRanges(CveAffectedDto affected, DateTimeOffset recordedAt, string identifier, RangePrimitives primitives)
    {
        var ranges = new List<AffectedVersionRange>();
        foreach (var version in affected.Versions)
        {
            var provenance = new AdvisoryProvenance(
                CveConnectorPlugin.SourceName,
                "affected-range",
                identifier,
                recordedAt);

            var kind = !string.IsNullOrWhiteSpace(version.VersionType)
                ? version.VersionType!.Trim().ToLowerInvariant()
                : "vendor";

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

            ranges.Add(new AffectedVersionRange(
                rangeKind: kind,
                introducedVersion: Normalize(version.Version),
                fixedVersion: Normalize(version.LessThan),
                lastAffectedVersion: Normalize(version.LessThanOrEqual),
                rangeExpression: Normalize(rangeExpression),
                provenance: provenance,
                primitives: primitives));
        }

        return ranges;
    }

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