<?php
/**
 * SCHEMAFY Validator Class
 * 
 * Centralizes all validation logic for the Custom Schema by SCHEMAFY plugin
 * 
 * @package CustomSchemaSchemafy
 * @since 1.1.85
 * @author SCHEMAFY Team
 */

// Prevent direct access
if (!defined('ABSPATH')) {
    exit;
}

/**
 * Class SCHEMAFY_Validator
 * 
 * Handles all validation operations for schemas, URLs, JSON data, and security checks
 */
class SCHEMAFY_Validator {
    
    /**
     * Allowed schema types
     * 
     * @var array
     */
    private $allowed_schema_types = array(
        'WebPage',
        'Article',
        'Product',
        'Organization',
        'LocalBusiness',
        'Person',
        'Event',
        'Recipe',
        'Review',
        'FAQ'
    );
    
    /**
     * Get list of allowed schema types
     * 
     * @return array List of allowed schema types
     */
    public function get_allowed_schema_types() {
        return apply_filters('schemafy_allowed_schema_types', $this->allowed_schema_types);
    }
    
    /**
     * Validate if schema type is allowed
     * 
     * @param string $type Schema type to validate
     * @return bool True if valid, false otherwise
     */
    public function is_valid_schema_type($type) {
        if (empty($type)) {
            return false;
        }
        
        $allowed_types = $this->get_allowed_schema_types();
        return in_array($type, $allowed_types, true);
    }
    
    /**
     * Validate JSON string
     * 
     * @param string $json_string JSON string to validate
     * @return bool True if valid JSON, false otherwise
     */
    public function is_valid_json($json_string) {
        if (empty($json_string)) {
            return false;
        }
        
        json_decode($json_string);
        return json_last_error() === JSON_ERROR_NONE;
    }
    
    /**
     * Validate and decode JSON string
     * 
     * @param string $json_string JSON string to decode
     * @return mixed|WP_Error Decoded JSON object or WP_Error on failure
     */
    public function validate_json_decode($json_string) {
        if (empty($json_string)) {
            return new WP_Error(
                'empty_json',
                __('JSON data is empty', 'schemafy'),
                array('status' => 400)
            );
        }
        
        // Check for script tags
        if ($this->has_script_tags($json_string)) {
            return new WP_Error(
                'script_tags_detected',
                __('Script tags detected. Paste only the JSON content starting with { and ending with }. Do not include <script type="application/ld+json"> or </script> wrapper tags.', 'schemafy'),
                array('status' => 400)
            );
        }
        
        $decoded = json_decode($json_string);
        
        if (json_last_error() !== JSON_ERROR_NONE) {
            return new WP_Error(
                'invalid_json',
                sprintf(
                    // translators: %s is a value inserted at runtime.
                    __('Invalid JSON: %s', 'schemafy'),
                    json_last_error_msg()
                ),
                array('status' => 400)
            );
        }
        
        return $decoded;
    }
    
    /**
     * Check if text contains script tags
     * 
     * @param string $text Text to check
     * @return bool True if script tags found, false otherwise
     */
    public function has_script_tags($text) {
        if (empty($text)) {
            return false;
        }
        
        return preg_match('/<script[\s\S]*?>[\s\S]*?<\/script>/i', $text) === 1 ||
               preg_match('/<script[\s\S]*?>/i', $text) === 1;
    }
    
    /**
     * Validate if URL belongs to the same domain as WordPress site
     * 
     * @param string $url URL to validate
     * @return bool True if same domain, false otherwise
     */
    public function is_same_domain($url) {
        if (empty($url)) {
            return false;
        }
        
        $url_parts = wp_parse_url($url);
        $site_parts = wp_parse_url(get_site_url());
        
        // wp_parse_url() returns false on failure
        if ($url_parts === false || $site_parts === false) {
            return false;
        }
        
        if (!isset($url_parts['host']) || !isset($site_parts['host'])) {
            return false;
        }
        
        return strtolower($url_parts['host']) === strtolower($site_parts['host']);
    }
    
    /**
     * Sanitize schema name
     * 
     * @param string $name Schema name to sanitize
     * @return string Sanitized schema name
     */
    public function sanitize_schema_name($name) {
        if (empty($name)) {
            return '';
        }
        
        return sanitize_text_field($name);
    }
    
    /**
     * Sanitize URL
     * 
     * @param string $url URL to sanitize
     * @return string Sanitized URL
     */
    public function sanitize_url($url) {
        if (empty($url)) {
            return '';
        }
        
        return esc_url_raw($url);
    }
    
    /**
     * Deep sanitize schema data (recursive)
     * 
     * @param mixed $data Data to sanitize
     * @return mixed Sanitized data
     */
    public function sanitize_schema_data($data) {
        if (is_string($data)) {
            // For strings, use sanitize_text_field to prevent HTML/script injection
            // Schema data should be JSON only, not HTML
            return sanitize_text_field($data);
        }
        
        if (is_array($data)) {
            return array_map(array($this, 'sanitize_schema_data'), $data);
        }
        
        if (is_object($data)) {
            $sanitized = new stdClass();
            foreach ($data as $key => $value) {
                $sanitized->{$key} = $this->sanitize_schema_data($value);
            }
            return $sanitized;
        }
        
        return $data;
    }
    
