<?php
/**
 * InputValidator - Validation et sanitisation avancée des entrées utilisateur
 * Protection contre XSS, injection SQL, et validation de format
 */
class InputValidator {
    
    // Règles de validation communes
    private static $validationRules = [
        'username' => [
            'required' => true,
            'type' => 'string',
            'min_length' => 3,
            'max_length' => 50,
            'pattern' => '/^[a-zA-Z0-9_-]+$/',
            'message' => 'Le nom d\'utilisateur doit contenir 3-50 caractères alphanumériques'
        ],
        'password' => [
            'required' => true,
            'type' => 'string',
            'min_length' => 6,
            'max_length' => 255,
            'message' => 'Le mot de passe doit contenir au moins 6 caractères'
        ],
        'email' => [
            'required' => false,
            'type' => 'email',
            'max_length' => 255,
            'message' => 'Format d\'email invalide'
        ],
        'nom_client' => [
            'required' => false,
            'type' => 'string',
            'max_length' => 100,
            'pattern' => '/^[a-zA-Z0-9\s\-\.\']+$/u',
            'message' => 'Nom de client invalide (lettres, chiffres, espaces, tirets autorisés)'
        ],
        'code_pesee' => [
            'required' => false,
            'type' => 'string',
            'max_length' => 20,
            'pattern' => '/^[A-Z0-9\-]+$/',
            'message' => 'Code pesée invalide (majuscules et chiffres seulement)'
        ],
        'poids' => [
            'required' => false,
            'type' => 'numeric',
            'min' => 0,
            'max' => 999999,
            'message' => 'Poids invalide (nombre positif requis)'
        ],
        'immatriculation' => [
            'required' => false,
            'type' => 'string',
            'max_length' => 20,
            'pattern' => '/^[A-Z0-9\s\-]+$/i',
            'message' => 'Immatriculation invalide'
        ],
        'date' => [
            'required' => false,
            'type' => 'date',
            'message' => 'Format de date invalide (YYYY-MM-DD attendu)'
        ],
        'limit' => [
            'required' => false,
            'type' => 'integer',
            'min' => 1,
            'max' => 1000,
            'default' => 50,
            'message' => 'Limite invalide (1-1000)'
        ],
        'page' => [
            'required' => false,
            'type' => 'integer',
            'min' => 1,
            'max' => 10000,
            'default' => 1,
            'message' => 'Numéro de page invalide'
        ]
    ];
    
    /**
     * Valider et nettoyer une valeur selon les règles
     */
    public static function validate($field, $value, $customRules = null) {
        $rules = $customRules ?? (self::$validationRules[$field] ?? []);
        
        // Valeur requise
        if (isset($rules['required']) && $rules['required'] && empty($value)) {
            throw new ValidationException($rules['message'] ?? "Le champ $field est requis");
        }
        
        // Valeur vide optionnelle
        if (empty($value) && !isset($rules['required'])) {
            return $rules['default'] ?? null;
        }
        
        // Nettoyage initial
        $cleanValue = self::sanitizeInput($value);
        
        // Validation par type
        switch ($rules['type'] ?? 'string') {
            case 'string':
                $cleanValue = self::validateString($cleanValue, $rules);
                break;
            case 'integer':
                $cleanValue = self::validateInteger($cleanValue, $rules);
                break;
            case 'numeric':
                $cleanValue = self::validateNumeric($cleanValue, $rules);
                break;
            case 'email':
                $cleanValue = self::validateEmail($cleanValue, $rules);
                break;
            case 'date':
                $cleanValue = self::validateDate($cleanValue, $rules);
                break;
            case 'boolean':
                $cleanValue = self::validateBoolean($cleanValue);
                break;
        }
        
        return $cleanValue;
    }
    
    /**
     * Valider plusieurs champs en une fois
     */
    public static function validateMultiple($data, $fieldRules = null) {
        $result = [];
        $errors = [];
        
        foreach ($data as $field => $value) {
            try {
                $customRules = $fieldRules[$field] ?? null;
                $result[$field] = self::validate($field, $value, $customRules);
            } catch (ValidationException $e) {
                $errors[$field] = $e->getMessage();
            }
        }
        
        if (!empty($errors)) {
            throw new ValidationException('Erreurs de validation', $errors);
        }
        
        return $result;
    }
    
    /**
     * Sanitisation de base contre XSS
     */
    public static function sanitizeInput($input) {
        if (is_array($input)) {
            return array_map([self::class, 'sanitizeInput'], $input);
        }
        
        if (!is_string($input)) {
            return $input;
        }
        
        // Supprimer les caractères de contrôle
        $input = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $input);
        
        // Trim
        $input = trim($input);
        
