<?php
/**
 * Input Validator for BoostBot.
 *
 * @package BoostBot
 */

// Prevent direct access to the file.
if (!defined('ABSPATH')) {
    exit;
}

/**
 * BoostBot_Input_Validator class.
 *
 * Provides input validation with customizable rules and translatable error messages.
 *
 * @since 1.0.0
 */
class BoostBot_Input_Validator
{
    /**
     * Validation errors.
     *
     * @since 1.0.0
     * @access private
     * @var array $errors
     */
    private $errors = [];

    /**
     * Data being validated.
     *
     * @since 1.0.0
     * @access private
     * @var array $data
     */
    private $data = [];

    /**
     * Validate data against rules.
     *
     * @since 1.0.0
     * @param array $data  Data to validate.
     * @param array $rules Validation rules.
     * @return bool True if valid, false if errors.
     */
    public function validate($data, $rules)
    {
        $this->errors = [];
        $this->data = $data;

        foreach ($rules as $field => $field_rules) {
            $value = $data[$field] ?? null;

            // Handle string rules (e.g., "required|email|max:100")
            if (is_string($field_rules)) {
                $field_rules = explode('|', $field_rules);
            }

            foreach ($field_rules as $rule) {
                $result = $this->apply_rule($field, $value, $rule);
                if (!$result) {
                    break; // Stop on first error for this field
                }
            }
        }

        return empty($this->errors);
    }

