<?php

declare(strict_types=1);

/**
 * 本文件属于 DBErp 进销存系统。
 *
 * 版权所有 © 2026 北京珑大钜商科技有限公司。保留所有权利。
 *
 * 未经官方授权，不得删除、篡改或隐藏本文件中的版权声明、产品标识与来源信息。
 * 商业授权、官方更新与技术支持服务，请以珑大官方支持中心记录为准。
 *
 * @link https://support.loongdom.cn
 * @see  https://www.dberp.com.cn
 */

namespace App\Tools\Support;

use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;

/**
 * V1 JSON Schema 子集校验：以 Tool metadata schema 为唯一真源，内部映射 Laravel Validator。
 */
final class ToolJsonSchemaValidator
{
    /**
     * @param  array<string, mixed>  $schema
     * @param  array<string, mixed>  $data
     */
    public function validate(array $schema, array $data, bool $applyDefaults = true): ToolSchemaValidationResult
    {
        if (($schema['type'] ?? null) !== 'object') {
            return new ToolSchemaValidationResult(false, $data, [
                '' => ['Schema type must be object.'],
            ]);
        }

        $normalized = $applyDefaults ? $this->applyDefaults($schema, $data) : $data;
        $additionalErrors = $this->collectAdditionalPropertyErrors($schema, $normalized, '');

        if ($additionalErrors !== []) {
            return new ToolSchemaValidationResult(false, $normalized, $additionalErrors);
        }

        $rules = $this->buildLaravelRules($schema, '');

        $validator = Validator::make($normalized, $rules);

        if ($validator->fails()) {
            /** @var array<string, list<string>> $messages */
            $messages = $validator->errors()->toArray();

            return new ToolSchemaValidationResult(false, $normalized, $messages);
        }

        return new ToolSchemaValidationResult(true, $normalized, []);
    }

    /**
     * @param  array<string, mixed>  $schema
     * @param  array<string, mixed>  $data
     * @return array<string, mixed>
     */
    private function applyDefaults(array $schema, array $data): array
    {
        $properties = $this->schemaProperties($schema);

        foreach ($properties as $key => $propertySchema) {
            if (! is_array($propertySchema)) {
                continue;
            }

            if (! array_key_exists($key, $data) && array_key_exists('default', $propertySchema)) {
                $data[$key] = $propertySchema['default'];
            }

            if (
                array_key_exists($key, $data)
                && is_array($data[$key])
                && ($propertySchema['type'] ?? null) === 'object'
            ) {
                /** @var array<string, mixed> $nested */
                $nested = $data[$key];
                $data[$key] = $this->applyDefaults($propertySchema, $nested);
            }
        }

        return $data;
    }

    /**
     * @param  array<string, mixed>  $schema
     * @param  array<string, mixed>  $data
     * @return array<string, list<string>>
     */
    private function collectAdditionalPropertyErrors(array $schema, array $data, string $path): array
    {
        $additionalProperties = $schema['additionalProperties'] ?? false;

        if ($additionalProperties !== false) {
            return $this->collectNestedAdditionalPropertyErrors($schema, $data, $path);
        }

        $allowed = array_keys($this->schemaProperties($schema));
        $errors = [];

        foreach (array_keys($data) as $key) {
            if (! is_string($key) || in_array($key, $allowed, true)) {
                continue;
            }

            $field = $this->joinPath($path, $key);
            $errors[$field] = [sprintf('Unknown field [%s] is not allowed.', $field)];
        }

        return array_merge($errors, $this->collectNestedAdditionalPropertyErrors($schema, $data, $path));
    }

    /**
     * @param  array<string, mixed>  $schema
     * @param  array<string, mixed>  $data
     * @return array<string, list<string>>
     */
    private function collectNestedAdditionalPropertyErrors(array $schema, array $data, string $path): array
    {
        $errors = [];
        $properties = $this->schemaProperties($schema);

        foreach ($properties as $key => $propertySchema) {
            if (! is_array($propertySchema) || ! array_key_exists($key, $data) || ! is_array($data[$key])) {
                continue;
            }

            if (($propertySchema['type'] ?? null) === 'object') {
                /** @var array<string, mixed> $nested */
                $nested = $data[$key];
                $errors = array_merge(
                    $errors,
                    $this->collectAdditionalPropertyErrors($propertySchema, $nested, $this->joinPath($path, $key)),
                );
            }

            if (
                ($propertySchema['type'] ?? null) === 'array'
                && isset($propertySchema['items'])
                && is_array($propertySchema['items'])
                && ($propertySchema['items']['type'] ?? null) === 'object'
            ) {
                foreach (array_values($data[$key]) as $index => $item) {
                    if (! is_array($item)) {
                        continue;
                    }

                    /** @var array<string, mixed> $item */
                    $errors = array_merge(
                        $errors,
                        $this->collectAdditionalPropertyErrors(
                            $propertySchema['items'],
                            $item,
                            $this->joinPath($this->joinPath($path, $key), (string) $index),
                        ),
                    );
                }
            }
        }

        return $errors;
    }

