<?php

/**
 * JCOGS Image Pro - Numeric Validator
 * ====================================
 * Validates numeric values with range and format constraints
 * 
 * @category   ExpressionEngine Add-on
 * @package    JCOGS Image Pro
 * @author     JCOGS Design <contact@jcogs.net>
 * @copyright  Copyright (c) 2021 - 2025 JCOGS Design
 * @license    https://jcogs.net/add-ons/license/jcogs_img_pro
 * @version    2.0.6
 * @link       https://JCOGS.net/
 * @since      2.0.0
 */

namespace JCOGSDesign\JCOGSImagePro\Service\Validation;

/**
 * Numeric Validator
 * 
 * Validates numeric values including integers, floats, percentages, and special keywords.
 * 
 * This validator unifies dimension validation, aspect ratio validation, and other numeric
 * parameter validation into a single reusable component.
 * 
 * Config Options:
 * - min (int|float): Minimum allowed value (default: no minimum)
 * - max (int|float): Maximum allowed value (default: no maximum)
 * - allow_negative (bool): Allow negative numbers (default: false)
 * - allow_decimal (bool): Allow decimal/float values (default: true)
 * - allow_percentage (bool): Allow percentage values (e.g., "50%") (default: false)
 * - special_values (array): Array of allowed special keywords (e.g., ['auto', 'orig'])
 * 
 * Example Usage:
 * 
 * ```php
 * // Width validation (1-10000, percentages allowed, special keywords)
 * $validator = new NumericValidator([
 *     'min' => 1,
 *     'max' => 10000,
 *     'allow_percentage' => true,
 *     'special_values' => ['auto', 'orig']
 * ]);
 * $result = $validator->validate('800'); // Success: 800
 * $result = $validator->validate('50%'); // Success: '50%'
 * $result = $validator->validate('auto'); // Success: 'auto'
 * $result = $validator->validate('-100'); // Error: negative not allowed
 * 
 * // Quality validation (1-100, integers only)
 * $validator = new NumericValidator([
 *     'min' => 1,
 *     'max' => 100,
 *     'allow_decimal' => false
 * ]);
 * $result = $validator->validate(85); // Success: 85
 * $result = $validator->validate(85.5); // Error: decimals not allowed
 * ```
 * 
 * @package JCOGSDesign\JCOGSImagePro\Service\Validation
 * @since 2.0.0
 */
class NumericValidator implements ValidatorInterface
{
    /**
     * Default configuration
     * 
     * @var array
     */
    private array $defaultConfig = [
        'min' => null,
        'max' => null,
        'allow_negative' => false,
        'allow_zero' => true,
        'allow_float' => true,
        'allow_decimal' => true, // Backward compatibility alias for allow_float
        'allow_percentage' => false,
        'allow_units' => [],
        'allow_keywords' => [],
        'special_values' => [] // Backward compatibility alias for allow_keywords
    ];
    
