using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Globalization;
using System.Linq;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Feedser.Merge.Options;
using StellaOps.Feedser.Models;

namespace StellaOps.Feedser.Merge.Services;

/// <summary>
/// Merges canonical advisories emitted by different sources into a single precedence-resolved advisory.
/// </summary>
public sealed class AdvisoryPrecedenceMerger
{
    private static readonly Meter MergeMeter = new("StellaOps.Feedser.Merge");
    private static readonly Counter<long> MergeCounter = MergeMeter.CreateCounter<long>(
        "feedser.merge.operations",
        unit: "count",
        description: "Number of merge invocations executed by the precedence engine.");

    private static readonly Counter<long> OverridesCounter = MergeMeter.CreateCounter<long>(
        "feedser.merge.overrides",
        unit: "count",
        description: "Number of times lower-precedence advisories were overridden by higher-precedence sources.");

    private static readonly Counter<long> RangeOverrideCounter = MergeMeter.CreateCounter<long>(
        "feedser.merge.range_overrides",
        unit: "count",
        description: "Number of affected-package range overrides performed during precedence merge.");

    private static readonly Counter<long> ConflictCounter = MergeMeter.CreateCounter<long>(
        "feedser.merge.conflicts",
        unit: "count",
        description: "Number of precedence conflicts detected (severity, rank ties, etc.).");

    private static readonly Counter<long> NormalizedRuleCounter = MergeMeter.CreateCounter<long>(
        "feedser.merge.normalized_rules",
        unit: "rule",
        description: "Number of normalized version rules retained after precedence merge.");

    private static readonly Counter<long> MissingNormalizedRuleCounter = MergeMeter.CreateCounter<long>(
        "feedser.merge.normalized_rules_missing",
        unit: "package",
        description: "Number of affected packages with version ranges but no normalized rules.");

    private static readonly Action<ILogger, MergeOverrideAudit, Exception?> OverrideLogged = LoggerMessage.Define<MergeOverrideAudit>(
        LogLevel.Information,
        new EventId(1000, "AdvisoryOverride"),
        "Advisory precedence override {@Override}");

    private static readonly Action<ILogger, PackageOverrideAudit, Exception?> RangeOverrideLogged = LoggerMessage.Define<PackageOverrideAudit>(
        LogLevel.Information,
        new EventId(1001, "PackageRangeOverride"),
        "Affected package precedence override {@Override}");

    private static readonly Action<ILogger, MergeFieldConflictAudit, Exception?> ConflictLogged = LoggerMessage.Define<MergeFieldConflictAudit>(
        LogLevel.Information,
        new EventId(1002, "PrecedenceConflict"),
        "Precedence conflict {@Conflict}");

    private readonly AffectedPackagePrecedenceResolver _packageResolver;
    private readonly IReadOnlyDictionary<string, int> _precedence;
    private readonly int _fallbackRank;
    private readonly System.TimeProvider _timeProvider;
    private readonly ILogger<AdvisoryPrecedenceMerger> _logger;

    public AdvisoryPrecedenceMerger()
        : this(new AffectedPackagePrecedenceResolver(), TimeProvider.System)
    {
    }

    public AdvisoryPrecedenceMerger(AffectedPackagePrecedenceResolver packageResolver, System.TimeProvider? timeProvider = null)
        : this(packageResolver, packageResolver?.Precedence ?? AdvisoryPrecedenceDefaults.Rankings, timeProvider ?? TimeProvider.System, NullLogger<AdvisoryPrecedenceMerger>.Instance)
    {
    }

    public AdvisoryPrecedenceMerger(
        AffectedPackagePrecedenceResolver packageResolver,
        IReadOnlyDictionary<string, int> precedence,
        System.TimeProvider timeProvider)
        : this(packageResolver, precedence, timeProvider, NullLogger<AdvisoryPrecedenceMerger>.Instance)
    {
    }

    public AdvisoryPrecedenceMerger(
        AffectedPackagePrecedenceResolver packageResolver,
        AdvisoryPrecedenceOptions? options,
        System.TimeProvider timeProvider,
        ILogger<AdvisoryPrecedenceMerger>? logger = null)
        : this(
            EnsureResolver(packageResolver, options, out var precedence),
            precedence,
            timeProvider,
            logger)
    {
    }

