using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Normalization.Identifiers;

namespace StellaOps.Feedser.Exporter.Json;

/// <summary>
/// Path resolver approximating the directory layout used by aquasecurity/vuln-list.
/// Handles common vendor, distro, and ecosystem shapes with deterministic fallbacks.
/// </summary>
public sealed class VulnListJsonExportPathResolver : IJsonExportPathResolver
{
    private static readonly Regex CvePattern = new("^CVE-(?<year>\\d{4})-(?<id>\\d{4,})$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
    private static readonly Regex GhsaPattern = new("^GHSA-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
    private static readonly Regex UsnPattern = new("^USN-(?<id>\\d+-\\d+)(?<suffix>[a-z])?$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
    private static readonly Regex DebianPattern = new("^(?<prefix>DLA|DSA|ELA)-(?<id>\\d+-\\d+)$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
    private static readonly Regex RedHatPattern = new("^RH(?<type>SA|BA|EA)-(?<rest>[0-9:.-]+)$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
    private static readonly Regex AmazonPattern = new("^ALAS(?<channel>2|2022|2023)?-(?<rest>[0-9A-Za-z:._-]+)$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
    private static readonly Regex OraclePattern = new("^(?<kind>ELSA|ELBA|ELSA-OCI|ELBA-OCI)-(?<rest>[0-9A-Za-z:._-]+)$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
    private static readonly Regex PhotonPattern = new("^PHSA-(?<rest>[0-9A-Za-z:._-]+)$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
    private static readonly Regex RockyPattern = new("^RLSA-(?<rest>[0-9A-Za-z:._-]+)$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
    private static readonly Regex SusePattern = new("^SUSE-(?<kind>SU|RU|OU|SB)-(?<rest>[0-9A-Za-z:._-]+)$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);

    private static readonly Dictionary<string, string[]> SourceDirectoryMap = new(StringComparer.OrdinalIgnoreCase)
    {
        ["nvd"] = new[] { "nvd" },
        ["ghsa"] = new[] { "ghsa" },
        ["github"] = new[] { "ghsa" },
        ["osv"] = new[] { "osv" },
        ["redhat"] = new[] { "redhat", "oval" },
        ["ubuntu"] = new[] { "ubuntu" },
        ["debian"] = new[] { "debian" },
        ["oracle"] = new[] { "oracle" },
        ["photon"] = new[] { "photon" },
        ["rocky"] = new[] { "rocky" },
        ["suse"] = new[] { "suse" },
        ["amazon"] = new[] { "amazon" },
        ["aws"] = new[] { "amazon" },
        ["alpine"] = new[] { "alpine" },
        ["wolfi"] = new[] { "wolfi" },
        ["chainguard"] = new[] { "chainguard" },
        ["cert-fr"] = new[] { "cert", "fr" },
        ["cert-in"] = new[] { "cert", "in" },
        ["cert-cc"] = new[] { "cert", "cc" },
        ["cert-bund"] = new[] { "cert", "bund" },
        ["acsc"] = new[] { "cert", "au" },
        ["cisa"] = new[] { "ics", "cisa" },
        ["ics-cisa"] = new[] { "ics", "cisa" },
        ["ics-kaspersky"] = new[] { "ics", "kaspersky" },
        ["kaspersky"] = new[] { "ics", "kaspersky" },
    };

    private static readonly Dictionary<string, string> GhsaEcosystemMap = new(StringComparer.OrdinalIgnoreCase)
    {
        ["go"] = "go",
        ["golang"] = "go",
        ["npm"] = "npm",
        ["maven"] = "maven",
        ["pypi"] = "pip",
        ["pip"] = "pip",
        ["nuget"] = "nuget",
        ["composer"] = "composer",
        ["packagist"] = "composer",
        ["rubygems"] = "rubygems",
        ["gem"] = "rubygems",
        ["swift"] = "swift",
        ["cargo"] = "cargo",
        ["hex"] = "hex",
        ["pub"] = "pub",
        ["github"] = "github",
        ["docker"] = "container",
    };

    public string GetRelativePath(Advisory advisory)
    {
        if (advisory is null)
        {
            throw new ArgumentNullException(nameof(advisory));
        }

        var identifier = SelectPreferredIdentifier(advisory);
        if (identifier.Length == 0)
        {
            throw new InvalidOperationException("Unable to derive identifier for advisory.");
        }

        var layout = ResolveLayout(advisory, identifier);
        var segments = new string[layout.Segments.Length + 1];
        for (var i = 0; i < layout.Segments.Length; i++)
        {
            segments[i] = layout.Segments[i];
        }
        segments[^1] = layout.FileName;
        return Path.Combine(segments);
    }

    private static Layout ResolveLayout(Advisory advisory, string identifier)
    {
        if (TryResolveCve(identifier, out var layout))
        {
            return layout;
        }

        if (TryResolveGhsa(advisory, identifier, out layout))
        {
            return layout;
        }

        if (TryResolveUsn(identifier, out layout) ||
            TryResolveDebian(identifier, out layout) ||
            TryResolveRedHat(identifier, out layout) ||
            TryResolveAmazon(identifier, out layout) ||
            TryResolveOracle(identifier, out layout) ||
            TryResolvePhoton(identifier, out layout) ||
            TryResolveRocky(identifier, out layout) ||
            TryResolveSuse(identifier, out layout))
        {
            return layout;
        }

        if (TryResolveByProvenance(advisory, identifier, out layout))
        {
            return layout;
        }

        return new Layout(new[] { "misc" }, CreateFileName(identifier));
    }

    private static bool TryResolveCve(string identifier, out Layout layout)
    {
        var match = CvePattern.Match(identifier);
        if (!match.Success)
        {
            layout = default;
            return false;
        }

        var year = match.Groups["year"].Value;
        layout = new Layout(new[] { "nvd", year }, CreateFileName(identifier, uppercase: true));
        return true;
    }

    private static bool TryResolveGhsa(Advisory advisory, string identifier, out Layout layout)
    {
        if (!GhsaPattern.IsMatch(identifier))
        {
            layout = default;
            return false;
        }

        if (TryGetGhsaPackage(advisory, out var ecosystem, out var packagePath))
        {
            layout = new Layout(new[] { "ghsa", ecosystem, packagePath }, CreateFileName(identifier, uppercase: true));
            return true;
        }

        layout = new Layout(new[] { "github", "advisories" }, CreateFileName(identifier, uppercase: true));
        return true;
    }

    private static bool TryResolveUsn(string identifier, out Layout layout)
    {
        if (!UsnPattern.IsMatch(identifier))
        {
            layout = default;
            return false;
    }

        layout = new Layout(new[] { "ubuntu" }, CreateFileName(identifier, uppercase: true));
        return true;
    }

    private static bool TryResolveDebian(string identifier, out Layout layout)
    {
        var match = DebianPattern.Match(identifier);
        if (!match.Success)
        {
            layout = default;
            return false;
        }

        layout = new Layout(new[] { "debian" }, CreateFileName(identifier, uppercase: true));
        return true;
    }

    private static bool TryResolveRedHat(string identifier, out Layout layout)
    {
        if (!RedHatPattern.IsMatch(identifier))
        {
            layout = default;
            return false;
        }

        layout = new Layout(new[] { "redhat", "oval" }, CreateFileName(identifier, uppercase: true));
        return true;
    }

    private static bool TryResolveAmazon(string identifier, out Layout layout)
    {
        var match = AmazonPattern.Match(identifier);
        if (!match.Success)
        {
            layout = default;
            return false;
        }

        var channel = match.Groups["channel"].Value;
        var subdirectory = channel switch
        {
            "2" => "2",
            "2023" => "2023",
            "2022" => "2022",
            _ => "1",
        };

        layout = new Layout(new[] { "amazon", subdirectory }, CreateFileName(identifier, uppercase: true));
        return true;
    }

    private static bool TryResolveOracle(string identifier, out Layout layout)
    {
        if (!OraclePattern.IsMatch(identifier))
        {
            layout = default;
            return false;
        }

        layout = new Layout(new[] { "oracle", "linux" }, CreateFileName(identifier, uppercase: true));
        return true;
    }

    private static bool TryResolvePhoton(string identifier, out Layout layout)
    {
        if (!PhotonPattern.IsMatch(identifier))
        {
            layout = default;
            return false;
        }

        layout = new Layout(new[] { "photon" }, CreateFileName(identifier, uppercase: true));
        return true;
    }

    private static bool TryResolveRocky(string identifier, out Layout layout)
    {
        if (!RockyPattern.IsMatch(identifier))
        {
            layout = default;
            return false;
        }

        layout = new Layout(new[] { "rocky" }, CreateFileName(identifier, uppercase: true));
        return true;
    }

    private static bool TryResolveSuse(string identifier, out Layout layout)
    {
        if (!SusePattern.IsMatch(identifier))
        {
            layout = default;
            return false;
        }

        layout = new Layout(new[] { "suse" }, CreateFileName(identifier, uppercase: true));
        return true;
    }

    private static bool TryResolveByProvenance(Advisory advisory, string identifier, out Layout layout)
    {
        foreach (var source in EnumerateDistinctProvenanceSources(advisory))
        {
            if (SourceDirectoryMap.TryGetValue(source, out var segments))
            {
                layout = new Layout(segments, CreateFileName(identifier));
                return true;
            }
        }

        layout = default;
        return false;
    }

    private static bool TryGetGhsaPackage(Advisory advisory, out string ecosystem, out string packagePath)
    {
        foreach (var package in advisory.AffectedPackages)
        {
            if (!TryParsePackageUrl(package.Identifier, out var type, out var encodedPath))
            {
                continue;
            }

            if (GhsaEcosystemMap.TryGetValue(type, out var mapped))
            {
                ecosystem = mapped;
            }
            else
            {
                ecosystem = type.ToLowerInvariant();
            }

            packagePath = encodedPath;
            return true;
        }

        ecosystem = "advisories";
        packagePath = "_";
        return false;
    }

    private static bool TryParsePackageUrl(string identifier, out string type, out string encodedPath)
    {
        type = string.Empty;
        encodedPath = string.Empty;

        if (!IdentifierNormalizer.TryNormalizePackageUrl(identifier, out _, out var packageUrl))
        {
            return false;
        }

        var segments = packageUrl!.NamespaceSegments.IsDefaultOrEmpty
            ? new[] { packageUrl.Name }
            : packageUrl.NamespaceSegments.Append(packageUrl.Name).ToArray();

        type = packageUrl.Type;
        encodedPath = string.Join("%2F", segments);
        return true;
    }

    private static string CreateFileName(string identifier, bool uppercase = false)
    {
        var candidate = uppercase ? identifier.ToUpperInvariant() : identifier;
        return $"{SanitizeFileName(candidate)}.json";
    }

    private static IEnumerable<string> EnumerateDistinctProvenanceSources(Advisory advisory)
    {
        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        foreach (var source in advisory.Provenance)
        {
            if (TryAddSource(source.Source))
            {
                yield return source.Source;
            }
        }

        foreach (var reference in advisory.References)
        {
            if (TryAddSource(reference.Provenance.Source))
            {
                yield return reference.Provenance.Source;
            }
        }

        foreach (var package in advisory.AffectedPackages)
        {
            foreach (var source in package.Provenance)
            {
                if (TryAddSource(source.Source))
                {
                    yield return source.Source;
                }
            }

            foreach (var range in package.VersionRanges)
            {
                if (TryAddSource(range.Provenance.Source))
                {
                    yield return range.Provenance.Source;
                }
            }
        }

        foreach (var metric in advisory.CvssMetrics)
        {
            if (TryAddSource(metric.Provenance.Source))
            {
                yield return metric.Provenance.Source;
            }
        }

        bool TryAddSource(string? value)
        {
            if (string.IsNullOrWhiteSpace(value))
            {
                return false;
            }

            return seen.Add(value);
        }
    }

    private static string SelectPreferredIdentifier(Advisory advisory)
    {
        if (TrySelectIdentifier(advisory.AdvisoryKey, out var preferred))
        {
            return preferred;
        }

        foreach (var alias in advisory.Aliases)
        {
            if (TrySelectIdentifier(alias, out preferred))
            {
                return preferred;
            }
        }

        return advisory.AdvisoryKey.Trim();
    }

    private static bool TrySelectIdentifier(string value, out string identifier)
    {
        identifier = string.Empty;
        if (string.IsNullOrWhiteSpace(value))
        {
            return false;
        }

        var trimmed = value.Trim();
        if (CvePattern.IsMatch(trimmed) || GhsaPattern.IsMatch(trimmed))
        {
            identifier = trimmed;
            return true;
        }

        identifier = trimmed;
        return false;
    }

    private static string SanitizeFileName(string name)
    {
        var invalid = Path.GetInvalidFileNameChars();
        Span<char> buffer = stackalloc char[name.Length];
        var count = 0;
        foreach (var ch in name)
        {
            if (ch == '/' || ch == '\\' || Array.IndexOf(invalid, ch) >= 0)
            {
                buffer[count++] = '_';
            }
            else
            {
                buffer[count++] = ch;
            }
        }

        var sanitized = new string(buffer[..count]).Trim();
        return string.IsNullOrEmpty(sanitized) ? "advisory" : sanitized;
    }

    private readonly record struct Layout(string[] Segments, string FileName);
}