    /**
     * Validate a numeric value
     * 
     * @param mixed $value Value to validate (string, int, or float)
     * @param array $config Configuration options (merged with defaults)
     * 
     * @return ValidationResult Success with sanitized numeric value or error
     */
    public function validate($value, array $config = []): ValidationResult
    {
        // Normalize config: allow_decimal is an alias for allow_float
        // If either is explicitly set in custom config, use that value for both
        if (array_key_exists('allow_decimal', $config)) {
            $config['allow_float'] = $config['allow_decimal'];
        } elseif (array_key_exists('allow_float', $config)) {
            $config['allow_decimal'] = $config['allow_float'];
        }
        
        // Merge config with defaults
        $config = array_merge($this->defaultConfig, $config);
        
        // Normalize config: allow_keywords takes precedence over special_values
        if (!empty($config['allow_keywords'])) {
            $config['special_values'] = $config['allow_keywords'];
        }
        
        // Handle null/empty values
        if ($value === null || $value === '') {
            return ValidationResult::error('Value is required', $value);
        }
        
        // Convert to string for consistent handling
        $value_str = (string)$value;
        $value_str = trim($value_str);
        
        // After trimming, check if empty (was only whitespace)
        if ($value_str === '') {
            return ValidationResult::error('Value is required', $value);
        }
        
        // Check special values/keywords first
        if (!empty($config['special_values'])) {
            $special_values_lower = array_map('strtolower', $config['special_values']);
            $value_lower = strtolower($value_str);
            
            if (in_array($value_lower, $special_values_lower)) {
                // Return normalized special value (lowercase)
                return ValidationResult::success($value_lower);
            }
        }
        
        // Check for and strip units
        $unit = '';
        $unit_found = false;
        
        // Build list of allowed units, including '%' if allow_percentage is true
        $allowed_units = $config['allow_units'];
        if ($config['allow_percentage'] && !in_array('%', $allowed_units)) {
            $allowed_units[] = '%';
        }
        
        if (!empty($allowed_units)) {
            foreach ($allowed_units as $allowed_unit) {
                if (preg_match('/(' . preg_quote($allowed_unit, '/') . ')$/i', $value_str, $matches)) {
                    $unit = $matches[1];
                    $unit_found = true;
                    $value_str = substr($value_str, 0, -strlen($unit));
                    $value_str = trim($value_str);
                    break;
                }
            }
            
            // If no unit matched from allowed units, check if there's any unit at all
            if (!$unit_found && preg_match('/([a-z%]+)$/i', $value_str, $matches)) {
                $found_unit = $matches[1];
                $allowed = implode(', ', $allowed_units);
                return ValidationResult::error(sprintf('Unit "%s" not allowed. Allowed units: %s', $found_unit, $allowed), $value);
            }
        } else {
            // Check for disallowed units (no units allowed at all)
            if (preg_match('/([a-z%]+)$/i', $value_str, $matches)) {
                return ValidationResult::error('Value must be numeric (units not allowed)', $value);
            }
        }
        
        // If percentage found, validate and return with '%' suffix
        if ($unit === '%') {
            $result = $this->validateNumericValue($value_str, $config, $value);
            if ($result->success) {
                // Return value with percentage suffix preserved
                return ValidationResult::success($result->sanitizedValue . '%');
            }
            return $result;
        }
        
        // Validate as numeric value
        return $this->validateNumericValue($value_str, $config, $value);
    }
    
    /**
     * Validate a pure numeric value (no percentage suffix or special keywords)
     * 
     * @param string $value_str String representation of numeric value
     * @param array $config Configuration options
     * @param mixed $original_value Original value for error reporting
     * 
     * @return ValidationResult Success with numeric value or error
     */
    private function validateNumericValue(string $value_str, array $config, mixed $original_value): ValidationResult
    {
        // Check if value is numeric
        if (!is_numeric($value_str)) {
            return ValidationResult::error('Value must be numeric', $original_value);
        }
        
        // Determine if float is allowed (allow_float takes precedence over allow_decimal)
        $allow_float = $config['allow_float'] ?? $config['allow_decimal'];
        
        // Convert to appropriate type based on actual value and config
        $has_decimal = str_contains($value_str, '.');
        
        if ($has_decimal && !$allow_float) {
            return ValidationResult::error('Value must be an integer', $original_value);
        }
        
        // Convert to proper type: integer if no decimal and allow_float is false, otherwise respect the input
        if (!$allow_float) {
            $numeric_value = (int)$value_str;
        } else {
            // If has decimal, return as float; otherwise as int
            // Exception: scientific notation (e.g., 1e2) should always be float
            if ($has_decimal || stripos($value_str, 'e') !== false) {
                $numeric_value = (float)$value_str;
            } else {
                $numeric_value = (int)$value_str;
            }
        }
        
        // Check for zero when not allowed
        if ($numeric_value === 0 && !$config['allow_zero']) {
            return ValidationResult::error('Value cannot be zero', $original_value);
        }
        
        // Check for negative values
        if ($numeric_value < 0 && !$config['allow_negative']) {
            return ValidationResult::error('Negative values are not allowed', $original_value);
        }
        
        // Check minimum value
        if ($config['min'] !== null && $numeric_value < $config['min']) {
            return ValidationResult::error(
                sprintf('Value must be greater than or equal to minimum (%s)', $config['min']),
                $original_value
            );
        }
        
        // Check maximum value
        if ($config['max'] !== null && $numeric_value > $config['max']) {
            return ValidationResult::error(
                sprintf('Value must be less than or equal to maximum (%s)', $config['max']),
                $original_value
            );
        }
        
        return ValidationResult::success($numeric_value);
    }
}
