<?php

namespace SugarCalendarBookings\Support\DataProcessing;

use DateTime;
use DateTimeZone;
use Exception;
use SugarCalendarBookings\Support\Helpers\Data;
use SugarCalendarBookings\Support\Helpers\Slug;
use WP_Error;

/**
 * Class Validator.
 *
 * Provides static validation for data arrays using simple rules.
 *
 * @since 1.0.0
 */
class Validator {

	use RulesTrait;

	/**
	 * Map of rule names to validation methods.
	 *
	 * @since 1.0.0
	 * @since 1.5.0 Added slug validation.
	 *
	 * @var array
	 */
	private static $rule_map = [
		'required'             => 'validate_required',
		'boolean'              => 'validate_boolean',
		'form_boolean'         => 'validate_boolean',
		'string'               => 'validate_string',
		'numeric'              => 'validate_numeric',
		'min'                  => 'validate_min',
		'max'                  => 'validate_max',
		'in'                   => 'validate_in',
		'array'                => 'validate_array',
		'required_if'          => 'validate_required_if',
		'required_if_accepted' => 'validate_required_if_accepted',
		'required_if_declined' => 'validate_required_if_declined',
		'id'                   => 'validate_id',
		'date_format'          => 'validate_date_format',
		'timezone'             => 'validate_timezone',
		'slug'                 => 'validate_slug',
	];