    public AdvisoryPrecedenceMerger(
        AffectedPackagePrecedenceResolver packageResolver,
        IReadOnlyDictionary<string, int> precedence,
        System.TimeProvider timeProvider,
        ILogger<AdvisoryPrecedenceMerger>? logger)
    {
        _packageResolver = packageResolver ?? throw new ArgumentNullException(nameof(packageResolver));
        _precedence = precedence ?? throw new ArgumentNullException(nameof(precedence));
        _fallbackRank = _precedence.Count == 0 ? 10 : _precedence.Values.Max() + 1;
        _timeProvider = timeProvider ?? TimeProvider.System;
        _logger = logger ?? NullLogger<AdvisoryPrecedenceMerger>.Instance;
    }

    public Advisory Merge(IEnumerable<Advisory> advisories)
    {
        if (advisories is null)
        {
            throw new ArgumentNullException(nameof(advisories));
        }

        var list = advisories.Where(static a => a is not null).ToList();
        if (list.Count == 0)
        {
            throw new ArgumentException("At least one advisory is required for merge.", nameof(advisories));
        }

        var advisoryKey = list[0].AdvisoryKey;
        if (list.Any(advisory => !string.Equals(advisory.AdvisoryKey, advisoryKey, StringComparison.Ordinal)))
        {
            throw new ArgumentException("All advisories must share the same advisory key.", nameof(advisories));
        }

        var ordered = list
            .Select(advisory => new AdvisoryEntry(advisory, GetRank(advisory)))
            .OrderBy(entry => entry.Rank)
            .ThenByDescending(entry => entry.Advisory.Provenance.Length)
            .ToArray();

        MergeCounter.Add(1, new KeyValuePair<string, object?>("inputs", list.Count));

        var primary = ordered[0].Advisory;

        var title = PickString(ordered, advisory => advisory.Title) ?? advisoryKey;
        var summary = PickString(ordered, advisory => advisory.Summary);
        var language = PickString(ordered, advisory => advisory.Language);
        var severity = PickString(ordered, advisory => advisory.Severity);

        var aliases = ordered
            .SelectMany(entry => entry.Advisory.Aliases)
            .Where(static alias => !string.IsNullOrWhiteSpace(alias))
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToArray();

        var credits = ordered
            .SelectMany(entry => entry.Advisory.Credits)
            .Distinct()
            .ToArray();

        var references = ordered
            .SelectMany(entry => entry.Advisory.References)
            .Distinct()
            .ToArray();

        var packageResult = _packageResolver.Merge(ordered.SelectMany(entry => entry.Advisory.AffectedPackages));
        RecordNormalizedRuleMetrics(packageResult.Packages);
        var affectedPackages = packageResult.Packages;
        var cvssMetrics = ordered
            .SelectMany(entry => entry.Advisory.CvssMetrics)
            .Distinct()
            .ToArray();

        var published = PickDateTime(ordered, static advisory => advisory.Published);
        var modified = PickDateTime(ordered, static advisory => advisory.Modified) ?? published;

        var provenance = ordered
            .SelectMany(entry => entry.Advisory.Provenance)
            .Distinct()
            .ToList();

        var precedenceTrace = ordered
            .SelectMany(entry => entry.Sources)
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .OrderBy(static source => source, StringComparer.OrdinalIgnoreCase)
            .ToArray();

        var mergeProvenance = new AdvisoryProvenance(
            source: "merge",
            kind: "precedence",
            value: string.Join("|", precedenceTrace),
            recordedAt: _timeProvider.GetUtcNow());

        provenance.Add(mergeProvenance);

        var exploitKnown = ordered.Any(entry => entry.Advisory.ExploitKnown);

        LogOverrides(advisoryKey, ordered);
        LogPackageOverrides(advisoryKey, packageResult.Overrides);
        RecordFieldConflicts(advisoryKey, ordered);

        return new Advisory(
            advisoryKey,
            title,
            summary,
            language,
            published,
            modified,
            severity,
            exploitKnown,
            aliases,
            credits,
            references,
            affectedPackages,
            cvssMetrics,
            provenance);
    }

    private static void RecordNormalizedRuleMetrics(IReadOnlyList<AffectedPackage> packages)
    {
        if (packages.Count == 0)
        {
            return;
        }

        foreach (var package in packages)
        {
            var packageType = package.Type ?? string.Empty;
            var normalizedVersions = package.NormalizedVersions;
            if (normalizedVersions.Length > 0)
            {
                foreach (var rule in normalizedVersions)
                {
                    var tags = new KeyValuePair<string, object?>[]
                    {
                        new("package_type", packageType),
                        new("scheme", rule.Scheme ?? string.Empty),
                    };

                    NormalizedRuleCounter.Add(1, tags);
                }
            }
            else if (package.VersionRanges.Length > 0)
            {
                var tags = new KeyValuePair<string, object?>[]
                {
                    new("package_type", packageType),
                };

                MissingNormalizedRuleCounter.Add(1, tags);
            }
        }
    }

