using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.Metrics;
using System.Linq;
using Microsoft.Extensions.Logging;

namespace StellaOps.Feedser.Models;

public static class ProvenanceInspector
{
    public static IReadOnlyList<MissingProvenance> FindMissingProvenance(Advisory advisory)
    {
        var results = new List<MissingProvenance>();
        var source = advisory.Provenance.FirstOrDefault()?.Source ?? "unknown";

        if (advisory.Provenance.Length == 0)
        {
            results.Add(new MissingProvenance(source, "advisory", null, ImmutableArray.Create(ProvenanceFieldMasks.Advisory)));
        }

        foreach (var reference in advisory.References)
        {
            if (IsMissing(reference.Provenance))
            {
                results.Add(new MissingProvenance(
                    reference.Provenance.Source ?? source,
                    $"reference:{reference.Url}",
                    reference.Provenance.RecordedAt,
                    NormalizeMask(reference.Provenance.FieldMask, ProvenanceFieldMasks.References)));
            }
        }

        foreach (var package in advisory.AffectedPackages)
        {
            if (package.Provenance.Length == 0)
            {
                results.Add(new MissingProvenance(
                    source,
                    $"package:{package.Identifier}",
                    null,
                    ImmutableArray.Create(ProvenanceFieldMasks.AffectedPackages)));
            }

            foreach (var range in package.VersionRanges)
            {
                ProvenanceDiagnostics.RecordRangePrimitive(range.Provenance.Source ?? source, range);

                if (IsMissing(range.Provenance))
                {
                    results.Add(new MissingProvenance(
                        range.Provenance.Source ?? source,
                        $"range:{package.Identifier}",
                        range.Provenance.RecordedAt,
                        NormalizeMask(range.Provenance.FieldMask, ProvenanceFieldMasks.VersionRanges)));
                }
            }

            foreach (var status in package.Statuses)
            {
                if (IsMissing(status.Provenance))
                {
                    results.Add(new MissingProvenance(
                        status.Provenance.Source ?? source,
                        $"status:{package.Identifier}:{status.Status}",
                        status.Provenance.RecordedAt,
                        NormalizeMask(status.Provenance.FieldMask, ProvenanceFieldMasks.PackageStatuses)));
                }
            }
        }

        foreach (var metric in advisory.CvssMetrics)
        {
            if (IsMissing(metric.Provenance))
            {
                results.Add(new MissingProvenance(
                    metric.Provenance.Source ?? source,
                    $"cvss:{metric.Version}",
                    metric.Provenance.RecordedAt,
                    NormalizeMask(metric.Provenance.FieldMask, ProvenanceFieldMasks.CvssMetrics)));
            }
        }

        return results;
    }

    private static bool IsMissing(AdvisoryProvenance provenance)
    {
        return provenance == AdvisoryProvenance.Empty
            || string.IsNullOrWhiteSpace(provenance.Source)
            || string.IsNullOrWhiteSpace(provenance.Kind);
    }

    private static ImmutableArray<string> NormalizeMask(ImmutableArray<string> mask, string fallback)
    {
        if (mask.IsDefaultOrEmpty)
        {
            return ImmutableArray.Create(fallback);
        }

        return mask;
    }

}

public sealed record MissingProvenance(
    string Source,
    string Component,
    DateTimeOffset? RecordedAt,
    ImmutableArray<string> FieldMask);

public static class ProvenanceDiagnostics
{
    private static readonly Meter Meter = new("StellaOps.Feedser.Models.Provenance");
    private static readonly Counter<long> MissingCounter = Meter.CreateCounter<long>(
        "feedser.provenance.missing",
        unit: "count",
        description: "Number of canonical objects missing provenance metadata.");
    private static readonly Counter<long> RangePrimitiveCounter = Meter.CreateCounter<long>(
        "feedser.range.primitives",
        unit: "count",
        description: "Range coverage by kind, primitive availability, and vendor extensions.");

    private static readonly object SyncRoot = new();
    private static readonly Dictionary<string, DateTimeOffset> EarliestMissing = new(StringComparer.OrdinalIgnoreCase);
    private static readonly HashSet<string> RecordedComponents = new(StringComparer.OrdinalIgnoreCase);