	/**
	 * Validate required rule.
	 *
	 * @since 1.0.0
	 *
	 * @param string $field Field name.
	 * @param mixed  $value Field value.
	 * @param mixed  $param Not used.
	 *
	 * @return string|null Error message or null if valid.
	 */
	private static function validate_required( $field, $value, $param = null ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed

		if (
			is_null( $value ) ||
			( is_string( $value ) && trim( $value ) === '' ) ||
			( is_array( $value ) && count( $value ) === 0 )
		) {
			return sprintf(
			/* translators: %1$s - field name. */
				esc_html__( 'The %1$s field is required.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
				esc_html( $field )
			);
		}

		return null;
	}

	/**
	 * Validate boolean rule.
	 *
	 * @since 1.0.0
	 *
	 * @param string $field Field name.
	 * @param mixed  $value Field value.
	 * @param mixed  $param Not used.
	 *
	 * @return string|null Error message or null if valid.
	 */
	private static function validate_boolean( $field, $value, $param ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed

		if ( ! is_bool( $value ) && ! in_array( $value, [ '0', '1', 0, 1 ], true ) ) {
			return sprintf(
			/* translators: %1$s - field name. */
				esc_html__( 'The %1$s field must be true or false.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
				esc_html( $field )
			);
		}

		return null;
	}

	/**
	 * Validate string rule.
	 *
	 * @since 1.0.0
	 *
	 * @param string $field Field name.
	 * @param mixed  $value Field value.
	 * @param mixed  $param Not used.
	 *
	 * @return string|null Error message or null if valid.
	 */
	private static function validate_string( $field, $value, $param ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed

		if ( ! is_string( $value ) ) {
			return sprintf(
			/* translators: %1$s - field name. */
				esc_html__( 'The %1$s field must be a string.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
				esc_html( $field )
			);
		}

		return null;
	}

	/**
	 * Validate numeric rule.
	 *
	 * @since 1.0.0
	 *
	 * @param string $field Field name.
	 * @param mixed  $value Field value.
	 * @param mixed  $param Not used.
	 *
	 * @return string|null Error message or null if valid.
	 */
	private static function validate_numeric( $field, $value, $param ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed

		if ( ! is_numeric( $value ) ) {
			return sprintf(
			/* translators: %1$s - field name. */
				esc_html__( 'The %1$s field must be a number.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
				esc_html( $field )
			);
		}

		return null;
	}

	/**
	 * Validate min rule.
	 *
	 * @since 1.0.0
	 *
	 * @param string $field Field name.
	 * @param mixed  $value Field value.
	 * @param mixed  $param Min value or length.
	 *
	 * @return string|null Error message or null if valid.
	 */
	private static function validate_min( $field, $value, $param ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh

		if ( is_string( $value ) ) {
			// TODO: add `mb_` functions polyfill.
			if ( mb_strlen( $value ) < (int) $param ) {
				return sprintf(
				/* translators: %1$s - field name; %2$d - minimum length. */
					esc_html__( 'The %1$s field must be at least %2$d characters.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
					esc_html( $field ),
					(int) $param
				);
			}
		} elseif ( is_numeric( $value ) ) {
			if ( $value < (int) $param ) {
				return sprintf(
				/* translators: %1$s - field name; %2$d - minimum value. */
					esc_html__( 'The %1$s field must be at least %2$d.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
					esc_html( $field ),
					(int) $param
				);
			}
		} elseif ( is_array( $value ) ) {
			if ( count( $value ) < (int) $param ) {
				return sprintf(
				/* translators: %1$s - field name; %2$d - minimum value. */
					esc_html__( 'The %1$s field must be at least %2$d items.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
					esc_html( $field ),
					(int) $param
				);
			}
		}

		return null;
	}

	/**
	 * Validate max rule.
	 *
	 * @since 1.0.0
	 *
	 * @param string $field Field name.
	 * @param mixed  $value Field value.
	 * @param mixed  $param Max value or length.
	 *
	 * @return string|null Error message or null if valid.
	 */
	private static function validate_max( $field, $value, $param ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh

		if ( is_string( $value ) ) {
			if ( mb_strlen( $value ) > (int) $param ) {
				return sprintf(
				/* translators: %1$s - field name; %2$d - maximum length. */
					esc_html__( 'The %1$s field may not be greater than %2$d characters.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
					esc_html( $field ),
					(int) $param
				);
			}
		} elseif ( is_numeric( $value ) ) {
			if ( $value > (int) $param ) {
				return sprintf(
				/* translators: %1$s - field name; %2$d - maximum value. */
					esc_html__( 'The %1$s field may not be greater than %2$d.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
					esc_html( $field ),
					(int) $param
				);
			}
		} elseif ( is_array( $value ) ) {
			if ( count( $value ) > (int) $param ) {
				return sprintf(
				/* translators: %1$s - field name; %2$d - maximum value. */
					esc_html__( 'The %1$s field may not be greater than %2$d items.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
					esc_html( $field ),
					(int) $param
				);
			}
		}

		return null;
	}

	/**
	 * Validate in rule.
	 *
	 * @since 1.0.0
	 *
	 * @param string $field Field name.
	 * @param mixed  $value Field value.
	 * @param mixed  $param Comma-separated allowed values.
	 *
	 * @return string|null Error message or null if valid.
	 */
	private static function validate_in( $field, $value, $param ) {

		$allowed = array_map( 'trim', explode( ',', (string) $param ) );

		if ( ! in_array( (string) $value, $allowed, true ) ) {
			return sprintf(
			/* translators: %1$s - field name; %2$s - allowed values. */
				esc_html__( 'The %1$s field must be one of: %2$s.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
				esc_html( $field ),
				esc_html( implode( ', ', $allowed ) )
			);
		}

		return null;
	}

	/**
	 * Validate array rule.
	 *
	 * @since 1.0.0
	 *
	 * @param string $field Field name.
	 * @param mixed  $value Field value.
	 * @param mixed  $param Not used.
	 *
	 * @return string|null Error message or null if valid.
	 */
	private static function validate_array( $field, $value, $param ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed

		if ( ! is_array( $value ) ) {
			return sprintf(
			/* translators: %1$s - field name. */
				esc_html__( 'The %1$s field must be an array.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
				esc_html( $field )
			);
		}

		return null;
	}

	/**
	 * Validate required_if rule.
	 *
	 * @since 1.0.0
	 *
	 * @param string $field Field name.
	 * @param mixed  $value Field value.
	 * @param mixed  $param Comma-separated: other_field,value.
	 * @param array  $data  The full data array.
	 *
	 * @return string|null Error message or null if valid.
	 */
	private static function validate_required_if( $field, $value, $param, $data ) {

		[ $other_field, $expected ] = array_map( 'trim', explode( ',', $param, 2 ) );

		$other_value = self::resolve_other_field_value( $field, $other_field, $data );

		if ( (string) $other_value === $expected ) {
			$error = self::validate_required( $field, $value );

			if ( $error ) {
				return sprintf(
				/* translators: %1$s - field name; %2$s - other field; %3$s - value. */
					esc_html__( 'The %1$s field is required when %2$s is %3$s.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
					esc_html( $field ),
					esc_html( $other_field ),
					esc_html( $expected )
				);
			}
		}

		return null;
	}

	/**
	 * Validate required_if_accepted rule.
	 *
	 * The field under validation must be present and not empty if the anotherfield field is equal to true.
	 *
	 * @since 1.0.0
	 *
	 * @param string $field Field name.
	 * @param mixed  $value Field value.
	 * @param mixed  $param Comma-separated: other_field (and optionally, value).
	 * @param array  $data  The full data array.
	 *
	 * @return string|null Error message or null if valid.
	 */
	private static function validate_required_if_accepted( $field, $value, $param, $data ) {

		[ $other_field ] = array_map( 'trim', explode( ',', $param, 2 ) );

		$other_value = self::resolve_other_field_value( $field, $other_field, $data );

		if ( $other_value === true ) {
			$error = self::validate_required( $field, $value );

			if ( $error ) {
				return sprintf(
				/* translators: %1$s - field name; %2$s - other field. */
					esc_html__( 'The %1$s field is required when %2$s is accepted.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
					esc_html( $field ),
					esc_html( $other_field )
				);
			}
		}

		return null;
	}

	/**
	 * Validate required_if_declined rule.
	 *
	 * The field under validation must be present and not empty if the anotherfield field is equal to false.
	 *
	 * @since 1.0.0
	 *
	 * @param string $field Field name.
	 * @param mixed  $value Field value.
	 * @param mixed  $param Comma-separated: other_field (and optionally, value).
	 * @param array  $data  The full data array.
	 *
	 * @return string|null Error message or null if valid.
	 */
	private static function validate_required_if_declined( $field, $value, $param, $data ) {

		[ $other_field ] = array_map( 'trim', explode( ',', $param, 2 ) );

		$other_value = self::resolve_other_field_value( $field, $other_field, $data );

		if ( $other_value === false ) {
			$error = self::validate_required( $field, $value );

			if ( $error ) {
				return sprintf(
				/* translators: %1$s - field name; %2$s - other field. */
					esc_html__( 'The %1$s field is required when %2$s is declined.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
					esc_html( $field ),
					esc_html( $other_field )
				);
			}
		}

		return null;
	}

	/**
	 * Resolve other field value.
	 *
	 * @since 1.0.0
	 *
	 * @param string $field       Field name.
	 * @param string $other_field Other field name.
	 * @param array  $data        The full data array.
	 *
	 * @return mixed
	 */
	private static function resolve_other_field_value( $field, $other_field, $data ) {

		// Resolve other_field contextually.
		if ( strpos( $other_field, '.' ) === false ) {
			$segments = explode( '.', $field );

			array_pop( $segments );

			if ( $segments ) {
				$other_field = implode( '.', $segments ) . '.' . $other_field;
			}
		}

		return Data::get( $data, $other_field );
	}

	/**
	 * Validate id rule.
	 *
	 * @since 1.0.0
	 *
	 * @param string $field Field name.
	 * @param mixed  $value Field value.
	 * @param mixed  $param Not used.
	 *
	 * @return string|null Error message or null if valid.
	 */
	private static function validate_id( $field, $value, $param ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed

		if ( ! is_numeric( $value ) || $value <= 0 ) {
			return sprintf(
			/* translators: %1$s - field name. */
				esc_html__( 'The %1$s field must be a valid ID greater than 0.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
				esc_html( $field )
			);
		}

		return null;
	}

	/**
	 * Validate date format rule.
	 *
	 * @since 1.0.0
	 *
	 * @param string $field Field name.
	 * @param mixed  $value Field value.
	 * @param mixed  $param Comma-separated date formats.
	 *
	 * @return string|null Error message or null if valid.
	 */
	private static function validate_date_format( $field, $value, $param ) {

		if ( ! is_string( $value ) ) {
			return sprintf(
			/* translators: %1$s - field name. */
				esc_html__( 'The %1$s field must be a valid date string.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
				esc_html( $field )
			);
		}

		$formats = array_map( 'trim', explode( ',', (string) $param ) );

		$valid = false;

		foreach ( $formats as $format ) {
			$date = DateTime::createFromFormat( $format, $value );

			if ( $date && $date->format( $format ) === $value ) {
				$valid = true;

				break;
			}
		}

		if ( ! $valid ) {
			return sprintf(
			/* translators: %1$s - field name; %2$s - allowed formats. */
				esc_html__( 'The %1$s field must match one of these formats: %2$s.', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
				esc_html( $field ),
				esc_html( implode( ', ', $formats ) )
			);
		}

		return null;
	}

	/**
	 * Validate timezone rule.
	 *
	 * Checks if the value is a valid PHP timezone identifier or offset (e.g., 'America/New_York', '+02:00').
	 *
	 * @since 1.0.0
	 *
	 * @param string $field Field name.
	 * @param mixed  $value Field value.
	 * @param mixed  $param Not used.
	 *
	 * @return string|null Error message or null if valid.
	 */
	private static function validate_timezone( $field, $value, $param ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed

		try {
			new DateTimeZone( $value );
		} catch ( Exception $e ) {
			return sprintf(
			/* translators: %1$s - field name; %2$s - example valid timezone. */
				esc_html__( 'The %1$s field must be a valid timezone string (e.g., %2$s).', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
				esc_html( $field ),
				'America/New_York'
			);
		}

		return null;
	}

	/**
	 * Validate that the slug does not collide with a reserved path or post type.
	 *
	 * @since 1.5.0
	 *
	 * @param string $field Field name.
	 * @param mixed  $value Field value.
	 * @param mixed  $param Not used.
	 *
	 * @return string|null Error message or null if valid.
	 */
	private static function validate_slug( $field, $value, $param ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed

		if ( ! is_string( $value ) || $value === '' ) {
			return null;
		}

		// Match what gets stored.
		$sanitized = sanitize_title( $value );

		if ( $sanitized === '' ) {
			return null;
		}

		return Slug::get_collision_reason( $sanitized );
	}

	/**
	 * Validate data against rules.
	 *
	 * Return first found error.
	 *
	 * @since 1.0.0
	 *
	 * @param array $data  The data to validate.
	 * @param array $rules The validation rules.
	 *
	 * @return true|WP_Error True if valid. WP_Error with errors if not.
	 */
	public static function validate( $data, $rules ) {

		foreach ( $rules as $field => $field_rules ) {
			$field_rules = self::parse_rules( $field_rules );

			if ( self::is_wildcard( $field ) ) {
				$error = self::process_wildcard_rule( $data, $field, $field_rules );

				if ( $error instanceof WP_Error ) {
					return $error;
				}

				continue;
			}

			$error = self::validate_field( $data, $field, $field_rules );

			if ( $error instanceof WP_Error ) {
				return $error;
			}
		}

		return true;
	}

	/**
	 * Validate a single field with its rules.
	 *
	 * @since 1.0.0
	 *
	 * @param array  $data        The data array.
	 * @param string $field       The field name.
	 * @param array  $field_rules The validation rules for the field.
	 *
	 * @return false|WP_Error
	 */
	private static function validate_field( $data, $field, $field_rules ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh

		$value = Data::get( $data, $field );

		// If nullable is present and value is empty, skip all other rules.
		if (
			in_array( 'nullable', $field_rules, true ) &&
			is_null( $value )
		) {
			return false;
		}

		$non_required_type_rules = [ 'string', 'numeric', 'array', 'boolean', 'form_boolean', 'id', 'in', 'date_format' ];

		foreach ( $field_rules as $rule ) {
			if ( $rule === 'nullable' ) {
				continue;
			}

			$rule_name  = $rule;
			$rule_param = null;

			if ( strpos( $rule, ':' ) !== false ) {
				[ $rule_name, $rule_param ] = explode( ':', $rule, 2 );
			}

			$method = self::$rule_map[ $rule_name ] ?? null;

			// Only apply type rules if value is present, or if field is required.
			if (
				in_array( $rule_name, $non_required_type_rules, true ) &&
				is_null( $value ) &&
				! in_array( 'required', $field_rules, true )
			) {
				continue;
			}

			if ( $method ) {
				if ( in_array( $rule_name, [ 'required_if', 'required_if_accepted', 'required_if_declined' ], true ) ) {
					$error = self::$method( $field, $value, $rule_param, $data );
				} else {
					$error = self::$method( $field, $value, $rule_param );
				}

				if ( $error ) {
					return new WP_Error( $field . '_' . $rule_name, $error );
				}
			}
		}

		return false;
	}

	/**
	 * Process wildcard rule.
	 *
	 * @since 1.0.0
	 *
	 * @param array  $data        The data array.
	 * @param string $field       The field name (with wildcard).
	 * @param array  $field_rules The validation rules for the field.
	 *
	 * @return false|WP_Error
	 */
	private static function process_wildcard_rule( $data, $field, $field_rules ) {

		$segments       = explode( '.', $field );
		$wildcard_index = array_search( '*', $segments, true );

		// Build prefix and suffix.
		$prefix = array_slice( $segments, 0, $wildcard_index );
		$suffix = array_slice( $segments, $wildcard_index + 1 );

		// Get the array to iterate.
		$array_path       = implode( '.', $prefix );
		$array_to_iterate = Data::get( $data, $array_path, [] );

		if ( is_array( $array_to_iterate ) ) {
			foreach ( $array_to_iterate as $i => $item ) {
				// Build the full key for this item.
				$item_key  = implode( '.', array_merge( $prefix, [ $i ], $suffix ) );
				$sub_rules = [ $item_key => $field_rules ];
				$error     = self::validate( $data, $sub_rules );

				if ( $error instanceof WP_Error ) {
					return $error;
				}
			}
		}

		return false;
	}
}