    /**
     * Validate data for creating a new schema (REST API POST)
     * 
     * @param array $data Data to validate
     * @return true|WP_Error True if valid, WP_Error otherwise
     */
    public function validate_schema_create_data($data) {
        $errors = array();
        
        // Validate URL (required)
        if (empty($data['url'])) {
            $errors['url'] = __('URL is required', 'schemafy');
        } elseif (!filter_var($data['url'], FILTER_VALIDATE_URL)) {
            $errors['url'] = __('Invalid URL format', 'schemafy');
        } elseif (!$this->is_same_domain($data['url'])) {
            $errors['url'] = __('URL must be from the same domain', 'schemafy');
        }
        
        // Validate schema type (required)
        if (empty($data['schema_type'])) {
            $errors['schema_type'] = __('Schema type is required', 'schemafy');
        } elseif (!$this->is_valid_schema_type($data['schema_type'])) {
            $errors['schema_type'] = __('Invalid schema type', 'schemafy');
        }
        
        // Validate schema name (required)
        if (empty($data['schema_name'])) {
            $errors['schema_name'] = __('Schema name is required', 'schemafy');
        } elseif (strlen($data['schema_name']) > 200) {
            $errors['schema_name'] = __('Schema name must be 200 characters or less', 'schemafy');
        }
        
        // Validate schema data (required, must be valid JSON)
        if (empty($data['schema_data'])) {
            $errors['schema_data'] = __('Schema data is required', 'schemafy');
        } else {
            $json_validation = $this->validate_json_decode($data['schema_data']);
            if (is_wp_error($json_validation)) {
                $errors['schema_data'] = $json_validation->get_error_message();
            }
        }
        
        // Validate meta_title (optional, max 500 chars)
        if (!empty($data['meta_title']) && strlen($data['meta_title']) > 500) {
            $errors['meta_title'] = __('Meta title must be 500 characters or less', 'schemafy');
        }
        
        // Validate meta_description (optional, max 1000 chars)
        if (!empty($data['meta_description']) && strlen($data['meta_description']) > 1000) {
            $errors['meta_description'] = __('Meta description must be 1000 characters or less', 'schemafy');
        }
        
        // Validate is_active (optional, must be 0 or 1)
        if (isset($data['is_active']) && !in_array($data['is_active'], array('0', '1', 0, 1, true, false), true)) {
            $errors['is_active'] = __('Invalid value for is_active', 'schemafy');
        }
        
        if (!empty($errors)) {
            return new WP_Error(
                'validation_failed',
                __('Validation failed', 'schemafy'),
                array('status' => 400, 'errors' => $errors)
            );
        }
        
        return true;
    }
    
    /**
     * Validate data for updating an existing schema (REST API PUT)
     * 
     * @param array $data Data to validate
     * @param bool $partial Whether this is a partial update (allows missing fields)
     * @return true|WP_Error True if valid, WP_Error otherwise
     */
    public function validate_schema_update_data($data, $partial = false) {
        $errors = array();
        
        // For partial updates, only validate fields that are present
        // For full updates, require all fields
        
        // Validate URL if present
        if (isset($data['url'])) {
            if (empty($data['url']) && !$partial) {
                $errors['url'] = __('URL is required', 'schemafy');
            } elseif (!empty($data['url'])) {
                if (!filter_var($data['url'], FILTER_VALIDATE_URL)) {
                    $errors['url'] = __('Invalid URL format', 'schemafy');
                } elseif (!$this->is_same_domain($data['url'])) {
                    $errors['url'] = __('URL must be from the same domain', 'schemafy');
                }
            }
        } elseif (!$partial) {
            $errors['url'] = __('URL is required', 'schemafy');
        }
        
        // Validate schema type if present
        if (isset($data['schema_type'])) {
            if (empty($data['schema_type']) && !$partial) {
                $errors['schema_type'] = __('Schema type is required', 'schemafy');
            } elseif (!empty($data['schema_type']) && !$this->is_valid_schema_type($data['schema_type'])) {
                $errors['schema_type'] = __('Invalid schema type', 'schemafy');
            }
        } elseif (!$partial) {
            $errors['schema_type'] = __('Schema type is required', 'schemafy');
        }
        
        // Validate schema name if present
        if (isset($data['schema_name'])) {
            if (empty($data['schema_name']) && !$partial) {
                $errors['schema_name'] = __('Schema name is required', 'schemafy');
            } elseif (!empty($data['schema_name']) && strlen($data['schema_name']) > 200) {
                $errors['schema_name'] = __('Schema name must be 200 characters or less', 'schemafy');
            }
        } elseif (!$partial) {
            $errors['schema_name'] = __('Schema name is required', 'schemafy');
        }
        
        // Validate schema data if present
        if (isset($data['schema_data'])) {
            if (empty($data['schema_data']) && !$partial) {
                $errors['schema_data'] = __('Schema data is required', 'schemafy');
            } elseif (!empty($data['schema_data'])) {
                $json_validation = $this->validate_json_decode($data['schema_data']);
                if (is_wp_error($json_validation)) {
                    $errors['schema_data'] = $json_validation->get_error_message();
                }
            }
        } elseif (!$partial) {
            $errors['schema_data'] = __('Schema data is required', 'schemafy');
        }
        
        // Validate meta_title if present
        if (isset($data['meta_title']) && !empty($data['meta_title']) && strlen($data['meta_title']) > 500) {
            $errors['meta_title'] = __('Meta title must be 500 characters or less', 'schemafy');
        }
        
        // Validate meta_description if present
        if (isset($data['meta_description']) && !empty($data['meta_description']) && strlen($data['meta_description']) > 1000) {
            $errors['meta_description'] = __('Meta description must be 1000 characters or less', 'schemafy');
        }
        
        // Validate is_active if present
        if (isset($data['is_active']) && !in_array($data['is_active'], array('0', '1', 0, 1, true, false), true)) {
            $errors['is_active'] = __('Invalid value for is_active', 'schemafy');
        }
        
        if (!empty($errors)) {
            return new WP_Error(
                'validation_failed',
                __('Validation failed', 'schemafy'),
                array('status' => 400, 'errors' => $errors)
            );
        }
        
        return true;
    }
    
