using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Normalization.Distro;
using StellaOps.Feedser.Storage.Mongo.Documents;

namespace StellaOps.Feedser.Source.Distro.Suse.Internal;

internal static class SuseMapper
{
    public static Advisory Map(SuseAdvisoryDto dto, DocumentRecord document, DateTimeOffset recordedAt)
    {
        ArgumentNullException.ThrowIfNull(dto);
        ArgumentNullException.ThrowIfNull(document);

        var aliases = BuildAliases(dto);
        var references = BuildReferences(dto, recordedAt);
        var packages = BuildPackages(dto, recordedAt);

        var fetchProvenance = new AdvisoryProvenance(
            SuseConnectorPlugin.SourceName,
            "document",
            document.Uri,
            document.FetchedAt.ToUniversalTime());

        var mapProvenance = new AdvisoryProvenance(
            SuseConnectorPlugin.SourceName,
            "mapping",
            dto.AdvisoryId,
            recordedAt);

        var published = dto.Published;
        var modified = DateTimeOffset.Compare(recordedAt, dto.Published) >= 0 ? recordedAt : dto.Published;

        return new Advisory(
            advisoryKey: dto.AdvisoryId,
            title: dto.Title ?? dto.AdvisoryId,
            summary: dto.Summary,
            language: "en",
            published: published,
            modified: modified,
            severity: null,
            exploitKnown: false,
            aliases: aliases,
            references: references,
            affectedPackages: packages,
            cvssMetrics: Array.Empty<CvssMetric>(),
            provenance: new[] { fetchProvenance, mapProvenance });
    }

    private static string[] BuildAliases(SuseAdvisoryDto dto)
    {
        var aliases = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            dto.AdvisoryId
        };

        foreach (var cve in dto.CveIds ?? Array.Empty<string>())
        {
            if (!string.IsNullOrWhiteSpace(cve))
            {
                aliases.Add(cve.Trim());
            }
        }

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

    private static AdvisoryReference[] BuildReferences(SuseAdvisoryDto 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;
            }

