using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;

namespace StellaOps.Policy;

public enum PolicyDocumentFormat
{
    Json,
    Yaml,
}

public sealed record PolicyBindingResult(
    bool Success,
    PolicyDocument Document,
    ImmutableArray<PolicyIssue> Issues,
    PolicyDocumentFormat Format);

public static class PolicyBinder
{
    private static readonly JsonSerializerOptions SerializerOptions = new()
    {
        PropertyNameCaseInsensitive = true,
        ReadCommentHandling = JsonCommentHandling.Skip,
        AllowTrailingCommas = true,
        NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString,
        Converters =
        {
            new JsonStringEnumConverter()
        },
    };

    private static readonly IDeserializer YamlDeserializer = new DeserializerBuilder()
        .WithNamingConvention(CamelCaseNamingConvention.Instance)
        .IgnoreUnmatchedProperties()
        .Build();

    public static PolicyBindingResult Bind(string content, PolicyDocumentFormat format)
    {
        if (string.IsNullOrWhiteSpace(content))
        {
            var issues = ImmutableArray.Create(
                PolicyIssue.Error("policy.empty", "Policy document is empty.", "$"));
            return new PolicyBindingResult(false, PolicyDocument.Empty, issues, format);
        }

        try
        {
            var node = ParseToNode(content, format);
            if (node is not JsonObject obj)
            {
                var issues = ImmutableArray.Create(
                    PolicyIssue.Error("policy.document.invalid", "Policy document must be an object.", "$"));
                return new PolicyBindingResult(false, PolicyDocument.Empty, issues, format);
            }

            var model = obj.Deserialize<PolicyDocumentModel>(SerializerOptions) ?? new PolicyDocumentModel();
            var normalization = PolicyNormalizer.Normalize(model);
            var success = normalization.Issues.All(static issue => issue.Severity != PolicyIssueSeverity.Error);
            return new PolicyBindingResult(success, normalization.Document, normalization.Issues, format);
        }
        catch (JsonException ex)
        {
            var issues = ImmutableArray.Create(
                PolicyIssue.Error("policy.parse.json", $"Failed to parse policy JSON: {ex.Message}", "$"));
            return new PolicyBindingResult(false, PolicyDocument.Empty, issues, format);
        }
        catch (YamlDotNet.Core.YamlException ex)
        {
            var issues = ImmutableArray.Create(
                PolicyIssue.Error("policy.parse.yaml", $"Failed to parse policy YAML: {ex.Message}", "$"));
            return new PolicyBindingResult(false, PolicyDocument.Empty, issues, format);
        }
    }

