// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0

using System.Numerics;
using System.Text.RegularExpressions;
using GFramework.Game.Abstractions.Config;

namespace GFramework.Game.Config;

/// <summary>
///     提供 YAML 配置文件与 JSON Schema 之间的最小运行时校验能力。
///     该校验器与当前配置生成器、VS Code 工具支持的 schema 子集保持一致，
///     并通过递归遍历方式覆盖嵌套对象、对象数组、标量数组与深层 enum / 引用约束。
///     当前共享子集额外支持 <c>multipleOf</c>、<c>uniqueItems</c>、
///     <c>contains</c> / <c>minContains</c> / <c>maxContains</c>、
///     <c>minProperties</c>、<c>maxProperties</c>、<c>dependentRequired</c>、
///     <c>dependentSchemas</c>、<c>allOf</c>、object-focused <c>if</c> / <c>then</c> / <c>else</c>
///     与稳定字符串 <c>format</c> 子集，让数值步进、数组去重、数组匹配计数、
///     对象属性数量、对象内字段依赖、条件对象子 schema、对象组合约束与条件分支约束
///     在运行时与生成器 / 工具侧保持一致。
/// </summary>
internal static partial class YamlConfigSchemaValidator
{
    // The runtime intentionally uses the same culture-invariant regex semantics as the
    // JS tooling so grouping and backreferences behave consistently across environments.
    private const RegexOptions SupportedPatternRegexOptions = RegexOptions.CultureInvariant;
    private const string SupportedStringFormatNames = "'date', 'date-time', 'duration', 'email', 'time', 'uri', 'uuid'";
    private static readonly TimeSpan SupportedFormatRegexTimeout = TimeSpan.FromSeconds(1);

    private static readonly Regex ExactDecimalPattern = new(
        @"^(?<sign>[+-]?)(?:(?<integer>\d+)(?:\.(?<fraction>\d*))?|\.(?<fractionOnly>\d+))(?:[eE](?<exponent>[+-]?\d+))?$",
        RegexOptions.CultureInvariant | RegexOptions.Compiled,
        SupportedFormatRegexTimeout);

    private static readonly Regex SupportedEmailFormatRegex = new(
        @"^[^@\s]+@[^@\s]+\.[^@\s]+$",
        RegexOptions.CultureInvariant | RegexOptions.Compiled,
        SupportedFormatRegexTimeout);

    private static readonly Regex SupportedDateFormatRegex = new(
        @"^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})$",
        RegexOptions.CultureInvariant | RegexOptions.Compiled,
        SupportedFormatRegexTimeout);

    private static readonly Regex SupportedDateTimeFormatRegex = new(
        @"^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})T(?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2})(?<fraction>\.\d+)?(?<offset>Z|[+-]\d{2}:\d{2})$",
        RegexOptions.CultureInvariant | RegexOptions.Compiled,
        SupportedFormatRegexTimeout);

    private static readonly Regex SupportedDurationFormatRegex = new(
        @"^P(?:(?<days>\d+)D)?(?:T(?:(?<hours>\d+)H)?(?:(?<minutes>\d+)M)?(?:(?<seconds>\d+(?:\.\d+)?)S)?)?$",
        RegexOptions.CultureInvariant | RegexOptions.Compiled,
        SupportedFormatRegexTimeout);

    private static readonly Regex SupportedTimeFormatRegex = new(
        @"^(?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2})(?<fraction>\.\d+)?(?<offset>Z|[+-]\d{2}:\d{2})$",
        RegexOptions.CultureInvariant | RegexOptions.Compiled,
        SupportedFormatRegexTimeout);

    private static readonly Regex SupportedUriSchemeRegex = new(
        @"^[A-Za-z][A-Za-z0-9+\.-]*:",
        RegexOptions.CultureInvariant | RegexOptions.Compiled,
        SupportedFormatRegexTimeout);

    /// <summary>
    ///     从磁盘加载并解析一个 JSON Schema 文件。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <param name="cancellationToken">取消令牌。</param>
    /// <returns>解析后的 schema 模型。</returns>
    /// <exception cref="ArgumentException">当 <paramref name="tableName" /> 为空时抛出。</exception>
    /// <exception cref="ArgumentException">当 <paramref name="schemaPath" /> 为空时抛出。</exception>
    /// <exception cref="ConfigLoadException">当 schema 文件不存在或内容非法时抛出。</exception>
    internal static async Task<YamlConfigSchema> LoadAsync(
        string tableName,
        string schemaPath,
        CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(tableName))
        {
            throw new ArgumentException("Table name cannot be null or whitespace.", nameof(tableName));
        }

        if (string.IsNullOrWhiteSpace(schemaPath))
        {
            throw new ArgumentException("Schema path cannot be null or whitespace.", nameof(schemaPath));
        }

