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

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

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

        var aliases = BuildAliases(dto);
        var references = BuildReferences(dto, recordedAt);
        var affectedPackages = BuildAffectedPackages(dto, recordedAt);

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

        var mappingProvenance = new AdvisoryProvenance(
            DebianConnectorPlugin.SourceName,
            "mapping",
            dto.AdvisoryId,
            recordedAt);

        return new Advisory(
            advisoryKey: dto.AdvisoryId,
            title: dto.Title ?? dto.AdvisoryId,
            summary: dto.Description,
            language: "en",
            published: dto.Packages.Select(p => p.Published).Where(p => p.HasValue).Select(p => p!.Value).Cast<DateTimeOffset?>().DefaultIfEmpty(null).Min(),
            modified: recordedAt,
            severity: null,
            exploitKnown: false,
            aliases: aliases,
            references: references,
            affectedPackages: affectedPackages,
            cvssMetrics: Array.Empty<CvssMetric>(),
            provenance: new[] { fetchProvenance, mappingProvenance });
    }

    private static string[] BuildAliases(DebianAdvisoryDto dto)
    {
        var aliases = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        if (!string.IsNullOrWhiteSpace(dto.AdvisoryId))
        {
            aliases.Add(dto.AdvisoryId.Trim());
        }

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

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

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

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

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

                references.Add(new AdvisoryReference(
                    reference.Url,
                    NormalizeReferenceKind(reference.Kind),
                    reference.Kind,
                    reference.Title,
                    provenance));
            }
            catch (ArgumentException)
            {
                // Ignore malformed URLs while keeping the rest of the advisory intact.
            }
        }

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

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

        return value.Trim().ToLowerInvariant() switch
        {
            "advisory" or "dsa" => "advisory",
            "cve" => "cve",
            "patch" => "patch",
            _ => null,
        };
    }

    private static AdvisoryProvenance BuildPackageProvenance(DebianPackageStateDto package, DateTimeOffset recordedAt)
        => new(DebianConnectorPlugin.SourceName, "affected", $"{package.Package}:{package.Release}", recordedAt);

    private static IReadOnlyList<AffectedPackage> BuildAffectedPackages(DebianAdvisoryDto 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.Package))
            {
                continue;
            }

            var provenance = new[] { BuildPackageProvenance(package, recordedAt) };
            var ranges = BuildVersionRanges(package, recordedAt);
            var normalizedVersions = BuildNormalizedVersions(package, ranges);

            packages.Add(new AffectedPackage(
                AffectedPackageTypes.Deb,
                identifier: package.Package.Trim(),
                platform: package.Release,
                versionRanges: ranges,
                statuses: Array.Empty<AffectedPackageStatus>(),
                provenance: provenance,
                normalizedVersions: normalizedVersions));
        }

        return packages;
    }

    private static IReadOnlyList<AffectedVersionRange> BuildVersionRanges(DebianPackageStateDto package, DateTimeOffset recordedAt)
    {
        var provenance = new AdvisoryProvenance(
            DebianConnectorPlugin.SourceName,
            "range",
            $"{package.Package}:{package.Release}",
            recordedAt);

        var introduced = package.IntroducedVersion;
        var fixedVersion = package.FixedVersion;
        var lastAffected = package.LastAffectedVersion;

        if (string.IsNullOrWhiteSpace(introduced) && string.IsNullOrWhiteSpace(fixedVersion) && string.IsNullOrWhiteSpace(lastAffected))
        {
            return Array.Empty<AffectedVersionRange>();
        }

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

        AddExtension(extensions, "debian.introduced", introduced);
        AddExtension(extensions, "debian.fixed", fixedVersion);
        AddExtension(extensions, "debian.lastAffected", lastAffected);

        var primitives = BuildEvrPrimitives(introduced, fixedVersion, lastAffected);
        return new[]
        {
            new AffectedVersionRange(
                rangeKind: "evr",
                introducedVersion: introduced,
                fixedVersion: fixedVersion,
                lastAffectedVersion: lastAffected,
                rangeExpression: BuildRangeExpression(introduced, fixedVersion, lastAffected),
                provenance: provenance,
                primitives: primitives is null && extensions.Count == 0
                    ? null
                    : new RangePrimitives(
                        SemVer: null,
                        Nevra: null,
                        Evr: primitives,
                        VendorExtensions: extensions.Count == 0 ? null : extensions))
        };
    }

    private static EvrPrimitive? BuildEvrPrimitives(string? introduced, string? fixedVersion, string? lastAffected)
    {
        var introducedComponent = ParseEvr(introduced);
        var fixedComponent = ParseEvr(fixedVersion);
        var lastAffectedComponent = ParseEvr(lastAffected);

        if (introducedComponent is null && fixedComponent is null && lastAffectedComponent is null)
        {
            return null;
        }

        return new EvrPrimitive(introducedComponent, fixedComponent, lastAffectedComponent);
    }

    private static EvrComponent? ParseEvr(string? value)
    {
        if (!DebianEvr.TryParse(value, out var evr) || evr is null)
        {
            return null;
        }

        return new EvrComponent(
            evr.Epoch,
            evr.Version,
            evr.Revision.Length == 0 ? null : evr.Revision);
    }

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

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

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

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

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

        var note = string.IsNullOrWhiteSpace(package.Release)
            ? null
            : $"debian:{package.Release.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;
    }

    private static void AddExtension(IDictionary<string, string> extensions, string key, string? value)
    {
        if (!string.IsNullOrWhiteSpace(value))
        {
            extensions[key] = value.Trim();
        }
    }
}
