using System;
using System.Collections.Immutable;

namespace StellaOps.Policy;

public static class PolicyEvaluation
{
    public static PolicyVerdict EvaluateFinding(PolicyDocument document, PolicyScoringConfig scoringConfig, PolicyFinding finding)
    {
        if (document is null)
        {
            throw new ArgumentNullException(nameof(document));
        }

        if (scoringConfig is null)
        {
            throw new ArgumentNullException(nameof(scoringConfig));
        }

        if (finding is null)
        {
            throw new ArgumentNullException(nameof(finding));
        }

        var severityWeight = scoringConfig.SeverityWeights.TryGetValue(finding.Severity, out var weight)
            ? weight
            : scoringConfig.SeverityWeights.GetValueOrDefault(PolicySeverity.Unknown, 0);

        foreach (var rule in document.Rules)
        {
            if (!RuleMatches(rule, finding))
            {
                continue;
            }

            return BuildVerdict(rule, finding, scoringConfig, severityWeight);
        }

        return PolicyVerdict.CreateBaseline(finding.FindingId, scoringConfig);
    }

    private static PolicyVerdict BuildVerdict(
        PolicyRule rule,
        PolicyFinding finding,
        PolicyScoringConfig config,
        double severityWeight)
    {
        var action = rule.Action;
        var status = MapAction(action);
        var notes = BuildNotes(action);
        var inputs = ImmutableDictionary.CreateBuilder<string, double>(StringComparer.OrdinalIgnoreCase);
        inputs["severityWeight"] = severityWeight;

        double score = severityWeight;
        string? quietedBy = null;
        var quiet = false;

        switch (status)
        {
            case PolicyVerdictStatus.Ignored:
                score = Math.Max(0, severityWeight - config.IgnorePenalty);
                inputs["ignorePenalty"] = config.IgnorePenalty;
                break;
            case PolicyVerdictStatus.Warned:
                score = Math.Max(0, severityWeight - config.WarnPenalty);
                inputs["warnPenalty"] = config.WarnPenalty;
                break;
            case PolicyVerdictStatus.Deferred:
                score = Math.Max(0, severityWeight - (config.WarnPenalty / 2));
                inputs["deferPenalty"] = config.WarnPenalty / 2;
                break;
        }

        if (action.Quiet)
        {
            var quietAllowed = action.RequireVex is not null || action.Type == PolicyActionType.RequireVex;
            if (quietAllowed)
            {
                score = Math.Max(0, score - config.QuietPenalty);
                inputs["quietPenalty"] = config.QuietPenalty;
                quietedBy = rule.Name;
                quiet = true;
            }
            else
            {
                inputs.Remove("ignorePenalty");
                var warnScore = Math.Max(0, severityWeight - config.WarnPenalty);
                inputs["warnPenalty"] = config.WarnPenalty;
                var warnNotes = AppendNote(notes, "Quiet flag ignored: rule must specify requireVex justifications.");

                return new PolicyVerdict(
                    finding.FindingId,
                    PolicyVerdictStatus.Warned,
                    rule.Name,
                    action.Type.ToString(),
                    warnNotes,
                    warnScore,
                    config.Version,
                    inputs.ToImmutable(),
                    QuietedBy: null,
                    Quiet: false);
            }
        }

        return new PolicyVerdict(
            finding.FindingId,
            status,
            rule.Name,
            action.Type.ToString(),
            notes,
            score,
            config.Version,
            inputs.ToImmutable(),
            quietedBy,
            quiet);
    }

    private static bool RuleMatches(PolicyRule rule, PolicyFinding finding)
    {
        if (!rule.Severities.IsDefaultOrEmpty && !rule.Severities.Contains(finding.Severity))
        {
            return false;
        }

        if (!Matches(rule.Environments, finding.Environment))
        {
            return false;
        }

        if (!Matches(rule.Sources, finding.Source))
        {
            return false;
        }

        if (!Matches(rule.Vendors, finding.Vendor))
        {
            return false;
        }

        if (!Matches(rule.Licenses, finding.License))
        {
            return false;
        }

        if (!RuleMatchCriteria(rule.Match, finding))
        {
            return false;
        }

        return true;
    }

