using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Storage.Mongo.Documents;
using StellaOps.Feedser.Storage.Mongo.Dtos;

namespace StellaOps.Feedser.Source.CertCc.Internal;

internal static class CertCcMapper
{
    private const string AdvisoryPrefix = "certcc";
    private const string VendorNormalizedVersionScheme = "certcc.vendor";

    public static Advisory Map(
        CertCcNoteDto dto,
        DocumentRecord document,
        DtoRecord dtoRecord,
        string sourceName)
    {
        ArgumentNullException.ThrowIfNull(dto);
        ArgumentNullException.ThrowIfNull(document);
        ArgumentNullException.ThrowIfNull(dtoRecord);
        ArgumentException.ThrowIfNullOrEmpty(sourceName);

        var recordedAt = dtoRecord.ValidatedAt.ToUniversalTime();
        var fetchedAt = document.FetchedAt.ToUniversalTime();

        var metadata = dto.Metadata ?? CertCcNoteMetadata.Empty;

        var advisoryKey = BuildAdvisoryKey(metadata);
        var title = string.IsNullOrWhiteSpace(metadata.Title) ? advisoryKey : metadata.Title.Trim();
        var summary = ExtractSummary(metadata);

        var aliases = BuildAliases(dto).ToArray();
        var references = BuildReferences(dto, metadata, sourceName, recordedAt).ToArray();
        var affectedPackages = BuildAffectedPackages(dto, metadata, sourceName, recordedAt).ToArray();

        var provenance = new[]
        {
            new AdvisoryProvenance(sourceName, "document", document.Uri, fetchedAt),
            new AdvisoryProvenance(sourceName, "map", metadata.VuId ?? metadata.IdNumber ?? advisoryKey, recordedAt),
        };

        return new Advisory(
            advisoryKey,
            title,
            summary,
            language: "en",
            metadata.Published?.ToUniversalTime(),
            metadata.Updated?.ToUniversalTime(),
            severity: null,
            exploitKnown: false,
            aliases,
            references,
            affectedPackages,
            cvssMetrics: Array.Empty<CvssMetric>(),
            provenance);
    }

    private static string BuildAdvisoryKey(CertCcNoteMetadata metadata)
    {
        if (metadata is null)
        {
            return $"{AdvisoryPrefix}/{Guid.NewGuid():N}";
        }

        var vuKey = NormalizeVuId(metadata.VuId);
        if (vuKey.Length > 0)
        {
            return $"{AdvisoryPrefix}/{vuKey}";
        }

        var id = SanitizeToken(metadata.IdNumber);
        if (id.Length > 0)
        {
            return $"{AdvisoryPrefix}/vu-{id}";
        }

        return $"{AdvisoryPrefix}/{Guid.NewGuid():N}";
    }

    private static string NormalizeVuId(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return string.Empty;
        }

        var digits = new string(value.Where(char.IsDigit).ToArray());
        if (digits.Length > 0)
        {
            return $"vu-{digits}";
        }

        var sanitized = value.Trim().ToLowerInvariant();
        sanitized = sanitized.Replace("vu#", "vu-", StringComparison.OrdinalIgnoreCase);
        sanitized = sanitized.Replace('#', '-');
        sanitized = sanitized.Replace(' ', '-');