        // Encoder les entités HTML pour prévenir XSS
        return htmlspecialchars($input, ENT_QUOTES | ENT_HTML5, 'UTF-8');
    }
    
    /**
     * Validation des chaînes de caractères
     */
    private static function validateString($value, $rules) {
        $message = $rules['message'] ?? 'Chaîne invalide';
        
        // Longueur minimum
        if (isset($rules['min_length']) && strlen($value) < $rules['min_length']) {
            throw new ValidationException($message);
        }
        
        // Longueur maximum
        if (isset($rules['max_length']) && strlen($value) > $rules['max_length']) {
            throw new ValidationException($message);
        }
        
        // Pattern regex
        if (isset($rules['pattern']) && !preg_match($rules['pattern'], $value)) {
            throw new ValidationException($message);
        }
        
        return $value;
    }
    
    /**
     * Validation des entiers
     */
    private static function validateInteger($value, $rules) {
        $message = $rules['message'] ?? 'Nombre entier invalide';
        
        if (!is_numeric($value) || floor($value) != $value) {
            throw new ValidationException($message);
        }
        
        $intValue = (int) $value;
        
        // Valeur minimum
        if (isset($rules['min']) && $intValue < $rules['min']) {
            throw new ValidationException($message);
        }
        
        // Valeur maximum
        if (isset($rules['max']) && $intValue > $rules['max']) {
            throw new ValidationException($message);
        }
        
        return $intValue;
    }
    
    /**
     * Validation des nombres
     */
    private static function validateNumeric($value, $rules) {
        $message = $rules['message'] ?? 'Nombre invalide';
        
        if (!is_numeric($value)) {
            throw new ValidationException($message);
        }
        
        $numValue = (float) $value;
        
        // Valeur minimum
        if (isset($rules['min']) && $numValue < $rules['min']) {
            throw new ValidationException($message);
        }
        
        // Valeur maximum
        if (isset($rules['max']) && $numValue > $rules['max']) {
            throw new ValidationException($message);
        }
        
        return $numValue;
    }
    
    /**
     * Validation des emails
     */
    private static function validateEmail($value, $rules) {
        $message = $rules['message'] ?? 'Format d\'email invalide';
        
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new ValidationException($message);
        }
        
        // Longueur maximum
        if (isset($rules['max_length']) && strlen($value) > $rules['max_length']) {
            throw new ValidationException($message);
        }
        
        return $value;
    }
    
    /**
     * Validation des dates
     */
    private static function validateDate($value, $rules) {
        $message = $rules['message'] ?? 'Format de date invalide';
        
        // Essayer de parser la date
        $date = DateTime::createFromFormat('Y-m-d', $value);
        if (!$date || $date->format('Y-m-d') !== $value) {
            // Essayer d'autres formats
            $date = DateTime::createFromFormat('d/m/Y', $value);
            if (!$date) {
                throw new ValidationException($message);
            }
            return $date->format('Y-m-d'); // Normaliser
        }
        
        return $value;
    }
    
    /**
     * Validation des booléens
     */
    private static function validateBoolean($value) {
        if (is_bool($value)) {
            return $value;
        }
        
        if (is_string($value)) {
            $lower = strtolower($value);
            if (in_array($lower, ['true', '1', 'yes', 'on'])) {
                return true;
            }
            if (in_array($lower, ['false', '0', 'no', 'off', ''])) {
                return false;
            }
        }
        
        if (is_numeric($value)) {
            return (bool) $value;
        }
        
        throw new ValidationException('Valeur booléenne invalide');
    }
    
    /**
     * Valider les paramètres de recherche
     */
    public static function validateSearchParams($params) {
        $allowedFields = [
            'codePesee', 'numTicket', 'client', 'fournisseur', 
            'codeSite', 'dateDebut', 'dateFin', 'produit', 
            'provenance', 'destination', 'immatriculation', 'nomPeseur1',
            'limit', 'page'
        ];
        
        $validated = [];
        
        foreach ($params as $field => $value) {
            if (!in_array($field, $allowedFields)) {
                continue; // Ignorer les champs non autorisés
            }
            
            try {
                $validated[$field] = self::validate($field, $value);
            } catch (ValidationException $e) {
                throw new ValidationException("Paramètre '$field': " . $e->getMessage());
            }
        }
        
        return $validated;
    }
    
    /**
     * Validation spéciale pour les données de pesée
     */
    public static function validatePeseeData($data) {
        $rules = [
            'CodePesee' => [
                'required' => true,
                'type' => 'string',
                'max_length' => 20,
                'pattern' => '/^[A-Z0-9\-]+$/',
                'message' => 'Code pesée requis (majuscules et chiffres)'
            ],
            'NumTicket' => [
                'required' => true,
                'type' => 'string',
                'max_length' => 20,
                'message' => 'Numéro de ticket requis'
            ],
            'NomClient' => [
                'required' => true,
                'type' => 'string',
                'max_length' => 100,
                'message' => 'Nom du client requis'
            ],
            'Poids1' => [
                'required' => true,
                'type' => 'numeric',
                'min' => 0,
                'message' => 'Poids 1 requis (nombre positif)'
            ],
            'Poids2' => [
                'required' => true,
                'type' => 'numeric',
                'min' => 0,
                'message' => 'Poids 2 requis (nombre positif)'
            ],
            'PoidsNet' => [
                'required' => false,
                'type' => 'numeric',
                'min' => 0,
                'message' => 'Poids net invalide'
            ]
        ];
        
        return self::validateMultiple($data, $rules);
    }
    
    /**
     * Nettoyage avancé pour SQL (en plus des requêtes préparées)
     */
    public static function sanitizeForSql($value) {
        if (is_array($value)) {
            return array_map([self::class, 'sanitizeForSql'], $value);
        }
        
        // Supprimer les caractères dangereux
        $dangerous = ['<', '>', '"', "'", '&', 'script', 'javascript', 'vbscript'];
        $value = str_ireplace($dangerous, '', $value);
        
        // Limiter la longueur
        if (strlen($value) > 1000) {
            $value = substr($value, 0, 1000);
        }
        
        return trim($value);
    }
}

/**
 * Exception personnalisée pour la validation
 */
class ValidationException extends Exception {
    private $details;
    
    public function __construct($message = "", $details = null, $code = 0, ?Throwable $previous = null) {
        parent::__construct($message, $code, $previous);
        $this->details = $details;
    }
    
    public function getDetails() {
        return $this->details;
    }
}
?>