using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Text.Json;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Normalization.Cvss;
using StellaOps.Feedser.Normalization.Identifiers;
using StellaOps.Feedser.Normalization.Text;
using StellaOps.Feedser.Source.Common;
using StellaOps.Feedser.Storage.Mongo.Documents;
using StellaOps.Feedser.Storage.Mongo.Dtos;

namespace StellaOps.Feedser.Source.Osv.Internal;

internal static class OsvMapper
{
    private static readonly string[] SeverityOrder = { "none", "low", "medium", "high", "critical" };

    private static readonly IReadOnlyDictionary<string, Func<string, string>> PackageUrlBuilders =
        new Dictionary<string, Func<string, string>>(StringComparer.OrdinalIgnoreCase)
        {
            ["pypi"] = static name => $"pkg:pypi/{NormalizePyPiName(name)}",
            ["python"] = static name => $"pkg:pypi/{NormalizePyPiName(name)}",
            ["maven"] = static name => $"pkg:maven/{NormalizeMavenName(name)}",
            ["go"] = static name => $"pkg:golang/{NormalizeGoName(name)}",
            ["golang"] = static name => $"pkg:golang/{NormalizeGoName(name)}",
            ["crates"] = static name => $"pkg:cargo/{NormalizeCratesName(name)}",
            ["crates.io"] = static name => $"pkg:cargo/{NormalizeCratesName(name)}",
            ["cargo"] = static name => $"pkg:cargo/{NormalizeCratesName(name)}",
            ["nuget"] = static name => $"pkg:nuget/{NormalizeNugetName(name)}",
            ["rubygems"] = static name => $"pkg:gem/{NormalizeRubyName(name)}",
            ["gem"] = static name => $"pkg:gem/{NormalizeRubyName(name)}",
            ["packagist"] = static name => $"pkg:composer/{NormalizeComposerName(name)}",
            ["composer"] = static name => $"pkg:composer/{NormalizeComposerName(name)}",
            ["hex"] = static name => $"pkg:hex/{NormalizeHexName(name)}",
            ["hex.pm"] = static name => $"pkg:hex/{NormalizeHexName(name)}",
        };

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

        var recordedAt = dtoRecord.ValidatedAt;
        var fetchProvenance = new AdvisoryProvenance(
            OsvConnectorPlugin.SourceName,
            "document",
            document.Uri,
            document.FetchedAt,
            new[] { ProvenanceFieldMasks.Advisory });
        var mappingProvenance = new AdvisoryProvenance(
            OsvConnectorPlugin.SourceName,
            "mapping",
            dto.Id,
            recordedAt,
            new[] { ProvenanceFieldMasks.Advisory });

        var aliases = BuildAliases(dto);
        var references = BuildReferences(dto, recordedAt);
        var credits = BuildCredits(dto, recordedAt);
        var affectedPackages = BuildAffectedPackages(dto, ecosystem, recordedAt);
        var cvssMetrics = BuildCvssMetrics(dto, recordedAt, out var severity);
        var databaseSpecificSeverity = ExtractDatabaseSpecificSeverity(dto.DatabaseSpecific);
        if (severity is null)
        {
            severity = databaseSpecificSeverity;
        }

        var weaknesses = BuildWeaknesses(dto, recordedAt);
        var canonicalMetricId = cvssMetrics.Count > 0
            ? $"{cvssMetrics[0].Version}|{cvssMetrics[0].Vector}"
            : null;

        if (canonicalMetricId is null && !string.IsNullOrWhiteSpace(severity))
        {
            canonicalMetricId = BuildSeverityCanonicalMetricId(severity);
        }

        var normalizedDescription = DescriptionNormalizer.Normalize(new[]
        {
            new LocalizedText(dto.Details, "en"),
            new LocalizedText(dto.Summary, "en"),
        });

        var title = string.IsNullOrWhiteSpace(dto.Summary) ? dto.Id : dto.Summary!.Trim();
        var summary = string.IsNullOrWhiteSpace(normalizedDescription.Text) ? dto.Summary : normalizedDescription.Text;
        var language = string.IsNullOrWhiteSpace(normalizedDescription.Language) ? null : normalizedDescription.Language;
        var descriptionText = Validation.TrimToNull(dto.Details);
        if (string.IsNullOrWhiteSpace(summary) && !string.IsNullOrWhiteSpace(descriptionText))
        {
            summary = descriptionText;
        }