        return SanitizeToken(sanitized);
    }

    private static string SanitizeToken(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return string.Empty;
        }

        var trimmed = value.Trim();
        var filtered = new string(trimmed
            .Select(ch => char.IsLetterOrDigit(ch) || ch is '-' or '_' ? ch : '-')
            .ToArray());

        return filtered.Trim('-').ToLowerInvariant();
    }

    private static readonly Regex HtmlTagRegex = new("<[^>]+>", RegexOptions.Compiled | RegexOptions.CultureInvariant);
    private static readonly Regex WhitespaceRegex = new("[ \t\f\r]+", RegexOptions.Compiled | RegexOptions.CultureInvariant);
    private static readonly Regex ParagraphRegex = new("<\\s*/?\\s*p[^>]*>", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);

    private static string? ExtractSummary(CertCcNoteMetadata metadata)
    {
        if (metadata is null)
        {
            return null;
        }

        var summary = string.IsNullOrWhiteSpace(metadata.Summary) ? metadata.Overview : metadata.Summary;
        if (string.IsNullOrWhiteSpace(summary))
        {
            return null;
        }

        return HtmlToPlainText(summary);
    }

    private static string HtmlToPlainText(string html)
    {
        if (string.IsNullOrWhiteSpace(html))
        {
            return string.Empty;
        }

        var normalized = html
            .Replace("<br>", "\n", StringComparison.OrdinalIgnoreCase)
            .Replace("<br/>", "\n", StringComparison.OrdinalIgnoreCase)
            .Replace("<br />", "\n", StringComparison.OrdinalIgnoreCase)
            .Replace("<li>", "\n", StringComparison.OrdinalIgnoreCase)
            .Replace("</li>", "\n", StringComparison.OrdinalIgnoreCase);

        normalized = ParagraphRegex.Replace(normalized, "\n");

        var withoutTags = HtmlTagRegex.Replace(normalized, " ");
        var decoded = WebUtility.HtmlDecode(withoutTags) ?? string.Empty;
        var collapsedSpaces = WhitespaceRegex.Replace(decoded, " ");
        var collapsedNewlines = Regex.Replace(collapsedSpaces, "\n{2,}", "\n", RegexOptions.Compiled);
        return collapsedNewlines.Trim();
    }

    private static IEnumerable<string> BuildAliases(CertCcNoteDto dto)
    {
        var aliases = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        var metadata = dto.Metadata ?? CertCcNoteMetadata.Empty;

        if (!string.IsNullOrWhiteSpace(metadata.VuId))
        {
            aliases.Add(metadata.VuId.Trim());
        }

        if (!string.IsNullOrWhiteSpace(metadata.IdNumber))
        {
            aliases.Add($"VU#{metadata.IdNumber.Trim()}");
        }

        foreach (var cve in metadata.CveIds ?? Array.Empty<string>())
        {
            if (string.IsNullOrWhiteSpace(cve))
            {
                continue;
            }

            aliases.Add(cve.Trim());
        }

        foreach (var vulnerability in dto.Vulnerabilities ?? Array.Empty<CertCcVulnerabilityDto>())
        {
            if (string.IsNullOrWhiteSpace(vulnerability.CveId))
            {
                continue;
            }

            aliases.Add(vulnerability.CveId.Trim());
        }

        return aliases.OrderBy(static alias => alias, StringComparer.OrdinalIgnoreCase);
    }

    private static IEnumerable<AdvisoryReference> BuildReferences(
        CertCcNoteDto dto,
        CertCcNoteMetadata metadata,
        string sourceName,
        DateTimeOffset recordedAt)
    {
        var references = new List<AdvisoryReference>();
        var canonicalUri = !string.IsNullOrWhiteSpace(metadata.PrimaryUrl)
            ? metadata.PrimaryUrl!
            : (string.IsNullOrWhiteSpace(metadata.IdNumber)
                ? "https://www.kb.cert.org/vuls/"
                : $"https://www.kb.cert.org/vuls/id/{metadata.IdNumber.Trim()}/");

        var provenance = new AdvisoryProvenance(sourceName, "reference", canonicalUri, recordedAt);

        TryAddReference(references, canonicalUri, "advisory", "certcc.note", null, provenance);

        foreach (var url in metadata.PublicUrls ?? Array.Empty<string>())
        {
            TryAddReference(references, url, "reference", "certcc.public", null, provenance);
        }

        foreach (var vendor in dto.Vendors ?? Array.Empty<CertCcVendorDto>())
        {
            foreach (var url in vendor.References ?? Array.Empty<string>())
            {
                TryAddReference(references, url, "reference", "certcc.vendor", vendor.Vendor, provenance);
            }

            var statementText = vendor.Statement ?? string.Empty;
            var patches = CertCcVendorStatementParser.Parse(statementText);
            foreach (var patch in patches)
            {
                if (!string.IsNullOrWhiteSpace(patch.RawLine) && TryFindEmbeddedUrl(patch.RawLine!, out var rawUrl))
                {
                    TryAddReference(references, rawUrl, "reference", "certcc.vendor.statement", vendor.Vendor, provenance);
                }
            }
        }

        foreach (var status in dto.VendorStatuses ?? Array.Empty<CertCcVendorStatusDto>())
        {
            foreach (var url in status.References ?? Array.Empty<string>())
            {
                TryAddReference(references, url, "reference", "certcc.vendor.status", status.Vendor, provenance);
            }

            if (!string.IsNullOrWhiteSpace(status.Statement) && TryFindEmbeddedUrl(status.Statement!, out var embedded))
            {
                TryAddReference(references, embedded, "reference", "certcc.vendor.status", status.Vendor, provenance);
            }
        }

        return references
            .GroupBy(static reference => reference.Url, StringComparer.OrdinalIgnoreCase)
            .Select(static group => group
                .OrderBy(static reference => reference.Kind ?? string.Empty, StringComparer.Ordinal)
                .ThenBy(static reference => reference.SourceTag ?? string.Empty, StringComparer.Ordinal)
                .ThenBy(static reference => reference.Url, StringComparer.OrdinalIgnoreCase)
                .First())
            .OrderBy(static reference => reference.Kind ?? string.Empty, StringComparer.Ordinal)
            .ThenBy(static reference => reference.Url, StringComparer.OrdinalIgnoreCase);
    }

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

        var candidate = url.Trim();
        if (!Uri.TryCreate(candidate, UriKind.Absolute, out var parsed))
        {
            return;
        }

        if (parsed.Scheme != Uri.UriSchemeHttp && parsed.Scheme != Uri.UriSchemeHttps)
        {
            return;
        }

        var normalized = parsed.ToString();

        try
        {
            references.Add(new AdvisoryReference(normalized, kind, sourceTag, summary, provenance));
        }
        catch (ArgumentException)
        {
            // ignore invalid references
        }
    }

    private static bool TryFindEmbeddedUrl(string text, out string? url)
    {
        url = null;
        if (string.IsNullOrWhiteSpace(text))
        {
            return false;
        }

        var tokens = text.Split(new[] { ' ', '\r', '\n', '\t' }, StringSplitOptions.RemoveEmptyEntries);
        foreach (var token in tokens)
        {
            var trimmed = token.Trim().TrimEnd('.', ',', ')', ';', ']', '}');
            if (trimmed.Length == 0)
            {
                continue;
            }

            if (!Uri.TryCreate(trimmed, UriKind.Absolute, out var parsed))
            {
                continue;
            }

            if (parsed.Scheme != Uri.UriSchemeHttp && parsed.Scheme != Uri.UriSchemeHttps)
            {
                continue;
            }

            url = parsed.ToString();
            return true;
        }

        return false;
    }

    private static IEnumerable<AffectedPackage> BuildAffectedPackages(
        CertCcNoteDto dto,
        CertCcNoteMetadata metadata,
        string sourceName,
        DateTimeOffset recordedAt)
    {
        var vendors = dto.Vendors ?? Array.Empty<CertCcVendorDto>();
        var statuses = dto.VendorStatuses ?? Array.Empty<CertCcVendorStatusDto>();

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

        var statusLookup = statuses
            .GroupBy(static status => NormalizeVendorKey(status.Vendor))
            .ToDictionary(static group => group.Key, static group => group.ToArray(), StringComparer.OrdinalIgnoreCase);

        var packages = new List<AffectedPackage>();

        foreach (var vendor in vendors.OrderBy(static v => v.Vendor, StringComparer.OrdinalIgnoreCase))
        {
            var key = NormalizeVendorKey(vendor.Vendor);
            var vendorStatuses = statusLookup.TryGetValue(key, out var value)
                ? value
                : Array.Empty<CertCcVendorStatusDto>();

            if (BuildVendorPackage(vendor, vendorStatuses, sourceName, recordedAt) is { } package)
            {
                packages.Add(package);
            }

            statusLookup.Remove(key);
        }

        foreach (var remaining in statusLookup.Values)
        {
            if (remaining.Length == 0)
            {
                continue;
            }

            var vendorName = remaining[0].Vendor;
            var fallbackVendor = new CertCcVendorDto(
                vendorName,
                ContactDate: null,
                StatementDate: null,
                Updated: remaining
                    .Select(static status => status.DateUpdated)
                    .Where(static update => update.HasValue)
                    .OrderByDescending(static update => update)
                    .FirstOrDefault(),
                Statement: remaining
                    .Select(static status => status.Statement)
                    .FirstOrDefault(static statement => !string.IsNullOrWhiteSpace(statement)),
                Addendum: null,
                References: remaining
                    .SelectMany(static status => status.References ?? Array.Empty<string>())
                    .ToArray());

            if (BuildVendorPackage(fallbackVendor, remaining, sourceName, recordedAt) is { } package)
            {
                packages.Add(package);
            }
        }

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

    private static AffectedPackage? BuildVendorPackage(
        CertCcVendorDto vendor,
        IReadOnlyList<CertCcVendorStatusDto> statuses,
        string sourceName,
        DateTimeOffset recordedAt)
    {
        var vendorName = string.IsNullOrWhiteSpace(vendor.Vendor)
            ? (statuses.FirstOrDefault()?.Vendor?.Trim() ?? string.Empty)
            : vendor.Vendor.Trim();

        if (vendorName.Length == 0)
        {
            return null;
        }

        var packageProvenance = new AdvisoryProvenance(sourceName, "vendor", vendorName, recordedAt);
        var rangeProvenance = new AdvisoryProvenance(sourceName, "vendor-range", vendorName, recordedAt);

        var patches = CertCcVendorStatementParser.Parse(vendor.Statement ?? string.Empty);
        var normalizedVersions = BuildNormalizedVersions(vendorName, patches);
        var vendorStatuses = BuildStatuses(vendorName, statuses, sourceName, recordedAt);
        var primitives = BuildRangePrimitives(vendor, vendorStatuses, patches);

        var range = new AffectedVersionRange(
            rangeKind: "vendor",
            introducedVersion: null,
            fixedVersion: null,
            lastAffectedVersion: null,
            rangeExpression: null,
            provenance: rangeProvenance,
            primitives: primitives);

        return new AffectedPackage(
            AffectedPackageTypes.Vendor,
            vendorName,
            platform: null,
            versionRanges: new[] { range },
            normalizedVersions: normalizedVersions,
            statuses: vendorStatuses,
            provenance: new[] { packageProvenance });
    }

    private static IReadOnlyList<NormalizedVersionRule> BuildNormalizedVersions(
        string vendorName,
        IReadOnlyList<CertCcVendorPatch> patches)
    {
        if (patches.Count == 0)
        {
            return Array.Empty<NormalizedVersionRule>();
        }

        var rules = new List<NormalizedVersionRule>();
        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        foreach (var patch in patches)
        {
            if (string.IsNullOrWhiteSpace(patch.Version))
            {
                continue;
            }

            var version = patch.Version.Trim();
            if (!seen.Add($"{patch.Product}|{version}"))
            {
                continue;
            }

            var notes = string.IsNullOrWhiteSpace(patch.Product)
                ? vendorName
                : $"{vendorName}::{patch.Product.Trim()}";

            rules.Add(new NormalizedVersionRule(
                VendorNormalizedVersionScheme,
                NormalizedVersionRuleTypes.Exact,
                value: version,
                notes: notes));
        }

        return rules.Count == 0 ? Array.Empty<NormalizedVersionRule>() : rules;
    }

    private static IReadOnlyList<AffectedPackageStatus> BuildStatuses(
        string vendorName,
        IReadOnlyList<CertCcVendorStatusDto> statuses,
        string sourceName,
        DateTimeOffset recordedAt)
    {
        if (statuses.Count == 0)
        {
            return Array.Empty<AffectedPackageStatus>();
        }

        var result = new List<AffectedPackageStatus>();
        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        foreach (var status in statuses)
        {
            if (!AffectedPackageStatusCatalog.TryNormalize(status.Status, out var normalized))
            {
                continue;
            }

            var cve = status.CveId?.Trim() ?? string.Empty;
            var key = string.IsNullOrWhiteSpace(cve)
                ? normalized
                : $"{normalized}|{cve}";

            if (!seen.Add(key))
            {
                continue;
            }

            var provenance = new AdvisoryProvenance(
                sourceName,
                "vendor-status",
                string.IsNullOrWhiteSpace(cve) ? vendorName : $"{vendorName}:{cve}",
                recordedAt);

            result.Add(new AffectedPackageStatus(normalized, provenance));
        }

        return result
            .OrderBy(static status => status.Status, StringComparer.Ordinal)
            .ThenBy(static status => status.Provenance.Value ?? string.Empty, StringComparer.Ordinal)
            .ToArray();
    }

    private static RangePrimitives? BuildRangePrimitives(
        CertCcVendorDto vendor,
        IReadOnlyList<AffectedPackageStatus> statuses,
        IReadOnlyList<CertCcVendorPatch> patches)
    {
        var extensions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

        AddVendorExtension(extensions, "certcc.vendor.name", vendor.Vendor);
        AddVendorExtension(extensions, "certcc.vendor.statement.raw", HtmlToPlainText(vendor.Statement ?? string.Empty), 2048);
        AddVendorExtension(extensions, "certcc.vendor.addendum", HtmlToPlainText(vendor.Addendum ?? string.Empty), 1024);
        AddVendorExtension(extensions, "certcc.vendor.contactDate", FormatDate(vendor.ContactDate));
        AddVendorExtension(extensions, "certcc.vendor.statementDate", FormatDate(vendor.StatementDate));
        AddVendorExtension(extensions, "certcc.vendor.updated", FormatDate(vendor.Updated));

        if (vendor.References is { Count: > 0 })
        {
            AddVendorExtension(extensions, "certcc.vendor.references", string.Join(" ", vendor.References));
        }

        if (statuses.Count > 0)
        {
            var serialized = string.Join(";", statuses
                .Select(static status => status.Provenance.Value is { Length: > 0 }
                    ? $"{status.Provenance.Value.Split(':').Last()}={status.Status}"
                    : status.Status));

            AddVendorExtension(extensions, "certcc.vendor.statuses", serialized);
        }

        if (patches.Count > 0)
        {
            var serialized = string.Join(";", patches.Select(static patch =>
            {
                var product = string.IsNullOrWhiteSpace(patch.Product) ? "unknown" : patch.Product.Trim();
                return $"{product}={patch.Version.Trim()}";
            }));

            AddVendorExtension(extensions, "certcc.vendor.patches", serialized, 2048);
        }

        return extensions.Count == 0
            ? null
            : new RangePrimitives(null, null, null, extensions);
    }

    private static void AddVendorExtension(IDictionary<string, string> extensions, 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)
        {
            return;
        }

        extensions[key] = trimmed;
    }

    private static string? FormatDate(DateTimeOffset? value)
        => value?.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture);

    private static string NormalizeVendorKey(string? value)
        => string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim().ToLowerInvariant();
}
