using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Storage.Mongo.Documents;
using StellaOps.Feedser.Storage.Mongo.Dtos;

namespace StellaOps.Feedser.Source.Acsc.Internal;

internal static class AcscMapper
{
    private static readonly Regex CveRegex = new("CVE-\\d{4}-\\d{4,7}", RegexOptions.IgnoreCase | RegexOptions.Compiled);

    public static IReadOnlyList<Advisory> Map(
        AcscFeedDto feed,
        DocumentRecord document,
        DtoRecord dtoRecord,
        string sourceName,
        DateTimeOffset mappedAt)
    {
        ArgumentNullException.ThrowIfNull(feed);
        ArgumentNullException.ThrowIfNull(document);
        ArgumentNullException.ThrowIfNull(dtoRecord);
        ArgumentException.ThrowIfNullOrEmpty(sourceName);

        if (feed.Entries is null || feed.Entries.Count == 0)
        {
            return Array.Empty<Advisory>();
        }

        var advisories = new List<Advisory>(feed.Entries.Count);
        foreach (var entry in feed.Entries)
        {
            if (entry is null)
            {
                continue;
            }

            var advisoryKey = CreateAdvisoryKey(sourceName, feed.FeedSlug, entry);
            var fetchProvenance = new AdvisoryProvenance(
                sourceName,
                "document",
                document.Uri,
                document.FetchedAt.ToUniversalTime(),
                fieldMask: new[] { "summary", "aliases", "references", "affectedPackages" });

            var feedProvenance = new AdvisoryProvenance(
                sourceName,
                "feed",
                feed.FeedSlug ?? string.Empty,
                feed.ParsedAt.ToUniversalTime(),
                fieldMask: new[] { "summary" });

            var mappingProvenance = new AdvisoryProvenance(
                sourceName,
                "mapping",
                entry.EntryId ?? entry.Link ?? advisoryKey,
                mappedAt.ToUniversalTime(),
                fieldMask: new[] { "summary", "aliases", "references", "affectedpackages" });

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

            var aliases = BuildAliases(entry);
            var severity = TryGetSeverity(entry.Fields);
            var references = BuildReferences(entry, sourceName, mappedAt);
            var affectedPackages = BuildAffectedPackages(entry, sourceName, mappedAt);

            var advisory = new Advisory(
                advisoryKey,
                string.IsNullOrWhiteSpace(entry.Title) ? $"ACSC Advisory {entry.EntryId}" : entry.Title,
                string.IsNullOrWhiteSpace(entry.Summary) ? null : entry.Summary,
                language: "en",
                published: entry.Published?.ToUniversalTime() ?? feed.FeedUpdated?.ToUniversalTime() ?? document.FetchedAt.ToUniversalTime(),
                modified: entry.Updated?.ToUniversalTime(),
                severity: severity,
                exploitKnown: false,
                aliases: aliases,
                references: references,
                affectedPackages: affectedPackages,
                cvssMetrics: Array.Empty<CvssMetric>(),
                provenance: provenance);

            advisories.Add(advisory);
        }

        return advisories;
    }