    public static PolicyBindingResult Bind(Stream stream, PolicyDocumentFormat format, Encoding? encoding = null)
    {
        if (stream is null)
        {
            throw new ArgumentNullException(nameof(stream));
        }

        encoding ??= Encoding.UTF8;
        using var reader = new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks: true, leaveOpen: true);
        var content = reader.ReadToEnd();
        return Bind(content, format);
    }

    private static JsonNode? ParseToNode(string content, PolicyDocumentFormat format)
    {
        return format switch
        {
            PolicyDocumentFormat.Json => JsonNode.Parse(content, documentOptions: new JsonDocumentOptions
            {
                AllowTrailingCommas = true,
                CommentHandling = JsonCommentHandling.Skip,
            }),
            PolicyDocumentFormat.Yaml => ConvertYamlToJsonNode(content),
            _ => throw new ArgumentOutOfRangeException(nameof(format), format, "Unsupported policy document format."),
        };
    }

    private static JsonNode? ConvertYamlToJsonNode(string content)
    {
        var yamlObject = YamlDeserializer.Deserialize<object?>(content);
        return ConvertYamlObject(yamlObject);
    }

    private static JsonNode? ConvertYamlObject(object? value)
    {
        switch (value)
        {
            case null:
                return null;
            case string s:
                return JsonValue.Create(s);
            case bool b:
                return JsonValue.Create(b);
            case sbyte or byte or short or ushort or int or uint or long or ulong or float or double or decimal:
                return JsonValue.Create(Convert.ToDecimal(value, CultureInfo.InvariantCulture));
            case DateTime dt:
                return JsonValue.Create(dt.ToString("O", CultureInfo.InvariantCulture));
            case DateTimeOffset dto:
                return JsonValue.Create(dto.ToString("O", CultureInfo.InvariantCulture));
            case Enum e:
                return JsonValue.Create(e.ToString());
            case IDictionary dictionary:
            {
                var obj = new JsonObject();
                foreach (DictionaryEntry entry in dictionary)
                {
                    if (entry.Key is null)
                    {
                        continue;
                    }

                    var key = Convert.ToString(entry.Key, CultureInfo.InvariantCulture);
                    if (string.IsNullOrWhiteSpace(key))
                    {
                        continue;
                    }

                    obj[key!] = ConvertYamlObject(entry.Value);
                }

                return obj;
            }
            case IEnumerable enumerable:
            {
                var array = new JsonArray();
                foreach (var item in enumerable)
                {
                    array.Add(ConvertYamlObject(item));
                }

                return array;
            }
            default:
                return JsonValue.Create(value.ToString());
        }
    }

    private sealed record PolicyDocumentModel
    {
        [JsonPropertyName("version")]
        public JsonNode? Version { get; init; }

        [JsonPropertyName("description")]
        public string? Description { get; init; }

        [JsonPropertyName("metadata")]
        public Dictionary<string, JsonNode?>? Metadata { get; init; }

        [JsonPropertyName("rules")]
        public List<PolicyRuleModel>? Rules { get; init; }

        [JsonExtensionData]
        public Dictionary<string, JsonElement>? Extensions { get; init; }
    }

    private sealed record PolicyRuleModel
    {
        [JsonPropertyName("id")]
        public string? Identifier { get; init; }

        [JsonPropertyName("name")]
        public string? Name { get; init; }

        [JsonPropertyName("description")]
        public string? Description { get; init; }

        [JsonPropertyName("severity")]
        public List<string>? Severity { get; init; }

        [JsonPropertyName("sources")]
        public List<string>? Sources { get; init; }

        [JsonPropertyName("vendors")]
        public List<string>? Vendors { get; init; }

        [JsonPropertyName("licenses")]
        public List<string>? Licenses { get; init; }

        [JsonPropertyName("tags")]
        public List<string>? Tags { get; init; }

        [JsonPropertyName("environments")]
        public List<string>? Environments { get; init; }

        [JsonPropertyName("images")]
        public List<string>? Images { get; init; }

        [JsonPropertyName("repositories")]
        public List<string>? Repositories { get; init; }

        [JsonPropertyName("packages")]
        public List<string>? Packages { get; init; }

        [JsonPropertyName("purls")]
        public List<string>? Purls { get; init; }

        [JsonPropertyName("cves")]
        public List<string>? Cves { get; init; }

        [JsonPropertyName("paths")]
        public List<string>? Paths { get; init; }

        [JsonPropertyName("layerDigests")]
        public List<string>? LayerDigests { get; init; }

        [JsonPropertyName("usedByEntrypoint")]
        public List<string>? UsedByEntrypoint { get; init; }

        [JsonPropertyName("action")]
        public JsonNode? Action { get; init; }

        [JsonPropertyName("expires")]
        public JsonNode? Expires { get; init; }

        [JsonPropertyName("until")]
        public JsonNode? Until { get; init; }

        [JsonPropertyName("justification")]
        public string? Justification { get; init; }

        [JsonPropertyName("quiet")]
        public bool? Quiet { get; init; }

        [JsonPropertyName("metadata")]
        public Dictionary<string, JsonNode?>? Metadata { get; init; }

        [JsonExtensionData]
        public Dictionary<string, JsonElement>? Extensions { get; init; }
    }

    private sealed class PolicyNormalizer
    {
        private static readonly ImmutableDictionary<string, PolicySeverity> SeverityMap =
            new Dictionary<string, PolicySeverity>(StringComparer.OrdinalIgnoreCase)
            {
                ["critical"] = PolicySeverity.Critical,
                ["high"] = PolicySeverity.High,
                ["medium"] = PolicySeverity.Medium,
                ["moderate"] = PolicySeverity.Medium,
                ["low"] = PolicySeverity.Low,
                ["informational"] = PolicySeverity.Informational,
                ["info"] = PolicySeverity.Informational,
                ["none"] = PolicySeverity.None,
                ["unknown"] = PolicySeverity.Unknown,
            }.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase);

        public static (PolicyDocument Document, ImmutableArray<PolicyIssue> Issues) Normalize(PolicyDocumentModel model)
        {
            var issues = ImmutableArray.CreateBuilder<PolicyIssue>();

            var version = NormalizeVersion(model.Version, issues);
            var metadata = NormalizeMetadata(model.Metadata, "$.metadata", issues);
            var rules = NormalizeRules(model.Rules, issues);

            if (model.Extensions is { Count: > 0 })
            {
                foreach (var pair in model.Extensions)
                {
                    issues.Add(PolicyIssue.Warning(
                        "policy.document.extension",
                        $"Unrecognized document property '{pair.Key}' has been ignored.",
                        $"$.{pair.Key}"));
                }
            }

            var document = new PolicyDocument(
                version ?? PolicySchema.CurrentVersion,
                rules,
                metadata);

            var orderedIssues = SortIssues(issues);
            return (document, orderedIssues);
        }

        private static string? NormalizeVersion(JsonNode? versionNode, ImmutableArray<PolicyIssue>.Builder issues)
        {
            if (versionNode is null)
            {
                issues.Add(PolicyIssue.Warning("policy.version.missing", "Policy version not specified; defaulting to 1.0.", "$.version"));
                return PolicySchema.CurrentVersion;
            }

            if (versionNode is JsonValue value)
            {
                if (value.TryGetValue(out string? versionText))
                {
                    versionText = versionText?.Trim();
                    if (string.IsNullOrEmpty(versionText))
                    {
                        issues.Add(PolicyIssue.Error("policy.version.empty", "Policy version is empty.", "$.version"));
                        return null;
                    }

                    if (IsSupportedVersion(versionText))
                    {
                        return CanonicalizeVersion(versionText);
                    }

                    issues.Add(PolicyIssue.Error("policy.version.unsupported", $"Unsupported policy version '{versionText}'. Expected '{PolicySchema.CurrentVersion}'.", "$.version"));
                    return null;
                }

                if (value.TryGetValue(out double numericVersion))
                {
                    var numericText = numericVersion.ToString("0.0###", CultureInfo.InvariantCulture);
                    if (IsSupportedVersion(numericText))
                    {
                        return CanonicalizeVersion(numericText);
                    }

                    issues.Add(PolicyIssue.Error("policy.version.unsupported", $"Unsupported policy version '{numericText}'.", "$.version"));
                    return null;
                }
            }

            var raw = versionNode.ToJsonString();
            issues.Add(PolicyIssue.Error("policy.version.invalid", $"Policy version must be a string. Received: {raw}", "$.version"));
            return null;
        }

        private static bool IsSupportedVersion(string versionText)
            => string.Equals(versionText, "1", StringComparison.OrdinalIgnoreCase)
               || string.Equals(versionText, "1.0", StringComparison.OrdinalIgnoreCase)
               || string.Equals(versionText, PolicySchema.CurrentVersion, StringComparison.OrdinalIgnoreCase);

        private static string CanonicalizeVersion(string versionText)
            => string.Equals(versionText, "1", StringComparison.OrdinalIgnoreCase)
                ? "1.0"
                : versionText;

        private static ImmutableDictionary<string, string> NormalizeMetadata(
            Dictionary<string, JsonNode?>? metadata,
            string path,
            ImmutableArray<PolicyIssue>.Builder issues)
        {
            if (metadata is null || metadata.Count == 0)
            {
                return ImmutableDictionary<string, string>.Empty;
            }

            var builder = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
            foreach (var pair in metadata)
            {
                var key = pair.Key?.Trim();
                if (string.IsNullOrEmpty(key))
                {
                    issues.Add(PolicyIssue.Warning("policy.metadata.key.empty", "Metadata keys must be non-empty strings.", path));
                    continue;
                }

                var value = ConvertNodeToString(pair.Value);
                builder[key] = value;
            }

            return builder.ToImmutable();
        }

        private static ImmutableArray<PolicyRule> NormalizeRules(
            List<PolicyRuleModel>? rules,
            ImmutableArray<PolicyIssue>.Builder issues)
        {
            if (rules is null || rules.Count == 0)
            {
                issues.Add(PolicyIssue.Error("policy.rules.empty", "At least one rule must be defined.", "$.rules"));
                return ImmutableArray<PolicyRule>.Empty;
            }

            var normalized = new List<(PolicyRule Rule, int Index)>(rules.Count);
            var seenNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

            for (var index = 0; index < rules.Count; index++)
            {
                var model = rules[index];
                var normalizedRule = NormalizeRule(model, index, issues);
                if (normalizedRule is null)
                {
                    continue;
                }

                if (!seenNames.Add(normalizedRule.Name))
                {
                    issues.Add(PolicyIssue.Warning(
                        "policy.rules.duplicateName",
                        $"Duplicate rule name '{normalizedRule.Name}' detected; evaluation order may be ambiguous.",
                        $"$.rules[{index}].name"));
                }

                normalized.Add((normalizedRule, index));
            }

            return normalized
                .OrderBy(static tuple => tuple.Rule.Name, StringComparer.OrdinalIgnoreCase)
                .ThenBy(static tuple => tuple.Rule.Identifier ?? string.Empty, StringComparer.OrdinalIgnoreCase)
                .ThenBy(static tuple => tuple.Index)
                .Select(static tuple => tuple.Rule)
                .ToImmutableArray();
        }

        private static PolicyRule? NormalizeRule(
            PolicyRuleModel model,
            int index,
            ImmutableArray<PolicyIssue>.Builder issues)
        {
            var basePath = $"$.rules[{index}]";

            var name = NormalizeRequiredString(model.Name, $"{basePath}.name", "Rule name", issues);
            if (name is null)
            {
                return null;
            }

            var identifier = NormalizeOptionalString(model.Identifier);
            var description = NormalizeOptionalString(model.Description);
            var metadata = NormalizeMetadata(model.Metadata, $"{basePath}.metadata", issues);

            var severities = NormalizeSeverityList(model.Severity, $"{basePath}.severity", issues);
            var environments = NormalizeStringList(model.Environments, $"{basePath}.environments", issues);
            var sources = NormalizeStringList(model.Sources, $"{basePath}.sources", issues);
            var vendors = NormalizeStringList(model.Vendors, $"{basePath}.vendors", issues);
            var licenses = NormalizeStringList(model.Licenses, $"{basePath}.licenses", issues);
            var tags = NormalizeStringList(model.Tags, $"{basePath}.tags", issues);

            var match = new PolicyRuleMatchCriteria(
                NormalizeStringList(model.Images, $"{basePath}.images", issues),
                NormalizeStringList(model.Repositories, $"{basePath}.repositories", issues),
                NormalizeStringList(model.Packages, $"{basePath}.packages", issues),
                NormalizeStringList(model.Purls, $"{basePath}.purls", issues),
                NormalizeStringList(model.Cves, $"{basePath}.cves", issues),
                NormalizeStringList(model.Paths, $"{basePath}.paths", issues),
                NormalizeStringList(model.LayerDigests, $"{basePath}.layerDigests", issues),
                NormalizeStringList(model.UsedByEntrypoint, $"{basePath}.usedByEntrypoint", issues));

            var action = NormalizeAction(model, basePath, issues);
            var justification = NormalizeOptionalString(model.Justification);
            var expires = NormalizeTemporal(model.Expires ?? model.Until, $"{basePath}.expires", issues);

            if (model.Extensions is { Count: > 0 })
            {
                foreach (var pair in model.Extensions)
                {
                    issues.Add(PolicyIssue.Warning(
                        "policy.rule.extension",
                        $"Unrecognized rule property '{pair.Key}' has been ignored.",
                        $"{basePath}.{pair.Key}"));
                }
            }

            return PolicyRule.Create(
                name,
                action,
                severities,
                environments,
                sources,
                vendors,
                licenses,
                tags,
                match,
                expires,
                justification,
                identifier,
                description,
                metadata);
        }

        private static PolicyAction NormalizeAction(
            PolicyRuleModel model,
            string basePath,
            ImmutableArray<PolicyIssue>.Builder issues)
        {
            var actionNode = model.Action;
            var quiet = model.Quiet ?? false;
            if (!quiet && model.Extensions is not null && model.Extensions.TryGetValue("quiet", out var quietExtension) && quietExtension.ValueKind == JsonValueKind.True)
            {
                quiet = true;
            }
            string? justification = NormalizeOptionalString(model.Justification);
            DateTimeOffset? until = NormalizeTemporal(model.Until, $"{basePath}.until", issues);
            DateTimeOffset? expires = NormalizeTemporal(model.Expires, $"{basePath}.expires", issues);

            var effectiveUntil = until ?? expires;

            if (actionNode is null)
            {
                issues.Add(PolicyIssue.Error("policy.action.missing", "Rule action is required.", $"{basePath}.action"));
                return new PolicyAction(PolicyActionType.Block, null, null, null, Quiet: false);
            }

            string? actionType = null;
            JsonObject? actionObject = null;

            switch (actionNode)
            {
                case JsonValue value when value.TryGetValue(out string? text):
                    actionType = text;
                    break;
                case JsonValue value when value.TryGetValue(out bool booleanValue):
                    actionType = booleanValue ? "block" : "ignore";
                    break;
                case JsonObject obj:
                    actionObject = obj;
                    if (obj.TryGetPropertyValue("type", out var typeNode) && typeNode is JsonValue typeValue && typeValue.TryGetValue(out string? typeText))
                    {
                        actionType = typeText;
                    }
                    else
                    {
                        issues.Add(PolicyIssue.Error("policy.action.type", "Action object must contain a 'type' property.", $"{basePath}.action.type"));
                    }

                    if (obj.TryGetPropertyValue("quiet", out var quietNode) && quietNode is JsonValue quietValue && quietValue.TryGetValue(out bool quietFlag))
                    {
                        quiet = quietFlag;
                    }

                    if (obj.TryGetPropertyValue("until", out var untilNode))
                    {
                        effectiveUntil ??= NormalizeTemporal(untilNode, $"{basePath}.action.until", issues);
                    }

                    if (obj.TryGetPropertyValue("justification", out var justificationNode) && justificationNode is JsonValue justificationValue && justificationValue.TryGetValue(out string? justificationText))
                    {
                        justification = NormalizeOptionalString(justificationText);
                    }

                    break;
                default:
                    actionType = actionNode.ToString();
                    break;
            }

            if (string.IsNullOrWhiteSpace(actionType))
            {
                issues.Add(PolicyIssue.Error("policy.action.type", "Action type is required.", $"{basePath}.action"));
                return new PolicyAction(PolicyActionType.Block, null, null, null, Quiet: quiet);
            }

            actionType = actionType.Trim();
            var (type, typeIssues) = MapActionType(actionType, $"{basePath}.action");
            foreach (var issue in typeIssues)
            {
                issues.Add(issue);
            }

            PolicyIgnoreOptions? ignoreOptions = null;
            PolicyEscalateOptions? escalateOptions = null;
            PolicyRequireVexOptions? requireVexOptions = null;

            if (type == PolicyActionType.Ignore)
            {
                ignoreOptions = new PolicyIgnoreOptions(effectiveUntil, justification);
            }
            else if (type == PolicyActionType.Escalate)
            {
                escalateOptions = NormalizeEscalateOptions(actionObject, $"{basePath}.action", issues);
            }
            else if (type == PolicyActionType.RequireVex)
            {
                requireVexOptions = NormalizeRequireVexOptions(actionObject, $"{basePath}.action", issues);
            }

            return new PolicyAction(type, ignoreOptions, escalateOptions, requireVexOptions, quiet);
        }

        private static (PolicyActionType Type, ImmutableArray<PolicyIssue> Issues) MapActionType(string value, string path)
        {
            var issues = ImmutableArray<PolicyIssue>.Empty;
            var lower = value.ToLowerInvariant();
            return lower switch
            {
                "block" or "fail" or "deny" => (PolicyActionType.Block, issues),
                "ignore" or "mute" => (PolicyActionType.Ignore, issues),
                "warn" or "warning" => (PolicyActionType.Warn, issues),
                "defer" => (PolicyActionType.Defer, issues),
                "escalate" => (PolicyActionType.Escalate, issues),
                "requirevex" or "require_vex" or "require-vex" => (PolicyActionType.RequireVex, issues),
                _ => (PolicyActionType.Block, ImmutableArray.Create(PolicyIssue.Warning(
                    "policy.action.unknown",
                    $"Unknown action '{value}' encountered. Defaulting to 'block'.",
                    path))),
            };
        }

        private static PolicyEscalateOptions? NormalizeEscalateOptions(
            JsonObject? actionObject,
            string path,
            ImmutableArray<PolicyIssue>.Builder issues)
        {
            if (actionObject is null)
            {
                return null;
            }

            PolicySeverity? minSeverity = null;
            bool requireKev = false;
            double? minEpss = null;

            if (actionObject.TryGetPropertyValue("severity", out var severityNode) && severityNode is JsonValue severityValue && severityValue.TryGetValue(out string? severityText))
            {
                if (SeverityMap.TryGetValue(severityText ?? string.Empty, out var mapped))
                {
                    minSeverity = mapped;
                }
                else
                {
                    issues.Add(PolicyIssue.Warning("policy.action.escalate.severity", $"Unknown escalate severity '{severityText}'.", $"{path}.severity"));
                }
            }

            if (actionObject.TryGetPropertyValue("kev", out var kevNode) && kevNode is JsonValue kevValue && kevValue.TryGetValue(out bool kevFlag))
            {
                requireKev = kevFlag;
            }

            if (actionObject.TryGetPropertyValue("epss", out var epssNode))
            {
                var parsed = ParseDouble(epssNode, $"{path}.epss", issues);
                if (parsed is { } epssValue)
                {
                    if (epssValue < 0 || epssValue > 1)
                    {
                        issues.Add(PolicyIssue.Warning("policy.action.escalate.epssRange", "EPS score must be between 0 and 1.", $"{path}.epss"));
                    }
                    else
                    {
                        minEpss = epssValue;
                    }
                }
            }

            return new PolicyEscalateOptions(minSeverity, requireKev, minEpss);
        }

        private static PolicyRequireVexOptions? NormalizeRequireVexOptions(
            JsonObject? actionObject,
            string path,
            ImmutableArray<PolicyIssue>.Builder issues)
        {
            if (actionObject is null)
            {
                return null;
            }

            var vendors = ImmutableArray<string>.Empty;
            var justifications = ImmutableArray<string>.Empty;

            if (actionObject.TryGetPropertyValue("vendors", out var vendorsNode))
            {
                vendors = NormalizeJsonStringArray(vendorsNode, $"{path}.vendors", issues);
            }

            if (actionObject.TryGetPropertyValue("justifications", out var justificationsNode))
            {
                justifications = NormalizeJsonStringArray(justificationsNode, $"{path}.justifications", issues);
            }

            return new PolicyRequireVexOptions(vendors, justifications);
        }

        private static ImmutableArray<string> NormalizeStringList(
            List<string>? values,
            string path,
            ImmutableArray<PolicyIssue>.Builder issues)
        {
            if (values is null || values.Count == 0)
            {
                return ImmutableArray<string>.Empty;
            }

            var builder = ImmutableHashSet.CreateBuilder<string>(StringComparer.OrdinalIgnoreCase);
            foreach (var value in values)
            {
                var normalized = NormalizeOptionalString(value);
                if (string.IsNullOrEmpty(normalized))
                {
                    issues.Add(PolicyIssue.Warning("policy.list.blank", $"Blank entry detected; ignoring value at {path}.", path));
                    continue;
                }

                builder.Add(normalized);
            }

            return builder.ToImmutable()
                .OrderBy(static item => item, StringComparer.OrdinalIgnoreCase)
                .ToImmutableArray();
        }

        private static ImmutableArray<PolicySeverity> NormalizeSeverityList(
            List<string>? values,
            string path,
            ImmutableArray<PolicyIssue>.Builder issues)
        {
            if (values is null || values.Count == 0)
            {
                return ImmutableArray<PolicySeverity>.Empty;
            }

            var builder = ImmutableArray.CreateBuilder<PolicySeverity>();
            foreach (var value in values)
            {
                var normalized = NormalizeOptionalString(value);
                if (string.IsNullOrEmpty(normalized))
                {
                    issues.Add(PolicyIssue.Warning("policy.severity.blank", "Blank severity was ignored.", path));
                    continue;
                }

                if (SeverityMap.TryGetValue(normalized, out var severity))
                {
                    builder.Add(severity);
                }
                else
                {
                    issues.Add(PolicyIssue.Error("policy.severity.invalid", $"Unknown severity '{value}'.", path));
                }
            }

            return builder.Distinct().OrderBy(static sev => sev).ToImmutableArray();
        }

        private static ImmutableArray<string> NormalizeJsonStringArray(
            JsonNode? node,
            string path,
            ImmutableArray<PolicyIssue>.Builder issues)
        {
            if (node is null)
            {
                return ImmutableArray<string>.Empty;
            }

            if (node is JsonArray array)
            {
                var values = new List<string>(array.Count);
                foreach (var element in array)
                {
                    var text = ConvertNodeToString(element);
                    if (string.IsNullOrWhiteSpace(text))
                    {
                        issues.Add(PolicyIssue.Warning("policy.list.blank", $"Blank entry detected; ignoring value at {path}.", path));
                    }
                    else
                    {
                        values.Add(text);
                    }
                }

                return values
                    .Distinct(StringComparer.OrdinalIgnoreCase)
                    .OrderBy(static entry => entry, StringComparer.OrdinalIgnoreCase)
                    .ToImmutableArray();
            }

            var single = ConvertNodeToString(node);
            return ImmutableArray.Create(single);
        }

        private static double? ParseDouble(JsonNode? node, string path, ImmutableArray<PolicyIssue>.Builder issues)
        {
            if (node is null)
            {
                return null;
            }

            if (node is JsonValue value)
            {
                if (value.TryGetValue(out double numeric))
                {
                    return numeric;
                }

                if (value.TryGetValue(out string? text) && double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out numeric))
                {
                    return numeric;
                }
            }

            issues.Add(PolicyIssue.Warning("policy.number.invalid", $"Value '{node.ToJsonString()}' is not a valid number.", path));
            return null;
        }

        private static DateTimeOffset? NormalizeTemporal(JsonNode? node, string path, ImmutableArray<PolicyIssue>.Builder issues)
        {
            if (node is null)
            {
                return null;
            }

            if (node is JsonValue value)
            {
                if (value.TryGetValue(out DateTimeOffset dto))
                {
                    return dto;
                }

                if (value.TryGetValue(out DateTime dt))
                {
                    return new DateTimeOffset(DateTime.SpecifyKind(dt, DateTimeKind.Utc));
                }

                if (value.TryGetValue(out string? text))
                {
                    if (DateTimeOffset.TryParse(text, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var parsed))
                    {
                        return parsed;
                    }

                    if (DateTime.TryParse(text, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var parsedDate))
                    {
                        return new DateTimeOffset(parsedDate);
                    }
                }
            }

            issues.Add(PolicyIssue.Warning("policy.date.invalid", $"Value '{node.ToJsonString()}' is not a valid ISO-8601 timestamp.", path));
            return null;
        }

        private static string? NormalizeRequiredString(
            string? value,
            string path,
            string fieldDescription,
            ImmutableArray<PolicyIssue>.Builder issues)
        {
            var normalized = NormalizeOptionalString(value);
            if (!string.IsNullOrEmpty(normalized))
            {
                return normalized;
            }

            issues.Add(PolicyIssue.Error(
                "policy.required",
                $"{fieldDescription} is required.",
                path));
            return null;
        }

        private static string? NormalizeOptionalString(string? value)
        {
            if (string.IsNullOrWhiteSpace(value))
            {
                return null;
            }

            return value.Trim();
        }

        private static string ConvertNodeToString(JsonNode? node)
        {
            if (node is null)
            {
                return string.Empty;
            }

            return node switch
            {
                JsonValue value when value.TryGetValue(out string? text) => text ?? string.Empty,
                JsonValue value when value.TryGetValue(out bool boolean) => boolean ? "true" : "false",
                JsonValue value when value.TryGetValue(out double numeric) => numeric.ToString(CultureInfo.InvariantCulture),
                JsonObject obj => obj.ToJsonString(),
                JsonArray array => array.ToJsonString(),
                _ => node.ToJsonString(),
            };
        }

        private static ImmutableArray<PolicyIssue> SortIssues(ImmutableArray<PolicyIssue>.Builder issues)
        {
            return issues.ToImmutable()
                .OrderBy(static issue => issue.Severity switch
                {
                    PolicyIssueSeverity.Error => 0,
                    PolicyIssueSeverity.Warning => 1,
                    _ => 2,
                })
                .ThenBy(static issue => issue.Path, StringComparer.Ordinal)
                .ThenBy(static issue => issue.Code, StringComparer.Ordinal)
                .ToImmutableArray();
        }
    }
}