    /**
     * @param  array<string, mixed>  $schema
     * @return array<string, mixed>
     */
    private function buildLaravelRules(array $schema, string $path): array
    {
        $rules = [];
        $properties = $this->schemaProperties($schema);
        $required = $this->schemaRequired($schema);

        foreach ($properties as $key => $propertySchema) {
            if (! is_array($propertySchema)) {
                continue;
            }

            $field = $this->joinPath($path, $key);
            $fieldRules = [];

            if (in_array($key, $required, true)) {
                // JSON Schema required 只要求键存在；空字符串 / 0 / false 仍合法。
                $fieldRules[] = 'present';
            } else {
                $fieldRules[] = 'nullable';
            }

            $fieldRules = array_merge($fieldRules, $this->typeRules($propertySchema));
            $rules[$field] = $fieldRules;

            if (($propertySchema['type'] ?? null) === 'object') {
                $rules = array_merge($rules, $this->buildLaravelRules($propertySchema, $field));
            }

            if (($propertySchema['type'] ?? null) === 'array' && isset($propertySchema['items']) && is_array($propertySchema['items'])) {
                $itemSchema = $propertySchema['items'];
                $itemPath = $field.'.*';
                $itemRules = $this->typeRules($itemSchema);

                if ($itemRules !== []) {
                    $rules[$itemPath] = $itemRules;
                }

                if (($itemSchema['type'] ?? null) === 'object') {
                    $rules = array_merge($rules, $this->buildLaravelRules($itemSchema, $itemPath));
                }
            }
        }

        return $rules;
    }

    /**
     * @param  array<string, mixed>  $schema
     * @return list<mixed>
     */
    private function typeRules(array $schema): array
    {
        $rules = [];
        $type = $schema['type'] ?? null;

        $rules[] = match ($type) {
            'string' => 'string',
            'integer' => 'integer',
            'number' => 'numeric',
            'boolean' => 'boolean',
            'array' => 'array',
            'object' => 'array',
            default => null,
        };

        if (isset($schema['enum']) && is_array($schema['enum'])) {
            $rules[] = Rule::in($schema['enum']);
        }

        if (array_key_exists('minimum', $schema) && is_numeric($schema['minimum'])) {
            $rules[] = 'min:'.$this->numericRuleValue($schema['minimum']);
        }

        if (array_key_exists('maximum', $schema) && is_numeric($schema['maximum'])) {
            $rules[] = 'max:'.$this->numericRuleValue($schema['maximum']);
        }

        if (array_key_exists('minLength', $schema) && is_numeric($schema['minLength'])) {
            $rules[] = 'min:'.(int) $schema['minLength'];
        }

        if (array_key_exists('maxLength', $schema) && is_numeric($schema['maxLength'])) {
            $rules[] = 'max:'.(int) $schema['maxLength'];
        }

        if (($schema['format'] ?? null) === 'date') {
            $rules[] = 'date_format:Y-m-d';
        }

        return array_values(array_filter($rules, static fn (mixed $rule): bool => $rule !== null));
    }

    private function numericRuleValue(mixed $value): int|float
    {
        if (is_int($value)) {
            return $value;
        }

        $asFloat = (float) $value;

        return $asFloat == (int) $asFloat ? (int) $asFloat : $asFloat;
    }

    /**
     * @param  array<string, mixed>  $schema
     * @return array<string, mixed>
     */
    private function schemaProperties(array $schema): array
    {
        $properties = $schema['properties'] ?? [];

        if (! is_array($properties)) {
            return [];
        }

        $normalized = [];

        foreach ($properties as $key => $value) {
            if (is_string($key)) {
                $normalized[$key] = $value;
            }
        }

        return $normalized;
    }

    /**
     * @param  array<string, mixed>  $schema
     * @return list<string>
     */
    private function schemaRequired(array $schema): array
    {
        $required = $schema['required'] ?? [];

        if (! is_array($required)) {
            return [];
        }

        return array_values(array_filter($required, static fn (mixed $value): bool => is_string($value) && $value !== ''));
    }

    private function joinPath(string $prefix, string $key): string
    {
        return $prefix === '' ? $key : $prefix.'.'.$key;
    }
}
