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

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

internal static class UbuntuMapper
{
    public static Advisory Map(UbuntuNoticeDto 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(
            UbuntuConnectorPlugin.SourceName,
            "document",
            document.Uri,
            document.FetchedAt.ToUniversalTime());

        var mapProvenance = new AdvisoryProvenance(
            UbuntuConnectorPlugin.SourceName,
            "mapping",
            dto.NoticeId,
            recordedAt);

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

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

        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(UbuntuNoticeDto 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(
                    UbuntuConnectorPlugin.SourceName,
                    "reference",
                    reference.Url,
                    recordedAt);

                references.Add(new AdvisoryReference(
                    reference.Url.Trim(),
                    NormalizeReferenceKind(reference.Kind),
                    reference.Kind,
                    reference.Title,
                    provenance));
            }
            catch (ArgumentException)
            {
                // ignore poorly formed URIs
            }
        }

        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
        {
            "external" => "external",
            "self" => "advisory",
            _ => null
        };
    }

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

        var list = new List<AffectedPackage>();
        foreach (var package in dto.Packages)
        {
            if (string.IsNullOrWhiteSpace(package.Package) || string.IsNullOrWhiteSpace(package.Version))
            {
                continue;
            }

            if (!DebianEvr.TryParse(package.Version, out var evr) || evr is null)
            {
                continue;
            }

            var provenance = new AdvisoryProvenance(
                UbuntuConnectorPlugin.SourceName,
                "affected",
                $"{dto.NoticeId}:{package.Release}:{package.Package}",
                recordedAt);

            var rangeProvenance = new AdvisoryProvenance(
                UbuntuConnectorPlugin.SourceName,
                "range",
                $"{dto.NoticeId}:{package.Release}:{package.Package}",
                recordedAt);

            var rangeExpression = $"fixed:{package.Version}";

            var extensions = new Dictionary<string, string>(StringComparer.Ordinal)
            {
                ["ubuntu.release"] = package.Release,
                ["ubuntu.pocket"] = package.Pocket ?? string.Empty
            };

            var range = new AffectedVersionRange(
                rangeKind: "evr",
                introducedVersion: null,
                fixedVersion: package.Version,
                lastAffectedVersion: null,
                rangeExpression: rangeExpression,
                provenance: rangeProvenance,
                primitives: new RangePrimitives(
                    SemVer: null,
                    Nevra: null,
                    Evr: new EvrPrimitive(
                        Introduced: null,
                        Fixed: new EvrComponent(evr.Epoch, evr.Version, evr.Revision.Length == 0 ? null : evr.Revision),
                        LastAffected: null),
                    VendorExtensions: extensions));

            var statuses = new[]
            {
                new AffectedPackageStatus(DetermineStatus(package), provenance)
            };

            var normalizedNote = string.IsNullOrWhiteSpace(package.Release)
                ? null
                : $"ubuntu:{package.Release.Trim()}";
            var normalizedRule = range.ToNormalizedVersionRule(normalizedNote);
            var normalizedVersions = normalizedRule is null
                ? Array.Empty<NormalizedVersionRule>()
                : new[] { normalizedRule };

            list.Add(new AffectedPackage(
                type: AffectedPackageTypes.Deb,
                identifier: package.Package,
                platform: package.Release,
                versionRanges: new[] { range },
                statuses: statuses,
                provenance: new[] { provenance },
                normalizedVersions: normalizedVersions));
        }

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

    private static string DetermineStatus(UbuntuReleasePackageDto package)
    {
        if (!string.IsNullOrWhiteSpace(package.Pocket) && package.Pocket.Contains("security", StringComparison.OrdinalIgnoreCase))
        {
            return "resolved";
        }

        if (!string.IsNullOrWhiteSpace(package.Pocket) && package.Pocket.Contains("esm", StringComparison.OrdinalIgnoreCase))
        {
            return "resolved";
        }

        return "resolved";
    }
}