    /**
     * Apply a single validation rule.
     *
     * @since 1.0.0
     * @param string $field Field name.
     * @param mixed  $value Field value.
     * @param string $rule  Rule to apply.
     * @return bool True if passes, false if fails.
     */
    private function apply_rule($field, $value, $rule)
    {
        // Parse rule (e.g., "max:100" => rule_name: "max", param: "100")
        list($rule_name, $param) = $this->parse_rule($rule);

        switch ($rule_name) {
            case 'required':
                if ($value === null || $value === '' || (is_array($value) && empty($value))) {
                    $this->add_error($field, __('This field is required', 'boostbot'));
                    return false;
                }
                break;

            case 'email':
                if (!empty($value) && !is_email($value)) {
                    $this->add_error($field, __('Please enter a valid email address', 'boostbot'));
                    return false;
                }
                break;

            case 'phone':
                if (!empty($value) && !$this->is_valid_phone($value)) {
                    $this->add_error($field, __('Please enter a valid phone number', 'boostbot'));
                    return false;
                }
                break;

            case 'url':
                if (!empty($value) && !filter_var($value, FILTER_VALIDATE_URL)) {
                    $this->add_error($field, __('Please enter a valid URL', 'boostbot'));
                    return false;
                }
                break;

            case 'min':
                $length = is_array($value) ? count($value) : mb_strlen((string) $value);
                if ($length < (int) $param) {
                    $this->add_error(
                        $field,
                        sprintf(
                            /* translators: %d: minimum character count */
                            __('Minimum %d characters required', 'boostbot'),
                            (int) $param
                        )
                    );
                    return false;
                }
                break;

            case 'max':
                $length = is_array($value) ? count($value) : mb_strlen((string) $value);
                if ($length > (int) $param) {
                    $this->add_error(
                        $field,
                        sprintf(
                            /* translators: %d: maximum character count */
                            __('Maximum %d characters allowed', 'boostbot'),
                            (int) $param
                        )
                    );
                    return false;
                }
                break;

            case 'between':
                list($min, $max) = explode(',', $param);
                $length = is_array($value) ? count($value) : mb_strlen((string) $value);
                if ($length < (int) $min || $length > (int) $max) {
                    $this->add_error(
                        $field,
                        sprintf(
                            /* translators: %1$d: minimum characters, %2$d: maximum characters */
                            __('Must be between %1$d and %2$d characters', 'boostbot'),
                            (int) $min,
                            (int) $max
                        )
                    );
                    return false;
                }
                break;

            case 'in':
                $allowed = array_map('trim', explode(',', $param));
                if (!empty($value) && !in_array($value, $allowed, true)) {
                    $this->add_error($field, __('Invalid option selected', 'boostbot'));
                    return false;
                }
                break;

            case 'not_in':
                $disallowed = array_map('trim', explode(',', $param));
                if (!empty($value) && in_array($value, $disallowed, true)) {
                    $this->add_error($field, __('This value is not allowed', 'boostbot'));
                    return false;
                }
                break;

            case 'numeric':
                if (!empty($value) && !is_numeric($value)) {
                    $this->add_error($field, __('Must be a number', 'boostbot'));
                    return false;
                }
                break;

            case 'integer':
                if (!empty($value) && filter_var($value, FILTER_VALIDATE_INT) === false) {
                    $this->add_error($field, __('Must be a whole number', 'boostbot'));
                    return false;
                }
                break;

            case 'positive':
                if (!empty($value) && (!is_numeric($value) || (float) $value <= 0)) {
                    $this->add_error($field, __('Must be a positive number', 'boostbot'));
                    return false;
                }
                break;

            case 'alpha':
                if (!empty($value) && !preg_match('/^[\p{L}]+$/u', $value)) {
                    $this->add_error($field, __('Must contain only letters', 'boostbot'));
                    return false;
                }
                break;

            case 'alpha_num':
                if (!empty($value) && !preg_match('/^[\p{L}\p{N}]+$/u', $value)) {
                    $this->add_error($field, __('Must contain only letters and numbers', 'boostbot'));
                    return false;
                }
                break;

            case 'alpha_spaces':
                if (!empty($value) && !preg_match('/^[\p{L}\s]+$/u', $value)) {
                    $this->add_error($field, __('Must contain only letters and spaces', 'boostbot'));
                    return false;
                }
                break;

            case 'date':
                if (!empty($value) && !$this->is_valid_date($value)) {
                    $this->add_error($field, __('Please enter a valid date', 'boostbot'));
                    return false;
                }
                break;

            case 'date_after':
                if (!empty($value) && !$this->is_date_after($value, $param)) {
                    $this->add_error(
                        $field,
                        sprintf(
                            /* translators: %s: date string */
                            __('Must be after %s', 'boostbot'),
                            $param
                        )
                    );
                    return false;
                }
                break;

            case 'date_before':
                if (!empty($value) && !$this->is_date_before($value, $param)) {
                    $this->add_error(
                        $field,
                        sprintf(
                            /* translators: %s: date string */
                            __('Must be before %s', 'boostbot'),
                            $param
                        )
                    );
                    return false;
                }
                break;

            case 'confirmed':
                $confirmation_field = $field . '_confirmation';
                if ($value !== ($this->data[$confirmation_field] ?? null)) {
                    $this->add_error($field, __('Confirmation does not match', 'boostbot'));
                    return false;
                }
                break;

            case 'same':
                if ($value !== ($this->data[$param] ?? null)) {
                    $this->add_error($field, __('Values do not match', 'boostbot'));
                    return false;
                }
                break;

            case 'different':
                if ($value === ($this->data[$param] ?? null)) {
                    $this->add_error($field, __('Values must be different', 'boostbot'));
                    return false;
                }
                break;

            case 'regex':
                if (!empty($value) && !preg_match($param, $value)) {
                    $this->add_error($field, __('Invalid format', 'boostbot'));
                    return false;
                }
                break;

            case 'color':
                // Accept 6-digit (#RRGGBB) or 8-digit (#RRGGBBAA) hex colors
                if (!empty($value) && !preg_match('/^#[a-fA-F0-9]{6}([a-fA-F0-9]{2})?$/', $value)) {
                    $this->add_error($field, __('Please enter a valid color code (HEX format)', 'boostbot'));
                    return false;
                }
                break;

            case 'boolean':
                if ($value !== null && !in_array($value, [true, false, 0, 1, '0', '1'], true)) {
                    $this->add_error($field, __('Must be a boolean value', 'boostbot'));
                    return false;
                }
                break;

            case 'array':
                if (!empty($value) && !is_array($value)) {
                    $this->add_error($field, __('Must be an array', 'boostbot'));
                    return false;
                }
                break;

            case 'json':
                if (!empty($value)) {
                    json_decode($value);
                    if (json_last_error() !== JSON_ERROR_NONE) {
                        $this->add_error($field, __('Invalid JSON format', 'boostbot'));
                        return false;
                    }
                }
                break;
        }

        return true;
    }