    private static IReadOnlyList<string> BuildAliases(AcscEntryDto entry)
    {
        var aliases = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        if (!string.IsNullOrWhiteSpace(entry.EntryId))
        {
            aliases.Add(entry.EntryId.Trim());
        }

        foreach (var alias in entry.Aliases ?? Array.Empty<string>())
        {
            if (!string.IsNullOrWhiteSpace(alias))
            {
                aliases.Add(alias.Trim());
            }
        }

        foreach (var match in CveRegex.Matches(entry.Summary ?? string.Empty).Cast<Match>())
        {
            var value = match.Value.ToUpperInvariant();
            aliases.Add(value);
        }

        foreach (var match in CveRegex.Matches(entry.ContentText ?? string.Empty).Cast<Match>())
        {
            var value = match.Value.ToUpperInvariant();
            aliases.Add(value);
        }

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

    private static IReadOnlyList<AdvisoryReference> BuildReferences(AcscEntryDto entry, string sourceName, DateTimeOffset recordedAt)
    {
        var references = new List<AdvisoryReference>();
        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        void AddReference(string? url, string? kind, string? sourceTag, string? summary)
        {
            if (string.IsNullOrWhiteSpace(url))
            {
                return;
            }

            if (!Validation.LooksLikeHttpUrl(url))
            {
                return;
            }

            if (!seen.Add(url))
            {
                return;
            }

            references.Add(new AdvisoryReference(
                url,
                kind,
                sourceTag,
                summary,
                new AdvisoryProvenance(sourceName, "reference", url, recordedAt.ToUniversalTime())));
        }

        AddReference(entry.Link, "advisory", entry.FeedSlug, entry.Title);

        foreach (var reference in entry.References ?? Array.Empty<AcscReferenceDto>())
        {
            if (reference is null)
            {
                continue;
            }

            AddReference(reference.Url, "reference", null, reference.Title);
        }

        return references.Count == 0
            ? Array.Empty<AdvisoryReference>()
            : references
                .OrderBy(static reference => reference.Url, StringComparer.OrdinalIgnoreCase)
                .ToArray();
    }

    private static IReadOnlyList<AffectedPackage> BuildAffectedPackages(AcscEntryDto entry, string sourceName, DateTimeOffset recordedAt)
    {
        if (entry.Fields is null || entry.Fields.Count == 0)
        {
            return Array.Empty<AffectedPackage>();
        }

        if (!entry.Fields.TryGetValue("systemsAffected", out var systemsAffected) && !entry.Fields.TryGetValue("productsAffected", out systemsAffected))
        {
            return Array.Empty<AffectedPackage>();
        }

        if (string.IsNullOrWhiteSpace(systemsAffected))
        {
            return Array.Empty<AffectedPackage>();
        }

        var identifiers = systemsAffected
            .Split(new[] { ',', ';', '\n' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
            .Select(static value => value.Trim())
            .Where(static value => !string.IsNullOrWhiteSpace(value))
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToArray();

        if (identifiers.Length == 0)
        {
            return Array.Empty<AffectedPackage>();
        }

        var packages = new List<AffectedPackage>(identifiers.Length);
        foreach (var identifier in identifiers)
        {
            var provenance = new[]
            {
                new AdvisoryProvenance(sourceName, "affected", identifier, recordedAt.ToUniversalTime(), fieldMask: new[] { "affectedpackages" }),
            };

            packages.Add(new AffectedPackage(
                AffectedPackageTypes.Vendor,
                identifier,
                platform: null,
                versionRanges: Array.Empty<AffectedVersionRange>(),
                statuses: Array.Empty<AffectedPackageStatus>(),
                provenance: provenance,
                normalizedVersions: Array.Empty<NormalizedVersionRule>()));
        }

        return packages
            .OrderBy(static package => package.Identifier, StringComparer.OrdinalIgnoreCase)
            .ToArray();
    }

    private static string? TryGetSeverity(IReadOnlyDictionary<string, string> fields)
    {
        if (fields is null || fields.Count == 0)
        {
            return null;
        }

        var keys = new[]
        {
            "severity",
            "riskLevel",
            "threatLevel",
            "impact",
        };

        foreach (var key in keys)
        {
            if (fields.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value))
            {
                return value.Trim();
            }
        }

        return null;
    }

    private static string CreateAdvisoryKey(string sourceName, string? feedSlug, AcscEntryDto entry)
    {
        var slug = string.IsNullOrWhiteSpace(feedSlug) ? "general" : ToSlug(feedSlug);
        var candidate = !string.IsNullOrWhiteSpace(entry.EntryId)
            ? entry.EntryId
            : !string.IsNullOrWhiteSpace(entry.Link)
                ? entry.Link
                : entry.Title;

        var identifier = !string.IsNullOrWhiteSpace(candidate) ? ToSlug(candidate!) : null;
        if (string.IsNullOrEmpty(identifier))
        {
            identifier = CreateHash(entry.Title ?? Guid.NewGuid().ToString());
        }

        return $"{sourceName}/{slug}/{identifier}";
    }

    private static string ToSlug(string value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return "unknown";
        }

        var builder = new StringBuilder(value.Length);
        var previousDash = false;

        foreach (var ch in value)
        {
            if (char.IsLetterOrDigit(ch))
            {
                builder.Append(char.ToLowerInvariant(ch));
                previousDash = false;
            }
            else if (!previousDash)
            {
                builder.Append('-');
                previousDash = true;
            }
        }

        var slug = builder.ToString().Trim('-');
        if (string.IsNullOrEmpty(slug))
        {
            slug = CreateHash(value);
        }

        return slug.Length <= 64 ? slug : slug[..64];
    }

    private static string CreateHash(string value)
    {
        var bytes = Encoding.UTF8.GetBytes(value);
        var hash = SHA256.HashData(bytes);
        return Convert.ToHexString(hash).ToLowerInvariant()[..16];
    }
}