        return new Advisory(
            dto.Id,
            title,
            summary,
            language,
            dto.Published?.ToUniversalTime(),
            dto.Modified?.ToUniversalTime(),
            severity,
            exploitKnown: false,
            aliases,
            credits,
            references,
            affectedPackages,
            cvssMetrics,
            new[] { fetchProvenance, mappingProvenance },
            descriptionText,
            weaknesses,
            canonicalMetricId);
    }

    private static string BuildSeverityCanonicalMetricId(string severity)
        => $"{OsvConnectorPlugin.SourceName}:severity/{severity}";

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

        if (dto.Aliases is not null)
        {
            foreach (var alias in dto.Aliases)
            {
                if (!string.IsNullOrWhiteSpace(alias))
                {
                    aliases.Add(alias.Trim());
                }
            }
        }

        if (dto.Related is not null)
        {
            foreach (var related in dto.Related)
            {
                if (!string.IsNullOrWhiteSpace(related))
                {
                    aliases.Add(related.Trim());
                }
            }
        }

        return aliases;
    }

    private static IReadOnlyList<AdvisoryReference> BuildReferences(OsvVulnerabilityDto dto, DateTimeOffset recordedAt)
    {
        if (dto.References is null || dto.References.Count == 0)
        {
            return Array.Empty<AdvisoryReference>();
        }

        var references = new List<AdvisoryReference>(dto.References.Count);
        foreach (var reference in dto.References)
        {
            if (string.IsNullOrWhiteSpace(reference.Url))
            {
                continue;
            }

            var kind = NormalizeReferenceKind(reference.Type);
            var provenance = new AdvisoryProvenance(
                OsvConnectorPlugin.SourceName,
                "reference",
                reference.Url,
                recordedAt,
                new[] { ProvenanceFieldMasks.References });

            try
            {
                references.Add(new AdvisoryReference(reference.Url, kind, reference.Type, null, provenance));
            }
            catch (ArgumentException)
            {
                // ignore invalid URLs
            }
        }

        if (references.Count <= 1)
        {
            return references;
        }

        references.Sort(CompareReferences);

        var deduped = new List<AdvisoryReference>(references.Count);
        string? lastUrl = null;
        foreach (var reference in references)
        {
            if (lastUrl is not null && string.Equals(lastUrl, reference.Url, StringComparison.OrdinalIgnoreCase))
            {
                continue;
            }

            deduped.Add(reference);
            lastUrl = reference.Url;
        }

        return deduped;
    }

    private static string? NormalizeReferenceKind(string? type)
    {
        if (string.IsNullOrWhiteSpace(type))
        {
            return null;
        }

        return type.Trim().ToLowerInvariant() switch
        {
            "advisory" => "advisory",
            "exploit" => "exploit",
            "fix" or "patch" => "patch",
            "report" => "report",
            "article" => "article",
            _ => null,
        };
    }

    private static IReadOnlyList<AffectedPackage> BuildAffectedPackages(OsvVulnerabilityDto dto, string ecosystem, DateTimeOffset recordedAt)
    {
        if (dto.Affected is null || dto.Affected.Count == 0)
        {
            return Array.Empty<AffectedPackage>();
        }

        var packages = new List<AffectedPackage>(dto.Affected.Count);
        foreach (var affected in dto.Affected)
        {
            if (affected.Package is null)
            {
                continue;
            }

            var identifier = DetermineIdentifier(affected.Package, ecosystem);
            if (identifier is null)
            {
                continue;
            }

            var provenance = new[]
            {
                new AdvisoryProvenance(
                    OsvConnectorPlugin.SourceName,
                    "affected",
                    identifier,
                    recordedAt,
                    new[] { ProvenanceFieldMasks.AffectedPackages }),
            };

            var ranges = BuildVersionRanges(affected, recordedAt, identifier);
            var noteEcosystem = DetermineNormalizedNoteEcosystem(affected, ecosystem);
            var normalizedVersions = BuildNormalizedVersions(dto.Id, noteEcosystem, identifier, ranges);

            packages.Add(new AffectedPackage(
                AffectedPackageTypes.SemVer,
                identifier,
                platform: affected.Package.Ecosystem,
                versionRanges: ranges,
                statuses: Array.Empty<AffectedPackageStatus>(),
                provenance: provenance,
                normalizedVersions: normalizedVersions));
        }

        return packages;
    }

    private static IReadOnlyList<AdvisoryCredit> BuildCredits(OsvVulnerabilityDto dto, DateTimeOffset recordedAt)
    {
        if (dto.Credits is null || dto.Credits.Count == 0)
        {
            return Array.Empty<AdvisoryCredit>();
        }

        var credits = new List<AdvisoryCredit>(dto.Credits.Count);
        foreach (var credit in dto.Credits)
        {
            var displayName = Validation.TrimToNull(credit.Name);
            if (displayName is null)
            {
                continue;
            }

            var contacts = credit.Contact is null
                ? Array.Empty<string>()
                : credit.Contact
                    .Where(static contact => !string.IsNullOrWhiteSpace(contact))
                    .Select(static contact => contact.Trim())
                    .Where(static contact => contact.Length > 0)
                    .ToArray();

            var provenance = new AdvisoryProvenance(
                OsvConnectorPlugin.SourceName,
                "credit",
                displayName,
                recordedAt,
                new[] { ProvenanceFieldMasks.Credits });

            credits.Add(new AdvisoryCredit(displayName, credit.Type, contacts, provenance));
        }

        return credits.Count == 0 ? Array.Empty<AdvisoryCredit>() : credits;
    }

    private static IReadOnlyList<AffectedVersionRange> BuildVersionRanges(OsvAffectedPackageDto affected, DateTimeOffset recordedAt, string identifier)
    {
        if (affected.Ranges is null || affected.Ranges.Count == 0)
        {
            return Array.Empty<AffectedVersionRange>();
        }

        var ranges = new List<AffectedVersionRange>();
        foreach (var range in affected.Ranges)
        {
            if (!"semver".Equals(range.Type, StringComparison.OrdinalIgnoreCase)
                && !"ecosystem".Equals(range.Type, StringComparison.OrdinalIgnoreCase))
            {
                continue;
            }

            var provenance = new AdvisoryProvenance(
                OsvConnectorPlugin.SourceName,
                "range",
                identifier,
                recordedAt,
                new[] { ProvenanceFieldMasks.VersionRanges });
            if (range.Events is null || range.Events.Count == 0)
            {
                continue;
            }

            string? introduced = null;
            string? lastAffected = null;

            foreach (var evt in range.Events)
            {
                if (!string.IsNullOrWhiteSpace(evt.Introduced))
                {
                    introduced = evt.Introduced.Trim();
                    lastAffected = null;
                }

                if (!string.IsNullOrWhiteSpace(evt.LastAffected))
                {
                    lastAffected = evt.LastAffected.Trim();
                }

                if (!string.IsNullOrWhiteSpace(evt.Fixed))
                {
                    var fixedVersion = evt.Fixed.Trim();
                    ranges.Add(new AffectedVersionRange(
                        "semver",
                        introduced,
                        fixedVersion,
                        lastAffected,
                        rangeExpression: null,
                        provenance: provenance,
                        primitives: BuildSemVerPrimitives(introduced, fixedVersion, lastAffected)));
                    introduced = null;
                    lastAffected = null;
                }

                if (!string.IsNullOrWhiteSpace(evt.Limit))
                {
                    lastAffected = evt.Limit.Trim();
                }
            }

            if (introduced is not null || lastAffected is not null)
            {
                ranges.Add(new AffectedVersionRange(
                    "semver",
                    introduced,
                    fixedVersion: null,
                    lastAffected,
                    rangeExpression: null,
                    provenance: provenance,
                    primitives: BuildSemVerPrimitives(introduced, null, lastAffected)));
            }
        }

        return ranges.Count == 0
            ? Array.Empty<AffectedVersionRange>()
            : ranges;
    }

    private static IReadOnlyList<NormalizedVersionRule> BuildNormalizedVersions(
        string? advisoryId,
        string ecosystem,
        string identifier,
        IReadOnlyList<AffectedVersionRange> ranges)
    {
        if (ranges.Count == 0)
        {
            return Array.Empty<NormalizedVersionRule>();
        }

        var note = BuildNormalizedVersionNote(ecosystem, advisoryId, identifier);
        var normalized = new List<NormalizedVersionRule>(ranges.Count);
        foreach (var range in ranges)
        {
            var rule = range.ToNormalizedVersionRule(note);
            if (rule is not null)
            {
                normalized.Add(rule);
            }
        }

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

    private static string? BuildNormalizedVersionNote(string ecosystem, string? advisoryId, string identifier)
    {
        var segments = new List<string>(4) { "osv" };

        var trimmedEcosystem = Validation.TrimToNull(ecosystem);
        if (trimmedEcosystem is not null)
        {
            segments.Add(trimmedEcosystem);
        }

        var trimmedAdvisory = Validation.TrimToNull(advisoryId);
        if (trimmedAdvisory is not null)
        {
            segments.Add(trimmedAdvisory);
        }

        segments.Add(Validation.EnsureNotNullOrWhiteSpace(identifier, nameof(identifier)));
        return string.Join(':', segments);
    }

    private static string DetermineNormalizedNoteEcosystem(OsvAffectedPackageDto affected, string defaultEcosystem)
    {
        if (affected.Package is not null && !string.IsNullOrWhiteSpace(affected.Package.Ecosystem))
        {
            return affected.Package.Ecosystem.Trim();
        }

        return Validation.TrimToNull(defaultEcosystem) ?? "osv";
    }

    private static RangePrimitives BuildSemVerPrimitives(string? introduced, string? fixedVersion, string? lastAffected)
    {
        var semver = new SemVerPrimitive(
            introduced,
            IntroducedInclusive: true,
            fixedVersion,
            FixedInclusive: false,
            lastAffected,
            LastAffectedInclusive: true,
            ConstraintExpression: null);

        return new RangePrimitives(semver, null, null, null);
    }

    private static string? DetermineIdentifier(OsvPackageDto package, string ecosystem)
    {
        if (IdentifierNormalizer.TryNormalizePackageUrl(package.Purl, out var normalized))
        {
            return normalized;
        }

        var name = Validation.TrimToNull(package.Name);
        if (name is null)
        {
            return null;
        }

        var ecosystemHint = Validation.TrimToNull(package.Ecosystem) ?? Validation.TrimToNull(ecosystem);
        if (ecosystemHint is not null
            && string.Equals(ecosystemHint, "npm", StringComparison.OrdinalIgnoreCase)
            && TryBuildNpmPackageUrl(name, out normalized))
        {
            return normalized;
        }

        if (TryBuildCanonicalPackageUrl(ecosystemHint, name, out normalized))
        {
            return normalized;
        }

        var fallbackEcosystem = ecosystemHint ?? Validation.TrimToNull(ecosystem) ?? "osv";
        return $"{fallbackEcosystem}:{name}";
    }

    private static IReadOnlyList<AdvisoryWeakness> BuildWeaknesses(OsvVulnerabilityDto dto, DateTimeOffset recordedAt)
    {
        if (dto.DatabaseSpecific.ValueKind != JsonValueKind.Object ||
            !dto.DatabaseSpecific.TryGetProperty("cwe_ids", out var cweIds) ||
            cweIds.ValueKind != JsonValueKind.Array)
        {
            return Array.Empty<AdvisoryWeakness>();
        }

        var list = new List<AdvisoryWeakness>(cweIds.GetArrayLength());
        foreach (var element in cweIds.EnumerateArray())
        {
            if (element.ValueKind != JsonValueKind.String)
            {
                continue;
            }

            var raw = element.GetString();
            if (string.IsNullOrWhiteSpace(raw))
            {
                continue;
            }

            var identifier = raw.Trim().ToUpperInvariant();
            var provenance = new AdvisoryProvenance(
                OsvConnectorPlugin.SourceName,
                "weakness",
                identifier,
                recordedAt,
                new[] { ProvenanceFieldMasks.Weaknesses },
                decisionReason: GetCweDecisionReason(dto.DatabaseSpecific, identifier));

            var provenanceArray = ImmutableArray.Create(provenance);
            list.Add(new AdvisoryWeakness(
                taxonomy: "cwe",
                identifier: identifier,
                name: null,
                uri: BuildCweUrl(identifier),
                provenance: provenanceArray));
        }

        return list.Count == 0 ? Array.Empty<AdvisoryWeakness>() : list;
    }

    private static string? BuildCweUrl(string? cweId)
    {
        if (string.IsNullOrWhiteSpace(cweId))
        {
            return null;
        }

        var trimmed = cweId.Trim();
        var dashIndex = trimmed.IndexOf('-');
        if (dashIndex < 0 || dashIndex == trimmed.Length - 1)
        {
            return null;
        }

        var digits = new StringBuilder();
        for (var i = dashIndex + 1; i < trimmed.Length; i++)
        {
            var ch = trimmed[i];
            if (char.IsDigit(ch))
            {
                digits.Append(ch);
            }
        }

        return digits.Length == 0 ? null : $"https://cwe.mitre.org/data/definitions/{digits}.html";
    }

    private static string? ExtractDatabaseSpecificSeverity(JsonElement databaseSpecific)
    {
        if (databaseSpecific.ValueKind != JsonValueKind.Object)
        {
            return null;
        }

        if (!databaseSpecific.TryGetProperty("severity", out var severityElement))
        {
            return null;
        }

        if (severityElement.ValueKind == JsonValueKind.String)
        {
            var severity = severityElement.GetString();
            return SeverityNormalization.Normalize(severity);
        }

        return null;
    }

    private static string? GetCweDecisionReason(JsonElement databaseSpecific, string identifier)
    {
        if (databaseSpecific.ValueKind != JsonValueKind.Object)
        {
            return null;
        }

        var hasCweIds = databaseSpecific.TryGetProperty("cwe_ids", out _);
        string? notes = null;

        if (databaseSpecific.TryGetProperty("cwe_notes", out var notesElement))
        {
            notes = NormalizeCweNotes(notesElement);
        }

        if (!string.IsNullOrWhiteSpace(notes))
        {
            return notes;
        }

        return hasCweIds ? "database_specific.cwe_ids" : null;
    }

    private static string? NormalizeCweNotes(JsonElement notesElement)
    {
        if (notesElement.ValueKind == JsonValueKind.String)
        {
            return Validation.TrimToNull(notesElement.GetString());
        }

        if (notesElement.ValueKind != JsonValueKind.Array)
        {
            return null;
        }

        var buffer = new List<string>();
        foreach (var item in notesElement.EnumerateArray())
        {
            if (item.ValueKind == JsonValueKind.String)
            {
                var value = Validation.TrimToNull(item.GetString());
                if (!string.IsNullOrEmpty(value))
                {
                    buffer.Add(value);
                }
            }
        }

        return buffer.Count == 0 ? null : string.Join(" | ", buffer);
    }

    private static IReadOnlyList<CvssMetric> BuildCvssMetrics(OsvVulnerabilityDto dto, DateTimeOffset recordedAt, out string? severity)
    {
        severity = null;
        if (dto.Severity is null || dto.Severity.Count == 0)
        {
            return Array.Empty<CvssMetric>();
        }

        var metrics = new List<CvssMetric>(dto.Severity.Count);
        var bestRank = -1;

        foreach (var severityEntry in dto.Severity)
        {
            if (string.IsNullOrWhiteSpace(severityEntry.Score))
            {
                continue;
            }

            if (!CvssMetricNormalizer.TryNormalize(severityEntry.Type, severityEntry.Score, null, null, out var normalized))
            {
                continue;
            }

            var provenance = new AdvisoryProvenance(OsvConnectorPlugin.SourceName, "cvss", severityEntry.Type ?? "osv", recordedAt);
            metrics.Add(normalized.ToModel(provenance));

            var rank = Array.IndexOf(SeverityOrder, normalized.BaseSeverity);
            if (rank > bestRank)
            {
                bestRank = rank;
                severity = normalized.BaseSeverity;
            }
        }

        if (bestRank < 0 && dto.DatabaseSpecific.ValueKind == JsonValueKind.Object &&
            dto.DatabaseSpecific.TryGetProperty("severity", out var severityProperty))
        {
            var fallback = severityProperty.GetString();
            if (!string.IsNullOrWhiteSpace(fallback))
            {
                severity = SeverityNormalization.Normalize(fallback);
            }
        }

        return metrics;
    }

    private static int CompareReferences(AdvisoryReference? left, AdvisoryReference? right)
    {
        if (ReferenceEquals(left, right))
        {
            return 0;
        }

        if (left is null)
        {
            return 1;
        }

        if (right is null)
        {
            return -1;
        }

        var compare = StringComparer.OrdinalIgnoreCase.Compare(left.Url, right.Url);
        if (compare != 0)
        {
            return compare;
        }

        compare = CompareNullable(left.Kind, right.Kind);
        if (compare != 0)
        {
            return compare;
        }

        compare = CompareNullable(left.SourceTag, right.SourceTag);
        if (compare != 0)
        {
            return compare;
        }

        return left.Provenance.RecordedAt.CompareTo(right.Provenance.RecordedAt);
    }

    private static int CompareNullable(string? left, string? right)
    {
        if (left is null && right is null)
        {
            return 0;
        }

        if (left is null)
        {
            return 1;
        }

        if (right is null)
        {
            return -1;
        }

        return StringComparer.Ordinal.Compare(left, right);
    }

    private static bool TryBuildCanonicalPackageUrl(string? ecosystem, string name, out string? canonical)
    {
        canonical = null;
        var trimmedEcosystem = Validation.TrimToNull(ecosystem);
        if (trimmedEcosystem is null)
        {
            return false;
        }

        if (!PackageUrlBuilders.TryGetValue(trimmedEcosystem, out var factory))
        {
            return false;
        }

        var candidate = factory(name.Trim());
        return IdentifierNormalizer.TryNormalizePackageUrl(candidate, out canonical);
    }

    private static string NormalizePyPiName(string name) => name.Trim().Replace('_', '-');

    private static bool TryBuildNpmPackageUrl(string name, out string? canonical)
    {
        canonical = null;
        var trimmed = name.Trim();
        if (trimmed.Length == 0)
        {
            return false;
        }

        if (trimmed[0] == '@')
        {
            var slashIndex = trimmed.IndexOf('/', 1);
            if (slashIndex <= 1 || slashIndex >= trimmed.Length - 1)
            {
                return false;
            }

            var scope = trimmed[..slashIndex].ToLowerInvariant();
            var package = trimmed[(slashIndex + 1)..].ToLowerInvariant();
            var candidate = $"pkg:npm/{Uri.EscapeDataString(scope)}/{Uri.EscapeDataString(package)}";
            return IdentifierNormalizer.TryNormalizePackageUrl(candidate, out canonical);
        }

        if (trimmed.Contains('/', StringComparison.Ordinal))
        {
            return false;
        }

        var normalized = trimmed.ToLowerInvariant();
        var simpleCandidate = $"pkg:npm/{Uri.EscapeDataString(normalized)}";
        return IdentifierNormalizer.TryNormalizePackageUrl(simpleCandidate, out canonical);
    }

    private static string NormalizeMavenName(string name)
    {
        var trimmed = name.Trim();
        return trimmed.Contains(':', StringComparison.Ordinal)
            ? trimmed.Replace(':', '/')
            : trimmed.Replace('\\', '/');
    }

    private static string NormalizeGoName(string name) => name.Trim();

    private static string NormalizeCratesName(string name) => name.Trim();

    private static string NormalizeNugetName(string name) => name.Trim();

    private static string NormalizeRubyName(string name) => name.Trim();

    private static string NormalizeComposerName(string name) => name.Trim();

    private static string NormalizeHexName(string name) => name.Trim();
}
