using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using StellaOps.Feedser.Models;

namespace StellaOps.Feedser.Source.Kev.Internal;

internal static class KevMapper
{
    public static IReadOnlyList<Advisory> Map(
        KevCatalogDto catalog,
        string sourceName,
        Uri feedUri,
        DateTimeOffset fetchedAt,
        DateTimeOffset validatedAt)
    {
        ArgumentNullException.ThrowIfNull(catalog);
        ArgumentNullException.ThrowIfNull(sourceName);
        ArgumentNullException.ThrowIfNull(feedUri);

        var advisories = new List<Advisory>();
        var fetchProvenance = new AdvisoryProvenance(sourceName, "document", feedUri.ToString(), fetchedAt);
        var mappingProvenance = new AdvisoryProvenance(
            sourceName,
            "mapping",
            catalog.CatalogVersion ?? feedUri.ToString(),
            validatedAt);

        if (catalog.Vulnerabilities is null || catalog.Vulnerabilities.Count == 0)
        {
            return advisories;
        }

        foreach (var entry in catalog.Vulnerabilities)
        {
            if (entry is null)
            {
                continue;
            }

            var cveId = Normalize(entry.CveId);
            if (string.IsNullOrEmpty(cveId))
            {
                continue;
            }

            var advisoryKey = $"kev/{cveId.ToLowerInvariant()}";
            var title = Normalize(entry.VulnerabilityName) ?? cveId;
            var summary = Normalize(entry.ShortDescription);
            var published = ParseDate(entry.DateAdded);
            var dueDate = ParseDate(entry.DueDate);

            var aliases = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { cveId };

            var references = BuildReferences(entry, sourceName, mappingProvenance, feedUri, cveId).ToArray();

            var affectedPackages = BuildAffectedPackages(
                entry,
                catalog,
                sourceName,
                mappingProvenance,
                published,
                dueDate).ToArray();

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

            advisories.Add(new Advisory(
                advisoryKey,
                title,
                summary,
                language: "en",
                published,
                modified: catalog.DateReleased?.ToUniversalTime(),
                severity: null,
                exploitKnown: true,
                aliases,
                references,
                affectedPackages,
                cvssMetrics: Array.Empty<CvssMetric>(),
                provenance));
        }

        return advisories
            .OrderBy(static advisory => advisory.AdvisoryKey, StringComparer.Ordinal)
            .ToArray();
    }

    private static IEnumerable<AdvisoryReference> BuildReferences(
        KevVulnerabilityDto entry,
        string sourceName,
        AdvisoryProvenance mappingProvenance,
        Uri feedUri,
        string cveId)
    {
        var references = new List<AdvisoryReference>();
        var provenance = new AdvisoryProvenance(sourceName, "reference", cveId, mappingProvenance.RecordedAt);

        var catalogUrl = BuildCatalogSearchUrl(cveId);
        if (catalogUrl is not null)
        {
            TryAddReference(references, catalogUrl, "advisory", "cisa-kev", provenance);
        }

        TryAddReference(references, feedUri.ToString(), "reference", "cisa-kev-feed", provenance);

        foreach (var url in ExtractUrls(entry.Notes))
        {
            TryAddReference(references, url, "reference", "kev.notes", provenance);
        }

        return references
            .GroupBy(static r => r.Url, StringComparer.OrdinalIgnoreCase)
            .Select(static group => group
                .OrderBy(static r => r.Kind, StringComparer.Ordinal)
                .ThenBy(static r => r.SourceTag, StringComparer.Ordinal)
                .First())
            .OrderBy(static r => r.Kind, StringComparer.Ordinal)
            .ThenBy(static r => r.Url, StringComparer.Ordinal)
            .ToArray();
    }

    private static void TryAddReference(
        ICollection<AdvisoryReference> references,
        string? url,
        string kind,
        string? sourceTag,
        AdvisoryProvenance provenance)
    {
        if (string.IsNullOrWhiteSpace(url))
        {
            return;
        }

        if (!Uri.TryCreate(url, UriKind.Absolute, out var parsed)
            || (parsed.Scheme != Uri.UriSchemeHttp && parsed.Scheme != Uri.UriSchemeHttps))
        {
            return;
        }

        try
        {
            references.Add(new AdvisoryReference(parsed.ToString(), kind, sourceTag, null, provenance));
        }
        catch (ArgumentException)
        {
            // Ignore invalid references while leaving traceability via diagnostics elsewhere.
        }
    }

    private static string? BuildCatalogSearchUrl(string cveId)
    {
        if (string.IsNullOrWhiteSpace(cveId))
        {
            return null;
        }

        var builder = new StringBuilder("https://www.cisa.gov/known-exploited-vulnerabilities-catalog?search=");
        builder.Append(Uri.EscapeDataString(cveId));
        return builder.ToString();
    }

