<?php

namespace Limb_Chatbot\Includes\Services;

use Limb_Chatbot\Includes\Scheme_Interface;
use Limb_Chatbot\Includes\Exceptions\Exception;
use WP_Error;

/**
 * Class Validator
 *
 * Validates data arrays against a given schema with support for custom callbacks,
 * enums, types, and error collection.
 *
 * @since 1.0.0
 */
class Validator {

	/**
	 * Schema class used for validation rules.
	 *
	 * @var Scheme_Interface
	 * @since 1.0.0
	 */
	private Scheme_Interface $schema_class;

	/**
	 * Validated data after successful validation.
	 *
	 * @var array
	 * @since 1.0.0
	 */
	protected array $validated;

	/**
	 * Stores validation errors.
	 *
	 * @var WP_Error
	 * @since 1.0.0
	 */
	protected WP_Error $error;

	/**
	 * Validator constructor.
	 *
	 * @param Scheme_Interface $schema_class Schema class with rules method.
	 * @since 1.0.0
	 */
	public function __construct( Scheme_Interface $schema_class ) {
		$this->schema_class = $schema_class;
		$this->error        = new WP_Error();
	}

	/**
	 * Validates input data against schema rules.
	 *
	 * @param array $data Input data to validate.
	 * @param string $context Validation context, e.g. 'create'.
	 * @param bool|null $preview Optional preview flag for rules.
	 * @param array|null $only Optional subset of keys to validate.
	 *
	 * @return bool True if validation passes, false otherwise.
	 * @since 1.0.0
	 */
	public function validate( array $data, string $context = 'create', ?bool $preview = false, ?array $only = [] ): bool {
		$this->validated = [];
		$rules           = ! empty( $only ) ? array_filter( $this->schema_class::rules( $preview ), function ( $key ) use ( $only ) {
			return in_array( $key, $only );
		}, ARRAY_FILTER_USE_KEY ) : $this->schema_class::rules( $preview );
		foreach ( $rules as $key => $rule ) {
			$value = $data[ $key ] ?? null;
			if ( $context === 'create' && ! empty( $rule['required'] ) && ( $value === null || $value === '' ) ) {
				$this->error->add( $key, __( 'Field is required.', 'limb-chatbot' ) );
				continue;
			}
			if ( is_null( $value ) ) {
				continue;
			}
			try {
				if ( ! $preview && isset( $rule['validate_callback'] ) && is_callable( $rule['validate_callback'] ) ) {
					// Pass $data as second argument to allow cross-field validation in batch updates
					if ( ! call_user_func( $rule['validate_callback'], $value, $data ) ) {
						$this->error->add( $key, __( 'Invalid value.', 'limb-chatbot' ) );
						continue;
					}
				}
			} catch ( Exception $e ) {
				$this->error->add( $key, $e->getMessage() );
				continue;
			}
			if ( isset( $rule['enum'] ) && ! in_array( $value, $rule['enum'], true ) ) {
				// translators: %s is a comma-separated list of allowed values.
				$this->error->add( $key, sprintf( __( 'Must be one of: %s', 'limb-chatbot' ), implode( ', ', $rule['enum'] ) ) );
				continue;
			}
			if ( isset( $rule['type'] ) ) {
				if ( ! $this->validate_type( $value, $rule['type'], $rule ) ) {
					// translators: %s is the expected data type.
					$this->error->add( $key, sprintf( __( "Expected type %s.", 'limb-chatbot' ), $rule['type'] ) );
					continue;
				}
			}
			if ( empty( $this->error->errors[ $key ] ) ) {
				$this->validated[ $key ] = $value;
			}
		}

		return ! $this->error->has_errors();
	}

	/**
	 * Validates a single value against its expected type.
	 *
	 * @param mixed $value Value to validate.
	 * @param string $type Expected type string.
	 * @param array $rule Validation rule.
	 *
	 * @return bool True if type is valid, false otherwise.
	 * @since 1.0.0
	 */
	private function validate_type( $value, string $type, array $rule ): bool {
		switch ( $type ) {
			case 'string':
				return is_string( $value );
			case 'int':
			case 'integer':
				return is_int( $value );
			case 'float':
				return is_float( $value );
			case 'bool':
			case 'boolean':
				return is_bool( $value );
			case 'array':
				if ( ! is_array( $value ) ) {
					return false;
				}
				if ( isset( $rule['items']['type'] ) ) {
					foreach ( $value as $item ) {
						if ( Helper::gettype( $item ) !== $rule['items']['type'] ) {
							return false;
						}
					}
				}

				return true;
			case 'object':
				return is_object( $value );
			default:
				return true;
		}
	}

	/**
	 * Returns the validated data after successful validation.
	 *
	 * @return array Validated data.
	 * @since 1.0.0
	 */
	public function get_validated(): array {
		return $this->validated;
	}

	/**
	 * Returns the WP_Error object containing validation errors.
	 *
	 * @return WP_Error Validation errors.
	 * @since 1.0.0
	 */
	public function get_error(): WP_Error {
		return $this->error;
	}

	/**
	 * Checks if there are any validation errors.
	 *
	 * @return bool True if errors exist, false otherwise.
	 * @since 1.0.0
	 */
	public function has_errors(): bool {
		return $this->error->has_errors();
	}

	/**
	 * Returns an array of error messages indexed by field keys.
	 *
	 * @return array Array of validation errors.
	 * @since 1.0.0
	 */
	public function get_error_array(): array {
		return $this->error->errors;
	}

}