using System.Collections.Immutable;
using System.Globalization;

namespace StellaOps.Vexer.Core;

public sealed class VexConsensusResolver
{
    private readonly IVexConsensusPolicy _policy;

    public VexConsensusResolver(IVexConsensusPolicy policy)
    {
        _policy = policy ?? throw new ArgumentNullException(nameof(policy));
    }

    public VexConsensusResolution Resolve(VexConsensusRequest request)
    {
        if (request is null)
        {
            throw new ArgumentNullException(nameof(request));
        }

        var orderedClaims = request.Claims
            .OrderBy(static claim => claim.ProviderId, StringComparer.Ordinal)
            .ThenBy(static claim => claim.Document.Digest, StringComparer.Ordinal)
            .ThenBy(static claim => claim.Document.SourceUri.ToString(), StringComparer.Ordinal)
            .ToArray();

        var decisions = ImmutableArray.CreateBuilder<VexConsensusDecisionTelemetry>(orderedClaims.Length);
        var acceptedSources = new List<VexConsensusSource>(orderedClaims.Length);
        var conflicts = new List<VexConsensusConflict>();
        var conflictKeys = new HashSet<string>(StringComparer.Ordinal);
        var weightByStatus = new Dictionary<VexClaimStatus, double>();

        foreach (var claim in orderedClaims)
        {
            request.Providers.TryGetValue(claim.ProviderId, out var provider);

            string? rejectionReason = null;
            double weight = 0;
            var included = false;

            if (provider is null)
            {
                rejectionReason = "provider_not_registered";
            }
            else
            {
                weight = NormalizeWeight(_policy.GetProviderWeight(provider));
                if (weight <= 0)
                {
                    rejectionReason = "weight_not_positive";
                }
                else if (!_policy.IsClaimEligible(claim, provider, out rejectionReason))
                {
                    rejectionReason ??= "rejected_by_policy";
                }
                else
                {
                    included = true;
                    TrackStatusWeight(weightByStatus, claim.Status, weight);
                    acceptedSources.Add(new VexConsensusSource(
                        claim.ProviderId,
                        claim.Status,
                        claim.Document.Digest,
                        weight,
                        claim.Justification,
                        claim.Detail,
                        claim.Confidence));
                }
            }

            if (!included)
            {
                var conflict = new VexConsensusConflict(
                    claim.ProviderId,
                    claim.Status,
                    claim.Document.Digest,
                    claim.Justification,
                    claim.Detail,
                    rejectionReason);
                if (conflictKeys.Add(CreateConflictKey(conflict.ProviderId, conflict.DocumentDigest)))
                {
                    conflicts.Add(conflict);
                }
            }

            decisions.Add(new VexConsensusDecisionTelemetry(
                claim.ProviderId,
                claim.Document.Digest,
                claim.Status,
                included,
                weight,
                rejectionReason,
                claim.Justification,
                claim.Detail));
        }

        var consensusStatus = DetermineConsensusStatus(weightByStatus);
        var summary = BuildSummary(weightByStatus, consensusStatus);

        var consensus = new VexConsensus(
            request.VulnerabilityId,
            request.Product,
            consensusStatus,
            request.CalculatedAt,
            acceptedSources,
            AttachConflictDetails(conflicts, acceptedSources, consensusStatus, conflictKeys),
            _policy.Version,
            summary,
            request.PolicyRevisionId,
            request.PolicyDigest);

        return new VexConsensusResolution(consensus, decisions.ToImmutable());
    }

    private static Dictionary<VexClaimStatus, double> TrackStatusWeight(
        Dictionary<VexClaimStatus, double> accumulator,
        VexClaimStatus status,
        double weight)
    {
        if (accumulator.TryGetValue(status, out var current))
        {
            accumulator[status] = current + weight;
        }
        else
        {
            accumulator[status] = weight;
        }

        return accumulator;
    }

    private static double NormalizeWeight(double weight)
    {
        if (double.IsNaN(weight) || double.IsInfinity(weight) || weight <= 0)
        {
            return 0;
        }

        if (weight >= 1)
        {
            return 1;
        }

        return weight;
    }

