using System.Collections.Immutable;
using System.Linq;
using StellaOps.Feedser.Models;

namespace StellaOps.Feedser.Normalization.Cvss;

/// <summary>
/// Provides helpers to canonicalize CVSS vectors and fill in derived score/severity information.
/// </summary>
public static class CvssMetricNormalizer
{
    private static readonly string[] Cvss3BaseMetrics = { "AV", "AC", "PR", "UI", "S", "C", "I", "A" };
    private static readonly string[] Cvss2BaseMetrics = { "AV", "AC", "AU", "C", "I", "A" };

    public static bool TryNormalize(
        string? version,
        string? vector,
        double? baseScore,
        string? baseSeverity,
        out CvssNormalizedMetric metric)
    {
        metric = default;
        if (string.IsNullOrWhiteSpace(vector))
        {
            return false;
        }

        var rawVector = vector.Trim();
        if (!TryDetermineVersion(version, rawVector, out var parsedVersion, out var vectorWithoutPrefix))
        {
            return false;
        }

        if (!TryParseMetrics(vectorWithoutPrefix, parsedVersion, out var canonicalVector, out var metrics))
        {
            return false;
        }

        if (!TryComputeBaseScore(parsedVersion, metrics, out var computedScore))
        {
            return false;
        }

        var normalizedScore = baseScore.HasValue
            ? Math.Round(baseScore.Value, 1, MidpointRounding.AwayFromZero)
            : computedScore;

        if (baseScore.HasValue && Math.Abs(normalizedScore - computedScore) > 0.2)
        {
            normalizedScore = computedScore;
        }

        var severity = NormalizeSeverity(baseSeverity, parsedVersion)
            ?? DetermineSeverity(normalizedScore, parsedVersion);

        metric = new CvssNormalizedMetric(
            ToVersionString(parsedVersion),
            canonicalVector,
            normalizedScore,
            severity);

        return true;
    }

    private static bool TryDetermineVersion(string? versionToken, string vector, out CvssVersion version, out string withoutPrefix)
    {
        if (TryExtractVersionFromVector(vector, out version, out withoutPrefix))
        {
            return true;
        }

        if (!string.IsNullOrWhiteSpace(versionToken) && TryMapVersion(versionToken!, out version))
        {
            withoutPrefix = StripPrefix(vector);
            return true;
        }

        var upper = vector.ToUpperInvariant();
        if (upper.Contains("PR:", StringComparison.Ordinal))
        {
            version = CvssVersion.V31;
            withoutPrefix = StripPrefix(vector);
            return true;
        }

        if (upper.Contains("AU:", StringComparison.Ordinal))
        {
            version = CvssVersion.V20;
            withoutPrefix = StripPrefix(vector);
            return true;
        }

        version = CvssVersion.V31;
        withoutPrefix = StripPrefix(vector);
        return true;
    }

    private static string StripPrefix(string vector)
    {
        if (!vector.StartsWith("CVSS:", StringComparison.OrdinalIgnoreCase))
        {
            return vector;
        }

        var remainder = vector[5..];
        var slashIndex = remainder.IndexOf('/');
        return slashIndex >= 0 && slashIndex < remainder.Length - 1
            ? remainder[(slashIndex + 1)..]
            : string.Empty;
    }

    private static bool TryExtractVersionFromVector(string vector, out CvssVersion version, out string withoutPrefix)
    {
        withoutPrefix = vector;
        if (!vector.StartsWith("CVSS:", StringComparison.OrdinalIgnoreCase))
        {
            version = default;
            return false;
        }

        var remainder = vector[5..];
        var slashIndex = remainder.IndexOf('/');
        if (slashIndex <= 0 || slashIndex >= remainder.Length - 1)
        {
            version = CvssVersion.V31;
            withoutPrefix = slashIndex > 0 && slashIndex < remainder.Length - 1
                ? remainder[(slashIndex + 1)..]
                : string.Empty;
            return false;
        }

        var versionToken = remainder[..slashIndex];
        withoutPrefix = remainder[(slashIndex + 1)..];
        if (TryMapVersion(versionToken, out version))
        {
            return true;
        }

        version = CvssVersion.V31;
        return false;
    }