    private string? PickString(IEnumerable<AdvisoryEntry> ordered, Func<Advisory, string?> selector)
    {
        foreach (var entry in ordered)
        {
            var value = selector(entry.Advisory);
            if (!string.IsNullOrWhiteSpace(value))
            {
                return value.Trim();
            }
        }

        return null;
    }

    private DateTimeOffset? PickDateTime(IEnumerable<AdvisoryEntry> ordered, Func<Advisory, DateTimeOffset?> selector)
    {
        foreach (var entry in ordered)
        {
            var value = selector(entry.Advisory);
            if (value.HasValue)
            {
                return value.Value.ToUniversalTime();
            }
        }

        return null;
    }

    private int GetRank(Advisory advisory)
    {
        var best = _fallbackRank;
        foreach (var provenance in advisory.Provenance)
        {
            if (string.IsNullOrWhiteSpace(provenance.Source))
            {
                continue;
            }

            if (_precedence.TryGetValue(provenance.Source, out var rank) && rank < best)
            {
                best = rank;
            }
        }

        return best;
    }

    private void LogOverrides(string advisoryKey, IReadOnlyList<AdvisoryEntry> ordered)
    {
        if (ordered.Count <= 1)
        {
            return;
        }

        var primary = ordered[0];
        var primaryRank = primary.Rank;

        for (var i = 1; i < ordered.Count; i++)
        {
            var candidate = ordered[i];
            if (candidate.Rank <= primaryRank)
            {
                continue;
            }

            var tags = new KeyValuePair<string, object?>[]
            {
                new("primary_source", FormatSourceLabel(primary.Sources)),
                new("suppressed_source", FormatSourceLabel(candidate.Sources)),
                new("primary_rank", primaryRank),
                new("suppressed_rank", candidate.Rank),
            };

            OverridesCounter.Add(1, tags);

            var audit = new MergeOverrideAudit(
                advisoryKey,
                primary.Sources,
                primaryRank,
                candidate.Sources,
                candidate.Rank,
                primary.Advisory.Aliases.Length,
                candidate.Advisory.Aliases.Length,
                primary.Advisory.Provenance.Length,
                candidate.Advisory.Provenance.Length);

            OverrideLogged(_logger, audit, null);
        }
    }

    private void LogPackageOverrides(string advisoryKey, IReadOnlyList<AffectedPackageOverride> overrides)
    {
        if (overrides.Count == 0)
        {
            return;
        }

        foreach (var record in overrides)
        {
            var tags = new KeyValuePair<string, object?>[]
            {
                new("advisory_key", advisoryKey),
                new("package_type", record.Type),
                new("primary_source", FormatSourceLabel(record.PrimarySources)),
                new("suppressed_source", FormatSourceLabel(record.SuppressedSources)),
                new("primary_rank", record.PrimaryRank),
                new("suppressed_rank", record.SuppressedRank),
                new("primary_range_count", record.PrimaryRangeCount),
                new("suppressed_range_count", record.SuppressedRangeCount),
            };

            RangeOverrideCounter.Add(1, tags);

            var audit = new PackageOverrideAudit(
                advisoryKey,
                record.Type,
                record.Identifier,
                record.Platform,
                record.PrimaryRank,
                record.SuppressedRank,
                record.PrimarySources,
                record.SuppressedSources,
                record.PrimaryRangeCount,
                record.SuppressedRangeCount);

            RangeOverrideLogged(_logger, audit, null);
        }
    }

    private void RecordFieldConflicts(string advisoryKey, IReadOnlyList<AdvisoryEntry> ordered)
    {
        if (ordered.Count <= 1)
        {
            return;
        }

        var primary = ordered[0];
        var primarySeverity = NormalizeSeverity(primary.Advisory.Severity);

        for (var i = 1; i < ordered.Count; i++)
        {
            var candidate = ordered[i];
            var candidateSeverity = NormalizeSeverity(candidate.Advisory.Severity);

            if (!string.IsNullOrEmpty(candidateSeverity))
            {
                var reason = string.IsNullOrEmpty(primarySeverity) ? "primary_missing" : "mismatch";
                if (string.IsNullOrEmpty(primarySeverity) || !string.Equals(primarySeverity, candidateSeverity, StringComparison.OrdinalIgnoreCase))
                {
                    RecordConflict(
                        advisoryKey,
                        "severity",
                        reason,
                        primary,
                        candidate,
                        primarySeverity ?? "(none)",
                        candidateSeverity);
                }
            }

            if (candidate.Rank == primary.Rank)
            {
                RecordConflict(
                    advisoryKey,
                    "precedence_tie",
                    "equal_rank",
                    primary,
                    candidate,
                    primary.Rank.ToString(CultureInfo.InvariantCulture),
                    candidate.Rank.ToString(CultureInfo.InvariantCulture));
            }
        }
    }