    public static void RecordMissing(
        string source,
        string component,
        DateTimeOffset? recordedAt,
        IReadOnlyList<string>? fieldMask = null)
    {
        if (string.IsNullOrWhiteSpace(source))
        {
            source = "unknown";
        }

        component = string.IsNullOrWhiteSpace(component) ? "unknown" : component.Trim();
        var maskKey = NormalizeMask(fieldMask);

        bool shouldRecord;
        lock (SyncRoot)
        {
            var key = $"{source}|{component}|{maskKey}";
            shouldRecord = RecordedComponents.Add(key);

            if (recordedAt.HasValue)
            {
                if (!EarliestMissing.TryGetValue(source, out var existing) || recordedAt.Value < existing)
                {
                    EarliestMissing[source] = recordedAt.Value;
                }
            }
        }

        if (!shouldRecord)
        {
            return;
        }

        var category = DetermineCategory(component);
        var severity = DetermineSeverity(category);

        var tags = new[]
        {
            new KeyValuePair<string, object?>("source", source),
            new KeyValuePair<string, object?>("component", component),
            new KeyValuePair<string, object?>("category", category),
            new KeyValuePair<string, object?>("severity", severity),
            new KeyValuePair<string, object?>("fieldMask", string.IsNullOrEmpty(maskKey) ? "none" : maskKey),
        };
        MissingCounter.Add(1, tags);
    }

    public static void ReportResumeWindow(string source, DateTimeOffset windowStart, ILogger logger)
    {
        if (string.IsNullOrWhiteSpace(source) || logger is null)
        {
            return;
        }

        DateTimeOffset earliest;
        var hasEntry = false;
        lock (SyncRoot)
        {
            if (EarliestMissing.TryGetValue(source, out earliest))
            {
                hasEntry = true;
                if (windowStart <= earliest)
                {
                    EarliestMissing.Remove(source);
                    var prefix = source + "|";
                    RecordedComponents.RemoveWhere(entry => entry.StartsWith(prefix, StringComparison.OrdinalIgnoreCase));
                }
            }
        }

        if (!hasEntry)
        {
            return;
        }

        if (windowStart <= earliest)
        {
            logger.LogInformation(
                "Resume window starting {WindowStart:o} for {Source} may backfill missing provenance recorded at {Earliest:o}.",
                windowStart,
                source,
                earliest);
        }
        else
        {
            logger.LogInformation(
                "Earliest missing provenance for {Source} remains at {Earliest:o}; current resume window begins at {WindowStart:o}. Consider widening overlap to backfill.",
                source,
                earliest,
                windowStart);
        }
    }

    public static void RecordRangePrimitive(string source, AffectedVersionRange range)
    {
        if (range is null)
        {
            return;
        }

        source = string.IsNullOrWhiteSpace(source) ? "unknown" : source.Trim();

        var primitives = range.Primitives;
        var primitiveKinds = DeterminePrimitiveKinds(primitives);
        var vendorExtensions = primitives?.VendorExtensions?.Count ?? 0;

        var tags = new[]
        {
            new KeyValuePair<string, object?>("source", source),
            new KeyValuePair<string, object?>("rangeKind", string.IsNullOrWhiteSpace(range.RangeKind) ? "unknown" : range.RangeKind),
            new KeyValuePair<string, object?>("primitiveKinds", primitiveKinds),
            new KeyValuePair<string, object?>("hasVendorExtensions", vendorExtensions > 0 ? "true" : "false"),
        };

        RangePrimitiveCounter.Add(1, tags);
    }

    private static string DetermineCategory(string component)
    {
        if (string.IsNullOrWhiteSpace(component))
        {
            return "unknown";
        }

        var index = component.IndexOf(':');
        var category = index > 0 ? component[..index] : component;
        return category.Trim().ToLowerInvariant();
    }

    private static string DetermineSeverity(string category)
        => category switch
        {
            "advisory" => "critical",
            "package" => "high",
            "range" => "high",
            "status" => "medium",
            "cvss" => "medium",
            "reference" => "low",
            _ => "info",
        };

    private static string DeterminePrimitiveKinds(RangePrimitives? primitives)
    {
        return primitives is null ? "none" : primitives.GetCoverageTag();
    }

    private static string NormalizeMask(IReadOnlyList<string>? fieldMask)
    {
        if (fieldMask is not { Count: > 0 })
        {
            return string.Empty;
        }

        if (fieldMask.Count == 1)
        {
            return fieldMask[0];
        }

        var ordered = fieldMask
            .Where(static value => !string.IsNullOrWhiteSpace(value))
            .Select(static value => value.Trim().ToLowerInvariant())
            .Distinct(StringComparer.Ordinal)
            .OrderBy(static value => value, StringComparer.Ordinal)
            .ToArray();

        return string.Join('|', ordered);
    }

}