        if (!File.Exists(schemaPath))
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaFileNotFound,
                tableName,
                $"Schema file '{schemaPath}' was not found.",
                schemaPath: schemaPath);
        }

        string schemaText;
        try
        {
            schemaText = await File.ReadAllTextAsync(schemaPath, cancellationToken).ConfigureAwait(false);
        }
        catch (Exception exception)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaReadFailed,
                tableName,
                $"Failed to read schema file '{schemaPath}'.",
                schemaPath: schemaPath,
                innerException: exception);
        }

        return ParseLoadedSchema(tableName, schemaPath, schemaText);
    }

    /// <summary>
    ///     从磁盘同步加载并解析一个 JSON Schema 文件。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <returns>解析后的 schema 模型。</returns>
    /// <exception cref="ArgumentException">当 <paramref name="tableName" /> 为空时抛出。</exception>
    /// <exception cref="ArgumentException">当 <paramref name="schemaPath" /> 为空时抛出。</exception>
    /// <exception cref="ConfigLoadException">当 schema 文件不存在或内容非法时抛出。</exception>
    internal static YamlConfigSchema Load(
        string tableName,
        string schemaPath)
    {
        if (string.IsNullOrWhiteSpace(tableName))
        {
            throw new ArgumentException("Table name cannot be null or whitespace.", nameof(tableName));
        }

        if (string.IsNullOrWhiteSpace(schemaPath))
        {
            throw new ArgumentException("Schema path cannot be null or whitespace.", nameof(schemaPath));
        }

        if (!File.Exists(schemaPath))
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaFileNotFound,
                tableName,
                $"Schema file '{schemaPath}' was not found.",
                schemaPath: schemaPath);
        }

        string schemaText;
        try
        {
            schemaText = File.ReadAllText(schemaPath);
        }
        catch (Exception exception)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaReadFailed,
                tableName,
                $"Failed to read schema file '{schemaPath}'.",
                schemaPath: schemaPath,
                innerException: exception);
        }

        return ParseLoadedSchema(tableName, schemaPath, schemaText);
    }

    /// <summary>
    ///     使用已解析的 schema 校验 YAML 文本。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schema">已解析的 schema 模型。</param>
    /// <param name="yamlPath">YAML 文件路径，仅用于诊断信息。</param>
    /// <param name="yamlText">YAML 文本内容。</param>
    /// <exception cref="ArgumentNullException">当参数为空时抛出。</exception>
    /// <exception cref="ConfigLoadException">当 YAML 内容与 schema 不匹配时抛出。</exception>
    internal static void Validate(
        string tableName,
        YamlConfigSchema schema,
        string yamlPath,
        string yamlText)
    {
        ValidateCore(tableName, schema, yamlPath, yamlText, references: null);
    }

    /// <summary>
    ///     使用已解析的 schema 校验 YAML 文本，并提取声明过的跨表引用。
    ///     该方法让结构校验与引用采集共享同一份 YAML 解析结果，避免加载器重复解析同一文件。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schema">已解析的 schema 模型。</param>
    /// <param name="yamlPath">YAML 文件路径，仅用于诊断信息。</param>
    /// <param name="yamlText">YAML 文本内容。</param>
    /// <returns>当前 YAML 文件中声明的跨表引用集合。</returns>
    /// <exception cref="ArgumentNullException">当参数为空时抛出。</exception>
    /// <exception cref="ConfigLoadException">当 YAML 内容与 schema 不匹配时抛出。</exception>
    internal static IReadOnlyList<YamlConfigReferenceUsage> ValidateAndCollectReferences(
        string tableName,
        YamlConfigSchema schema,
        string yamlPath,
        string yamlText)
    {
        var references = new List<YamlConfigReferenceUsage>();
        ValidateCore(tableName, schema, yamlPath, yamlText, references);
        return references;
    }

    /// <summary>
    ///     执行共享的 YAML 结构校验流程，并按需收集跨表引用。
    ///     这样 <see cref="Validate" /> 可以复用同一条校验链路，同时避免为“不关心引用结果”的调用方分配临时列表。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schema">已解析的 schema 模型。</param>
    /// <param name="yamlPath">YAML 文件路径，仅用于诊断信息。</param>
    /// <param name="yamlText">YAML 文本内容。</param>
    /// <param name="references">可选的跨表引用收集器；为 <see langword="null" /> 时只做结构校验。</param>
    /// <exception cref="ArgumentNullException">当参数为空时抛出。</exception>
    /// <exception cref="ConfigLoadException">当 YAML 内容与 schema 不匹配时抛出。</exception>
    private static void ValidateCore(
        string tableName,
        YamlConfigSchema schema,
        string yamlPath,
        string yamlText,
        ICollection<YamlConfigReferenceUsage>? references)
    {
        if (string.IsNullOrWhiteSpace(tableName))
        {
            throw new ArgumentException("Table name cannot be null or whitespace.", nameof(tableName));
        }

        ArgumentNullException.ThrowIfNull(schema);
        ArgumentNullException.ThrowIfNull(yamlPath);
        ArgumentNullException.ThrowIfNull(yamlText);

        YamlStream yamlStream = new();
        try
        {
            using var reader = new StringReader(yamlText);
            yamlStream.Load(reader);
        }
        catch (Exception exception)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.YamlParseFailed,
                tableName,
                $"Config file '{yamlPath}' could not be parsed as YAML before schema validation.",
                yamlPath: yamlPath,
                schemaPath: schema.SchemaPath,
                innerException: exception);
        }

        if (yamlStream.Documents.Count != 1)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.InvalidYamlDocument,
                tableName,
                $"Config file '{yamlPath}' must contain exactly one YAML document.",
                yamlPath: yamlPath,
                schemaPath: schema.SchemaPath);
        }

        ValidateNode(tableName, yamlPath, string.Empty, yamlStream.Documents[0].RootNode, schema.RootNode, references);
    }

    /// <summary>
    ///     解析已读取到内存中的 schema 文本，并构造运行时最小模型。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径，仅用于诊断信息。</param>
    /// <param name="schemaText">Schema 文本内容。</param>
    /// <returns>解析后的 schema 模型。</returns>
    private static YamlConfigSchema ParseLoadedSchema(
        string tableName,
        string schemaPath,
        string schemaText)
    {
        try
        {
            using var document = JsonDocument.Parse(schemaText);
            var root = document.RootElement;
            var rootNode = ParseNode(tableName, schemaPath, "<root>", root, isRoot: true);
            if (rootNode.NodeType != YamlConfigSchemaPropertyType.Object)
            {
                throw ConfigLoadExceptionFactory.Create(
                    ConfigLoadFailureKind.SchemaUnsupported,
                    tableName,
                    $"Schema file '{schemaPath}' must declare a root object schema.",
                    schemaPath: schemaPath);
            }

            var referencedTableNames = new HashSet<string>(StringComparer.Ordinal);
            CollectReferencedTableNames(rootNode, referencedTableNames);
            // Preserve a deterministic dependency order so hot-reload bookkeeping and tests
            // do not depend on HashSet enumeration details.
            var orderedReferencedTableNames = referencedTableNames
                .OrderBy(static name => name, StringComparer.Ordinal)
                .ToArray();

            return new YamlConfigSchema(schemaPath, rootNode, orderedReferencedTableNames);
        }
        catch (JsonException exception)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaInvalidJson,
                tableName,
                $"Schema file '{schemaPath}' contains invalid JSON.",
                schemaPath: schemaPath,
                innerException: exception);
        }
    }

    /// <summary>
    ///     递归解析 schema 节点，使运行时只保留校验真正需要的最小结构信息。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <param name="propertyPath">当前节点的逻辑属性路径。</param>
    /// <param name="element">Schema JSON 节点。</param>
    /// <param name="isRoot">是否为根节点。</param>
    /// <returns>可用于运行时校验的节点模型。</returns>
    private static YamlConfigSchemaNode ParseNode(
        string tableName,
        string schemaPath,
        string propertyPath,
        JsonElement element,
        bool isRoot = false)
    {
        ValidateUnsupportedCombinatorKeywords(tableName, schemaPath, propertyPath, element);
        ValidateUnsupportedOpenObjectKeywords(tableName, schemaPath, propertyPath, element);
        ValidateUnsupportedArrayShapeKeywords(tableName, schemaPath, propertyPath, element);
        var typeName = ResolveNodeTypeName(tableName, schemaPath, propertyPath, element);
        var referenceTableName = TryGetReferenceTableName(tableName, schemaPath, propertyPath, element);
        ValidateObjectOnlyKeywords(tableName, schemaPath, propertyPath, element, typeName);

        var parsedNode = CreateParsedNodeForType(
            tableName,
            schemaPath,
            propertyPath,
            element,
            typeName,
            referenceTableName,
            isRoot);
        return parsedNode.WithNegatedSchemaNode(ParseNegatedSchemaNode(tableName, schemaPath, propertyPath, element));
    }

    /// <summary>
    ///     显式拒绝当前共享子集中尚未支持、且会改变生成类型形状的组合关键字。
    ///     这样 Runtime / Generator / Tooling 会对同一份 schema 给出一致失败，
    ///     而不是默默忽略 <c>oneOf</c> / <c>anyOf</c> 造成接受范围漂移。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <param name="propertyPath">当前节点的逻辑属性路径。</param>
    /// <param name="element">当前 schema 节点。</param>
    private static void ValidateUnsupportedCombinatorKeywords(
        string tableName,
        string schemaPath,
        string propertyPath,
        JsonElement element)
    {
        if (TryGetUnsupportedCombinatorKeywordName(element) is not { } keywordName)
        {
            return;
        }

        throw ConfigLoadExceptionFactory.Create(
            ConfigLoadFailureKind.SchemaUnsupported,
            tableName,
            $"Property '{propertyPath}' in schema file '{schemaPath}' declares unsupported combinator keyword '{keywordName}'. " +
            "The current config schema subset does not support combinators that can change generated type shape.",
            schemaPath: schemaPath,
            displayPath: GetDiagnosticPath(propertyPath));
    }

    /// <summary>
    ///     显式拒绝当前共享子集中尚未支持的开放对象关键字形状。
    ///     当前配置系统默认采用闭合对象字段集；这里只接受显式重复该语义的
    ///     <c>additionalProperties: false</c>，并继续拒绝
    ///     <c>patternProperties</c>、<c>propertyNames</c> 与
    ///     <c>unevaluatedProperties</c> 这类会重新打开对象形状的关键字。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <param name="propertyPath">当前节点的逻辑属性路径。</param>
    /// <param name="element">当前 schema 节点。</param>
    private static void ValidateUnsupportedOpenObjectKeywords(
        string tableName,
        string schemaPath,
        string propertyPath,
        JsonElement element)
    {
        if (TryGetUnsupportedOpenObjectKeywordName(element) is not { } keywordName)
        {
            return;
        }

        throw ConfigLoadExceptionFactory.Create(
            ConfigLoadFailureKind.SchemaUnsupported,
            tableName,
            $"Property '{propertyPath}' in schema file '{schemaPath}' uses unsupported '{keywordName}' metadata. " +
            "The current config schema subset only accepts 'additionalProperties: false' and rejects keywords that reopen object shapes so fields remain closed and strongly typed.",
            schemaPath: schemaPath,
            displayPath: GetDiagnosticPath(propertyPath));
    }

    /// <summary>
    ///     显式拒绝当前共享子集中尚未支持的数组形状关键字。
    ///     当前配置系统只接受单个 object-valued <c>items</c> schema，
    ///     并继续拒绝 tuple / open-array 关键字，避免 Runtime / Generator / Tooling
    ///     对数组元素形状产生静默漂移。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <param name="propertyPath">当前节点的逻辑属性路径。</param>
    /// <param name="element">当前 schema 节点。</param>
    private static void ValidateUnsupportedArrayShapeKeywords(
        string tableName,
        string schemaPath,
        string propertyPath,
        JsonElement element)
    {
        if (TryGetUnsupportedArrayShapeKeywordName(element) is not { } keywordName)
        {
            return;
        }

        throw ConfigLoadExceptionFactory.Create(
            ConfigLoadFailureKind.SchemaUnsupported,
            tableName,
            $"Property '{propertyPath}' in schema file '{schemaPath}' uses unsupported '{keywordName}' metadata. " +
            "The current config schema subset only accepts one object-valued 'items' schema and rejects tuple or open-array keywords that can change item shape across Runtime, Generator, and Tooling.",
            schemaPath: schemaPath,
            displayPath: GetDiagnosticPath(propertyPath));
    }

    /// <summary>
    ///     返回当前节点声明的首个未支持组合关键字。
    /// </summary>
    /// <param name="element">当前 schema 节点。</param>
    /// <returns>命中的关键字名称；未声明时返回空。</returns>
    private static string? TryGetUnsupportedCombinatorKeywordName(JsonElement element)
    {
        return element.TryGetProperty("oneOf", out _) ? "oneOf" :
            element.TryGetProperty("anyOf", out _) ? "anyOf" :
            null;
    }

    /// <summary>
    ///     返回当前节点声明的首个未支持开放对象关键字。
    /// </summary>
    /// <param name="element">当前 schema 节点。</param>
    /// <returns>命中的关键字名称；未声明时返回空。</returns>
    private static string? TryGetUnsupportedOpenObjectKeywordName(JsonElement element)
    {
        if (element.TryGetProperty("additionalProperties", out var additionalPropertiesElement) &&
            additionalPropertiesElement.ValueKind != JsonValueKind.False)
        {
            return "additionalProperties";
        }

        return element.TryGetProperty("patternProperties", out _) ? "patternProperties" :
            element.TryGetProperty("propertyNames", out _) ? "propertyNames" :
            element.TryGetProperty("unevaluatedProperties", out _) ? "unevaluatedProperties" :
            null;
    }

    /// <summary>
    ///     返回当前节点声明的首个未支持数组形状关键字。
    /// </summary>
    /// <param name="element">当前 schema 节点。</param>
    /// <returns>命中的关键字名称；未声明时返回空。</returns>
    private static string? TryGetUnsupportedArrayShapeKeywordName(JsonElement element)
    {
        return element.TryGetProperty("prefixItems", out _) ? "prefixItems" :
            element.TryGetProperty("additionalItems", out _) ? "additionalItems" :
            element.TryGetProperty("unevaluatedItems", out _) ? "unevaluatedItems" :
            null;
    }

    /// <summary>
    ///     解析 schema 节点声明的类型名称，并在缺失或类型错误时立刻给出定位清晰的诊断。
    /// </summary>
    private static string ResolveNodeTypeName(
        string tableName,
        string schemaPath,
        string propertyPath,
        JsonElement element)
    {
        if (!element.TryGetProperty("type", out var typeElement) ||
            typeElement.ValueKind != JsonValueKind.String)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"Property '{propertyPath}' in schema file '{schemaPath}' must declare a string 'type'.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath));
        }

        return typeElement.GetString() ?? string.Empty;
    }

    /// <summary>
    ///     限制只允许对象 schema 使用对象专属关键字，避免后续分支在运行时才发现语义不兼容。
    /// </summary>
    private static void ValidateObjectOnlyKeywords(
        string tableName,
        string schemaPath,
        string propertyPath,
        JsonElement element,
        string typeName)
    {
        if (string.Equals(typeName, "object", StringComparison.Ordinal) ||
            TryGetObjectOnlyKeywordName(element) is not { } objectOnlyKeywordName)
        {
            return;
        }

        throw ConfigLoadExceptionFactory.Create(
            ConfigLoadFailureKind.SchemaUnsupported,
            tableName,
            $"Property '{propertyPath}' in schema file '{schemaPath}' can only declare '{objectOnlyKeywordName}' on object schemas.",
            schemaPath: schemaPath,
            displayPath: GetDiagnosticPath(propertyPath));
    }

    /// <summary>
    ///     根据声明的 schema 类型分派到对应的节点解析器。
    /// </summary>
    private static YamlConfigSchemaNode CreateParsedNodeForType(
        string tableName,
        string schemaPath,
        string propertyPath,
        JsonElement element,
        string typeName,
        string? referenceTableName,
        bool isRoot)
    {
        return typeName switch
        {
            "object" => ParseObjectSchemaNode(
                tableName,
                schemaPath,
                propertyPath,
                element,
                referenceTableName,
                isRoot),
            "array" => ParseArrayNode(tableName, schemaPath, propertyPath, element, referenceTableName),
            "integer" => CreateScalarNode(
                tableName,
                schemaPath,
                propertyPath,
                YamlConfigSchemaPropertyType.Integer,
                element,
                referenceTableName),
            "number" => CreateScalarNode(
                tableName,
                schemaPath,
                propertyPath,
                YamlConfigSchemaPropertyType.Number,
                element,
                referenceTableName),
            "boolean" => CreateScalarNode(
                tableName,
                schemaPath,
                propertyPath,
                YamlConfigSchemaPropertyType.Boolean,
                element,
                referenceTableName),
            "string" => CreateScalarNode(
                tableName,
                schemaPath,
                propertyPath,
                YamlConfigSchemaPropertyType.String,
                element,
                referenceTableName),
            _ => throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"Property '{propertyPath}' in schema file '{schemaPath}' uses unsupported type '{typeName}'.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath),
                rawValue: typeName)
        };
    }

    /// <summary>
    ///     解析对象类型 schema，并在进入对象节点解析前先校验 ref-table 是否兼容。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <param name="propertyPath">对象属性路径。</param>
    /// <param name="element">对象 schema 节点。</param>
    /// <param name="referenceTableName">声明在当前节点上的目标引用表。</param>
    /// <param name="isRoot">是否为根节点。</param>
    /// <returns>对象节点模型。</returns>
    private static YamlConfigSchemaNode ParseObjectSchemaNode(
        string tableName,
        string schemaPath,
        string propertyPath,
        JsonElement element,
        string? referenceTableName,
        bool isRoot)
    {
        EnsureReferenceKeywordIsSupported(
            tableName,
            schemaPath,
            propertyPath,
            YamlConfigSchemaPropertyType.Object,
            referenceTableName);
        return ParseObjectNode(tableName, schemaPath, propertyPath, element, isRoot);
    }

    /// <summary>
    ///     解析对象节点，保留属性字典与必填集合，以便后续递归校验时逐层定位错误。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <param name="propertyPath">对象属性路径。</param>
    /// <param name="element">对象 schema 节点。</param>
    /// <param name="isRoot">是否为根节点。</param>
    /// <returns>对象节点模型。</returns>
    private static YamlConfigSchemaNode ParseObjectNode(
        string tableName,
        string schemaPath,
        string propertyPath,
        JsonElement element,
        bool isRoot)
    {
        if (!element.TryGetProperty("properties", out var propertiesElement) ||
            propertiesElement.ValueKind != JsonValueKind.Object)
        {
            var subject = isRoot ? "root schema" : $"object property '{propertyPath}'";
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"The {subject} in schema file '{schemaPath}' must declare an object-valued 'properties' section.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath));
        }

        var requiredProperties = new HashSet<string>(StringComparer.Ordinal);
        if (element.TryGetProperty("required", out var requiredElement) &&
            requiredElement.ValueKind == JsonValueKind.Array)
        {
            foreach (var item in requiredElement.EnumerateArray())
            {
                if (item.ValueKind != JsonValueKind.String)
                {
                    continue;
                }

                var requiredPropertyName = item.GetString();
                if (!string.IsNullOrWhiteSpace(requiredPropertyName))
                {
                    requiredProperties.Add(requiredPropertyName);
                }
            }
        }

        var properties = new Dictionary<string, YamlConfigSchemaNode>(StringComparer.Ordinal);
        foreach (var property in propertiesElement.EnumerateObject())
        {
            properties[property.Name] = ParseNode(
                tableName,
                schemaPath,
                CombineSchemaPath(propertyPath, property.Name),
                property.Value);
        }

        var objectNode = YamlConfigSchemaNode.CreateObject(
            properties,
            requiredProperties,
            ParseObjectConstraints(tableName, schemaPath, propertyPath, element, properties),
            schemaPath);
        objectNode = objectNode.WithAllowedValues(
            ParseEnumValues(tableName, schemaPath, propertyPath, element, objectNode, "enum"));
        return objectNode.WithConstantValue(
            ParseConstantValue(tableName, schemaPath, propertyPath, element, objectNode));
    }

    /// <summary>
    ///     解析数组节点。
    ///     当前子集支持标量数组和对象数组，不支持数组嵌套数组。
    ///     当数组声明跨表引用时，会把引用语义挂到元素节点上，便于后续逐项校验。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <param name="propertyPath">数组属性路径。</param>
    /// <param name="element">数组 schema 节点。</param>
    /// <param name="referenceTableName">声明在数组节点上的目标引用表。</param>
    /// <returns>数组节点模型。</returns>
    private static YamlConfigSchemaNode ParseArrayNode(
        string tableName,
        string schemaPath,
        string propertyPath,
        JsonElement element,
        string? referenceTableName)
    {
        if (!element.TryGetProperty("items", out var itemsElement) ||
            itemsElement.ValueKind != JsonValueKind.Object)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"Array property '{propertyPath}' in schema file '{schemaPath}' must declare an object-valued 'items' schema.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath));
        }

        var itemNode = ParseNode(tableName, schemaPath, $"{propertyPath}[]", itemsElement);
        if (!string.IsNullOrWhiteSpace(referenceTableName))
        {
            if (itemNode.NodeType != YamlConfigSchemaPropertyType.String &&
                itemNode.NodeType != YamlConfigSchemaPropertyType.Integer)
            {
                throw ConfigLoadExceptionFactory.Create(
                    ConfigLoadFailureKind.SchemaUnsupported,
                    tableName,
                    $"Property '{propertyPath}' in schema file '{schemaPath}' uses 'x-gframework-ref-table', but only string, integer, or arrays of those scalar types can declare cross-table references.",
                    schemaPath: schemaPath,
                    displayPath: GetDiagnosticPath(propertyPath),
                    referencedTableName: referenceTableName);
            }

            itemNode = itemNode.WithReferenceTable(referenceTableName);
        }

        if (itemNode.NodeType == YamlConfigSchemaPropertyType.Array)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"Array property '{propertyPath}' in schema file '{schemaPath}' uses unsupported nested array items.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath));
        }

        var arrayNode = YamlConfigSchemaNode.CreateArray(
            itemNode,
            allowedValues: null,
            ParseArrayConstraints(tableName, schemaPath, propertyPath, element),
            schemaPath);
        arrayNode = arrayNode.WithAllowedValues(
            ParseEnumValues(tableName, schemaPath, propertyPath, element, arrayNode, "enum"));
        return arrayNode.WithConstantValue(
            ParseConstantValue(tableName, schemaPath, propertyPath, element, arrayNode));
    }

    /// <summary>
    ///     创建标量节点，并在解析阶段就完成 enum 与引用约束的兼容性检查。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <param name="propertyPath">标量属性路径。</param>
    /// <param name="nodeType">标量类型。</param>
    /// <param name="element">标量 schema 节点。</param>
    /// <param name="referenceTableName">目标引用表名称。</param>
    /// <returns>标量节点模型。</returns>
    private static YamlConfigSchemaNode CreateScalarNode(
        string tableName,
        string schemaPath,
        string propertyPath,
        YamlConfigSchemaPropertyType nodeType,
        JsonElement element,
        string? referenceTableName)
    {
        EnsureReferenceKeywordIsSupported(tableName, schemaPath, propertyPath, nodeType, referenceTableName);
        var scalarNode = YamlConfigSchemaNode.CreateScalar(
            nodeType,
            referenceTableName,
            allowedValues: null,
            ParseScalarConstraints(tableName, schemaPath, propertyPath, element, nodeType),
            schemaPath);
        scalarNode = scalarNode.WithAllowedValues(
            ParseEnumValues(tableName, schemaPath, propertyPath, element, scalarNode, "enum"));
        return scalarNode.WithConstantValue(
            ParseConstantValue(tableName, schemaPath, propertyPath, element, scalarNode));
    }

    /// <summary>
    ///     解析节点上的 <c>not</c> 约束。
    ///     该子 schema 继续复用同一套节点解析逻辑，保证 Runtime / Generator / Tooling
    ///     对深层结构与格式白名单的解释保持一致。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <param name="propertyPath">当前节点路径。</param>
    /// <param name="element">Schema JSON 节点。</param>
    /// <returns>解析后的 negated schema；未声明时返回空。</returns>
    private static YamlConfigSchemaNode? ParseNegatedSchemaNode(
        string tableName,
        string schemaPath,
        string propertyPath,
        JsonElement element)
    {
        if (!element.TryGetProperty("not", out var notElement))
        {
            return null;
        }

        if (notElement.ValueKind != JsonValueKind.Object)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"Property '{propertyPath}' in schema file '{schemaPath}' must declare 'not' as an object-valued schema.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath));
        }

        return ParseNode(
            tableName,
            schemaPath,
            BuildNestedSchemaPath(propertyPath, "not"),
            notElement);
    }

    /// <summary>
    ///     为 <c>contains</c> / <c>not</c> / <c>dependentSchemas</c> 这类内联子 schema 构建稳定的诊断路径。
    /// </summary>
    /// <param name="propertyPath">当前节点路径。</param>
    /// <param name="suffix">内联子 schema 后缀。</param>
    /// <returns>带内联后缀的 schema 路径。</returns>
    private static string BuildNestedSchemaPath(string propertyPath, string suffix)
    {
        return string.IsNullOrWhiteSpace(propertyPath)
            ? $"[{suffix}]"
            : $"{propertyPath}[{suffix}]";
    }

    /// <summary>
    ///     递归校验 YAML 节点。
    ///     每层都带上逻辑字段路径，这样深层对象与数组元素的错误也能直接定位。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="yamlPath">YAML 文件路径。</param>
    /// <param name="displayPath">当前字段路径；根节点时为空。</param>
    /// <param name="node">实际 YAML 节点。</param>
    /// <param name="schemaNode">对应的 schema 节点。</param>
    /// <param name="references">已收集的跨表引用。</param>
    /// <param name="allowUnknownObjectProperties">
    ///     是否允许对象节点出现当前 schema 子树未声明的额外字段。
    ///     该开关仅用于 <c>contains</c> 试匹配，让对象子 schema 可以按“声明属性子集匹配”工作；
    ///     正常加载主链路仍保持未知字段即失败的严格语义。
    /// </param>
    private static void ValidateNode(
        string tableName,
        string yamlPath,
        string displayPath,
        YamlNode node,
        YamlConfigSchemaNode schemaNode,
        ICollection<YamlConfigReferenceUsage>? references,
        bool allowUnknownObjectProperties = false)
    {
        switch (schemaNode.NodeType)
        {
            case YamlConfigSchemaPropertyType.Object:
                ValidateObjectNode(
                    tableName,
                    yamlPath,
                    displayPath,
                    node,
                    schemaNode,
                    references,
                    allowUnknownObjectProperties);
                return;

            case YamlConfigSchemaPropertyType.Array:
                ValidateArrayNode(
                    tableName,
                    yamlPath,
                    displayPath,
                    node,
                    schemaNode,
                    references,
                    allowUnknownObjectProperties);
                return;

            case YamlConfigSchemaPropertyType.Integer:
            case YamlConfigSchemaPropertyType.Number:
            case YamlConfigSchemaPropertyType.Boolean:
            case YamlConfigSchemaPropertyType.String:
                ValidateScalarNode(tableName, yamlPath, displayPath, node, schemaNode, references);
                return;

            default:
                throw ConfigLoadExceptionFactory.Create(
                    ConfigLoadFailureKind.UnexpectedFailure,
                    tableName,
                    $"Schema node '{displayPath}' uses unsupported runtime node type '{schemaNode.NodeType}'.",
                    yamlPath: yamlPath,
                    schemaPath: schemaNode.SchemaPathHint,
                    displayPath: GetDiagnosticPath(displayPath),
                    rawValue: schemaNode.NodeType.ToString());
        }
    }

    /// <summary>
    ///     校验对象节点，同时处理重复字段、未知字段和深层必填字段。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="yamlPath">YAML 文件路径。</param>
    /// <param name="displayPath">当前对象的逻辑字段路径。</param>
    /// <param name="node">实际 YAML 节点。</param>
    /// <param name="schemaNode">对象 schema 节点。</param>
    /// <param name="references">已收集的跨表引用。</param>
    /// <param name="allowUnknownObjectProperties">
    ///     是否允许当前对象包含 schema 子树未声明的额外字段。
    /// </param>
    private static void ValidateObjectNode(
        string tableName,
        string yamlPath,
        string displayPath,
        YamlNode node,
        YamlConfigSchemaNode schemaNode,
        ICollection<YamlConfigReferenceUsage>? references,
        bool allowUnknownObjectProperties)
    {
        var mappingNode = GetObjectMappingNode(tableName, yamlPath, displayPath, node, schemaNode);
        var seenProperties = ValidateObjectPropertyEntries(
            tableName,
            yamlPath,
            displayPath,
            mappingNode,
            schemaNode,
            references,
            allowUnknownObjectProperties);
        ValidateRequiredObjectProperties(tableName, yamlPath, displayPath, schemaNode, seenProperties);

        ValidateObjectConstraints(
            tableName,
            yamlPath,
            displayPath,
            mappingNode,
            seenProperties,
            schemaNode,
            references);

        ValidateAllowedValues(tableName, yamlPath, displayPath, mappingNode, schemaNode);
        ValidateConstantValue(tableName, yamlPath, displayPath, mappingNode, schemaNode);
        ValidateNegatedSchemaConstraint(tableName, yamlPath, displayPath, mappingNode, schemaNode);
    }

    /// <summary>
    ///     确认当前 YAML 节点确实是对象节点，避免后续属性枚举阶段再做重复判空与类型判断。
    /// </summary>
    private static YamlMappingNode GetObjectMappingNode(
        string tableName,
        string yamlPath,
        string displayPath,
        YamlNode node,
        YamlConfigSchemaNode schemaNode)
    {
        if (node is YamlMappingNode mappingNode)
        {
            return mappingNode;
        }

        var subject = displayPath.Length == 0 ? "Root object" : $"Property '{displayPath}'";
        throw ConfigLoadExceptionFactory.Create(
            ConfigLoadFailureKind.PropertyTypeMismatch,
            tableName,
            $"{subject} in config file '{yamlPath}' must be an object.",
            yamlPath: yamlPath,
            schemaPath: schemaNode.SchemaPathHint,
            displayPath: GetDiagnosticPath(displayPath));
    }

    /// <summary>
    ///     遍历对象属性并递归校验每个已声明字段，同时记录用于后续 required 与 dependency 判断的 sibling 集合。
    /// </summary>
    private static HashSet<string> ValidateObjectPropertyEntries(
        string tableName,
        string yamlPath,
        string displayPath,
        YamlMappingNode mappingNode,
        YamlConfigSchemaNode schemaNode,
        ICollection<YamlConfigReferenceUsage>? references,
        bool allowUnknownObjectProperties)
    {
        var seenProperties = new HashSet<string>(StringComparer.Ordinal);
        foreach (var entry in mappingNode.Children)
        {
            if (entry.Key is not YamlScalarNode keyNode ||
                string.IsNullOrWhiteSpace(keyNode.Value))
            {
                var subject = displayPath.Length == 0 ? "root object" : $"object property '{displayPath}'";
                throw ConfigLoadExceptionFactory.Create(
                    ConfigLoadFailureKind.InvalidYamlDocument,
                    tableName,
                    $"Config file '{yamlPath}' contains a non-scalar or empty property name inside {subject}.",
                    yamlPath: yamlPath,
                    schemaPath: schemaNode.SchemaPathHint,
                    displayPath: GetDiagnosticPath(displayPath));
            }

            var propertyName = keyNode.Value;
            var propertyPath = CombineDisplayPath(displayPath, propertyName);
            if (!seenProperties.Add(propertyName))
            {
                throw ConfigLoadExceptionFactory.Create(
                    ConfigLoadFailureKind.DuplicateProperty,
                    tableName,
                    $"Config file '{yamlPath}' contains duplicate property '{propertyPath}'.",
                    yamlPath: yamlPath,
                    schemaPath: schemaNode.SchemaPathHint,
                    displayPath: propertyPath);
            }

            if (schemaNode.Properties is null ||
                !schemaNode.Properties.TryGetValue(propertyName, out var propertySchema))
            {
                if (allowUnknownObjectProperties)
                {
                    continue;
                }

                throw ConfigLoadExceptionFactory.Create(
                    ConfigLoadFailureKind.UnknownProperty,
                    tableName,
                    $"Config file '{yamlPath}' contains unknown property '{propertyPath}' that is not declared in schema '{schemaNode.SchemaPathHint}'.",
                    yamlPath: yamlPath,
                    schemaPath: schemaNode.SchemaPathHint,
                    displayPath: propertyPath);
            }

            ValidateNode(
                tableName,
                yamlPath,
                propertyPath,
                entry.Value,
                propertySchema,
                references,
                allowUnknownObjectProperties);
        }

        return seenProperties;
    }

    /// <summary>
    ///     在对象主体字段遍历结束后统一检查缺失的 required 字段，保证错误消息使用稳定的完整路径。
    /// </summary>
    private static void ValidateRequiredObjectProperties(
        string tableName,
        string yamlPath,
        string displayPath,
        YamlConfigSchemaNode schemaNode,
        HashSet<string> seenProperties)
    {
        if (schemaNode.RequiredProperties is null)
        {
            return;
        }

        foreach (var requiredProperty in schemaNode.RequiredProperties)
        {
            if (seenProperties.Contains(requiredProperty))
            {
                continue;
            }

            var requiredPath = CombineDisplayPath(displayPath, requiredProperty);
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.MissingRequiredProperty,
                tableName,
                $"Config file '{yamlPath}' is missing required property '{requiredPath}' defined by schema '{schemaNode.SchemaPathHint}'.",
                yamlPath: yamlPath,
                schemaPath: schemaNode.SchemaPathHint,
                displayPath: requiredPath);
        }
    }

    /// <summary>
    ///     校验对象节点声明的数量约束与条件对象约束。
    ///     该阶段除了检查 <c>minProperties</c> / <c>maxProperties</c>，还会复用同一份 sibling 集合处理
    ///     <c>dependentRequired</c>，并在 <c>dependentSchemas</c> 命中时以 focused constraint block 语义
    ///     对整个 <paramref name="mappingNode" /> 做额外试匹配；若声明了 object-focused
    ///     <c>if</c> / <c>then</c> / <c>else</c>，则先按同样的 focused matcher 判断条件分支，再只对命中的分支追加约束。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="yamlPath">YAML 文件路径。</param>
    /// <param name="displayPath">对象字段路径；根对象时为空。</param>
    /// <param name="mappingNode">当前 YAML 对象节点；用于让条件子 schema 在完整对象视图上做匹配。</param>
    /// <param name="seenProperties">当前对象已出现的属性集合。</param>
    /// <param name="schemaNode">对象 schema 节点。</param>
    /// <param name="references">
    ///     可选的跨表引用收集器；当 <c>dependentSchemas</c>、<c>allOf</c> 或条件分支命中且匹配成功时，
    ///     只会回写对应内联分支新增的引用。
    /// </param>
    private static void ValidateObjectConstraints(
        string tableName,
        string yamlPath,
        string displayPath,
        YamlMappingNode mappingNode,
        HashSet<string> seenProperties,
        YamlConfigSchemaNode schemaNode,
        ICollection<YamlConfigReferenceUsage>? references)
    {
        var constraints = schemaNode.ObjectConstraints;
        if (constraints is null)
        {
            return;
        }

        var propertyCount = seenProperties.Count;
        var subject = GetObjectConstraintSubject(displayPath);

        ValidateObjectPropertyCountConstraints(
            tableName,
            yamlPath,
            displayPath,
            schemaNode,
            constraints,
            subject,
            propertyCount);
        ValidateDependentRequiredConstraints(
            tableName,
            yamlPath,
            displayPath,
            schemaNode,
            constraints,
            seenProperties);
        ValidateDependentSchemas(
            tableName,
            yamlPath,
            displayPath,
            mappingNode,
            schemaNode,
            constraints,
            references,
            seenProperties,
            subject);
        ValidateAllOfSchemas(
            tableName,
            yamlPath,
            displayPath,
            mappingNode,
            schemaNode,
            constraints,
            references,
            subject);
        ValidateConditionalObjectSchemas(
            tableName,
            yamlPath,
            displayPath,
            mappingNode,
            schemaNode,
            constraints,
            references,
            subject);
    }

    /// <summary>
    ///     为对象约束构造统一的诊断主语，保证根对象与嵌套对象的错误消息格式一致。
    /// </summary>
    private static string GetObjectConstraintSubject(string displayPath)
    {
        return string.IsNullOrWhiteSpace(displayPath)
            ? "Root object"
            : $"Property '{displayPath}'";
    }

    /// <summary>
    ///     校验对象属性数量上下限。
    /// </summary>
    private static void ValidateObjectPropertyCountConstraints(
        string tableName,
        string yamlPath,
        string displayPath,
        YamlConfigSchemaNode schemaNode,
        YamlConfigObjectConstraints constraints,
        string subject,
        int propertyCount)
    {
        var rawValue = propertyCount.ToString(CultureInfo.InvariantCulture);
        if (constraints.MinProperties.HasValue &&
            propertyCount < constraints.MinProperties.Value)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.ConstraintViolation,
                tableName,
                $"{subject} in config file '{yamlPath}' must contain at least {constraints.MinProperties.Value.ToString(CultureInfo.InvariantCulture)} properties.",
                yamlPath: yamlPath,
                schemaPath: schemaNode.SchemaPathHint,
                displayPath: GetDiagnosticPath(displayPath),
                rawValue: rawValue,
                detail:
                $"Minimum property count: {constraints.MinProperties.Value.ToString(CultureInfo.InvariantCulture)}.");
        }

        if (constraints.MaxProperties.HasValue &&
            propertyCount > constraints.MaxProperties.Value)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.ConstraintViolation,
                tableName,
                $"{subject} in config file '{yamlPath}' must contain at most {constraints.MaxProperties.Value.ToString(CultureInfo.InvariantCulture)} properties.",
                yamlPath: yamlPath,
                schemaPath: schemaNode.SchemaPathHint,
                displayPath: GetDiagnosticPath(displayPath),
                rawValue: rawValue,
                detail:
                $"Maximum property count: {constraints.MaxProperties.Value.ToString(CultureInfo.InvariantCulture)}.");
        }
    }

    /// <summary>
    ///     使用已见 sibling 集合校验 dependentRequired，确保对象主路径与试匹配路径共用同一判定语义。
    /// </summary>
    private static void ValidateDependentRequiredConstraints(
        string tableName,
        string yamlPath,
        string displayPath,
        YamlConfigSchemaNode schemaNode,
        YamlConfigObjectConstraints constraints,
        HashSet<string> seenProperties)
    {
        if (constraints.DependentRequired is null ||
            constraints.DependentRequired.Count == 0)
        {
            return;
        }

        // Reuse the collected sibling-name set so the main validation path and
        // the contains/not matcher both interpret object dependencies identically.
        foreach (var dependency in constraints.DependentRequired)
        {
            if (!seenProperties.Contains(dependency.Key))
            {
                continue;
            }

            var triggerPath = CombineDisplayPath(displayPath, dependency.Key);
            foreach (var dependentProperty in dependency.Value)
            {
                if (seenProperties.Contains(dependentProperty))
                {
                    continue;
                }

                var requiredPath = CombineDisplayPath(displayPath, dependentProperty);
                throw ConfigLoadExceptionFactory.Create(
                    ConfigLoadFailureKind.MissingRequiredProperty,
                    tableName,
                    $"Property '{requiredPath}' in config file '{yamlPath}' is required when sibling property '{triggerPath}' is present.",
                    yamlPath: yamlPath,
                    schemaPath: schemaNode.SchemaPathHint,
                    displayPath: requiredPath,
                    detail:
                    $"Dependent requirement: when '{triggerPath}' exists, '{requiredPath}' must also be declared.");
            }
        }
    }

    /// <summary>
    ///     在触发字段出现时，以 focused matcher 语义试跑 dependentSchemas。
    /// </summary>
    private static void ValidateDependentSchemas(
        string tableName,
        string yamlPath,
        string displayPath,
        YamlMappingNode mappingNode,
        YamlConfigSchemaNode schemaNode,
        YamlConfigObjectConstraints constraints,
        ICollection<YamlConfigReferenceUsage>? references,
        HashSet<string> seenProperties,
        string subject)
    {
        if (constraints.DependentSchemas is null ||
            constraints.DependentSchemas.Count == 0)
        {
            return;
        }

        foreach (var dependency in constraints.DependentSchemas)
        {
            if (!seenProperties.Contains(dependency.Key))
            {
                continue;
            }

            var triggerPath = CombineDisplayPath(displayPath, dependency.Key);
            // dependentSchemas acts as an additional conditional constraint block on the
            // current object. Keep undeclared sibling fields outside the dependent sub-schema
            // from blocking the match so schema authors can express focused follow-up rules.
            // The trial matcher merges only new reference usages back into the outer collector,
            // so re-checking the same scalar via a conditional sub-schema does not duplicate
            // cross-table validation work later in the loader pipeline.
            if (TryMatchSchemaNode(
                    tableName,
                    yamlPath,
                    displayPath,
                    mappingNode,
                    dependency.Value,
                    references,
                    allowUnknownObjectProperties: true))
            {
                continue;
            }

            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.ConstraintViolation,
                tableName,
                $"{subject} in config file '{yamlPath}' must satisfy the 'dependentSchemas' schema triggered by sibling property '{triggerPath}'.",
                yamlPath: yamlPath,
                schemaPath: schemaNode.SchemaPathHint,
                displayPath: GetDiagnosticPath(displayPath),
                detail:
                $"Dependent schema: when '{triggerPath}' exists, the current object must satisfy the corresponding inline schema.");
        }
    }

    /// <summary>
    ///     逐条校验 allOf 约束，保持与 dependentSchemas 相同的 focused object 匹配语义。
    /// </summary>
    private static void ValidateAllOfSchemas(
        string tableName,
        string yamlPath,
        string displayPath,
        YamlMappingNode mappingNode,
        YamlConfigSchemaNode schemaNode,
        YamlConfigObjectConstraints constraints,
        ICollection<YamlConfigReferenceUsage>? references,
        string subject)
    {
        if (constraints.AllOfSchemas is null ||
            constraints.AllOfSchemas.Count == 0)
        {
            return;
        }

        for (var index = 0; index < constraints.AllOfSchemas.Count; index++)
        {
            var allOfSchema = constraints.AllOfSchemas[index];
            // allOf follows the same focused constraint block semantics as dependentSchemas:
            // the inline schema may validate a subset of the current object without forcing
            // unrelated sibling fields to be restated.
            if (TryMatchSchemaNode(
                    tableName,
                    yamlPath,
                    displayPath,
                    mappingNode,
                    allOfSchema,
                    references,
                    allowUnknownObjectProperties: true))
            {
                continue;
            }

            var allOfEntryNumber = index + 1;
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.ConstraintViolation,
                tableName,
                $"{subject} in config file '{yamlPath}' must satisfy all 'allOf' schemas, but entry #{allOfEntryNumber.ToString(CultureInfo.InvariantCulture)} did not match.",
                yamlPath: yamlPath,
                schemaPath: schemaNode.SchemaPathHint,
                displayPath: GetDiagnosticPath(displayPath),
                detail:
                $"allOf entry #{allOfEntryNumber.ToString(CultureInfo.InvariantCulture)} must match the current object.");
        }
    }

    /// <summary>
    ///     执行对象级 if/then/else 约束，并沿用 focused matcher 允许条件 schema 只声明关注字段。
    /// </summary>
    private static void ValidateConditionalObjectSchemas(
        string tableName,
        string yamlPath,
        string displayPath,
        YamlMappingNode mappingNode,
        YamlConfigSchemaNode schemaNode,
        YamlConfigObjectConstraints constraints,
        ICollection<YamlConfigReferenceUsage>? references,
        string subject)
    {
        var conditionalSchemas = constraints.ConditionalSchemas;
        if (conditionalSchemas is null)
        {
            return;
        }

        // if/then/else follows the same object-focused matcher contract as dependentSchemas/allOf:
        // condition evaluation can inspect a subset of the current object without forcing unrelated
        // sibling fields to be re-declared inside the branch schema.
        var ifMatched = TryMatchSchemaNode(
            tableName,
            yamlPath,
            displayPath,
            mappingNode,
            conditionalSchemas.IfSchema,
            references,
            allowUnknownObjectProperties: true);
        ValidateConditionalSchemaBranch(
            tableName,
            yamlPath,
            displayPath,
            mappingNode,
            schemaNode,
            references,
            subject,
            ifMatched,
            conditionalSchemas.ThenSchema,
            branchName: "then",
            failureDetail:
            "Conditional schema: the current object matched the inline 'if' schema, so it must also satisfy the corresponding 'then' schema.");
        ValidateConditionalSchemaBranch(
            tableName,
            yamlPath,
            displayPath,
            mappingNode,
            schemaNode,
            references,
            subject,
            !ifMatched,
            conditionalSchemas.ElseSchema,
            branchName: "else",
            failureDetail:
            "Conditional schema: the current object did not match the inline 'if' schema, so it must satisfy the corresponding 'else' schema.");
    }

    /// <summary>
    ///     校验 if/then/else 的单个分支，并在条件命中但分支未通过时提供统一诊断。
    /// </summary>
    private static void ValidateConditionalSchemaBranch(
        string tableName,
        string yamlPath,
        string displayPath,
        YamlMappingNode mappingNode,
        YamlConfigSchemaNode schemaNode,
        ICollection<YamlConfigReferenceUsage>? references,
        string subject,
        bool shouldValidate,
        YamlConfigSchemaNode? branchSchema,
        string branchName,
        string failureDetail)
    {
        if (!shouldValidate ||
            branchSchema is null ||
            TryMatchSchemaNode(
                tableName,
                yamlPath,
                displayPath,
                mappingNode,
                branchSchema,
                references,
                allowUnknownObjectProperties: true))
        {
            return;
        }

        throw ConfigLoadExceptionFactory.Create(
            ConfigLoadFailureKind.ConstraintViolation,
            tableName,
            $"{subject} in config file '{yamlPath}' must satisfy the '{branchName}' schema because the inline 'if' condition {(string.Equals(branchName, "then", StringComparison.Ordinal) ? "matched" : "did not match")}.",
            yamlPath: yamlPath,
            schemaPath: schemaNode.SchemaPathHint,
            displayPath: GetDiagnosticPath(displayPath),
            detail: failureDetail);
    }

    /// <summary>
    ///     校验数组节点，并递归验证每个元素。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="yamlPath">YAML 文件路径。</param>
    /// <param name="displayPath">数组字段路径。</param>
    /// <param name="node">实际 YAML 节点。</param>
    /// <param name="schemaNode">数组 schema 节点。</param>
    /// <param name="references">已收集的跨表引用。</param>
    /// <param name="allowUnknownObjectProperties">
    ///     是否允许数组元素内的对象节点包含 schema 子树未声明的额外字段。
    /// </param>
    private static void ValidateArrayNode(
        string tableName,
        string yamlPath,
        string displayPath,
        YamlNode node,
        YamlConfigSchemaNode schemaNode,
        ICollection<YamlConfigReferenceUsage>? references,
        bool allowUnknownObjectProperties)
    {
        if (node is not YamlSequenceNode sequenceNode)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.PropertyTypeMismatch,
                tableName,
                $"Property '{displayPath}' in config file '{yamlPath}' must be an array.",
                yamlPath: yamlPath,
                schemaPath: schemaNode.SchemaPathHint,
                displayPath: GetDiagnosticPath(displayPath));
        }

        if (schemaNode.ItemNode is null)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.UnexpectedFailure,
                tableName,
                $"Schema node '{displayPath}' is missing array item information.",
                yamlPath: yamlPath,
                schemaPath: schemaNode.SchemaPathHint,
                displayPath: GetDiagnosticPath(displayPath));
        }

        if (schemaNode.ArrayConstraints is not null)
        {
            ValidateArrayConstraints(tableName, yamlPath, displayPath, sequenceNode.Children.Count, schemaNode);
        }

        for (var itemIndex = 0; itemIndex < sequenceNode.Children.Count; itemIndex++)
        {
            ValidateNode(
                tableName,
                yamlPath,
                $"{displayPath}[{itemIndex}]",
                sequenceNode.Children[itemIndex],
                schemaNode.ItemNode,
                references,
                allowUnknownObjectProperties);
        }

        ValidateArrayUniqueItemsConstraint(tableName, yamlPath, displayPath, sequenceNode, schemaNode);
        ValidateArrayContainsConstraints(tableName, yamlPath, displayPath, sequenceNode, schemaNode, references);
        ValidateAllowedValues(tableName, yamlPath, displayPath, sequenceNode, schemaNode);
        ValidateConstantValue(tableName, yamlPath, displayPath, sequenceNode, schemaNode);
        ValidateNegatedSchemaConstraint(tableName, yamlPath, displayPath, sequenceNode, schemaNode);
    }

    /// <summary>
    ///     校验标量节点，并在值有效时收集跨表引用。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="yamlPath">YAML 文件路径。</param>
    /// <param name="displayPath">标量字段路径。</param>
    /// <param name="node">实际 YAML 节点。</param>
    /// <param name="schemaNode">标量 schema 节点。</param>
    /// <param name="references">已收集的跨表引用。</param>
    private static void ValidateScalarNode(
        string tableName,
        string yamlPath,
        string displayPath,
        YamlNode node,
        YamlConfigSchemaNode schemaNode,
        ICollection<YamlConfigReferenceUsage>? references)
    {
        var scalarNode = GetScalarNodeOrThrow(tableName, yamlPath, displayPath, node, schemaNode);
        var value = GetScalarValueOrThrow(tableName, yamlPath, displayPath, scalarNode, schemaNode);
        ValidateScalarTypeOrThrow(tableName, yamlPath, displayPath, scalarNode, schemaNode, value);
        var normalizedValue = NormalizeScalarValue(schemaNode.NodeType, value);
        ValidateAllowedValues(tableName, yamlPath, displayPath, scalarNode, schemaNode);

        if (schemaNode.Constraints is not null)
        {
            ValidateScalarConstraints(tableName, yamlPath, displayPath, value, normalizedValue, schemaNode);
        }

        ValidateConstantValue(tableName, yamlPath, displayPath, scalarNode, schemaNode);
        ValidateNegatedSchemaConstraint(tableName, yamlPath, displayPath, scalarNode, schemaNode);
        CollectScalarReferenceUsage(yamlPath, displayPath, schemaNode, references, normalizedValue);
    }

    /// <summary>
    ///     确认 schema 期望的节点在 YAML 中仍然表现为标量。
    /// </summary>
    private static YamlScalarNode GetScalarNodeOrThrow(
        string tableName,
        string yamlPath,
        string displayPath,
        YamlNode node,
        YamlConfigSchemaNode schemaNode)
    {
        if (node is YamlScalarNode scalarNode)
        {
            return scalarNode;
        }

        throw ConfigLoadExceptionFactory.Create(
            ConfigLoadFailureKind.PropertyTypeMismatch,
            tableName,
            $"Property '{displayPath}' in config file '{yamlPath}' must be a scalar value of type '{GetTypeName(schemaNode.NodeType)}'.",
            yamlPath: yamlPath,
            schemaPath: schemaNode.SchemaPathHint,
            displayPath: GetDiagnosticPath(displayPath));
    }

    /// <summary>
    ///     读取标量文本值，并把 YAML 显式 null 与普通空字符串区分开。
    /// </summary>
    private static string GetScalarValueOrThrow(
        string tableName,
        string yamlPath,
        string displayPath,
        YamlScalarNode scalarNode,
        YamlConfigSchemaNode schemaNode)
    {
        if (scalarNode.Value is { } value)
        {
            return value;
        }

        throw ConfigLoadExceptionFactory.Create(
            ConfigLoadFailureKind.NullScalarValue,
            tableName,
            $"Property '{displayPath}' in config file '{yamlPath}' cannot be null when schema type is '{GetTypeName(schemaNode.NodeType)}'.",
            yamlPath: yamlPath,
            schemaPath: schemaNode.SchemaPathHint,
            displayPath: GetDiagnosticPath(displayPath));
    }

    /// <summary>
    ///     按 schema 声明的标量类型验证 YAML 文本值。
    /// </summary>
    private static void ValidateScalarTypeOrThrow(
        string tableName,
        string yamlPath,
        string displayPath,
        YamlScalarNode scalarNode,
        YamlConfigSchemaNode schemaNode,
        string value)
    {
        var tag = scalarNode.Tag.ToString();
        var isValid = schemaNode.NodeType switch
        {
            YamlConfigSchemaPropertyType.String => IsStringScalar(tag),
            YamlConfigSchemaPropertyType.Integer => long.TryParse(
                value,
                NumberStyles.Integer,
                CultureInfo.InvariantCulture,
                out _),
            YamlConfigSchemaPropertyType.Number => double.TryParse(
                value,
                NumberStyles.Float | NumberStyles.AllowThousands,
                CultureInfo.InvariantCulture,
                out _),
            YamlConfigSchemaPropertyType.Boolean => bool.TryParse(value, out _),
            _ => false
        };
        if (isValid)
        {
            return;
        }

        throw ConfigLoadExceptionFactory.Create(
            ConfigLoadFailureKind.PropertyTypeMismatch,
            tableName,
            $"Property '{displayPath}' in config file '{yamlPath}' must be of type '{GetTypeName(schemaNode.NodeType)}', but the current YAML scalar value is '{value}'.",
            yamlPath: yamlPath,
            schemaPath: schemaNode.SchemaPathHint,
            displayPath: GetDiagnosticPath(displayPath),
            rawValue: value);
    }

    /// <summary>
    ///     在标量值成功通过本地校验后，再把引用信息回写给外层收集器。
    /// </summary>
    private static void CollectScalarReferenceUsage(
        string yamlPath,
        string displayPath,
        YamlConfigSchemaNode schemaNode,
        ICollection<YamlConfigReferenceUsage>? references,
        string normalizedValue)
    {
        if (schemaNode.ReferenceTableName is null ||
            references is null)
        {
            return;
        }

        references.Add(
            new YamlConfigReferenceUsage(
                yamlPath,
                schemaNode.SchemaPathHint,
                displayPath,
                normalizedValue,
                schemaNode.ReferenceTableName,
                schemaNode.NodeType));
    }

    /// <summary>
    ///     解析 <c>enum</c>，并在读取阶段将每个候选值预归一化成与运行时 YAML 相同的稳定比较键。
    ///     这样对象、数组和标量节点都可以复用同一套匹配逻辑，而不必在每次加载时重新解释 schema 字面量。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <param name="propertyPath">字段路径。</param>
    /// <param name="element">Schema 节点。</param>
    /// <param name="schemaNode">当前节点的已解析 schema 模型。</param>
    /// <param name="keywordName">当前读取的关键字名称。</param>
    /// <returns>归一化后的允许值集合；未声明时返回空。</returns>
    private static IReadOnlyCollection<YamlConfigAllowedValue>? ParseEnumValues(
        string tableName,
        string schemaPath,
        string propertyPath,
        JsonElement element,
        YamlConfigSchemaNode schemaNode,
        string keywordName)
    {
        if (!element.TryGetProperty("enum", out var enumElement))
        {
            return null;
        }

        if (enumElement.ValueKind != JsonValueKind.Array)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"Property '{propertyPath}' in schema file '{schemaPath}' must declare '{keywordName}' as an array.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath));
        }

        var allowedValues = new List<YamlConfigAllowedValue>();
        foreach (var item in enumElement.EnumerateArray())
        {
            allowedValues.Add(
                new YamlConfigAllowedValue(
                    BuildComparableConstantValue(tableName, schemaPath, propertyPath, keywordName, item, schemaNode),
                    BuildEnumDisplayValue(item)));
        }

        return allowedValues;
    }

    /// <summary>
    ///     读取一个 <c>enum</c> 候选值的展示文本。
    ///     这里直接保留原始 JSON 字面量，避免字符串引号、对象字段顺序或数组结构在诊断中被二次格式化后丢失上下文。
    /// </summary>
    /// <param name="element">Schema 中的单个枚举值。</param>
    /// <returns>适合放入诊断消息的展示文本。</returns>
    private static string BuildEnumDisplayValue(JsonElement element)
    {
        return element.GetRawText();
    }

    /// <summary>
    ///     解析 <c>const</c>，并把 schema 常量预归一化成与运行时 YAML 相同的稳定比较键。
    ///     这样运行时只需要复用现有递归比较逻辑，而不必在每次加载时重新解释 JSON 常量。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <param name="propertyPath">字段路径。</param>
    /// <param name="element">Schema 节点。</param>
    /// <param name="schemaNode">已解析的 schema 节点。</param>
    /// <returns>常量约束模型；未声明时返回空。</returns>
    private static YamlConfigConstantValue? ParseConstantValue(
        string tableName,
        string schemaPath,
        string propertyPath,
        JsonElement element,
        YamlConfigSchemaNode schemaNode)
    {
        if (!element.TryGetProperty("const", out var constantElement))
        {
            return null;
        }

        return new YamlConfigConstantValue(
            BuildComparableConstantValue(tableName, schemaPath, propertyPath, "const", constantElement, schemaNode),
            constantElement.GetRawText());
    }

    /// <summary>
    ///     把 schema 中的 <c>const</c> JSON 值转换成与 YAML 运行时一致的比较键。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <param name="propertyPath">字段路径。</param>
    /// <param name="keywordName">关键字名称。</param>
    /// <param name="element">常量 JSON 值。</param>
    /// <param name="schemaNode">目标 schema 节点。</param>
    /// <returns>可稳定比较的归一化键。</returns>
    private static string BuildComparableConstantValue(
        string tableName,
        string schemaPath,
        string propertyPath,
        string keywordName,
        JsonElement element,
        YamlConfigSchemaNode schemaNode)
    {
        return schemaNode.NodeType switch
        {
            YamlConfigSchemaPropertyType.Object => BuildComparableConstantObjectValue(
                tableName,
                schemaPath,
                propertyPath,
                keywordName,
                element,
                schemaNode),
            YamlConfigSchemaPropertyType.Array => BuildComparableConstantArrayValue(
                tableName,
                schemaPath,
                propertyPath,
                keywordName,
                element,
                schemaNode),
            YamlConfigSchemaPropertyType.Integer => BuildComparableConstantScalarValue(
                tableName,
                schemaPath,
                propertyPath,
                keywordName,
                element,
                schemaNode),
            YamlConfigSchemaPropertyType.Number => BuildComparableConstantScalarValue(
                tableName,
                schemaPath,
                propertyPath,
                keywordName,
                element,
                schemaNode),
            YamlConfigSchemaPropertyType.Boolean => BuildComparableConstantScalarValue(
                tableName,
                schemaPath,
                propertyPath,
                keywordName,
                element,
                schemaNode),
            YamlConfigSchemaPropertyType.String => BuildComparableConstantScalarValue(
                tableName,
                schemaPath,
                propertyPath,
                keywordName,
                element,
                schemaNode),
            _ => throw new InvalidOperationException($"Unsupported schema node type '{schemaNode.NodeType}'.")
        };
    }

    /// <summary>
    ///     构建对象常量的稳定比较键。
    ///     这里同样忽略 JSON 对象字段顺序，避免 schema 文本格式影响常量比较结果。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <param name="propertyPath">字段路径。</param>
    /// <param name="keywordName">关键字名称。</param>
    /// <param name="element">常量 JSON 值。</param>
    /// <param name="schemaNode">对象 schema 节点。</param>
    /// <returns>对象常量的可比较键。</returns>
    private static string BuildComparableConstantObjectValue(
        string tableName,
        string schemaPath,
        string propertyPath,
        string keywordName,
        JsonElement element,
        YamlConfigSchemaNode schemaNode)
    {
        if (element.ValueKind != JsonValueKind.Object)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"Property '{propertyPath}' in schema file '{schemaPath}' uses '{keywordName}', but only object values are compatible with schema type '{GetTypeName(schemaNode.NodeType)}'.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath));
        }

        var properties = schemaNode.Properties
                         ?? throw new InvalidOperationException("Object schema nodes must expose declared properties.");
        var objectEntries = new List<KeyValuePair<string, string>>();
        foreach (var property in element.EnumerateObject())
        {
            if (!properties.TryGetValue(property.Name, out var propertySchema))
            {
                var childPath = CombineSchemaPath(propertyPath, property.Name);
                throw ConfigLoadExceptionFactory.Create(
                    ConfigLoadFailureKind.SchemaUnsupported,
                    tableName,
                    $"Property '{propertyPath}' in schema file '{schemaPath}' uses '{keywordName}', but nested property '{childPath}' is not declared in the object schema.",
                    schemaPath: schemaPath,
                    displayPath: GetDiagnosticPath(childPath));
            }

            objectEntries.Add(
                new KeyValuePair<string, string>(
                    property.Name,
                    BuildComparableConstantValue(
                        tableName,
                        schemaPath,
                        CombineSchemaPath(propertyPath, property.Name),
                        keywordName,
                        property.Value,
                        propertySchema)));
        }

        objectEntries.Sort(static (left, right) => string.CompareOrdinal(left.Key, right.Key));
        return string.Join(
            "|",
            objectEntries.Select(static entry =>
                $"{entry.Key.Length.ToString(CultureInfo.InvariantCulture)}:{entry.Key}={entry.Value.Length.ToString(CultureInfo.InvariantCulture)}:{entry.Value}"));
    }

    /// <summary>
    ///     构建数组常量的稳定比较键。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <param name="propertyPath">字段路径。</param>
    /// <param name="keywordName">关键字名称。</param>
    /// <param name="element">常量 JSON 值。</param>
    /// <param name="schemaNode">数组 schema 节点。</param>
    /// <returns>数组常量的可比较键。</returns>
    private static string BuildComparableConstantArrayValue(
        string tableName,
        string schemaPath,
        string propertyPath,
        string keywordName,
        JsonElement element,
        YamlConfigSchemaNode schemaNode)
    {
        if (element.ValueKind != JsonValueKind.Array)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"Property '{propertyPath}' in schema file '{schemaPath}' uses '{keywordName}', but only array values are compatible with schema type '{GetTypeName(schemaNode.NodeType)}'.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath));
        }

        if (schemaNode.ItemNode is null)
        {
            throw new InvalidOperationException("Array schema nodes must expose their item schema.");
        }

        return "[" +
               string.Join(
                   ",",
                   element.EnumerateArray().Select((item, index) =>
                   {
                       var comparableValue = BuildComparableConstantValue(
                           tableName,
                           schemaPath,
                           $"{propertyPath}[{index}]",
                           keywordName,
                           item,
                           schemaNode.ItemNode);
                       return
                           $"{comparableValue.Length.ToString(CultureInfo.InvariantCulture)}:{comparableValue}";
                   })) +
               "]";
    }

    /// <summary>
    ///     构建标量常量的稳定比较键。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <param name="propertyPath">字段路径。</param>
    /// <param name="keywordName">关键字名称。</param>
    /// <param name="element">常量 JSON 值。</param>
    /// <param name="schemaNode">标量 schema 节点。</param>
    /// <returns>标量常量的可比较键。</returns>
    private static string BuildComparableConstantScalarValue(
        string tableName,
        string schemaPath,
        string propertyPath,
        string keywordName,
        JsonElement element,
        YamlConfigSchemaNode schemaNode)
    {
        var normalizedValue = NormalizeKeywordScalarValue(
            tableName,
            schemaPath,
            propertyPath,
            keywordName,
            schemaNode.NodeType,
            element);
        return
            $"{schemaNode.NodeType}:{normalizedValue.Length.ToString(CultureInfo.InvariantCulture)}:{normalizedValue}";
    }

    /// <summary>
    ///     解析标量字段支持的范围、长度与模式约束。
    ///     当前共享子集支持：
    ///     `integer/number` 上的 `minimum/maximum/exclusiveMinimum/exclusiveMaximum`，
    ///     以及 `string` 上的 `minLength/maxLength/pattern/format`。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <param name="propertyPath">字段路径。</param>
    /// <param name="element">Schema 节点。</param>
    /// <param name="nodeType">标量类型。</param>
    /// <returns>解析后的约束模型；未声明时返回空。</returns>
    private static YamlConfigScalarConstraints? ParseScalarConstraints(
        string tableName,
        string schemaPath,
        string propertyPath,
        JsonElement element,
        YamlConfigSchemaPropertyType nodeType)
    {
        var minimum = TryParseNumericConstraint(tableName, schemaPath, propertyPath, element, nodeType, "minimum");
        var maximum = TryParseNumericConstraint(tableName, schemaPath, propertyPath, element, nodeType, "maximum");
        var exclusiveMinimum =
            TryParseNumericConstraint(tableName, schemaPath, propertyPath, element, nodeType, "exclusiveMinimum");
        var exclusiveMaximum =
            TryParseNumericConstraint(tableName, schemaPath, propertyPath, element, nodeType, "exclusiveMaximum");
        var multipleOf = TryParseMultipleOfConstraint(tableName, schemaPath, propertyPath, element, nodeType);
        var minLength = TryParseLengthConstraint(tableName, schemaPath, propertyPath, element, nodeType, "minLength");
        var maxLength = TryParseLengthConstraint(tableName, schemaPath, propertyPath, element, nodeType, "maxLength");
        var pattern = TryParsePatternConstraint(tableName, schemaPath, propertyPath, element, nodeType);
        var formatConstraint = TryParseFormatConstraint(tableName, schemaPath, propertyPath, element, nodeType);

        if (minimum.HasValue && maximum.HasValue && minimum.Value > maximum.Value)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"Property '{propertyPath}' in schema file '{schemaPath}' declares 'minimum' greater than 'maximum'.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath));
        }

        ValidateNumericConstraintRange(
            tableName,
            schemaPath,
            propertyPath,
            minimum,
            maximum,
            exclusiveMinimum,
            exclusiveMaximum);

        if (minLength.HasValue && maxLength.HasValue && minLength.Value > maxLength.Value)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"Property '{propertyPath}' in schema file '{schemaPath}' declares 'minLength' greater than 'maxLength'.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath));
        }

        var numericConstraints = CreateNumericScalarConstraints(
            minimum,
            maximum,
            exclusiveMinimum,
            exclusiveMaximum,
            multipleOf);
        var stringConstraints = CreateStringScalarConstraints(
            minLength,
            maxLength,
            pattern,
            formatConstraint);

        return numericConstraints is null && stringConstraints is null
            ? null
            : new YamlConfigScalarConstraints(numericConstraints, stringConstraints);
    }

    /// <summary>
    ///     解析数组节点支持的元素数量、去重与 <c>contains</c> 匹配数量约束。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <param name="propertyPath">数组字段路径。</param>
    /// <param name="element">Schema 节点。</param>
    /// <returns>数组约束模型；未声明时返回空。</returns>
    private static YamlConfigArrayConstraints? ParseArrayConstraints(
        string tableName,
        string schemaPath,
        string propertyPath,
        JsonElement element)
    {
        var minItems = TryParseArrayLengthConstraint(tableName, schemaPath, propertyPath, element, "minItems");
        var maxItems = TryParseArrayLengthConstraint(tableName, schemaPath, propertyPath, element, "maxItems");
        var uniqueItems = TryParseUniqueItemsConstraint(tableName, schemaPath, propertyPath, element);
        var containsConstraints = ParseArrayContainsConstraints(tableName, schemaPath, propertyPath, element);

        if (minItems.HasValue && maxItems.HasValue && minItems.Value > maxItems.Value)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"Property '{propertyPath}' in schema file '{schemaPath}' declares 'minItems' greater than 'maxItems'.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath));
        }

        return !minItems.HasValue && !maxItems.HasValue && !uniqueItems && containsConstraints is null
            ? null
            : new YamlConfigArrayConstraints(minItems, maxItems, uniqueItems, containsConstraints);
    }

    /// <summary>
    ///     解析数组节点声明的 <c>contains</c> 约束及其匹配数量边界。
    ///     运行时会把 <c>contains</c> 解析成独立的 schema 子树，后续逐项复用同一套递归校验逻辑判断“是否匹配”。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <param name="propertyPath">数组字段路径。</param>
    /// <param name="element">Schema 节点。</param>
    /// <returns>数组 contains 约束模型；未声明时返回空。</returns>
    private static YamlConfigArrayContainsConstraints? ParseArrayContainsConstraints(
        string tableName,
        string schemaPath,
        string propertyPath,
        JsonElement element)
    {
        var minContains = TryParseArrayLengthConstraint(tableName, schemaPath, propertyPath, element, "minContains");
        var maxContains = TryParseArrayLengthConstraint(tableName, schemaPath, propertyPath, element, "maxContains");
        if (!element.TryGetProperty("contains", out var containsElement))
        {
            if (minContains.HasValue || maxContains.HasValue)
            {
                var keywordName = minContains.HasValue ? "minContains" : "maxContains";
                throw ConfigLoadExceptionFactory.Create(
                    ConfigLoadFailureKind.SchemaUnsupported,
                    tableName,
                    $"Property '{propertyPath}' in schema file '{schemaPath}' declares '{keywordName}' without a companion 'contains' schema.",
                    schemaPath: schemaPath,
                    displayPath: GetDiagnosticPath(propertyPath));
            }

            return null;
        }

        if (containsElement.ValueKind != JsonValueKind.Object)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"Property '{propertyPath}' in schema file '{schemaPath}' must declare 'contains' as an object-valued schema.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath));
        }

        var containsNode = ParseNode(tableName, schemaPath, $"{propertyPath}[contains]", containsElement);
        if (containsNode.NodeType == YamlConfigSchemaPropertyType.Array)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"Property '{propertyPath}' in schema file '{schemaPath}' uses unsupported nested array 'contains' schemas.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath));
        }

        var effectiveMinContains = minContains ?? 1;
        if (maxContains.HasValue && effectiveMinContains > maxContains.Value)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"Property '{propertyPath}' in schema file '{schemaPath}' declares 'minContains' greater than 'maxContains'.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath));
        }

        return new YamlConfigArrayContainsConstraints(containsNode, minContains, maxContains);
    }

    /// <summary>
    ///     读取数值区间约束。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <param name="propertyPath">字段路径。</param>
    /// <param name="element">Schema 节点。</param>
    /// <param name="nodeType">字段类型。</param>
    /// <param name="keywordName">关键字名称。</param>
    /// <returns>数值约束；未声明时返回空。</returns>
    private static double? TryParseNumericConstraint(
        string tableName,
        string schemaPath,
        string propertyPath,
        JsonElement element,
        YamlConfigSchemaPropertyType nodeType,
        string keywordName)
    {
        if (!element.TryGetProperty(keywordName, out var constraintElement))
        {
            return null;
        }

        if (nodeType != YamlConfigSchemaPropertyType.Integer &&
            nodeType != YamlConfigSchemaPropertyType.Number)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"Property '{propertyPath}' in schema file '{schemaPath}' uses '{keywordName}', but only 'integer' and 'number' scalar types support numeric range constraints.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath));
        }

        if (constraintElement.ValueKind != JsonValueKind.Number ||
            !constraintElement.TryGetDouble(out var constraintValue) ||
            double.IsNaN(constraintValue) ||
            double.IsInfinity(constraintValue))
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"Property '{propertyPath}' in schema file '{schemaPath}' must declare '{keywordName}' as a finite number.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath));
        }

        return constraintValue;
    }

    /// <summary>
    ///     读取 <c>multipleOf</c> 约束。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <param name="propertyPath">字段路径。</param>
    /// <param name="element">Schema 节点。</param>
    /// <param name="nodeType">字段类型。</param>
    /// <returns>步进约束；未声明时返回空。</returns>
    private static double? TryParseMultipleOfConstraint(
        string tableName,
        string schemaPath,
        string propertyPath,
        JsonElement element,
        YamlConfigSchemaPropertyType nodeType)
    {
        var multipleOf =
            TryParseNumericConstraint(tableName, schemaPath, propertyPath, element, nodeType, "multipleOf");
        if (!multipleOf.HasValue)
        {
            return null;
        }

        if (multipleOf.Value <= 0d)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"Property '{propertyPath}' in schema file '{schemaPath}' must declare 'multipleOf' as a positive finite number.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath));
        }

        return multipleOf;
    }

    /// <summary>
    ///     读取字符串长度约束。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <param name="propertyPath">字段路径。</param>
    /// <param name="element">Schema 节点。</param>
    /// <param name="nodeType">字段类型。</param>
    /// <param name="keywordName">关键字名称。</param>
    /// <returns>长度约束；未声明时返回空。</returns>
    private static int? TryParseLengthConstraint(
        string tableName,
        string schemaPath,
        string propertyPath,
        JsonElement element,
        YamlConfigSchemaPropertyType nodeType,
        string keywordName)
    {
        if (!element.TryGetProperty(keywordName, out var constraintElement))
        {
            return null;
        }

        if (nodeType != YamlConfigSchemaPropertyType.String)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"Property '{propertyPath}' in schema file '{schemaPath}' uses '{keywordName}', but only 'string' scalar types support length constraints.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath));
        }

        if (constraintElement.ValueKind != JsonValueKind.Number ||
            !constraintElement.TryGetInt32(out var constraintValue) ||
            constraintValue < 0)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"Property '{propertyPath}' in schema file '{schemaPath}' must declare '{keywordName}' as a non-negative integer.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath));
        }

        return constraintValue;
    }

    /// <summary>
    ///     读取字符串正则约束。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <param name="propertyPath">字段路径。</param>
    /// <param name="element">Schema 节点。</param>
    /// <param name="nodeType">字段类型。</param>
    /// <returns>正则模式；未声明时返回空。</returns>
    private static string? TryParsePatternConstraint(
        string tableName,
        string schemaPath,
        string propertyPath,
        JsonElement element,
        YamlConfigSchemaPropertyType nodeType)
    {
        if (!element.TryGetProperty("pattern", out var patternElement))
        {
            return null;
        }

        if (nodeType != YamlConfigSchemaPropertyType.String)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"Property '{propertyPath}' in schema file '{schemaPath}' uses 'pattern', but only 'string' scalar types support regular-expression constraints.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath));
        }

        if (patternElement.ValueKind != JsonValueKind.String)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"Property '{propertyPath}' in schema file '{schemaPath}' must declare 'pattern' as a string.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath));
        }

        var pattern = patternElement.GetString() ?? string.Empty;
        try
        {
            _ = new Regex(pattern, SupportedPatternRegexOptions, SupportedFormatRegexTimeout);
        }
        catch (ArgumentException exception)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"Property '{propertyPath}' in schema file '{schemaPath}' declares an invalid 'pattern' regular expression.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath),
                rawValue: pattern,
                innerException: exception);
        }

        return pattern;
    }

    /// <summary>
    ///     读取字符串 format 约束。
    ///     运行时只接受当前三端共享并已验证收益的稳定子集，避免把开放格式名误当成“全部支持”。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <param name="propertyPath">字段路径。</param>
    /// <param name="element">Schema 节点。</param>
    /// <param name="nodeType">字段类型。</param>
    /// <returns>归一化后的 format 约束；未声明时返回空。</returns>
    private static YamlConfigStringFormatConstraint? TryParseFormatConstraint(
        string tableName,
        string schemaPath,
        string propertyPath,
        JsonElement element,
        YamlConfigSchemaPropertyType nodeType)
    {
        if (!element.TryGetProperty("format", out var formatElement))
        {
            return null;
        }

        if (nodeType != YamlConfigSchemaPropertyType.String)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"Property '{propertyPath}' in schema file '{schemaPath}' uses 'format', but only 'string' scalar types support string formats.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath));
        }

        if (formatElement.ValueKind != JsonValueKind.String)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"Property '{propertyPath}' in schema file '{schemaPath}' must declare 'format' as a string.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath));
        }

        var formatName = formatElement.GetString() ?? string.Empty;
        if (TryMapSupportedStringFormat(formatName, out var kind))
        {
            return new YamlConfigStringFormatConstraint(formatName, kind);
        }

        throw ConfigLoadExceptionFactory.Create(
            ConfigLoadFailureKind.SchemaUnsupported,
            tableName,
            $"Property '{propertyPath}' in schema file '{schemaPath}' declares unsupported string format '{formatName}'. Supported formats are {SupportedStringFormatNames}.",
            schemaPath: schemaPath,
            displayPath: GetDiagnosticPath(propertyPath),
            rawValue: formatName);
    }

    /// <summary>
    ///     将 schema 原文中的 format 名称映射为运行时共享枚举。
    ///     映射阶段统一使用大小写敏感比较，避免同一 schema 在不同环境下被“宽松容错”解释成不同语义。
    /// </summary>
    /// <param name="formatName">schema 原始 format 名称。</param>
    /// <param name="kind">解析成功时输出归一化枚举。</param>
    /// <returns>当前格式是否属于共享支持子集。</returns>
    private static bool TryMapSupportedStringFormat(
        string formatName,
        out YamlConfigStringFormatKind kind)
    {
        switch (formatName)
        {
            case "date":
                kind = YamlConfigStringFormatKind.Date;
                return true;

            case "date-time":
                kind = YamlConfigStringFormatKind.DateTime;
                return true;

            case "duration":
                kind = YamlConfigStringFormatKind.Duration;
                return true;

            case "email":
                kind = YamlConfigStringFormatKind.Email;
                return true;

            case "time":
                kind = YamlConfigStringFormatKind.Time;
                return true;

            case "uri":
                kind = YamlConfigStringFormatKind.Uri;
                return true;

            case "uuid":
                kind = YamlConfigStringFormatKind.Uuid;
                return true;

            default:
                kind = default;
                return false;
        }
    }

    /// <summary>
    ///     读取数组元素数量约束。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <param name="propertyPath">字段路径。</param>
    /// <param name="element">Schema 节点。</param>
    /// <param name="keywordName">关键字名称。</param>
    /// <returns>数组元素数量约束；未声明时返回空。</returns>
    private static int? TryParseArrayLengthConstraint(
        string tableName,
        string schemaPath,
        string propertyPath,
        JsonElement element,
        string keywordName)
    {
        if (!element.TryGetProperty(keywordName, out var constraintElement))
        {
            return null;
        }

        if (constraintElement.ValueKind != JsonValueKind.Number ||
            !constraintElement.TryGetInt32(out var constraintValue) ||
            constraintValue < 0)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"Property '{propertyPath}' in schema file '{schemaPath}' must declare '{keywordName}' as a non-negative integer.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath));
        }

        return constraintValue;
    }

    /// <summary>
    ///     读取数组去重约束。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <param name="propertyPath">字段路径。</param>
    /// <param name="element">Schema 节点。</param>
    /// <returns>是否启用 <c>uniqueItems</c>。</returns>
    private static bool TryParseUniqueItemsConstraint(
        string tableName,
        string schemaPath,
        string propertyPath,
        JsonElement element)
    {
        if (!element.TryGetProperty("uniqueItems", out var constraintElement))
        {
            return false;
        }

        if (constraintElement.ValueKind != JsonValueKind.True &&
            constraintElement.ValueKind != JsonValueKind.False)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"Property '{propertyPath}' in schema file '{schemaPath}' must declare 'uniqueItems' as a boolean.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath));
        }

        return constraintElement.GetBoolean();
    }

    /// <summary>
    ///     校验数值上下界组合不会形成空区间。
    ///     这里把闭区间与开区间统一折算为最强边界，避免 schema 进入“无任何合法值”的状态。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <param name="propertyPath">字段路径。</param>
    /// <param name="minimum">闭区间最小值。</param>
    /// <param name="maximum">闭区间最大值。</param>
    /// <param name="exclusiveMinimum">开区间最小值。</param>
    /// <param name="exclusiveMaximum">开区间最大值。</param>
    private static void ValidateNumericConstraintRange(
        string tableName,
        string schemaPath,
        string propertyPath,
        double? minimum,
        double? maximum,
        double? exclusiveMinimum,
        double? exclusiveMaximum)
    {
        var hasLowerBound = false;
        var lowerBound = double.MinValue;
        var isLowerBoundExclusive = false;

        if (minimum.HasValue)
        {
            hasLowerBound = true;
            lowerBound = minimum.Value;
        }

        if (exclusiveMinimum.HasValue &&
            (!hasLowerBound ||
             exclusiveMinimum.Value > lowerBound ||
             (exclusiveMinimum.Value.Equals(lowerBound) && !isLowerBoundExclusive)))
        {
            hasLowerBound = true;
            lowerBound = exclusiveMinimum.Value;
            isLowerBoundExclusive = true;
        }

        var hasUpperBound = false;
        var upperBound = double.MaxValue;
        var isUpperBoundExclusive = false;

        if (maximum.HasValue)
        {
            hasUpperBound = true;
            upperBound = maximum.Value;
        }

        if (exclusiveMaximum.HasValue &&
            (!hasUpperBound ||
             exclusiveMaximum.Value < upperBound ||
             (exclusiveMaximum.Value.Equals(upperBound) && !isUpperBoundExclusive)))
        {
            hasUpperBound = true;
            upperBound = exclusiveMaximum.Value;
            isUpperBoundExclusive = true;
        }

        if (!hasLowerBound || !hasUpperBound)
        {
            return;
        }

        if (lowerBound > upperBound ||
            (lowerBound.Equals(upperBound) && (isLowerBoundExclusive || isUpperBoundExclusive)))
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"Property '{propertyPath}' in schema file '{schemaPath}' declares numeric constraints that do not leave any valid value range.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath));
        }
    }

    /// <summary>
    ///     校验标量值是否满足范围与长度约束。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="yamlPath">YAML 文件路径。</param>
    /// <param name="displayPath">字段路径。</param>
    /// <param name="rawValue">原始 YAML 标量值。</param>
    /// <param name="normalizedValue">归一化后的比较值。</param>
    /// <param name="schemaNode">标量 schema 节点。</param>
    private static void ValidateScalarConstraints(
        string tableName,
        string yamlPath,
        string displayPath,
        string rawValue,
        string normalizedValue,
        YamlConfigSchemaNode schemaNode)
    {
        var constraints = schemaNode.Constraints;
        if (constraints is null)
        {
            return;
        }

        switch (schemaNode.NodeType)
        {
            case YamlConfigSchemaPropertyType.Integer:
            case YamlConfigSchemaPropertyType.Number:
                ValidateNumericScalarConstraints(
                    tableName,
                    yamlPath,
                    displayPath,
                    rawValue,
                    normalizedValue,
                    schemaNode,
                    constraints.NumericConstraints);
                return;

            case YamlConfigSchemaPropertyType.String:
                ValidateStringScalarConstraints(
                    tableName,
                    yamlPath,
                    displayPath,
                    rawValue,
                    schemaNode,
                    constraints.StringConstraints);
                return;

            default:
                throw ConfigLoadExceptionFactory.Create(
                    ConfigLoadFailureKind.UnexpectedFailure,
                    tableName,
                    $"Property '{displayPath}' in config file '{yamlPath}' resolved unsupported constraint host type '{schemaNode.NodeType}'.",
                    yamlPath: yamlPath,
                    schemaPath: schemaNode.SchemaPathHint,
                    displayPath: GetDiagnosticPath(displayPath),
                    rawValue: schemaNode.NodeType.ToString());
        }
    }

    /// <summary>
    ///     校验节点值是否命中声明的 <c>enum</c> 集合。
    ///     该实现与 <c>const</c> 共享同一套可比较键，保证对象字段顺序、数组顺序和数值归一化语义稳定一致。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="yamlPath">YAML 文件路径。</param>
    /// <param name="displayPath">字段路径；根节点时为空。</param>
    /// <param name="node">当前 YAML 节点。</param>
    /// <param name="schemaNode">对应的 schema 节点。</param>
    private static void ValidateAllowedValues(
        string tableName,
        string yamlPath,
        string displayPath,
        YamlNode node,
        YamlConfigSchemaNode schemaNode)
    {
        var allowedValues = schemaNode.AllowedValues;
        if (allowedValues is null || allowedValues.Count == 0)
        {
            return;
        }

        var comparableValue = BuildComparableNodeValue(node, schemaNode);
        if (allowedValues.Any(allowedValue =>
                string.Equals(allowedValue.ComparableValue, comparableValue, StringComparison.Ordinal)))
        {
            return;
        }

        var subject = string.IsNullOrWhiteSpace(displayPath)
            ? "Root object"
            : $"Property '{displayPath}'";
        var displayValues = string.Join(", ", allowedValues.Select(static allowedValue => allowedValue.DisplayValue));
        throw ConfigLoadExceptionFactory.Create(
            ConfigLoadFailureKind.EnumValueNotAllowed,
            tableName,
            $"{subject} in config file '{yamlPath}' must be one of [{displayValues}].",
            yamlPath: yamlPath,
            schemaPath: schemaNode.SchemaPathHint,
            displayPath: GetDiagnosticPath(displayPath),
            rawValue: DescribeYamlNodeForDiagnostics(node, schemaNode),
            detail: $"Allowed values: {displayValues}.");
    }

    /// <summary>
    ///     校验节点值是否满足 <c>const</c> 约束。
    ///     该检查复用与 <c>uniqueItems</c> 相同的稳定比较键，保证对象字段顺序、数字字面量和布尔大小写不会造成伪差异。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="yamlPath">YAML 文件路径。</param>
    /// <param name="displayPath">字段路径；根节点时为空。</param>
    /// <param name="node">当前 YAML 节点。</param>
    /// <param name="schemaNode">对应的 schema 节点。</param>
    private static void ValidateConstantValue(
        string tableName,
        string yamlPath,
        string displayPath,
        YamlNode node,
        YamlConfigSchemaNode schemaNode)
    {
        var constantValue = schemaNode.ConstantValue;
        if (constantValue is null)
        {
            return;
        }

        var comparableValue = BuildComparableNodeValue(node, schemaNode);
        if (string.Equals(comparableValue, constantValue.ComparableValue, StringComparison.Ordinal))
        {
            return;
        }

        var subject = string.IsNullOrWhiteSpace(displayPath)
            ? "Root object"
            : $"Property '{displayPath}'";
        throw ConfigLoadExceptionFactory.Create(
            ConfigLoadFailureKind.ConstraintViolation,
            tableName,
            $"{subject} in config file '{yamlPath}' must match constant value {constantValue.DisplayValue}.",
            yamlPath: yamlPath,
            schemaPath: schemaNode.SchemaPathHint,
            displayPath: GetDiagnosticPath(displayPath),
            rawValue: DescribeYamlNodeForDiagnostics(node, schemaNode),
            detail: $"Required constant value: {constantValue.DisplayValue}.");
    }

    /// <summary>
    ///     根据已读取的数值关键字创建数值约束对象。
    ///     该分组让调用方不必再维护一个超过 Sonar 默认阈值的长参数构造函数。
    /// </summary>
    /// <param name="minimum">最小值约束。</param>
    /// <param name="maximum">最大值约束。</param>
    /// <param name="exclusiveMinimum">开区间最小值约束。</param>
    /// <param name="exclusiveMaximum">开区间最大值约束。</param>
    /// <param name="multipleOf">数值步进约束。</param>
    /// <returns>数值约束对象；未声明任何数值约束时返回空。</returns>
    private static YamlConfigNumericConstraints? CreateNumericScalarConstraints(
        double? minimum,
        double? maximum,
        double? exclusiveMinimum,
        double? exclusiveMaximum,
        double? multipleOf)
    {
        return !minimum.HasValue &&
               !maximum.HasValue &&
               !exclusiveMinimum.HasValue &&
               !exclusiveMaximum.HasValue &&
               !multipleOf.HasValue
            ? null
            : new YamlConfigNumericConstraints(
                minimum,
                maximum,
                exclusiveMinimum,
                exclusiveMaximum,
                multipleOf);
    }

    /// <summary>
    ///     根据已读取的字符串关键字创建字符串约束对象。
    ///     正则会在 schema 解析阶段预编译，字符串 format 也会先归一化成共享枚举，
    ///     避免每次校验都重新解释 schema 原文。
    /// </summary>
    /// <param name="minLength">最小长度约束。</param>
    /// <param name="maxLength">最大长度约束。</param>
    /// <param name="pattern">正则模式约束。</param>
    /// <param name="formatConstraint">字符串 format 约束。</param>
    /// <returns>字符串约束对象；未声明任何字符串约束时返回空。</returns>
    private static YamlConfigStringConstraints? CreateStringScalarConstraints(
        int? minLength,
        int? maxLength,
        string? pattern,
        YamlConfigStringFormatConstraint? formatConstraint)
    {
        return !minLength.HasValue &&
               !maxLength.HasValue &&
               pattern is null &&
               formatConstraint is null
            ? null
            : new YamlConfigStringConstraints(
                minLength,
                maxLength,
                pattern,
                pattern is null
                    ? null
                    : new Regex(
                        pattern,
                        SupportedPatternRegexOptions,
                        SupportedFormatRegexTimeout),
                formatConstraint);
    }

    /// <summary>
    ///     校验数值标量的区间与步进约束。
    ///     该方法把解析失败、闭区间、开区间和步进诊断集中到数值路径，避免主调度方法继续增长。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="yamlPath">YAML 文件路径。</param>
    /// <param name="displayPath">字段路径。</param>
    /// <param name="rawValue">原始 YAML 标量值。</param>
    /// <param name="normalizedValue">归一化后的比较值。</param>
    /// <param name="schemaNode">标量 schema 节点。</param>
    /// <param name="constraints">数值约束对象。</param>
    private static void ValidateNumericScalarConstraints(
        string tableName,
        string yamlPath,
        string displayPath,
        string rawValue,
        string normalizedValue,
        YamlConfigSchemaNode schemaNode,
        YamlConfigNumericConstraints? constraints)
    {
        if (constraints is null)
        {
            return;
        }

        var numericValue = ParseComparableNumericValue(
            tableName,
            yamlPath,
            displayPath,
            rawValue,
            normalizedValue,
            schemaNode);
        ValidateNumericLowerBounds(
            tableName,
            yamlPath,
            displayPath,
            rawValue,
            schemaNode,
            constraints,
            numericValue);
        ValidateNumericUpperBounds(
            tableName,
            yamlPath,
            displayPath,
            rawValue,
            schemaNode,
            constraints,
            numericValue);
        ValidateNumericMultipleOfConstraint(
            tableName,
            yamlPath,
            displayPath,
            rawValue,
            normalizedValue,
            schemaNode,
            constraints,
            numericValue);
    }

    /// <summary>
    ///     校验数值的最小值与开区间下界。
    /// </summary>
    private static void ValidateNumericLowerBounds(
        string tableName,
        string yamlPath,
        string displayPath,
        string rawValue,
        YamlConfigSchemaNode schemaNode,
        YamlConfigNumericConstraints constraints,
        double numericValue)
    {
        if (constraints.Minimum.HasValue && numericValue < constraints.Minimum.Value)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.ConstraintViolation,
                tableName,
                $"Property '{displayPath}' in config file '{yamlPath}' must be greater than or equal to {constraints.Minimum.Value.ToString(CultureInfo.InvariantCulture)}, but the current YAML scalar value is '{rawValue}'.",
                yamlPath: yamlPath,
                schemaPath: schemaNode.SchemaPathHint,
                displayPath: GetDiagnosticPath(displayPath),
                rawValue: rawValue,
                detail: $"Minimum allowed value: {constraints.Minimum.Value.ToString(CultureInfo.InvariantCulture)}.");
        }

        if (constraints.ExclusiveMinimum.HasValue && numericValue <= constraints.ExclusiveMinimum.Value)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.ConstraintViolation,
                tableName,
                $"Property '{displayPath}' in config file '{yamlPath}' must be greater than {constraints.ExclusiveMinimum.Value.ToString(CultureInfo.InvariantCulture)}, but the current YAML scalar value is '{rawValue}'.",
                yamlPath: yamlPath,
                schemaPath: schemaNode.SchemaPathHint,
                displayPath: GetDiagnosticPath(displayPath),
                rawValue: rawValue,
                detail:
                $"Exclusive minimum allowed value: {constraints.ExclusiveMinimum.Value.ToString(CultureInfo.InvariantCulture)}.");
        }
    }

    /// <summary>
    ///     校验数值的最大值与开区间上界。
    /// </summary>
    private static void ValidateNumericUpperBounds(
        string tableName,
        string yamlPath,
        string displayPath,
        string rawValue,
        YamlConfigSchemaNode schemaNode,
        YamlConfigNumericConstraints constraints,
        double numericValue)
    {
        if (constraints.Maximum.HasValue && numericValue > constraints.Maximum.Value)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.ConstraintViolation,
                tableName,
                $"Property '{displayPath}' in config file '{yamlPath}' must be less than or equal to {constraints.Maximum.Value.ToString(CultureInfo.InvariantCulture)}, but the current YAML scalar value is '{rawValue}'.",
                yamlPath: yamlPath,
                schemaPath: schemaNode.SchemaPathHint,
                displayPath: GetDiagnosticPath(displayPath),
                rawValue: rawValue,
                detail: $"Maximum allowed value: {constraints.Maximum.Value.ToString(CultureInfo.InvariantCulture)}.");
        }

        if (constraints.ExclusiveMaximum.HasValue && numericValue >= constraints.ExclusiveMaximum.Value)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.ConstraintViolation,
                tableName,
                $"Property '{displayPath}' in config file '{yamlPath}' must be less than {constraints.ExclusiveMaximum.Value.ToString(CultureInfo.InvariantCulture)}, but the current YAML scalar value is '{rawValue}'.",
                yamlPath: yamlPath,
                schemaPath: schemaNode.SchemaPathHint,
                displayPath: GetDiagnosticPath(displayPath),
                rawValue: rawValue,
                detail:
                $"Exclusive maximum allowed value: {constraints.ExclusiveMaximum.Value.ToString(CultureInfo.InvariantCulture)}.");
        }
    }

    /// <summary>
    ///     校验数值是否满足 multipleOf 步进约束。
    /// </summary>
    private static void ValidateNumericMultipleOfConstraint(
        string tableName,
        string yamlPath,
        string displayPath,
        string rawValue,
        string normalizedValue,
        YamlConfigSchemaNode schemaNode,
        YamlConfigNumericConstraints constraints,
        double numericValue)
    {
        if (!constraints.MultipleOf.HasValue ||
            IsMultipleOf(normalizedValue, numericValue, constraints.MultipleOf.Value))
        {
            return;
        }

        throw ConfigLoadExceptionFactory.Create(
            ConfigLoadFailureKind.ConstraintViolation,
            tableName,
            $"Property '{displayPath}' in config file '{yamlPath}' must be a multiple of {constraints.MultipleOf.Value.ToString(CultureInfo.InvariantCulture)}, but the current YAML scalar value is '{rawValue}'.",
            yamlPath: yamlPath,
            schemaPath: schemaNode.SchemaPathHint,
            displayPath: GetDiagnosticPath(displayPath),
            rawValue: rawValue,
            detail:
            $"Required numeric step: {constraints.MultipleOf.Value.ToString(CultureInfo.InvariantCulture)}.");
    }

    /// <summary>
    ///     将归一化后的数值文本还原为双精度值，用于统一后续区间比较。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="yamlPath">YAML 文件路径。</param>
    /// <param name="displayPath">字段路径。</param>
    /// <param name="rawValue">原始 YAML 标量值。</param>
    /// <param name="normalizedValue">归一化后的比较值。</param>
    /// <param name="schemaNode">标量 schema 节点。</param>
    /// <returns>可比较的双精度值。</returns>
    private static double ParseComparableNumericValue(
        string tableName,
        string yamlPath,
        string displayPath,
        string rawValue,
        string normalizedValue,
        YamlConfigSchemaNode schemaNode)
    {
        if (double.TryParse(
                normalizedValue,
                NumberStyles.Float | NumberStyles.AllowThousands,
                CultureInfo.InvariantCulture,
                out var numericValue))
        {
            return numericValue;
        }

        throw ConfigLoadExceptionFactory.Create(
            ConfigLoadFailureKind.UnexpectedFailure,
            tableName,
            $"Property '{displayPath}' in config file '{yamlPath}' could not be normalized into a comparable numeric value.",
            yamlPath: yamlPath,
            schemaPath: schemaNode.SchemaPathHint,
            displayPath: GetDiagnosticPath(displayPath),
            rawValue: rawValue);
    }

    /// <summary>
    ///     校验字符串标量的长度、模式与 format 约束。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="yamlPath">YAML 文件路径。</param>
    /// <param name="displayPath">字段路径。</param>
    /// <param name="rawValue">原始 YAML 标量值。</param>
    /// <param name="schemaNode">标量 schema 节点。</param>
    /// <param name="constraints">字符串约束对象。</param>
    private static void ValidateStringScalarConstraints(
        string tableName,
        string yamlPath,
        string displayPath,
        string rawValue,
        YamlConfigSchemaNode schemaNode,
        YamlConfigStringConstraints? constraints)
    {
        if (constraints is null)
        {
            return;
        }

        var stringLength = rawValue.Length;
        if (constraints.MinLength.HasValue && stringLength < constraints.MinLength.Value)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.ConstraintViolation,
                tableName,
                $"Property '{displayPath}' in config file '{yamlPath}' must be at least {constraints.MinLength.Value} characters long, but the current YAML scalar value is '{rawValue}'.",
                yamlPath: yamlPath,
                schemaPath: schemaNode.SchemaPathHint,
                displayPath: GetDiagnosticPath(displayPath),
                rawValue: rawValue,
                detail: $"Minimum length: {constraints.MinLength.Value}.");
        }

        if (constraints.MaxLength.HasValue && stringLength > constraints.MaxLength.Value)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.ConstraintViolation,
                tableName,
                $"Property '{displayPath}' in config file '{yamlPath}' must be at most {constraints.MaxLength.Value} characters long, but the current YAML scalar value is '{rawValue}'.",
                yamlPath: yamlPath,
                schemaPath: schemaNode.SchemaPathHint,
                displayPath: GetDiagnosticPath(displayPath),
                rawValue: rawValue,
                detail: $"Maximum length: {constraints.MaxLength.Value}.");
        }

        if (constraints.PatternRegex is not null &&
            !constraints.PatternRegex.IsMatch(rawValue))
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.ConstraintViolation,
                tableName,
                $"Property '{displayPath}' in config file '{yamlPath}' must match regular expression '{constraints.Pattern}', but the current YAML scalar value is '{rawValue}'.",
                yamlPath: yamlPath,
                schemaPath: schemaNode.SchemaPathHint,
                displayPath: GetDiagnosticPath(displayPath),
                rawValue: rawValue,
                detail: $"Expected pattern: {constraints.Pattern}.");
        }

        if (constraints.FormatConstraint is not null &&
            !MatchesSupportedStringFormat(rawValue, constraints.FormatConstraint.Kind))
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.ConstraintViolation,
                tableName,
                $"Property '{displayPath}' in config file '{yamlPath}' must satisfy string format '{constraints.FormatConstraint.SchemaName}', but the current YAML scalar value is '{rawValue}'.",
                yamlPath: yamlPath,
                schemaPath: schemaNode.SchemaPathHint,
                displayPath: GetDiagnosticPath(displayPath),
                rawValue: rawValue,
                detail: $"Expected string format: {constraints.FormatConstraint.SchemaName}.");
        }
    }

    /// <summary>
    ///     判断一个字符串是否满足当前共享支持的 format 子集。
    ///     这里只接受 Runtime / Generator / Tooling 三端都能稳定解释的格式，
    ///     避免把 JSON Schema 的开放格式名直接当成“全部支持”。
    /// </summary>
    /// <param name="value">待校验的字符串值。</param>
    /// <param name="formatKind">共享 format 枚举。</param>
    /// <returns>当前字符串是否满足指定 format。</returns>
    private static bool MatchesSupportedStringFormat(
        string value,
        YamlConfigStringFormatKind formatKind)
    {
        ArgumentNullException.ThrowIfNull(value);

        return formatKind switch
        {
            YamlConfigStringFormatKind.Date => MatchesSupportedDateFormat(value),
            YamlConfigStringFormatKind.DateTime => MatchesSupportedDateTimeFormat(value),
            YamlConfigStringFormatKind.Duration => MatchesSupportedDurationFormat(value),
            YamlConfigStringFormatKind.Email => SupportedEmailFormatRegex.IsMatch(value),
            YamlConfigStringFormatKind.Time => MatchesSupportedTimeFormat(value),
            YamlConfigStringFormatKind.Uri => MatchesSupportedUriFormat(value),
            YamlConfigStringFormatKind.Uuid => Guid.TryParseExact(value, "D", out _),
            _ => false
        };
    }

    /// <summary>
    ///     判断字符串是否满足共享支持的 <c>date</c> 格式。
    ///     这里固定采用 <c>yyyy-MM-dd</c>，避免文化区域或宽松解析导致工具与运行时漂移。
    /// </summary>
    /// <param name="value">待校验的字符串值。</param>
    /// <returns>当前值是否是合法日期。</returns>
    private static bool MatchesSupportedDateFormat(string value)
    {
        return SupportedDateFormatRegex.IsMatch(value) &&
               DateOnly.TryParseExact(
                   value,
                   "yyyy-MM-dd",
                   CultureInfo.InvariantCulture,
                   DateTimeStyles.None,
                   out _);
    }

    /// <summary>
    ///     判断字符串是否满足共享支持的 <c>date-time</c> 格式。
    ///     该格式固定要求显式时区偏移，避免消费者误把本地时区文本当成跨进程稳定配置。
    /// </summary>
    /// <param name="value">待校验的字符串值。</param>
    /// <returns>当前值是否是合法时间戳。</returns>
    private static bool MatchesSupportedDateTimeFormat(string value)
    {
        if (!SupportedDateTimeFormatRegex.IsMatch(value))
        {
            return false;
        }

        return DateTimeOffset.TryParseExact(
            value,
            ["yyyy'-'MM'-'dd'T'HH':'mm':'ssK", "yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFFK"],
            CultureInfo.InvariantCulture,
            DateTimeStyles.None,
            out _);
    }

    /// <summary>
    ///     判断字符串是否满足共享支持的 <c>duration</c> 格式。
    ///     当前共享子集只接受 day-time duration：可声明 <c>D/H/M/S</c>，秒允许小数，
    ///     但拒绝 <c>Y</c> / <c>M(月)</c> / <c>W</c> 等依赖日历语义的部分，
    ///     避免不同宿主对“一个月/一年到底多长”出现解释漂移。
    /// </summary>
    /// <param name="value">待校验的字符串值。</param>
    /// <returns>当前值是否是合法持续时间文本。</returns>
    private static bool MatchesSupportedDurationFormat(string value)
    {
        var match = SupportedDurationFormatRegex.Match(value);
        if (!match.Success)
        {
            return false;
        }

        var hasDayComponent = match.Groups["days"].Success;
        var hasHourComponent = match.Groups["hours"].Success;
        var hasMinuteComponent = match.Groups["minutes"].Success;
        var hasSecondComponent = match.Groups["seconds"].Success;
        var hasAnyComponent = hasDayComponent || hasHourComponent || hasMinuteComponent || hasSecondComponent;
        if (!hasAnyComponent)
        {
            return false;
        }

        var hasTimeSection = value.Contains('T', StringComparison.Ordinal);
        if (hasTimeSection &&
            !hasHourComponent &&
            !hasMinuteComponent &&
            !hasSecondComponent)
        {
            return false;
        }

        return true;
    }

    /// <summary>
    ///     判断字符串是否满足共享支持的 <c>time</c> 格式。
    ///     该格式固定要求显式时区偏移，并只接受 24 小时制的合法时分秒文本，
    ///     避免不同宿主对“time-only + offset”解析默认日期或时区时产生漂移。
    /// </summary>
    /// <param name="value">待校验的字符串值。</param>
    /// <returns>当前值是否是合法时间文本。</returns>
    private static bool MatchesSupportedTimeFormat(string value)
    {
        var match = SupportedTimeFormatRegex.Match(value);
        if (!match.Success)
        {
            return false;
        }

        var hour = int.Parse(match.Groups["hour"].Value, CultureInfo.InvariantCulture);
        var minute = int.Parse(match.Groups["minute"].Value, CultureInfo.InvariantCulture);
        var second = int.Parse(match.Groups["second"].Value, CultureInfo.InvariantCulture);
        if (hour > 23 || minute > 59 || second > 59)
        {
            return false;
        }

        var offset = match.Groups["offset"].Value;
        if (string.Equals(offset, "Z", StringComparison.Ordinal))
        {
            return true;
        }

        var offsetHour = int.Parse(offset.AsSpan(1, 2), CultureInfo.InvariantCulture);
        var offsetMinute = int.Parse(offset.AsSpan(4, 2), CultureInfo.InvariantCulture);
        return offsetHour <= 23 && offsetMinute <= 59;
    }

    /// <summary>
    ///     判断字符串是否满足共享支持的 <c>uri</c> 格式。
    ///     这里要求输入显式包含 scheme，避免把普通路径意外解释成平台相关的绝对 URI。
    /// </summary>
    /// <param name="value">待校验的字符串值。</param>
    /// <returns>当前值是否是合法绝对 URI。</returns>
    private static bool MatchesSupportedUriFormat(string value)
    {
        return SupportedUriSchemeRegex.IsMatch(value) &&
               Uri.TryCreate(value, UriKind.Absolute, out var uri) &&
               uri.IsAbsoluteUri;
    }

    /// <summary>
    ///     校验数组值是否满足元素数量约束。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="yamlPath">YAML 文件路径。</param>
    /// <param name="displayPath">字段路径。</param>
    /// <param name="itemCount">当前数组元素数量。</param>
    /// <param name="schemaNode">数组 schema 节点。</param>
    private static void ValidateArrayConstraints(
        string tableName,
        string yamlPath,
        string displayPath,
        int itemCount,
        YamlConfigSchemaNode schemaNode)
    {
        var constraints = schemaNode.ArrayConstraints;
        if (constraints is null)
        {
            return;
        }

        if (constraints.MinItems.HasValue && itemCount < constraints.MinItems.Value)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.ConstraintViolation,
                tableName,
                $"Property '{displayPath}' in config file '{yamlPath}' must contain at least {constraints.MinItems.Value} items, but the current YAML sequence contains {itemCount}.",
                yamlPath: yamlPath,
                schemaPath: schemaNode.SchemaPathHint,
                displayPath: GetDiagnosticPath(displayPath),
                rawValue: itemCount.ToString(CultureInfo.InvariantCulture),
                detail: $"Minimum item count: {constraints.MinItems.Value}.");
        }

        if (constraints.MaxItems.HasValue && itemCount > constraints.MaxItems.Value)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.ConstraintViolation,
                tableName,
                $"Property '{displayPath}' in config file '{yamlPath}' must contain at most {constraints.MaxItems.Value} items, but the current YAML sequence contains {itemCount}.",
                yamlPath: yamlPath,
                schemaPath: schemaNode.SchemaPathHint,
                displayPath: GetDiagnosticPath(displayPath),
                rawValue: itemCount.ToString(CultureInfo.InvariantCulture),
                detail: $"Maximum item count: {constraints.MaxItems.Value}.");
        }
    }

    /// <summary>
    ///     校验数组是否满足去重约束。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="yamlPath">YAML 文件路径。</param>
    /// <param name="displayPath">字段路径。</param>
    /// <param name="sequenceNode">实际数组节点。</param>
    /// <param name="schemaNode">数组 schema 节点。</param>
    private static void ValidateArrayUniqueItemsConstraint(
        string tableName,
        string yamlPath,
        string displayPath,
        YamlSequenceNode sequenceNode,
        YamlConfigSchemaNode schemaNode)
    {
        var constraints = schemaNode.ArrayConstraints;
        if (constraints is null ||
            !constraints.UniqueItems ||
            schemaNode.ItemNode is null)
        {
            return;
        }

        // The canonical item key uses schema-aware normalization so object key order,
        // scalar quoting, and numeric formatting do not accidentally bypass uniqueItems.
        Dictionary<string, int> seenItems = new(StringComparer.Ordinal);
        for (var itemIndex = 0; itemIndex < sequenceNode.Children.Count; itemIndex++)
        {
            var itemNode = sequenceNode.Children[itemIndex];
            var comparableValue = BuildComparableNodeValue(itemNode, schemaNode.ItemNode);
            if (seenItems.TryGetValue(comparableValue, out var existingIndex))
            {
                var itemPath = $"{displayPath}[{itemIndex}]";
                throw ConfigLoadExceptionFactory.Create(
                    ConfigLoadFailureKind.ConstraintViolation,
                    tableName,
                    $"Property '{displayPath}' in config file '{yamlPath}' requires unique array items, but item '{itemPath}' duplicates '{displayPath}[{existingIndex}]'.",
                    yamlPath: yamlPath,
                    schemaPath: schemaNode.SchemaPathHint,
                    displayPath: itemPath,
                    rawValue: DescribeYamlNodeForDiagnostics(itemNode, schemaNode.ItemNode),
                    detail: "The schema declares uniqueItems = true.");
            }

            seenItems.Add(comparableValue, itemIndex);
        }
    }

    /// <summary>
    ///     校验数组是否满足 <c>contains</c> 声明的匹配数量边界。
    ///     该实现会对每个数组项复用同一套递归校验逻辑做“非抛出式匹配”，避免 contains 与主校验链各自维护不同的 schema 解释规则。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="yamlPath">YAML 文件路径。</param>
    /// <param name="displayPath">字段路径。</param>
    /// <param name="sequenceNode">实际数组节点。</param>
    /// <param name="schemaNode">数组 schema 节点。</param>
    /// <param name="references">匹配成功的 <c>contains</c> 子树所声明的跨表引用收集器。</param>
    private static void ValidateArrayContainsConstraints(
        string tableName,
        string yamlPath,
        string displayPath,
        YamlSequenceNode sequenceNode,
        YamlConfigSchemaNode schemaNode,
        ICollection<YamlConfigReferenceUsage>? references)
    {
        var containsConstraints = schemaNode.ArrayConstraints?.ContainsConstraints;
        if (containsConstraints is null)
        {
            return;
        }

        var matchingCount = CountMatchingContainsItems(
            tableName,
            yamlPath,
            displayPath,
            sequenceNode,
            containsConstraints.ContainsNode,
            references);
        var rawValue = matchingCount.ToString(CultureInfo.InvariantCulture);
        var requiredMinContains = containsConstraints.MinContains ?? 1;
        if (matchingCount < requiredMinContains)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.ConstraintViolation,
                tableName,
                $"Property '{displayPath}' in config file '{yamlPath}' must contain at least {requiredMinContains} items matching the 'contains' schema, but the current YAML sequence contains {matchingCount}.",
                yamlPath: yamlPath,
                schemaPath: schemaNode.SchemaPathHint,
                displayPath: GetDiagnosticPath(displayPath),
                rawValue: rawValue,
                detail: $"Minimum matching contains count: {requiredMinContains}.");
        }

        if (containsConstraints.MaxContains.HasValue &&
            matchingCount > containsConstraints.MaxContains.Value)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.ConstraintViolation,
                tableName,
                $"Property '{displayPath}' in config file '{yamlPath}' must contain at most {containsConstraints.MaxContains.Value} items matching the 'contains' schema, but the current YAML sequence contains {matchingCount}.",
                yamlPath: yamlPath,
                schemaPath: schemaNode.SchemaPathHint,
                displayPath: GetDiagnosticPath(displayPath),
                rawValue: rawValue,
                detail: $"Maximum matching contains count: {containsConstraints.MaxContains.Value}.");
        }
    }

    /// <summary>
    ///     统计当前数组中有多少元素满足 <c>contains</c> 子 schema。
    ///     非预期内部错误会继续抛出，只有正常的 schema 不匹配才会被当成“当前元素不计数”。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="yamlPath">YAML 文件路径。</param>
    /// <param name="displayPath">数组字段路径。</param>
    /// <param name="sequenceNode">实际数组节点。</param>
    /// <param name="containsNode">contains 子 schema。</param>
    /// <param name="references">匹配成功元素的可选跨表引用收集器。</param>
    /// <returns>匹配 <c>contains</c> 子 schema 的元素数量。</returns>
    private static int CountMatchingContainsItems(
        string tableName,
        string yamlPath,
        string displayPath,
        YamlSequenceNode sequenceNode,
        YamlConfigSchemaNode containsNode,
        ICollection<YamlConfigReferenceUsage>? references)
    {
        var matchingCount = 0;
        for (var itemIndex = 0; itemIndex < sequenceNode.Children.Count; itemIndex++)
        {
            if (TryMatchSchemaNode(
                    tableName,
                    yamlPath,
                    $"{displayPath}[{itemIndex}]",
                    sequenceNode.Children[itemIndex],
                    containsNode,
                    references,
                    allowUnknownObjectProperties: true))
            {
                matchingCount++;
            }
        }

        return matchingCount;
    }

    /// <summary>
    ///     判断当前 YAML 节点是否满足给定 schema 子树。
    ///     contains / not 都通过该路径复用主校验逻辑，因此普通约束失败会返回 <see langword="false" />，
    ///     但内部意外状态仍会继续抛出。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="yamlPath">YAML 文件路径。</param>
    /// <param name="displayPath">当前节点路径。</param>
    /// <param name="node">实际 YAML 节点。</param>
    /// <param name="schemaNode">要试匹配的 schema 子树。</param>
    /// <param name="references">当前节点匹配成功后要写回的可选跨表引用收集器。</param>
    /// <param name="allowUnknownObjectProperties">对象试匹配时是否允许额外字段。</param>
    /// <returns>当前节点是否匹配指定 schema 子树。</returns>
    private static bool TryMatchSchemaNode(
        string tableName,
        string yamlPath,
        string displayPath,
        YamlNode node,
        YamlConfigSchemaNode schemaNode,
        ICollection<YamlConfigReferenceUsage>? references,
        bool allowUnknownObjectProperties)
    {
        // 约束试匹配不能把失败路径的引用泄漏给外层，但匹配成功的分支仍需要把引用写回，
        // 这样 contains / not 等内联 schema 才能与主校验链复用同一套递归解释规则。
        List<YamlConfigReferenceUsage>? matchedReferences = references is null ? null : new();

        try
        {
            ValidateNode(
                tableName,
                yamlPath,
                displayPath,
                node,
                schemaNode,
                matchedReferences,
                allowUnknownObjectProperties);

            if (references is not null &&
                matchedReferences is not null)
            {
                AddUniqueReferenceUsages(references, matchedReferences);
            }

            return true;
        }
        catch (ConfigLoadException exception) when (exception.Diagnostic.FailureKind !=
                                                    ConfigLoadFailureKind.UnexpectedFailure)
        {
            return false;
        }
    }

    /// <summary>
    ///     将试匹配分支采集到的引用回写到外层集合，并按结构化标识去重。
    /// </summary>
    /// <param name="references">外层引用集合。</param>
    /// <param name="matchedReferences">当前成功匹配分支采集到的引用。</param>
    private static void AddUniqueReferenceUsages(
        ICollection<YamlConfigReferenceUsage> references,
        IEnumerable<YamlConfigReferenceUsage> matchedReferences)
    {
        foreach (var referenceUsage in matchedReferences)
        {
            if (!ContainsReferenceUsage(references, referenceUsage))
            {
                references.Add(referenceUsage);
            }
        }
    }

    /// <summary>
    ///     判断外层引用集合中是否已经存在同一条引用使用记录。
    /// </summary>
    /// <param name="references">要检查的引用集合。</param>
    /// <param name="candidate">当前待合并的引用记录。</param>
    /// <returns>当集合中已存在语义相同的记录时返回 <see langword="true" />。</returns>
    private static bool ContainsReferenceUsage(
        IEnumerable<YamlConfigReferenceUsage> references,
        YamlConfigReferenceUsage candidate)
    {
        foreach (var referenceUsage in references)
        {
            if (string.Equals(referenceUsage.YamlPath, candidate.YamlPath, StringComparison.Ordinal) &&
                string.Equals(referenceUsage.SchemaPath, candidate.SchemaPath, StringComparison.Ordinal) &&
                string.Equals(referenceUsage.PropertyPath, candidate.PropertyPath, StringComparison.Ordinal) &&
                string.Equals(referenceUsage.RawValue, candidate.RawValue, StringComparison.Ordinal) &&
                string.Equals(referenceUsage.ReferencedTableName, candidate.ReferencedTableName, StringComparison.Ordinal) &&
                referenceUsage.ValueType == candidate.ValueType)
            {
                return true;
            }
        }

        return false;
    }

    /// <summary>
    ///     校验节点是否命中了 <c>not</c> 声明的禁用 schema。
    ///     与 contains 不同，not 会沿用主校验链的严格对象语义，避免把“声明属性子集”误当成完整命中。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="yamlPath">YAML 文件路径。</param>
    /// <param name="displayPath">当前字段路径。</param>
    /// <param name="node">当前 YAML 节点。</param>
    /// <param name="schemaNode">当前 schema 节点。</param>
    private static void ValidateNegatedSchemaConstraint(
        string tableName,
        string yamlPath,
        string displayPath,
        YamlNode node,
        YamlConfigSchemaNode schemaNode)
    {
        if (schemaNode.NegatedSchemaNode is null ||
            !TryMatchSchemaNode(
                tableName,
                yamlPath,
                displayPath,
                node,
                schemaNode.NegatedSchemaNode,
                references: null,
                allowUnknownObjectProperties: false))
        {
            return;
        }

        var subject = string.IsNullOrWhiteSpace(displayPath)
            ? "Root object"
            : $"Property '{displayPath}'";
        throw ConfigLoadExceptionFactory.Create(
            ConfigLoadFailureKind.ConstraintViolation,
            tableName,
            $"{subject} in config file '{yamlPath}' must not match the 'not' schema.",
            yamlPath: yamlPath,
            schemaPath: schemaNode.SchemaPathHint,
            displayPath: GetDiagnosticPath(displayPath),
            rawValue: DescribeYamlNodeForDiagnostics(node, schemaNode.NegatedSchemaNode),
            detail: "The current YAML value matches the forbidden 'not' schema.");
    }

    /// <summary>
    ///     将一个已通过结构校验的 YAML 节点归一化为可比较字符串。
    ///     该键同时服务于 <c>uniqueItems</c> 与 <c>const</c>，
    ///     因此要忽略对象字段顺序和字符串引号形式。
    /// </summary>
    /// <param name="node">YAML 节点。</param>
    /// <param name="schemaNode">对应 schema 节点。</param>
    /// <returns>可稳定比较的归一化键。</returns>
    private static string BuildComparableNodeValue(YamlNode node, YamlConfigSchemaNode schemaNode)
    {
        return schemaNode.NodeType switch
        {
            YamlConfigSchemaPropertyType.Object => BuildComparableObjectValue(node, schemaNode),
            YamlConfigSchemaPropertyType.Array => BuildComparableArrayValue(node, schemaNode),
            YamlConfigSchemaPropertyType.Integer => BuildComparableScalarValue(node, schemaNode),
            YamlConfigSchemaPropertyType.Number => BuildComparableScalarValue(node, schemaNode),
            YamlConfigSchemaPropertyType.Boolean => BuildComparableScalarValue(node, schemaNode),
            YamlConfigSchemaPropertyType.String => BuildComparableScalarValue(node, schemaNode),
            _ => throw new InvalidOperationException($"Unsupported schema node type '{schemaNode.NodeType}'.")
        };
    }

    /// <summary>
    ///     构建对象节点的可比较键。
    ///     对象字段会先按属性名排序，避免 YAML 原始字段顺序影响 <c>uniqueItems</c> 的等价关系。
    /// </summary>
    /// <param name="node">YAML 节点。</param>
    /// <param name="schemaNode">对象 schema 节点。</param>
    /// <returns>对象节点的稳定比较键。</returns>
    private static string BuildComparableObjectValue(YamlNode node, YamlConfigSchemaNode schemaNode)
    {
        if (node is not YamlMappingNode mappingNode)
        {
            throw new InvalidOperationException("Validated object nodes must be YAML mappings.");
        }

        var properties = schemaNode.Properties
                         ?? throw new InvalidOperationException(
                             "Validated object nodes must expose declared properties.");
        var objectEntries = new List<KeyValuePair<string, string>>(mappingNode.Children.Count);
        foreach (var entry in mappingNode.Children)
        {
            if (entry.Key is not YamlScalarNode keyNode ||
                keyNode.Value is null ||
                !properties.TryGetValue(keyNode.Value, out var propertySchema))
            {
                throw new InvalidOperationException("Validated object nodes must use declared scalar property names.");
            }

            objectEntries.Add(
                new KeyValuePair<string, string>(
                    keyNode.Value,
                    BuildComparableNodeValue(entry.Value, propertySchema)));
        }

        objectEntries.Sort(static (left, right) => string.CompareOrdinal(left.Key, right.Key));
        return string.Join(
            "|",
            objectEntries.Select(static entry =>
                $"{entry.Key.Length.ToString(CultureInfo.InvariantCulture)}:{entry.Key}={entry.Value.Length.ToString(CultureInfo.InvariantCulture)}:{entry.Value}"));
    }

    /// <summary>
    ///     构建数组节点的可比较键。
    ///     数组仍保留元素顺序，因为 <c>uniqueItems</c> 只忽略对象字段顺序，不忽略数组顺序。
    /// </summary>
    /// <param name="node">YAML 节点。</param>
    /// <param name="schemaNode">数组 schema 节点。</param>
    /// <returns>数组节点的稳定比较键。</returns>
    private static string BuildComparableArrayValue(YamlNode node, YamlConfigSchemaNode schemaNode)
    {
        if (node is not YamlSequenceNode sequenceNode ||
            schemaNode.ItemNode is null)
        {
            throw new InvalidOperationException("Validated array nodes must be YAML sequences with item schema.");
        }

        return "[" +
               string.Join(
                   ",",
                   sequenceNode.Children.Select(item =>
                   {
                       var comparableValue = BuildComparableNodeValue(item, schemaNode.ItemNode);
                       return $"{comparableValue.Length.ToString(CultureInfo.InvariantCulture)}:{comparableValue}";
                   })) +
               "]";
    }

    /// <summary>
    ///     构建标量节点的可比较键。
    ///     标量会沿用与 enum / 引用校验一致的归一化规则，避免数字格式和引号形式导致伪差异。
    /// </summary>
    /// <param name="node">YAML 节点。</param>
    /// <param name="schemaNode">标量 schema 节点。</param>
    /// <returns>标量节点的稳定比较键。</returns>
    private static string BuildComparableScalarValue(YamlNode node, YamlConfigSchemaNode schemaNode)
    {
        if (node is not YamlScalarNode scalarNode ||
            scalarNode.Value is null)
        {
            throw new InvalidOperationException("Validated scalar nodes must be YAML scalars.");
        }

        var normalizedScalar = NormalizeScalarValue(schemaNode.NodeType, scalarNode.Value);
        return
            $"{schemaNode.NodeType}:{normalizedScalar.Length.ToString(CultureInfo.InvariantCulture)}:{normalizedScalar}";
    }

    /// <summary>
    ///     为唯一性诊断提取一个可读的节点摘要。
    /// </summary>
    /// <param name="node">YAML 节点。</param>
    /// <param name="schemaNode">对应 schema 节点。</param>
    /// <returns>诊断摘要。</returns>
    private static string DescribeYamlNodeForDiagnostics(YamlNode node, YamlConfigSchemaNode schemaNode)
    {
        return schemaNode.NodeType switch
        {
            YamlConfigSchemaPropertyType.Object => "{...}",
            YamlConfigSchemaPropertyType.Array => "[...]",
            _ when node is YamlScalarNode scalarNode => scalarNode.Value ?? string.Empty,
            _ => node.GetType().Name
        };
    }

    /// <summary>
    ///     判断数值是否满足 <c>multipleOf</c>。
    ///     优先按十进制字面量做精确整倍数判断，
    ///     以同时避免 0.1 / 0.01 这类十进制步进的伪失败和大数量级非整倍数的伪通过；
    ///     只有当值超出精确十进制路径时才退回双精度容差比较。
    /// </summary>
    /// <param name="normalizedValue">用于数值比较的规范化 YAML 标量文本。</param>
    /// <param name="value">当前值。</param>
    /// <param name="divisor">步进约束。</param>
    /// <returns>是否满足整倍数关系。</returns>
    private static bool IsMultipleOf(string normalizedValue, double value, double divisor)
    {
        if (TryIsExactDecimalMultiple(normalizedValue, divisor, out var exactResult))
        {
            return exactResult;
        }

        var quotient = value / divisor;
        var nearestInteger = Math.Round(quotient);
        var tolerance = 1e-9 * Math.Max(1d, Math.Abs(quotient));
        return Math.Abs(quotient - nearestInteger) <= tolerance;
    }

    /// <summary>
    ///     尝试按十进制字面量精确判断 <c>multipleOf</c>。
    ///     该路径直接对齐 YAML / JSON 中常见的有限十进制写法，
    ///     避免双精度舍入把明显的非整倍数误判为合法。
    /// </summary>
    /// <param name="valueText">规范化后的 YAML 数值文本。</param>
    /// <param name="divisor">Schema 声明的步进约束。</param>
    /// <param name="isMultiple">精确路径下的判断结果。</param>
    /// <returns>是否成功进入精确十进制判断路径。</returns>
    private static bool TryIsExactDecimalMultiple(string valueText, double divisor, out bool isMultiple)
    {
        var divisorText = divisor.ToString("R", CultureInfo.InvariantCulture);
        if (!TryParseExactDecimal(valueText, out var valueSignificand, out var valueScale) ||
            !TryParseExactDecimal(divisorText, out var divisorSignificand, out var divisorScale) ||
            divisorSignificand.IsZero)
        {
            isMultiple = false;
            return false;
        }

        var commonScale = Math.Max(valueScale, divisorScale);
        var scaledValue = ScaleDecimalSignificand(valueSignificand, valueScale, commonScale);
        var scaledDivisor = ScaleDecimalSignificand(divisorSignificand, divisorScale, commonScale);
        isMultiple = scaledValue % scaledDivisor == BigInteger.Zero;
        return true;
    }

    /// <summary>
    ///     将有限十进制或科学计数法文本拆成“整数有效数字 + 十进制位数”形式。
    ///     这样可以把整倍数判断转成同一尺度下的整数取模，避免浮点误差参与计算。
    /// </summary>
    /// <param name="text">待解析的数值文本。</param>
    /// <param name="significand">去掉小数点后的有效数字。</param>
    /// <param name="scale">十进制缩放位数；原值等于 <paramref name="significand" /> / 10^<paramref name="scale" />。</param>
    /// <returns>是否成功解析为有限十进制数。</returns>
    private static bool TryParseExactDecimal(string text, out BigInteger significand, out int scale)
    {
        var match = ExactDecimalPattern.Match(text);
        if (!match.Success)
        {
            significand = BigInteger.Zero;
            scale = 0;
            return false;
        }

        var exponentGroup = match.Groups["exponent"].Value;
        var exponent = 0;
        if (!string.IsNullOrEmpty(exponentGroup) &&
            !int.TryParse(exponentGroup, NumberStyles.Integer, CultureInfo.InvariantCulture, out exponent))
        {
            significand = BigInteger.Zero;
            scale = 0;
            return false;
        }

        var integerDigits = match.Groups["integer"].Value;
        var fractionDigits = match.Groups["fraction"].Success
            ? match.Groups["fraction"].Value
            : match.Groups["fractionOnly"].Value;
        var digits = string.Concat(integerDigits, fractionDigits);
        if (digits.Length == 0)
        {
            digits = "0";
        }

        digits = digits.TrimStart('0');
        if (digits.Length == 0)
        {
            significand = BigInteger.Zero;
            scale = 0;
            return true;
        }

        scale = checked(fractionDigits.Length - exponent);
        if (scale < 0)
        {
            digits = string.Concat(digits, new string('0', -scale));
            scale = 0;
        }

        while (scale > 0 && digits[^1] == '0')
        {
            digits = digits[..^1];
            scale--;
        }

        significand = BigInteger.Parse(digits, CultureInfo.InvariantCulture);
        if (string.Equals(match.Groups["sign"].Value, "-", StringComparison.Ordinal))
        {
            significand = BigInteger.Negate(significand);
        }

        return true;
    }

    /// <summary>
    ///     将十进制有效数字放大到目标尺度，便于在同一量纲下执行整数取模。
    /// </summary>
    /// <param name="significand">原始有效数字。</param>
    /// <param name="currentScale">当前十进制位数。</param>
    /// <param name="targetScale">目标十进制位数。</param>
    /// <returns>放大到目标尺度后的有效数字。</returns>
    private static BigInteger ScaleDecimalSignificand(BigInteger significand, int currentScale, int targetScale)
    {
        if (currentScale == targetScale)
        {
            return significand;
        }

        return significand * BigInteger.Pow(10, targetScale - currentScale);
    }

    /// <summary>
    ///     解析跨表引用目标表名称。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <param name="propertyPath">字段路径。</param>
    /// <param name="element">Schema 节点。</param>
    /// <returns>目标表名称；未声明时返回空。</returns>
    private static string? TryGetReferenceTableName(
        string tableName,
        string schemaPath,
        string propertyPath,
        JsonElement element)
    {
        if (!element.TryGetProperty("x-gframework-ref-table", out var referenceTableElement))
        {
            return null;
        }

        if (referenceTableElement.ValueKind != JsonValueKind.String)
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"Property '{propertyPath}' in schema file '{schemaPath}' must declare a string 'x-gframework-ref-table' value.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath));
        }

        var referenceTableName = referenceTableElement.GetString();
        if (string.IsNullOrWhiteSpace(referenceTableName))
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"Property '{propertyPath}' in schema file '{schemaPath}' must declare a non-empty 'x-gframework-ref-table' value.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath));
        }

        return referenceTableName;
    }

    /// <summary>
    ///     验证哪些 schema 类型允许声明跨表引用。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <param name="propertyPath">字段路径。</param>
    /// <param name="propertyType">字段类型。</param>
    /// <param name="referenceTableName">目标表名称。</param>
    private static void EnsureReferenceKeywordIsSupported(
        string tableName,
        string schemaPath,
        string propertyPath,
        YamlConfigSchemaPropertyType propertyType,
        string? referenceTableName)
    {
        if (referenceTableName == null)
        {
            return;
        }

        if (propertyType == YamlConfigSchemaPropertyType.String ||
            propertyType == YamlConfigSchemaPropertyType.Integer)
        {
            return;
        }

        throw ConfigLoadExceptionFactory.Create(
            ConfigLoadFailureKind.SchemaUnsupported,
            tableName,
            $"Property '{propertyPath}' in schema file '{schemaPath}' uses 'x-gframework-ref-table', but only string, integer, or arrays of those scalar types can declare cross-table references.",
            schemaPath: schemaPath,
            displayPath: GetDiagnosticPath(propertyPath),
            referencedTableName: referenceTableName);
    }

    /// <summary>
    ///     递归收集 schema 中声明的目标表名称。
    /// </summary>
    /// <param name="node">当前 schema 节点。</param>
    /// <param name="referencedTableNames">输出集合。</param>
    private static void CollectReferencedTableNames(
        YamlConfigSchemaNode node,
        ISet<string> referencedTableNames)
    {
        if (node.ReferenceTableName != null)
        {
            referencedTableNames.Add(node.ReferenceTableName);
        }

        if (node.Properties is not null)
        {
            foreach (var property in node.Properties.Values)
            {
                CollectReferencedTableNames(property, referencedTableNames);
            }
        }

        if (node.ItemNode is not null)
        {
            CollectReferencedTableNames(node.ItemNode, referencedTableNames);
        }

        var containsNode = node.ArrayConstraints?.ContainsConstraints?.ContainsNode;
        if (containsNode is not null)
        {
            CollectReferencedTableNames(containsNode, referencedTableNames);
        }

        var dependentSchemas = node.ObjectConstraints?.DependentSchemas;
        if (dependentSchemas is not null)
        {
            foreach (var dependentSchemaNode in dependentSchemas.Values)
            {
                CollectReferencedTableNames(dependentSchemaNode, referencedTableNames);
            }
        }

        var allOfSchemas = node.ObjectConstraints?.AllOfSchemas;
        if (allOfSchemas is not null)
        {
            foreach (var allOfSchemaNode in allOfSchemas)
            {
                CollectReferencedTableNames(allOfSchemaNode, referencedTableNames);
            }
        }

        var conditionalSchemas = node.ObjectConstraints?.ConditionalSchemas;
        if (conditionalSchemas is not null)
        {
            CollectReferencedTableNames(conditionalSchemas.IfSchema, referencedTableNames);
            if (conditionalSchemas.ThenSchema is not null)
            {
                CollectReferencedTableNames(conditionalSchemas.ThenSchema, referencedTableNames);
            }

            if (conditionalSchemas.ElseSchema is not null)
            {
                CollectReferencedTableNames(conditionalSchemas.ElseSchema, referencedTableNames);
            }
        }
    }

    /// <summary>
    ///     将 schema 关键字中的标量值归一化到运行时比较字符串。
    /// </summary>
    /// <param name="tableName">所属配置表名称。</param>
    /// <param name="schemaPath">Schema 文件路径。</param>
    /// <param name="propertyPath">字段路径。</param>
    /// <param name="keywordName">关键字名称。</param>
    /// <param name="expectedType">期望的标量类型。</param>
    /// <param name="item">当前关键字值节点。</param>
    /// <returns>归一化后的字符串值。</returns>
    private static string NormalizeKeywordScalarValue(
        string tableName,
        string schemaPath,
        string propertyPath,
        string keywordName,
        YamlConfigSchemaPropertyType expectedType,
        JsonElement item)
    {
        try
        {
            return expectedType switch
            {
                YamlConfigSchemaPropertyType.String when item.ValueKind == JsonValueKind.String =>
                    item.GetString() ?? string.Empty,
                YamlConfigSchemaPropertyType.Integer when item.ValueKind == JsonValueKind.Number =>
                    item.GetInt64().ToString(CultureInfo.InvariantCulture),
                YamlConfigSchemaPropertyType.Number when item.ValueKind == JsonValueKind.Number =>
                    item.GetDouble().ToString(CultureInfo.InvariantCulture),
                YamlConfigSchemaPropertyType.Boolean when item.ValueKind == JsonValueKind.True =>
                    bool.TrueString.ToLowerInvariant(),
                YamlConfigSchemaPropertyType.Boolean when item.ValueKind == JsonValueKind.False =>
                    bool.FalseString.ToLowerInvariant(),
                _ => throw new InvalidOperationException()
            };
        }
        catch
        {
            throw ConfigLoadExceptionFactory.Create(
                ConfigLoadFailureKind.SchemaUnsupported,
                tableName,
                $"Property '{propertyPath}' in schema file '{schemaPath}' contains a '{keywordName}' value that is incompatible with schema type '{GetTypeName(expectedType)}'.",
                schemaPath: schemaPath,
                displayPath: GetDiagnosticPath(propertyPath));
        }
    }

    /// <summary>
    ///     将内部路径转换为适合放入诊断对象的可选字段路径。
    /// </summary>
    /// <param name="path">内部使用的属性路径。</param>
    /// <returns>可用于诊断的路径；根节点时返回空。</returns>
    private static string? GetDiagnosticPath(string path)
    {
        return string.IsNullOrWhiteSpace(path) || string.Equals(path, "<root>", StringComparison.Ordinal)
            ? null
            : path;
    }

    /// <summary>
    ///     将 YAML 标量值规范化成运行时比较格式。
    /// </summary>
    /// <param name="expectedType">期望的标量类型。</param>
    /// <param name="value">原始字符串值。</param>
    /// <returns>归一化后的字符串。</returns>
    private static string NormalizeScalarValue(YamlConfigSchemaPropertyType expectedType, string value)
    {
        return expectedType switch
        {
            YamlConfigSchemaPropertyType.String => value,
            YamlConfigSchemaPropertyType.Integer when long.TryParse(
                    value,
                    NumberStyles.Integer,
                    CultureInfo.InvariantCulture,
                    out var integerValue) =>
                integerValue.ToString(CultureInfo.InvariantCulture),
            YamlConfigSchemaPropertyType.Number when double.TryParse(
                    value,
                    NumberStyles.Float | NumberStyles.AllowThousands,
                    CultureInfo.InvariantCulture,
                    out var numberValue) =>
                numberValue.ToString(CultureInfo.InvariantCulture),
            YamlConfigSchemaPropertyType.Boolean when bool.TryParse(value, out var booleanValue) =>
                booleanValue.ToString().ToLowerInvariant(),
            YamlConfigSchemaPropertyType.Integer =>
                throw new InvalidOperationException($"Value '{value}' cannot be normalized as integer."),
            YamlConfigSchemaPropertyType.Number =>
                throw new InvalidOperationException($"Value '{value}' cannot be normalized as number."),
            YamlConfigSchemaPropertyType.Boolean =>
                throw new InvalidOperationException($"Value '{value}' cannot be normalized as boolean."),
            _ =>
                throw new InvalidOperationException(
                    $"Schema node type '{expectedType}' cannot be normalized as a scalar value.")
        };
    }

    /// <summary>
    ///     获取 schema 类型的可读名称，用于错误信息。
    /// </summary>
    /// <param name="type">Schema 节点类型。</param>
    /// <returns>可读类型名。</returns>
    private static string GetTypeName(YamlConfigSchemaPropertyType type)
    {
        return type switch
        {
            YamlConfigSchemaPropertyType.Integer => "integer",
            YamlConfigSchemaPropertyType.Number => "number",
            YamlConfigSchemaPropertyType.Boolean => "boolean",
            YamlConfigSchemaPropertyType.String => "string",
            YamlConfigSchemaPropertyType.Array => "array",
            YamlConfigSchemaPropertyType.Object => "object",
            _ => type.ToString()
        };
    }

    /// <summary>
    ///     组合 schema 中的逻辑路径，便于诊断时指出深层字段。
    /// </summary>
    /// <param name="parentPath">父级路径。</param>
    /// <param name="propertyName">当前属性名。</param>
    /// <returns>组合后的路径。</returns>
    private static string CombineSchemaPath(string parentPath, string propertyName)
    {
        return string.Equals(parentPath, "<root>", StringComparison.Ordinal)
            ? propertyName
            : $"{parentPath}.{propertyName}";
    }

    /// <summary>
    ///     组合 YAML 诊断展示路径。
    /// </summary>
    /// <param name="parentPath">父级路径。</param>
    /// <param name="propertyName">当前属性名。</param>
    /// <returns>组合后的路径。</returns>
    private static string CombineDisplayPath(string parentPath, string propertyName)
    {
        return string.IsNullOrWhiteSpace(parentPath) ? propertyName : $"{parentPath}.{propertyName}";
    }

    /// <summary>
    ///     返回当前节点上声明的“仅对象可用”关键字名称。
    /// </summary>
    /// <param name="element">Schema 节点。</param>
    /// <returns>命中的关键字名称；未命中时返回空。</returns>
    private static string? TryGetObjectOnlyKeywordName(JsonElement element)
    {
        if (element.TryGetProperty("allOf", out _))
        {
            return "allOf";
        }

        if (element.TryGetProperty("if", out _))
        {
            return "if";
        }

        if (element.TryGetProperty("then", out _))
        {
            return "then";
        }

        return element.TryGetProperty("else", out _)
            ? "else"
            : null;
    }

    /// <summary>
    ///     判断当前标量是否应按字符串处理。
    ///     这里显式排除 YAML 的数字、布尔和 null 标签，避免未加引号的值被当成字符串混入运行时。
    /// </summary>
    /// <param name="tag">YAML 标量标签。</param>
    /// <returns>是否为字符串标量。</returns>
    private static bool IsStringScalar(string tag)
    {
        if (string.IsNullOrWhiteSpace(tag))
        {
            return true;
        }

        return !string.Equals(tag, "tag:yaml.org,2002:int", StringComparison.Ordinal) &&
               !string.Equals(tag, "tag:yaml.org,2002:float", StringComparison.Ordinal) &&
               !string.Equals(tag, "tag:yaml.org,2002:bool", StringComparison.Ordinal) &&
               !string.Equals(tag, "tag:yaml.org,2002:null", StringComparison.Ordinal);
    }
}
