<?php

namespace WPUltimateSecurity\UltimateSecurity\Support\Validation;

use WPUltimateSecurity\UltimateSecurity\Support\Validation\Rules\ArrayRule;
use WPUltimateSecurity\UltimateSecurity\Support\Validation\Rules\BooleanRule;
use WPUltimateSecurity\UltimateSecurity\Support\Validation\Rules\EmailRule;
use WPUltimateSecurity\UltimateSecurity\Support\Validation\Rules\InArrayRule;
use WPUltimateSecurity\UltimateSecurity\Support\Validation\Rules\IntegerRule;
use WPUltimateSecurity\UltimateSecurity\Support\Validation\Rules\IpAddressRule;
use WPUltimateSecurity\UltimateSecurity\Support\Validation\Rules\RequiredRule;
use WPUltimateSecurity\UltimateSecurity\Support\Validation\Rules\StringRule;
use WPUltimateSecurity\UltimateSecurity\Support\Validation\Rules\UrlRule;
use WP_REST_Request;

/**
 * Request Validator
 *
 * Validates incoming request data against a set of rules.
 *
 * Usage:
 * ```php
 * $validator = new RequestValidator();
 *
 * // Validate with rule definitions
 * $result = $validator->validate($request, [
 *     'email' => [Rule::required(), Rule::email()],
 *     'name' => [Rule::string()->min(2)->max(100)],
 *     'age' => [Rule::integer()->min(0)->max(150)],
 *     'role' => [Rule::in(['admin', 'editor', 'subscriber'])],
 * ]);
 *
 * if ($result->isFailed()) {
 *     return new WP_Error('validation_error', $result->getFirstErrorMessage());
 * }
 *
 * $validData = $result->getData();
 * ```
 *
 * @package WPUltimateSecurity\UltimateSecurity\Support\Validation
 * @since 2.0.0
 */
class RequestValidator
{
    /**
     * Custom sanitizers for fields.
     *
     * @var array<string, callable>
     */
    private array $sanitizers = [];

    /**
     * Validate request data against rules.
     *
     * @param WP_REST_Request|array $request The request or data array.
     * @param array                 $rules   The validation rules.
     *
     * @return ValidationResult
     */
    public function validate(WP_REST_Request|array $request, array $rules): ValidationResult
    {
        $data = $request instanceof WP_REST_Request
            ? $request->get_params()
            : $request;

        $errors = [];
        $validatedData = [];

        foreach ($rules as $field => $fieldRules) {
            // Get the value from data
            $value = $this->getNestedValue($data, $field);

            // Sanitize the value if a sanitizer is set
            if (isset($this->sanitizers[$field])) {
                $value = ($this->sanitizers[$field])($value);
            }

            // Ensure rules is an array
            if (!is_array($fieldRules)) {
                $fieldRules = [$fieldRules];
            }

            // Run each rule
            foreach ($fieldRules as $rule) {
                if (!$rule instanceof ValidationRuleInterface) {
                    continue;
                }

                // Skip validation for null/empty values unless it's a required rule
                if ($value === null || $value === '') {
                    if ($rule instanceof RequiredRule) {
                        if (!$rule->validate($value, $field, $data)) {
                            $errors[$field][] = $rule->getMessage($field);
                        }
                    }
                    continue;
                }

                if (!$rule->validate($value, $field, $data)) {
                    $errors[$field][] = $rule->getMessage($field);
                }
            }

            // Store validated value
            $this->setNestedValue($validatedData, $field, $value);
        }

        if (!empty($errors)) {
            return ValidationResult::failure($errors, $data);
        }

        return ValidationResult::success($validatedData);
    }

    /**
     * Set a custom sanitizer for a field.
     *
     * @param string   $field     The field name.
     * @param callable $sanitizer The sanitizer function.
     *
     * @return self
     */
    public function sanitize(string $field, callable $sanitizer): self
    {
        $this->sanitizers[$field] = $sanitizer;

        return $this;
    }