    private static bool Matches(ImmutableArray<string> ruleValues, string? candidate)
    {
        if (ruleValues.IsDefaultOrEmpty)
        {
            return true;
        }

        if (string.IsNullOrWhiteSpace(candidate))
        {
            return false;
        }

        return ruleValues.Contains(candidate, StringComparer.OrdinalIgnoreCase);
    }

    private static bool RuleMatchCriteria(PolicyRuleMatchCriteria criteria, PolicyFinding finding)
    {
        if (!criteria.Images.IsDefaultOrEmpty && !ContainsValue(criteria.Images, finding.Image, StringComparer.OrdinalIgnoreCase))
        {
            return false;
        }

        if (!criteria.Repositories.IsDefaultOrEmpty && !ContainsValue(criteria.Repositories, finding.Repository, StringComparer.OrdinalIgnoreCase))
        {
            return false;
        }

        if (!criteria.Packages.IsDefaultOrEmpty && !ContainsValue(criteria.Packages, finding.Package, StringComparer.OrdinalIgnoreCase))
        {
            return false;
        }

        if (!criteria.Purls.IsDefaultOrEmpty && !ContainsValue(criteria.Purls, finding.Purl, StringComparer.OrdinalIgnoreCase))
        {
            return false;
        }

        if (!criteria.Cves.IsDefaultOrEmpty && !ContainsValue(criteria.Cves, finding.Cve, StringComparer.OrdinalIgnoreCase))
        {
            return false;
        }

        if (!criteria.Paths.IsDefaultOrEmpty && !ContainsValue(criteria.Paths, finding.Path, StringComparer.Ordinal))
        {
            return false;
        }

        if (!criteria.LayerDigests.IsDefaultOrEmpty && !ContainsValue(criteria.LayerDigests, finding.LayerDigest, StringComparer.OrdinalIgnoreCase))
        {
            return false;
        }

        if (!criteria.UsedByEntrypoint.IsDefaultOrEmpty)
        {
            var match = false;
            foreach (var tag in criteria.UsedByEntrypoint)
            {
                if (finding.Tags.Contains(tag, StringComparer.OrdinalIgnoreCase))
                {
                    match = true;
                    break;
                }
            }

            if (!match)
            {
                return false;
            }
        }

        return true;
    }

    private static bool ContainsValue(ImmutableArray<string> values, string? candidate, StringComparer comparer)
    {
        if (values.IsDefaultOrEmpty)
        {
            return true;
        }

        if (string.IsNullOrWhiteSpace(candidate))
        {
            return false;
        }

        return values.Contains(candidate, comparer);
    }

    private static PolicyVerdictStatus MapAction(PolicyAction action)
        => action.Type switch
        {
            PolicyActionType.Block => PolicyVerdictStatus.Blocked,
            PolicyActionType.Ignore => PolicyVerdictStatus.Ignored,
            PolicyActionType.Warn => PolicyVerdictStatus.Warned,
            PolicyActionType.Defer => PolicyVerdictStatus.Deferred,
            PolicyActionType.Escalate => PolicyVerdictStatus.Escalated,
            PolicyActionType.RequireVex => PolicyVerdictStatus.RequiresVex,
            _ => PolicyVerdictStatus.Pass,
        };

    private static string? BuildNotes(PolicyAction action)
    {
        if (action.Ignore is { } ignore && !string.IsNullOrWhiteSpace(ignore.Justification))
        {
            return ignore.Justification;
        }

        if (action.Escalate is { } escalate && escalate.MinimumSeverity is { } severity)
        {
            return $"Escalate >= {severity}";
        }

        return null;
    }

    private static string? AppendNote(string? existing, string addition)
        => string.IsNullOrWhiteSpace(existing) ? addition : string.Concat(existing, " | ", addition);
}