    private static IEnumerable<AffectedPackage> BuildAffectedPackages(
        KevVulnerabilityDto entry,
        KevCatalogDto catalog,
        string sourceName,
        AdvisoryProvenance mappingProvenance,
        DateTimeOffset? published,
        DateTimeOffset? dueDate)
    {
        var identifier = BuildIdentifier(entry) ?? entry.CveId ?? "kev";
        var rangeExtensions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

        void TryAddExtension(string key, string? value, int maxLength = 512)
        {
            if (string.IsNullOrWhiteSpace(value))
            {
                return;
            }

            var trimmed = value.Trim();
            if (trimmed.Length > maxLength)
            {
                trimmed = trimmed[..maxLength].Trim();
            }

            if (trimmed.Length > 0)
            {
                rangeExtensions[key] = trimmed;
            }
        }

        TryAddExtension("kev.vendorProject", entry.VendorProject, 256);
        TryAddExtension("kev.product", entry.Product, 256);
        TryAddExtension("kev.requiredAction", entry.RequiredAction);
        TryAddExtension("kev.knownRansomwareCampaignUse", entry.KnownRansomwareCampaignUse, 64);
        TryAddExtension("kev.notes", entry.Notes);
        TryAddExtension("kev.catalogVersion", catalog.CatalogVersion, 64);

        if (catalog.DateReleased.HasValue)
        {
            TryAddExtension("kev.catalogReleased", catalog.DateReleased.Value.ToString("O", CultureInfo.InvariantCulture));
        }

        if (published.HasValue)
        {
            TryAddExtension("kev.dateAdded", published.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
        }

        if (dueDate.HasValue)
        {
            TryAddExtension("kev.dueDate", dueDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
        }

        if (entry.Cwes is { Count: > 0 })
        {
            TryAddExtension("kev.cwe", string.Join(",", entry.Cwes.Where(static cwe => !string.IsNullOrWhiteSpace(cwe)).OrderBy(static cwe => cwe, StringComparer.Ordinal)));
        }

        if (rangeExtensions.Count == 0)
        {
            return Array.Empty<AffectedPackage>();
        }

        var rangeProvenance = new AdvisoryProvenance(sourceName, "kev-range", identifier, mappingProvenance.RecordedAt);
        var range = new AffectedVersionRange(
            rangeKind: AffectedPackageTypes.Vendor,
            introducedVersion: null,
            fixedVersion: null,
            lastAffectedVersion: null,
            rangeExpression: null,
            provenance: rangeProvenance,
            primitives: new RangePrimitives(null, null, null, rangeExtensions));

        var normalizedVersions = BuildNormalizedVersions(identifier, catalog, published, dueDate);

        var affectedPackage = new AffectedPackage(
            AffectedPackageTypes.Vendor,
            identifier,
            platform: null,
            versionRanges: new[] { range },
            statuses: Array.Empty<AffectedPackageStatus>(),
            provenance: new[] { mappingProvenance },
            normalizedVersions: normalizedVersions);

        return new[] { affectedPackage };
    }

    private static string? BuildIdentifier(KevVulnerabilityDto entry)
    {
        var vendor = Normalize(entry.VendorProject);
        var product = Normalize(entry.Product);

        if (!string.IsNullOrEmpty(vendor) && !string.IsNullOrEmpty(product))
        {
            return $"{vendor}::{product}";
        }

        return vendor ?? product;
    }

    private static IEnumerable<string> ExtractUrls(string? notes)
    {
        if (string.IsNullOrWhiteSpace(notes))
        {
            return Array.Empty<string>();
        }

        var tokens = notes.Split(new[] { ';', ',', ' ', '\r', '\n', '\t' }, StringSplitOptions.RemoveEmptyEntries);
        var results = new List<string>();

        foreach (var token in tokens)
        {
            var trimmed = token.Trim().TrimEnd('.', ')', ';', ',');
            if (trimmed.Length == 0)
            {
                continue;
            }

            if (Uri.TryCreate(trimmed, UriKind.Absolute, out var uri)
                && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps))
            {
                results.Add(uri.ToString());
            }
        }

        return results.Count == 0
            ? Array.Empty<string>()
            : results.Distinct(StringComparer.OrdinalIgnoreCase).OrderBy(static value => value, StringComparer.Ordinal).ToArray();
    }

    private static string? Normalize(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return null;
        }

        var trimmed = value.Trim();
        return trimmed.Length == 0 ? null : trimmed;
    }

    private static DateTimeOffset? ParseDate(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return null;
        }

        if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var parsed))
        {
            return parsed.ToUniversalTime();
        }

        if (DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date))
        {
            return new DateTimeOffset(DateTime.SpecifyKind(date, DateTimeKind.Utc));
        }

        return null;
    }

    private static IReadOnlyList<NormalizedVersionRule> BuildNormalizedVersions(
        string identifier,
        KevCatalogDto catalog,
        DateTimeOffset? published,
        DateTimeOffset? dueDate)
    {
        var rules = new List<NormalizedVersionRule>();
        var notes = Validation.TrimToNull(identifier);

        if (!string.IsNullOrWhiteSpace(catalog.CatalogVersion))
        {
            rules.Add(new NormalizedVersionRule(
                scheme: "kev.catalog",
                type: NormalizedVersionRuleTypes.Exact,
                value: catalog.CatalogVersion.Trim(),
                notes: notes));
        }

        if (published.HasValue)
        {
            rules.Add(new NormalizedVersionRule(
                scheme: "kev.date-added",
                type: NormalizedVersionRuleTypes.Exact,
                value: published.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
                notes: notes));
        }

        if (dueDate.HasValue)
        {
            rules.Add(new NormalizedVersionRule(
                scheme: "kev.due-date",
                type: NormalizedVersionRuleTypes.LessThanOrEqual,
                max: dueDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
                maxInclusive: true,
                notes: notes));
        }

        return rules.Count == 0
            ? Array.Empty<NormalizedVersionRule>()
            : rules
                .OrderBy(static rule => rule.Scheme, StringComparer.Ordinal)
                .ThenBy(static rule => rule.Type, StringComparer.Ordinal)
                .ThenBy(static rule => rule.Value ?? rule.Max ?? string.Empty, StringComparer.Ordinal)
                .ToArray();
    }
}