            try
            {
                var provenance = new AdvisoryProvenance(
                    SuseConnectorPlugin.SourceName,
                    "reference",
                    reference.Url,
                    recordedAt);

                references.Add(new AdvisoryReference(
                    reference.Url.Trim(),
                    NormalizeReferenceKind(reference.Kind),
                    reference.Kind,
                    reference.Title,
                    provenance));
            }
            catch (ArgumentException)
            {
                // Ignore malformed URLs to keep advisory mapping resilient.
            }
        }

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

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

        return kind.Trim().ToLowerInvariant() switch
        {
            "cve" => "cve",
            "self" => "advisory",
            "external" => "external",
            _ => null,
        };
    }

    private static IReadOnlyList<AffectedPackage> BuildPackages(SuseAdvisoryDto dto, DateTimeOffset recordedAt)
    {
        if (dto.Packages is null || dto.Packages.Count == 0)
        {
            return Array.Empty<AffectedPackage>();
        }

        var packages = new List<AffectedPackage>(dto.Packages.Count);
        foreach (var package in dto.Packages)
        {
            if (string.IsNullOrWhiteSpace(package.CanonicalNevra))
            {
                continue;
            }

            Nevra? nevra;
            if (!Nevra.TryParse(package.CanonicalNevra, out nevra))
            {
                continue;
            }

            var affectedProvenance = new AdvisoryProvenance(
                SuseConnectorPlugin.SourceName,
                "affected",
                $"{package.Platform}:{package.CanonicalNevra}",
                recordedAt);

            var ranges = BuildVersionRanges(package, nevra!, recordedAt);
            if (ranges.Count == 0 && string.Equals(package.Status, "not_affected", StringComparison.OrdinalIgnoreCase))
            {
                continue;
            }

            var normalizedVersions = BuildNormalizedVersions(package, ranges);

            packages.Add(new AffectedPackage(
                AffectedPackageTypes.Rpm,
                identifier: nevra!.ToCanonicalString(),
                platform: package.Platform,
                versionRanges: ranges,
                statuses: BuildStatuses(package, affectedProvenance),
                provenance: new[] { affectedProvenance },
                normalizedVersions: normalizedVersions));
        }

        return packages.Count == 0
            ? Array.Empty<AffectedPackage>()
            : packages
                .OrderBy(static pkg => pkg.Platform, StringComparer.OrdinalIgnoreCase)
                .ThenBy(static pkg => pkg.Identifier, StringComparer.OrdinalIgnoreCase)
                .ToArray();
    }

    private static IReadOnlyList<AffectedPackageStatus> BuildStatuses(SusePackageStateDto package, AdvisoryProvenance provenance)
    {
        if (string.IsNullOrWhiteSpace(package.Status))
        {
            return Array.Empty<AffectedPackageStatus>();
        }

        return new[]
        {
            new AffectedPackageStatus(package.Status, provenance)
        };
    }

    private static IReadOnlyList<AffectedVersionRange> BuildVersionRanges(SusePackageStateDto package, Nevra nevra, DateTimeOffset recordedAt)
    {
        var introducedComponent = ParseNevraComponent(package.IntroducedVersion, nevra);
        var fixedComponent = ParseNevraComponent(package.FixedVersion, nevra);
        var lastAffectedComponent = ParseNevraComponent(package.LastAffectedVersion, nevra);

        if (introducedComponent is null && fixedComponent is null && lastAffectedComponent is null)
        {
            return Array.Empty<AffectedVersionRange>();
        }

        var rangeProvenance = new AdvisoryProvenance(
            SuseConnectorPlugin.SourceName,
            "range",
            $"{package.Platform}:{nevra.ToCanonicalString()}",
            recordedAt);

        var extensions = new Dictionary<string, string>(StringComparer.Ordinal)
        {
            ["suse.status"] = package.Status
        };

        var rangeExpression = BuildRangeExpression(package.IntroducedVersion, package.FixedVersion, package.LastAffectedVersion);

        var range = new AffectedVersionRange(
            rangeKind: "nevra",
            introducedVersion: package.IntroducedVersion,
            fixedVersion: package.FixedVersion,
            lastAffectedVersion: package.LastAffectedVersion,
            rangeExpression: rangeExpression,
            provenance: rangeProvenance,
            primitives: new RangePrimitives(
                SemVer: null,
                Nevra: new NevraPrimitive(introducedComponent, fixedComponent, lastAffectedComponent),
                Evr: null,
                VendorExtensions: extensions));

        return new[] { range };
    }

    private static NevraComponent? ParseNevraComponent(string? version, Nevra nevra)
    {
        if (string.IsNullOrWhiteSpace(version))
        {
            return null;
        }

        if (!TrySplitNevraVersion(version.Trim(), out var epoch, out var ver, out var rel))
        {
            return null;
        }

        return new NevraComponent(
            nevra.Name,
            epoch,
            ver,
            rel,
            string.IsNullOrWhiteSpace(nevra.Architecture) ? null : nevra.Architecture);
    }

    private static bool TrySplitNevraVersion(string value, out int epoch, out string version, out string release)
    {
        epoch = 0;
        version = string.Empty;
        release = string.Empty;

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

        var trimmed = value.Trim();
        var dashIndex = trimmed.LastIndexOf('-');
        if (dashIndex <= 0 || dashIndex >= trimmed.Length - 1)
        {
            return false;
        }

        release = trimmed[(dashIndex + 1)..];
        var versionSegment = trimmed[..dashIndex];

        var epochIndex = versionSegment.IndexOf(':');
        if (epochIndex >= 0)
        {
            var epochPart = versionSegment[..epochIndex];
            version = epochIndex < versionSegment.Length - 1 ? versionSegment[(epochIndex + 1)..] : string.Empty;
            if (epochPart.Length > 0 && !int.TryParse(epochPart, NumberStyles.Integer, CultureInfo.InvariantCulture, out epoch))
            {
                epoch = 0;
                return false;
            }
        }
        else
        {
            version = versionSegment;
        }

        return !string.IsNullOrWhiteSpace(version) && !string.IsNullOrWhiteSpace(release);
    }

    private static string? BuildRangeExpression(string? introduced, string? fixedVersion, string? lastAffected)
    {
        var parts = new List<string>(3);
        if (!string.IsNullOrWhiteSpace(introduced))
        {
            parts.Add($"introduced:{introduced}");
        }

        if (!string.IsNullOrWhiteSpace(fixedVersion))
        {
            parts.Add($"fixed:{fixedVersion}");
        }

        if (!string.IsNullOrWhiteSpace(lastAffected))
        {
            parts.Add($"last:{lastAffected}");
        }

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

    private static IReadOnlyList<NormalizedVersionRule> BuildNormalizedVersions(
        SusePackageStateDto package,
        IReadOnlyList<AffectedVersionRange> ranges)
    {
        if (ranges.Count == 0)
        {
            return Array.Empty<NormalizedVersionRule>();
        }

        var note = string.IsNullOrWhiteSpace(package.Platform)
            ? null
            : $"suse:{package.Platform.Trim()}";

        var rules = new List<NormalizedVersionRule>(ranges.Count);
        foreach (var range in ranges)
        {
            var rule = range.ToNormalizedVersionRule(note);
            if (rule is not null)
            {
                rules.Add(rule);
            }
        }

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