    /**
     * Set multiple sanitizers.
     *
     * @param array<string, callable> $sanitizers Field => sanitizer mapping.
     *
     * @return self
     */
    public function sanitizers(array $sanitizers): self
    {
        foreach ($sanitizers as $field => $sanitizer) {
            $this->sanitize($field, $sanitizer);
        }

        return $this;
    }

    /**
     * Get a nested value using dot notation.
     *
     * @param array  $data    The data array.
     * @param string $key     The dot-notation key.
     * @param mixed  $default Default value.
     *
     * @return mixed
     */
    private function getNestedValue(array $data, string $key, mixed $default = null): mixed
    {
        $keys = explode('.', $key);
        $value = $data;

        foreach ($keys as $segment) {
            if (!is_array($value) || !array_key_exists($segment, $value)) {
                return $default;
            }
            $value = $value[$segment];
        }

        return $value;
    }

    /**
     * Set a nested value using dot notation.
     *
     * @param array  $data  The data array (by reference).
     * @param string $key   The dot-notation key.
     * @param mixed  $value The value to set.
     *
     * @return void
     */
    private function setNestedValue(array &$data, string $key, mixed $value): void
    {
        $keys = explode('.', $key);
        $current = &$data;

        foreach ($keys as $i => $segment) {
            if ($i === count($keys) - 1) {
                $current[$segment] = $value;
                return;
            }

            if (!isset($current[$segment]) || !is_array($current[$segment])) {
                $current[$segment] = [];
            }

            $current = &$current[$segment];
        }
    }

    /**
     * Quick validation method for simple cases.
     *
     * @param array $data  The data to validate.
     * @param array $rules The validation rules.
     *
     * @return ValidationResult
     */
    public static function make(array $data, array $rules): ValidationResult
    {
        return (new self())->validate($data, $rules);
    }
}

/**
 * Rule Factory
 *
 * Factory class for creating validation rules with a fluent interface.
 *
 * Usage:
 * ```php
 * $rules = [
 *     'email' => [Rule::required(), Rule::email()],
 *     'name' => [Rule::string()->min(2)->max(100)],
 *     'count' => [Rule::integer()->min(1)],
 * ];
 * ```
 *
 * @package WPUltimateSecurity\UltimateSecurity\Support\Validation
 * @since 2.0.0
 */
class Rule
{
    /**
     * Create a required rule.
     *
     * @return RequiredRule
     */
    public static function required(): RequiredRule
    {
        return new RequiredRule();
    }

    /**
     * Create a string rule.
     *
     * @return StringRule
     */
    public static function string(): StringRule
    {
        return new StringRule();
    }

    /**
     * Create an integer rule.
     *
     * @return IntegerRule
     */
    public static function integer(): IntegerRule
    {
        return new IntegerRule();
    }

    /**
     * Create a boolean rule.
     *
     * @return BooleanRule
     */
    public static function boolean(): BooleanRule
    {
        return new BooleanRule();
    }

    /**
     * Create an array rule.
     *
     * @return ArrayRule
     */
    public static function array(): ArrayRule
    {
        return new ArrayRule();
    }

    /**
     * Create an email rule.
     *
     * @return EmailRule
     */
    public static function email(): EmailRule
    {
        return new EmailRule();
    }

    /**
     * Create a URL rule.
     *
     * @return UrlRule
     */
    public static function url(): UrlRule
    {
        return new UrlRule();
    }

    /**
     * Create an in-array rule.
     *
     * @param array $values Allowed values.
     *
     * @return InArrayRule
     */
    public static function in(array $values): InArrayRule
    {
        return new InArrayRule($values);
    }

    /**
     * Create an IP address rule.
     *
     * @return IpAddressRule
     */
    public static function ip(): IpAddressRule
    {
        return new IpAddressRule();
    }
}