    /**
     * Validate bulk apply data
     * 
     * @param array $data Data to validate
     * @return true|WP_Error True if valid, WP_Error otherwise
     */
    public function validate_bulk_apply_data($data) {
        $errors = array();
        
        // Validate URLs array (required)
        if (!isset($data['urls']) || !is_array($data['urls']) || empty($data['urls'])) {
            $errors['urls'] = __('URLs array is required and must not be empty', 'schemafy');
        } else {
            foreach ($data['urls'] as $index => $url) {
                if (!filter_var($url, FILTER_VALIDATE_URL)) {
                    // translators: %d is a value inserted at runtime.
                    $errors['urls'][] = sprintf(__('Invalid URL at index %d', 'schemafy'), $index);
                } elseif (!$this->is_same_domain($url)) {
                    // translators: %d is a value inserted at runtime.
                    $errors['urls'][] = sprintf(__('URL at index %d must be from the same domain', 'schemafy'), $index);
                }
            }
        }
        
        // Validate schema_type (required)
        if (empty($data['schema_type'])) {
            $errors['schema_type'] = __('Schema type is required', 'schemafy');
        } elseif (!$this->is_valid_schema_type($data['schema_type'])) {
            $errors['schema_type'] = __('Invalid schema type', 'schemafy');
        }
        
        // Validate schema_name (required)
        if (empty($data['schema_name'])) {
            $errors['schema_name'] = __('Schema name is required', 'schemafy');
        }
        
        // Validate schema_data (required, must be valid JSON)
        if (empty($data['schema_data'])) {
            $errors['schema_data'] = __('Schema data is required', 'schemafy');
        } else {
            $json_validation = $this->validate_json_decode($data['schema_data']);
            if (is_wp_error($json_validation)) {
                $errors['schema_data'] = $json_validation->get_error_message();
            }
        }
        
        if (!empty($errors)) {
            return new WP_Error(
                'validation_failed',
                __('Validation failed', 'schemafy'),
                array('status' => 400, 'errors' => $errors)
            );
        }
        
        return true;
    }
    
    /**
     * Validate nonce for AJAX requests
     * 
     * @param string $nonce Nonce to validate
     * @param string $action Nonce action
     * @return bool True if valid, false otherwise
     */
    public function validate_nonce($nonce, $action = 'schemafy_schema_nonce') {
        return wp_verify_nonce($nonce, $action) !== false;
    }
    
    /**
     * Validate user capability
     * 
     * @param string $capability Capability to check (default: manage_options)
     * @return bool True if user has capability, false otherwise
     */
    public function validate_user_capability($capability = 'manage_options') {
        return current_user_can(apply_filters('schemafy_schema_capability', $capability));
    }
    
    /**
     * Sanitize integer ID
     * 
     * @param mixed $id ID to sanitize
     * @return int Sanitized integer ID
     */
    public function sanitize_id($id) {
        return absint($id);
    }
    
    /**
     * Validate and sanitize pagination parameters
     * 
     * @param int $page Page number
     * @param int $per_page Items per page
     * @return array Sanitized pagination parameters
     */
    public function sanitize_pagination($page, $per_page) {
        $page = max(1, absint($page));
        $per_page = min(100, max(1, absint($per_page)));
        
        return array(
            'page' => $page,
            'per_page' => $per_page
        );
    }
    
    /**
     * Validate search query
     * 
     * @param string $query Search query
     * @return string Sanitized search query
     */
    public function sanitize_search_query($query) {
        if (empty($query)) {
            return '';
        }
        
        return sanitize_text_field($query);
    }
}