    private void RecordConflict(
        string advisoryKey,
        string conflictType,
        string reason,
        AdvisoryEntry primary,
        AdvisoryEntry suppressed,
        string? primaryValue,
        string? suppressedValue)
    {
        var tags = new KeyValuePair<string, object?>[]
        {
            new("type", conflictType),
            new("reason", reason),
            new("primary_source", FormatSourceLabel(primary.Sources)),
            new("suppressed_source", FormatSourceLabel(suppressed.Sources)),
            new("primary_rank", primary.Rank),
            new("suppressed_rank", suppressed.Rank),
        };

        ConflictCounter.Add(1, tags);

        var audit = new MergeFieldConflictAudit(
            advisoryKey,
            conflictType,
            reason,
            primary.Sources,
            primary.Rank,
            suppressed.Sources,
            suppressed.Rank,
            primaryValue,
            suppressedValue);

        ConflictLogged(_logger, audit, null);
    }

    private readonly record struct AdvisoryEntry(Advisory Advisory, int Rank)
    {
        public IReadOnlyCollection<string> Sources { get; } = Advisory.Provenance
            .Select(static p => p.Source)
            .Where(static source => !string.IsNullOrWhiteSpace(source))
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToArray();
    }

    private static string? NormalizeSeverity(string? severity)
        => SeverityNormalization.Normalize(severity);

    private static AffectedPackagePrecedenceResolver EnsureResolver(
        AffectedPackagePrecedenceResolver? resolver,
        AdvisoryPrecedenceOptions? options,
        out IReadOnlyDictionary<string, int> precedence)
    {
        precedence = AdvisoryPrecedenceTable.Merge(AdvisoryPrecedenceDefaults.Rankings, options);

        if (resolver is null)
        {
            return new AffectedPackagePrecedenceResolver(precedence);
        }

        if (DictionaryEquals(resolver.Precedence, precedence))
        {
            return resolver;
        }

        return new AffectedPackagePrecedenceResolver(precedence);
    }

    private static bool DictionaryEquals(
        IReadOnlyDictionary<string, int> left,
        IReadOnlyDictionary<string, int> right)
    {
        if (ReferenceEquals(left, right))
        {
            return true;
        }

        if (left.Count != right.Count)
        {
            return false;
        }

        foreach (var (key, value) in left)
        {
            if (!right.TryGetValue(key, out var other) || other != value)
            {
                return false;
            }
        }

        return true;
    }

    private static string FormatSourceLabel(IReadOnlyCollection<string> sources)
    {
        if (sources.Count == 0)
        {
            return "unknown";
        }

        if (sources.Count == 1)
        {
            return sources.First();
        }

        return string.Join('|', sources.OrderBy(static s => s, StringComparer.OrdinalIgnoreCase).Take(3));
    }

    private readonly record struct MergeOverrideAudit(
        string AdvisoryKey,
        IReadOnlyCollection<string> PrimarySources,
        int PrimaryRank,
        IReadOnlyCollection<string> SuppressedSources,
        int SuppressedRank,
        int PrimaryAliasCount,
        int SuppressedAliasCount,
        int PrimaryProvenanceCount,
        int SuppressedProvenanceCount);

    private readonly record struct PackageOverrideAudit(
        string AdvisoryKey,
        string PackageType,
        string Identifier,
        string? Platform,
        int PrimaryRank,
        int SuppressedRank,
        IReadOnlyCollection<string> PrimarySources,
        IReadOnlyCollection<string> SuppressedSources,
        int PrimaryRangeCount,
        int SuppressedRangeCount);

    private readonly record struct MergeFieldConflictAudit(
        string AdvisoryKey,
        string ConflictType,
        string Reason,
        IReadOnlyCollection<string> PrimarySources,
        int PrimaryRank,
        IReadOnlyCollection<string> SuppressedSources,
        int SuppressedRank,
        string? PrimaryValue,
        string? SuppressedValue);
}
