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

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

internal static class AppleMapper
{
    public static (Advisory Advisory, PsirtFlagRecord? Flag) Map(
        AppleDetailDto dto,
        DocumentRecord document,
        DtoRecord dtoRecord)
    {
        ArgumentNullException.ThrowIfNull(dto);
        ArgumentNullException.ThrowIfNull(document);
        ArgumentNullException.ThrowIfNull(dtoRecord);

        var recordedAt = dtoRecord.ValidatedAt.ToUniversalTime();

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

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

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

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

        PsirtFlagRecord? flag = dto.RapidSecurityResponse
            ? new PsirtFlagRecord(dto.AdvisoryId, "Apple", VndrAppleConnectorPlugin.SourceName, dto.ArticleId, recordedAt)
            : null;

        return (advisory, flag);
    }

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

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

        var aliases = set.ToList();
        aliases.Sort(StringComparer.OrdinalIgnoreCase);
        return aliases;
    }

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

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

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

                list.Add(new AdvisoryReference(
                    url: reference.Url,
                    kind: reference.Kind,
                    sourceTag: null,
                    summary: reference.Title,
                    provenance: provenance));
            }
            catch (ArgumentException)
            {
                // ignore invalid URLs
            }
        }

        if (list.Count == 0)
        {
            return Array.Empty<AdvisoryReference>();
        }

        list.Sort(static (left, right) => StringComparer.OrdinalIgnoreCase.Compare(left.Url, right.Url));
        return list;
    }

    private static IReadOnlyList<AffectedPackage> BuildAffected(AppleDetailDto dto, DateTimeOffset recordedAt)
    {
        if (dto.Affected.Count == 0)
        {
            return Array.Empty<AffectedPackage>();
        }

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

            var provenance = new[]
            {
                new AdvisoryProvenance(
                    VndrAppleConnectorPlugin.SourceName,
                    "affected",
                    product.Name,
                    recordedAt),
            };

            var ranges = BuildRanges(product, recordedAt);
            var normalizedVersions = BuildNormalizedVersions(product, ranges);

            packages.Add(new AffectedPackage(
                type: AffectedPackageTypes.Vendor,
                identifier: product.Name,
                platform: product.Platform,
                versionRanges: ranges,
                statuses: Array.Empty<AffectedPackageStatus>(),
                provenance: provenance,
                normalizedVersions: normalizedVersions));
        }

        return packages.Count == 0 ? Array.Empty<AffectedPackage>() : packages;
    }

    private static IReadOnlyList<AffectedVersionRange> BuildRanges(AppleAffectedProductDto product, DateTimeOffset recordedAt)
    {
        if (string.IsNullOrWhiteSpace(product.Version) && string.IsNullOrWhiteSpace(product.Build))
        {
            return Array.Empty<AffectedVersionRange>();
        }

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

        var extensions = new Dictionary<string, string>(StringComparer.Ordinal);
        if (!string.IsNullOrWhiteSpace(product.Version))
        {
            extensions["apple.version.raw"] = product.Version;
        }

        if (!string.IsNullOrWhiteSpace(product.Build))
        {
            extensions["apple.build"] = product.Build;
        }

        if (!string.IsNullOrWhiteSpace(product.Platform))
        {
            extensions["apple.platform"] = product.Platform;
        }

        var primitives = extensions.Count == 0
            ? null
            : new RangePrimitives(
                SemVer: TryCreateSemVerPrimitive(product.Version),
                Nevra: null,
                Evr: null,
                VendorExtensions: extensions);

        var sanitizedVersion = PackageCoordinateHelper.TryParseSemVer(product.Version, out _, out var normalizedVersion)
            ? normalizedVersion
            : product.Version;

        return new[]
        {
            new AffectedVersionRange(
                rangeKind: "vendor",
                introducedVersion: null,
                fixedVersion: sanitizedVersion,
                lastAffectedVersion: null,
                rangeExpression: product.Version,
                provenance: provenance,
                primitives: primitives),
        };
    }

    private static SemVerPrimitive? TryCreateSemVerPrimitive(string? version)
    {
        if (string.IsNullOrWhiteSpace(version))
        {
            return null;
        }

        if (!PackageCoordinateHelper.TryParseSemVer(version, out _, out var normalized))
        {
            return null;
        }

        // treat as fixed version, unknown introduced/last affected
        return new SemVerPrimitive(
            Introduced: null,
            IntroducedInclusive: true,
            Fixed: normalized,
            FixedInclusive: true,
            LastAffected: null,
            LastAffectedInclusive: true,
            ConstraintExpression: null);
    }

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

        var segments = new List<string>();
        if (!string.IsNullOrWhiteSpace(product.Platform))
        {
            segments.Add(product.Platform.Trim());
        }

        if (!string.IsNullOrWhiteSpace(product.Name))
        {
            segments.Add(product.Name.Trim());
        }

        var note = segments.Count == 0 ? null : $"apple:{string.Join(':', segments)}";

        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.ToArray();
    }
}