    /**
     * Get all validation errors.
     *
     * @since 1.0.0
     * @return array Validation errors.
     */
    public function get_errors()
    {
        return $this->errors;
    }

    /**
     * Get first error message.
     *
     * @since 1.0.0
     * @return string|null First error message or null.
     */
    public function get_first_error()
    {
        if (empty($this->errors)) {
            return null;
        }

        $first_field = array_key_first($this->errors);
        return $this->errors[$first_field][0] ?? null;
    }

    /**
     * Get errors for a specific field.
     *
     * @since 1.0.0
     * @param string $field Field name.
     * @return array Field errors.
     */
    public function get_field_errors($field)
    {
        return $this->errors[$field] ?? [];
    }

    /**
     * Check if a field has errors.
     *
     * @since 1.0.0
     * @param string $field Field name.
     * @return bool True if field has errors.
     */
    public function has_error($field)
    {
        return !empty($this->errors[$field]);
    }

    /**
     * Add a custom error.
     *
     * @since 1.0.0
     * @param string $field   Field name.
     * @param string $message Error message.
     */
    public function add_error($field, $message)
    {
        if (!isset($this->errors[$field])) {
            $this->errors[$field] = [];
        }
        $this->errors[$field][] = $message;
    }

    /**
     * Parse a rule string into name and parameter.
     *
     * @since 1.0.0
     * @param string $rule Rule string.
     * @return array [rule_name, parameter]
     */
    private function parse_rule($rule)
    {
        if (strpos($rule, ':') !== false) {
            return explode(':', $rule, 2);
        }
        return [$rule, null];
    }

    /**
     * Validate phone number format.
     *
     * @since 1.0.0
     * @param string $phone Phone number.
     * @return bool True if valid.
     */
    private function is_valid_phone($phone)
    {
        // Remove common formatting
        $phone = preg_replace('/[\s\-\(\)]/', '', $phone);

        // Israeli phone: starts with 0, 9-10 digits
        // International: starts with +, 10-15 digits
        return preg_match('/^(\+?\d{9,15}|0\d{8,9})$/', $phone);
    }

    /**
     * Validate date format.
     *
     * @since 1.0.0
     * @param string $date Date string.
     * @return bool True if valid.
     */
    private function is_valid_date($date)
    {
        $timestamp = strtotime($date);
        return $timestamp !== false;
    }

    /**
     * Check if date is after another date.
     *
     * @since 1.0.0
     * @param string $date       Date to check.
     * @param string $after_date Date to compare against (or 'today', 'now').
     * @return bool True if after.
     */
    private function is_date_after($date, $after_date)
    {
        $timestamp = strtotime($date);
        $compare = strtotime($after_date);

        return $timestamp > $compare;
    }

    /**
     * Check if date is before another date.
     *
     * @since 1.0.0
     * @param string $date        Date to check.
     * @param string $before_date Date to compare against.
     * @return bool True if before.
     */
    private function is_date_before($date, $before_date)
    {
        $timestamp = strtotime($date);
        $compare = strtotime($before_date);

        return $timestamp < $compare;
    }

    /**
     * Static helper for quick validation.
     *
     * @since 1.0.0
     * @param array $data  Data to validate.
     * @param array $rules Rules to apply.
     * @return array|true True if valid, array of errors if not.
     */
    public static function quick_validate($data, $rules)
    {
        $validator = new self();
        if ($validator->validate($data, $rules)) {
            return true;
        }
        return $validator->get_errors();
    }
}