    private static bool TryMapVersion(string token, out CvssVersion version)
    {
        var trimmed = token.Trim();
        if (trimmed.Length == 0)
        {
            version = default;
            return false;
        }

        if (trimmed.StartsWith("v", StringComparison.OrdinalIgnoreCase))
        {
            trimmed = trimmed[1..];
        }

        trimmed = trimmed switch
        {
            "3" or "3.1.0" or "3.1" => "3.1",
            "3.0" or "3.0.0" => "3.0",
            "2" or "2.0.0" => "2.0",
            _ => trimmed,
        };

        version = trimmed switch
        {
            "2" or "2.0" => CvssVersion.V20,
            "3.0" => CvssVersion.V30,
            "3.1" => CvssVersion.V31,
            _ => CvssVersion.Unknown,
        };

        return version != CvssVersion.Unknown;
    }

    private static bool TryParseMetrics(
        string vector,
        CvssVersion version,
        out string canonicalVector,
        out ImmutableDictionary<string, string> metrics)
    {
        canonicalVector = string.Empty;
        var parsed = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        var segments = vector.Split('/', StringSplitOptions.RemoveEmptyEntries);
        if (segments.Length == 0)
        {
            metrics = ImmutableDictionary<string, string>.Empty;
            return false;
        }

        foreach (var segment in segments)
        {
            var trimmed = segment.Trim();
            if (trimmed.Length == 0)
            {
                continue;
            }

            var index = trimmed.IndexOf(':');
            if (index <= 0 || index == trimmed.Length - 1)
            {
                metrics = ImmutableDictionary<string, string>.Empty;
                return false;
            }

            var key = trimmed[..index].Trim().ToUpperInvariant();
            var value = trimmed[(index + 1)..].Trim().ToUpperInvariant();
            if (key.Length == 0 || value.Length == 0)
            {
                metrics = ImmutableDictionary<string, string>.Empty;
                return false;
            }

            parsed[key] = value;
        }

        var required = version == CvssVersion.V20 ? Cvss2BaseMetrics : Cvss3BaseMetrics;
        foreach (var metric in required)
        {
            if (!parsed.ContainsKey(metric))
            {
                metrics = ImmutableDictionary<string, string>.Empty;
                return false;
            }
        }

        var canonicalSegments = new List<string>(parsed.Count + 1);
        foreach (var metric in required)
        {
            canonicalSegments.Add($"{metric}:{parsed[metric]}");
        }

        foreach (var entry in parsed.OrderBy(static pair => pair.Key, StringComparer.Ordinal))
        {
            if (required.Contains(entry.Key))
            {
                continue;
            }

            canonicalSegments.Add($"{entry.Key}:{entry.Value}");
        }

        canonicalVector = $"CVSS:{ToVersionString(version)}/{string.Join('/', canonicalSegments)}";
        metrics = parsed.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase);
        return true;
    }

    private static bool TryComputeBaseScore(CvssVersion version, IReadOnlyDictionary<string, string> metrics, out double score)
    {
        return version switch
        {
            CvssVersion.V20 => TryComputeCvss2(metrics, out score),
            CvssVersion.V30 or CvssVersion.V31 => TryComputeCvss3(metrics, out score),
            _ => (score = 0) == 0,
        };
    }

    private static bool TryComputeCvss3(IReadOnlyDictionary<string, string> metrics, out double score)
    {
        try
        {
            var av = metrics["AV"] switch
            {
                "N" => 0.85,
                "A" => 0.62,
                "L" => 0.55,
                "P" => 0.2,
                _ => throw new InvalidOperationException(),
            };

            var ac = metrics["AC"] switch
            {
                "L" => 0.77,
                "H" => 0.44,
                _ => throw new InvalidOperationException(),
            };

            var scopeChanged = metrics["S"] switch
            {
                "U" => false,
                "C" => true,
                _ => throw new InvalidOperationException(),
            };

            var pr = metrics["PR"] switch
            {
                "N" => 0.85,
                "L" => scopeChanged ? 0.68 : 0.62,
                "H" => scopeChanged ? 0.5 : 0.27,
                _ => throw new InvalidOperationException(),
            };

            var ui = metrics["UI"] switch
            {
                "N" => 0.85,
                "R" => 0.62,
                _ => throw new InvalidOperationException(),
            };

            var confidentiality = metrics["C"] switch
            {
                "N" => 0.0,
                "L" => 0.22,
                "H" => 0.56,
                _ => throw new InvalidOperationException(),
            };

            var integrity = metrics["I"] switch
            {
                "N" => 0.0,
                "L" => 0.22,
                "H" => 0.56,
                _ => throw new InvalidOperationException(),
            };

            var availability = metrics["A"] switch
            {
                "N" => 0.0,
                "L" => 0.22,
                "H" => 0.56,
                _ => throw new InvalidOperationException(),
            };

            var impactSub = 1 - (1 - confidentiality) * (1 - integrity) * (1 - availability);
            impactSub = Math.Clamp(impactSub, 0, 1);

            var impact = scopeChanged
                ? 7.52 * (impactSub - 0.029) - 3.25 * Math.Pow(impactSub - 0.02, 15)
                : 6.42 * impactSub;

            var exploitability = 8.22 * av * ac * pr * ui;

            if (impact <= 0)
            {
                score = 0;
                return true;
            }

            var baseScore = scopeChanged
                ? Math.Min(1.08 * (impact + exploitability), 10)
                : Math.Min(impact + exploitability, 10);

            score = RoundUp(baseScore);
            return true;
        }
        catch (KeyNotFoundException)
        {
            score = 0;
            return false;
        }
        catch (InvalidOperationException)
        {
            score = 0;
            return false;
        }
    }

    private static bool TryComputeCvss2(IReadOnlyDictionary<string, string> metrics, out double score)
    {
        try
        {
            var av = metrics["AV"] switch
            {
                "L" => 0.395,
                "A" => 0.646,
                "N" => 1.0,
                _ => throw new InvalidOperationException(),
            };

            var ac = metrics["AC"] switch
            {
                "H" => 0.35,
                "M" => 0.61,
                "L" => 0.71,
                _ => throw new InvalidOperationException(),
            };

            var authValue = metrics.TryGetValue("AU", out var primaryAuth)
                ? primaryAuth
                : metrics.TryGetValue("AUTH", out var fallbackAuth)
                    ? fallbackAuth
                    : null;

            if (string.IsNullOrEmpty(authValue))
            {
                throw new InvalidOperationException();
            }

            var authentication = authValue switch
            {
                "M" => 0.45,
                "S" => 0.56,
                "N" => 0.704,
                _ => throw new InvalidOperationException(),
            };

            var confidentiality = metrics["C"] switch
            {
                "N" => 0.0,
                "P" => 0.275,
                "C" => 0.660,
                _ => throw new InvalidOperationException(),
            };

            var integrity = metrics["I"] switch
            {
                "N" => 0.0,
                "P" => 0.275,
                "C" => 0.660,
                _ => throw new InvalidOperationException(),
            };

            var availability = metrics["A"] switch
            {
                "N" => 0.0,
                "P" => 0.275,
                "C" => 0.660,
                _ => throw new InvalidOperationException(),
            };

            var impact = 10.41 * (1 - (1 - confidentiality) * (1 - integrity) * (1 - availability));
            var exploitability = 20 * av * ac * authentication;
            var fImpact = impact == 0 ? 0.0 : 1.176;
            var baseScore = ((0.6 * impact) + (0.4 * exploitability) - 1.5) * fImpact;
            score = Math.Round(Math.Max(baseScore, 0), 1, MidpointRounding.AwayFromZero);
            return true;
        }
        catch (KeyNotFoundException)
        {
            score = 0;
            return false;
        }
        catch (InvalidOperationException)
        {
            score = 0;
            return false;
        }
    }

    private static string DetermineSeverity(double score, CvssVersion version)
    {
        if (score <= 0)
        {
            return "none";
        }

        if (version == CvssVersion.V20)
        {
            if (score < 4.0)
            {
                return "low";
            }

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

            return "high";
        }

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

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

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

        return "critical";
    }

    private static string? NormalizeSeverity(string? severity, CvssVersion version)
    {
        if (string.IsNullOrWhiteSpace(severity))
        {
            return null;
        }

        var normalized = severity.Trim().ToLowerInvariant();
        return normalized switch
        {
            "none" or "informational" or "info" => "none",
            "critical" when version != CvssVersion.V20 => "critical",
            "critical" when version == CvssVersion.V20 => "high",
            "high" => "high",
            "medium" or "moderate" => "medium",
            "low" => "low",
            _ => null,
        };
    }

    private static double RoundUp(double value)
    {
        return Math.Ceiling(value * 10.0) / 10.0;
    }

    private static string ToVersionString(CvssVersion version)
        => version switch
        {
            CvssVersion.V20 => "2.0",
            CvssVersion.V30 => "3.0",
            _ => "3.1",
        };

    private enum CvssVersion
    {
        Unknown = 0,
        V20,
        V30,
        V31,
    }
}

/// <summary>
/// Represents a normalized CVSS metric ready for canonical serialization.
/// </summary>
public readonly record struct CvssNormalizedMetric(string Version, string Vector, double BaseScore, string BaseSeverity)
{
    public CvssMetric ToModel(AdvisoryProvenance provenance)
        => new(Version, Vector, BaseScore, BaseSeverity, provenance);
}
