using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using StellaOps.Feedser.Models;
using StellaOps.Feedser.Storage.Mongo.PsirtFlags;

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

internal static class ChromiumMapper
{
    private const string VendorIdentifier = "google:chrome";

    public static (Advisory Advisory, PsirtFlagRecord Flag) Map(ChromiumDto dto, string sourceName, DateTimeOffset recordedAt)
    {
        ArgumentNullException.ThrowIfNull(dto);
        ArgumentException.ThrowIfNullOrEmpty(sourceName);

        var advisoryKey = $"chromium/post/{dto.PostId}";
        var provenance = new AdvisoryProvenance(sourceName, "document", dto.PostId, recordedAt.ToUniversalTime());

        var aliases = BuildAliases(dto).ToArray();
        var references = BuildReferences(dto, provenance).ToArray();
        var affectedPackages = BuildAffected(dto, provenance).ToArray();

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

        var flag = new PsirtFlagRecord(
            advisoryKey,
            "Google",
            sourceName,
            dto.PostId,
            recordedAt.ToUniversalTime());

        return (advisory, flag);
    }

    private static IEnumerable<string> BuildAliases(ChromiumDto dto)
    {
        yield return $"CHROMIUM-POST:{dto.PostId}";
        yield return $"CHROMIUM-POST:{dto.Published:yyyy-MM-dd}";

        foreach (var cve in dto.Cves)
        {
            yield return cve;
        }
    }

    private static IEnumerable<AdvisoryReference> BuildReferences(ChromiumDto dto, AdvisoryProvenance provenance)
    {
        var comparer = StringComparer.OrdinalIgnoreCase;
        var references = new List<(AdvisoryReference Reference, int Priority)>
        {
            (new AdvisoryReference(dto.DetailUrl, "advisory", "chromium-blog", summary: null, provenance), 0),
        };

        foreach (var reference in dto.References)
        {
            var summary = string.IsNullOrWhiteSpace(reference.Label) ? null : reference.Label;
            var sourceTag = string.IsNullOrWhiteSpace(reference.Kind) ? null : reference.Kind;
            var advisoryReference = new AdvisoryReference(reference.Url, reference.Kind, sourceTag, summary, provenance);
            references.Add((advisoryReference, 1));
        }

        return references
            .GroupBy(tuple => tuple.Reference.Url, comparer)
            .Select(group => group
                .OrderBy(t => t.Priority)
                .ThenBy(t => t.Reference.Kind ?? string.Empty, comparer)
                .ThenBy(t => t.Reference.SourceTag ?? string.Empty, comparer)
                .ThenBy(t => t.Reference.Url, comparer)
                .First())
            .OrderBy(t => t.Priority)
            .ThenBy(t => t.Reference.Kind ?? string.Empty, comparer)
            .ThenBy(t => t.Reference.Url, comparer)
            .Select(t => t.Reference);
    }

    private static IEnumerable<AffectedPackage> BuildAffected(ChromiumDto dto, AdvisoryProvenance provenance)
    {
        foreach (var version in dto.Versions)
        {
            var identifier = version.Channel switch
            {
                "extended-stable" => $"{VendorIdentifier}:extended-stable",
                "beta" => $"{VendorIdentifier}:beta",
                "dev" => $"{VendorIdentifier}:dev",
                _ => VendorIdentifier,
            };

            var range = new AffectedVersionRange(
                rangeKind: "vendor",
                introducedVersion: null,
                fixedVersion: version.Version,
                lastAffectedVersion: null,
                rangeExpression: null,
                provenance,
                primitives: BuildRangePrimitives(version));

            yield return new AffectedPackage(
                AffectedPackageTypes.Vendor,
                identifier,
                version.Platform,
                new[] { range },
                statuses: Array.Empty<AffectedPackageStatus>(),
                provenance: new[] { provenance });
        }
    }

    private static RangePrimitives? BuildRangePrimitives(ChromiumVersionInfo version)
    {
        var extensions = new Dictionary<string, string>(StringComparer.Ordinal);
        AddExtension(extensions, "chromium.channel", version.Channel);
        AddExtension(extensions, "chromium.platform", version.Platform);
        AddExtension(extensions, "chromium.version.raw", version.Version);

        if (Version.TryParse(version.Version, out var parsed))
        {
            AddExtension(extensions, "chromium.version.normalized", BuildNormalizedVersion(parsed));
            extensions["chromium.version.major"] = parsed.Major.ToString(CultureInfo.InvariantCulture);
            extensions["chromium.version.minor"] = parsed.Minor.ToString(CultureInfo.InvariantCulture);

            if (parsed.Build >= 0)
            {
                extensions["chromium.version.build"] = parsed.Build.ToString(CultureInfo.InvariantCulture);
            }

            if (parsed.Revision >= 0)
            {
                extensions["chromium.version.patch"] = parsed.Revision.ToString(CultureInfo.InvariantCulture);
            }
        }

        return extensions.Count == 0 ? null : new RangePrimitives(null, null, null, extensions);
    }

    private static string BuildNormalizedVersion(Version version)
    {
        if (version.Build >= 0 && version.Revision >= 0)
        {
            return $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}";
        }

        if (version.Build >= 0)
        {
            return $"{version.Major}.{version.Minor}.{version.Build}";
        }

        return $"{version.Major}.{version.Minor}";
    }

    private static void AddExtension(Dictionary<string, string> extensions, string key, string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return;
        }

        extensions[key] = value.Trim();
    }
}