    private static VexConsensusStatus DetermineConsensusStatus(
        IReadOnlyDictionary<VexClaimStatus, double> weights)
    {
        if (weights.Count == 0)
        {
            return VexConsensusStatus.UnderInvestigation;
        }

        var ordered = weights
            .OrderByDescending(static pair => pair.Value)
            .ThenBy(static pair => pair.Key)
            .ToArray();

        var topStatus = ordered[0].Key;
        var topWeight = ordered[0].Value;
        var totalWeight = ordered.Sum(static pair => pair.Value);
        var remainder = totalWeight - topWeight;

        if (topWeight <= 0)
        {
            return VexConsensusStatus.UnderInvestigation;
        }

        if (topWeight > remainder)
        {
            return topStatus switch
            {
                VexClaimStatus.Affected => VexConsensusStatus.Affected,
                VexClaimStatus.Fixed => VexConsensusStatus.Fixed,
                VexClaimStatus.NotAffected => VexConsensusStatus.NotAffected,
                _ => VexConsensusStatus.UnderInvestigation,
            };
        }

        return VexConsensusStatus.UnderInvestigation;
    }

    private static string BuildSummary(
        IReadOnlyDictionary<VexClaimStatus, double> weights,
        VexConsensusStatus status)
    {
        if (weights.Count == 0)
        {
            return "No eligible claims met policy requirements.";
        }

        var breakdown = string.Join(
            ", ",
            weights
                .OrderByDescending(static pair => pair.Value)
                .ThenBy(static pair => pair.Key)
                .Select(pair => $"{FormatStatus(pair.Key)}={pair.Value.ToString("0.###", CultureInfo.InvariantCulture)}"));

        if (status == VexConsensusStatus.UnderInvestigation)
        {
            return $"No majority consensus; weighted breakdown {breakdown}.";
        }

        return $"{FormatStatus(status)} determined via weighted majority; breakdown {breakdown}.";
    }

    private static List<VexConsensusConflict> AttachConflictDetails(
        List<VexConsensusConflict> conflicts,
        IEnumerable<VexConsensusSource> acceptedSources,
        VexConsensusStatus status,
        HashSet<string> conflictKeys)
    {
        var consensusClaimStatus = status switch
        {
            VexConsensusStatus.Affected => VexClaimStatus.Affected,
            VexConsensusStatus.NotAffected => VexClaimStatus.NotAffected,
            VexConsensusStatus.Fixed => VexClaimStatus.Fixed,
            VexConsensusStatus.UnderInvestigation => (VexClaimStatus?)null,
            VexConsensusStatus.Divergent => (VexClaimStatus?)null,
            _ => null,
        };

        foreach (var source in acceptedSources)
        {
            if (consensusClaimStatus is null || source.Status != consensusClaimStatus.Value)
            {
                var conflict = new VexConsensusConflict(
                    source.ProviderId,
                    source.Status,
                    source.DocumentDigest,
                    source.Justification,
                    source.Detail,
                    consensusClaimStatus is null ? "no_majority" : "status_conflict");

                if (conflictKeys.Add(CreateConflictKey(conflict.ProviderId, conflict.DocumentDigest)))
                {
                    conflicts.Add(conflict);
                }
            }
        }

        return conflicts;
    }

    private static string FormatStatus(VexClaimStatus status)
        => status switch
        {
            VexClaimStatus.Affected => "affected",
            VexClaimStatus.NotAffected => "not_affected",
            VexClaimStatus.Fixed => "fixed",
            VexClaimStatus.UnderInvestigation => "under_investigation",
            _ => status.ToString().ToLowerInvariant(),
        };

    private static string CreateConflictKey(string providerId, string documentDigest)
        => $"{providerId}|{documentDigest}";

    private static string FormatStatus(VexConsensusStatus status)
        => status switch
        {
            VexConsensusStatus.Affected => "affected",
            VexConsensusStatus.NotAffected => "not_affected",
            VexConsensusStatus.Fixed => "fixed",
            VexConsensusStatus.UnderInvestigation => "under_investigation",
            VexConsensusStatus.Divergent => "divergent",
            _ => status.ToString().ToLowerInvariant(),
        };
}

public sealed record VexConsensusRequest(
    string VulnerabilityId,
    VexProduct Product,
    IReadOnlyList<VexClaim> Claims,
    IReadOnlyDictionary<string, VexProvider> Providers,
    DateTimeOffset CalculatedAt,
    string? PolicyRevisionId = null,
    string? PolicyDigest = null);

public sealed record VexConsensusResolution(
    VexConsensus Consensus,
    ImmutableArray<VexConsensusDecisionTelemetry> DecisionLog);

public sealed record VexConsensusDecisionTelemetry(
    string ProviderId,
    string DocumentDigest,
    VexClaimStatus Status,
    bool Included,
    double Weight,
    string? Reason,
    VexJustification? Justification,
    string? Detail);
