using System;
using System.Collections.Generic;
using System.Linq;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Source.Common.Packages;
using StellaOps.Feedser.Storage.Mongo.Documents;
using StellaOps.Feedser.Storage.Mongo.Dtos;

namespace StellaOps.Feedser.Source.Vndr.Cisco.Internal;

public static class CiscoMapper
{
    public static Advisory Map(CiscoAdvisoryDto dto, DocumentRecord document, DtoRecord dtoRecord)
    {
        ArgumentNullException.ThrowIfNull(dto);
        ArgumentNullException.ThrowIfNull(document);
        ArgumentNullException.ThrowIfNull(dtoRecord);

        var recordedAt = dtoRecord.ValidatedAt.ToUniversalTime();
        var fetchProvenance = new AdvisoryProvenance(
            VndrCiscoConnectorPlugin.SourceName,
            "document",
            document.Uri,
            document.FetchedAt.ToUniversalTime());

        var mapProvenance = new AdvisoryProvenance(
            VndrCiscoConnectorPlugin.SourceName,
            "map",
            dto.AdvisoryId,
            recordedAt);

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

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

    private static IReadOnlyList<string> BuildAliases(CiscoAdvisoryDto dto)
    {
        var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            dto.AdvisoryId,
        };

        foreach (var cve in dto.Cves)
        {
            if (!string.IsNullOrWhiteSpace(cve))
            {
                set.Add(cve.Trim());
            }
        }

        foreach (var bugId in dto.BugIds)
        {
            if (!string.IsNullOrWhiteSpace(bugId))
            {
                set.Add(bugId.Trim());
            }
        }

        if (dto.PublicationUrl is not null)
        {
            set.Add(dto.PublicationUrl);
        }

        return set.Count == 0
            ? Array.Empty<string>()
            : set.OrderBy(static value => value, StringComparer.OrdinalIgnoreCase).ToArray();
    }

    private static IReadOnlyList<AdvisoryReference> BuildReferences(CiscoAdvisoryDto dto, DateTimeOffset recordedAt)
    {
        var list = new List<AdvisoryReference>(3);
        AddReference(list, dto.PublicationUrl, "publication", recordedAt);
        AddReference(list, dto.CvrfUrl, "cvrf", recordedAt);
        AddReference(list, dto.CsafUrl, "csaf", recordedAt);

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

    private static void AddReference(ICollection<AdvisoryReference> references, string? url, string kind, DateTimeOffset recordedAt)
    {
        if (string.IsNullOrWhiteSpace(url))
        {
            return;
        }

        if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
        {
            return;
        }

        var provenance = new AdvisoryProvenance(
            VndrCiscoConnectorPlugin.SourceName,
            $"reference:{kind}",
            uri.ToString(),
            recordedAt);

        try
        {
            references.Add(new AdvisoryReference(
                url: uri.ToString(),
                kind: kind,
                sourceTag: null,
                summary: null,
                provenance: provenance));
        }
        catch (ArgumentException)
        {
            // ignore invalid URLs
        }
    }

    private static IReadOnlyList<AffectedPackage> BuildAffectedPackages(CiscoAdvisoryDto dto, DateTimeOffset recordedAt)
    {
        if (dto.Products.Count == 0)
        {
            return Array.Empty<AffectedPackage>();
        }

        var packages = new List<AffectedPackage>(dto.Products.Count);
        foreach (var product in dto.Products)
        {
            if (string.IsNullOrWhiteSpace(product.Name))
            {
                continue;
            }

            var range = BuildVersionRange(product, recordedAt);
            var statuses = BuildStatuses(product, recordedAt);
            var provenance = new[]
            {
                new AdvisoryProvenance(
                    VndrCiscoConnectorPlugin.SourceName,
                    "affected",
                    product.ProductId ?? product.Name,
                    recordedAt),
            };

            packages.Add(new AffectedPackage(
                type: AffectedPackageTypes.Vendor,
                identifier: product.Name,
                platform: null,
                versionRanges: range is null ? Array.Empty<AffectedVersionRange>() : new[] { range },
                statuses: statuses,
                provenance: provenance,
                normalizedVersions: Array.Empty<NormalizedVersionRule>()));
        }

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

    private static AffectedVersionRange? BuildVersionRange(CiscoAffectedProductDto product, DateTimeOffset recordedAt)
    {
        if (string.IsNullOrWhiteSpace(product.Version))
        {
            return null;
        }

        var version = product.Version.Trim();
        RangePrimitives? primitives = null;
        string rangeKind = "vendor";
        string? rangeExpression = version;

        if (PackageCoordinateHelper.TryParseSemVer(version, out _, out var normalized))
        {
            var semver = new SemVerPrimitive(
                Introduced: null,
                IntroducedInclusive: true,
                Fixed: null,
                FixedInclusive: false,
                LastAffected: null,
                LastAffectedInclusive: true,
                ConstraintExpression: null,
                ExactValue: normalized);

            primitives = new RangePrimitives(semver, null, null, BuildVendorExtensions(product));
            rangeKind = "semver";
            rangeExpression = normalized;
        }
        else
        {
            primitives = new RangePrimitives(null, null, null, BuildVendorExtensions(product, includeVersion: true));
        }

        var provenance = new AdvisoryProvenance(
            VndrCiscoConnectorPlugin.SourceName,
            "range",
            product.ProductId ?? product.Name,
            recordedAt);

        return new AffectedVersionRange(
            rangeKind: rangeKind,
            introducedVersion: null,
            fixedVersion: null,
            lastAffectedVersion: null,
            rangeExpression: rangeExpression,
            provenance: provenance,
            primitives: primitives);
    }

    private static IReadOnlyDictionary<string, string>? BuildVendorExtensions(CiscoAffectedProductDto product, bool includeVersion = false)
    {
        var dictionary = new Dictionary<string, string>(StringComparer.Ordinal);
        if (!string.IsNullOrWhiteSpace(product.ProductId))
        {
            dictionary["cisco.productId"] = product.ProductId!;
        }

        if (includeVersion && !string.IsNullOrWhiteSpace(product.Version))
        {
            dictionary["cisco.version.raw"] = product.Version!;
        }

        return dictionary.Count == 0 ? null : dictionary;
    }

    private static IReadOnlyList<AffectedPackageStatus> BuildStatuses(CiscoAffectedProductDto product, DateTimeOffset recordedAt)
    {
        if (product.Statuses is null || product.Statuses.Count == 0)
        {
            return Array.Empty<AffectedPackageStatus>();
        }

        var list = new List<AffectedPackageStatus>(product.Statuses.Count);
        foreach (var status in product.Statuses)
        {
            if (!AffectedPackageStatusCatalog.TryNormalize(status, out var normalized)
                || string.IsNullOrWhiteSpace(normalized))
            {
                continue;
            }

            var provenance = new AdvisoryProvenance(
                VndrCiscoConnectorPlugin.SourceName,
                "status",
                product.ProductId ?? product.Name,
                recordedAt);

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

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