<?php
/**
 * HVAC AJAX Security Handler
 * 
 * Centralized security layer for all AJAX endpoints
 * Implements OWASP Top 10 security best practices
 *
 * @package HVAC_Community_Events
 * @since 2.0.0
 */

if (!defined('ABSPATH')) {
    exit;
}

/**
 * Class HVAC_Ajax_Security
 * 
 * Provides comprehensive security for AJAX endpoints including:
 * - Rate limiting
 * - Input validation
 * - CSRF protection
 * - Audit logging
 * - Security headers
 */
class HVAC_Ajax_Security {
    
    /**
     * Instance of this class
     *
     * @var HVAC_Ajax_Security
     */
    private static $instance = null;
    
    /**
     * Rate limiting configuration
     */
    const RATE_LIMIT_WINDOW = 60; // seconds
    const RATE_LIMIT_REQUESTS = 30; // max requests per window
    const RATE_LIMIT_SENSITIVE = 5; // max sensitive actions per window
    
    /**
     * Security nonce actions
     */
    const NONCE_GENERAL = 'hvac_ajax_nonce';
    const NONCE_APPROVAL = 'hvac_master_approvals';
    const NONCE_ANNOUNCEMENT = 'hvac_announcements_nonce';
    
    /**
     * Get instance of this class
     *
     * @return HVAC_Ajax_Security
     */
    public static function get_instance() {
        if (null === self::$instance) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    
    /**
     * Constructor
     */
    private function __construct() {
        $this->init_hooks();
    }
    
    /**
     * Initialize security hooks
     */
    private function init_hooks() {
        // Add security headers
        add_action('send_headers', array($this, 'send_security_headers'));
        
        // AJAX security middleware
        add_action('wp_ajax_nopriv_hvac_security_check', array($this, 'block_nopriv_ajax'));
        
        // Initialize audit logging
        add_action('init', array($this, 'init_audit_logging'));
    }
    
    /**
     * Send security headers
     *
     * SECURITY FIX (U3): Fixed condition to apply headers to AJAX requests.
     * is_admin() returns true for admin-ajax.php, so previous condition never matched.
     * Also removed 'unsafe-eval' from CSP (M1) - rarely needed and weakens security.
     */
    public function send_security_headers() {
        // Apply to AJAX requests (wp_doing_ajax covers admin-ajax.php)
        // Skip if headers already sent
        if (wp_doing_ajax() && !headers_sent()) {
            // Content Security Policy - removed unsafe-eval (M1 fix)
            header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';");
            
            // Additional security headers
            header('X-Content-Type-Options: nosniff');
            header('X-Frame-Options: SAMEORIGIN');
            header('X-XSS-Protection: 1; mode=block');
            header('Referrer-Policy: strict-origin-when-cross-origin');
            
            // CORS configuration (adjust origin as needed)
            $allowed_origin = get_site_url();
            header("Access-Control-Allow-Origin: $allowed_origin");
            header('Access-Control-Allow-Credentials: true');
            header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
            header('Access-Control-Max-Age: 86400');
        }
    }
    
    /**
     * Verify AJAX request security
     * 
     * @param string $action AJAX action being performed
     * @param string $nonce_action Nonce action to verify
     * @param array $capabilities Required capabilities
     * @param bool $is_sensitive Whether this is a sensitive action
     * @return bool|WP_Error True if valid, WP_Error on failure
     */
    public static function verify_ajax_request($action, $nonce_action = null, $capabilities = array(), $is_sensitive = false) {
        $security = self::get_instance();
        
        // 1. Check if user is logged in
        if (!is_user_logged_in()) {
            $security->log_security_event('ajax_unauthorized', $action);
            return new WP_Error('unauthorized', 'Authentication required', array('status' => 401));
        }
        
        // 2. Verify nonce
        $nonce = isset($_REQUEST['nonce']) ? sanitize_text_field($_REQUEST['nonce']) : '';
        $nonce_action = $nonce_action ?: self::NONCE_GENERAL;
        
        if (!wp_verify_nonce($nonce, $nonce_action)) {
            $security->log_security_event('ajax_invalid_nonce', $action);
            return new WP_Error('invalid_nonce', 'Security token expired or invalid', array('status' => 403));
        }
        
        // 3. Check rate limiting
        $rate_limit = $security->check_rate_limit($action, $is_sensitive);
        if (is_wp_error($rate_limit)) {
            return $rate_limit;
        }
        
        // 4. Verify capabilities
        if (!empty($capabilities)) {
            $has_cap = false;
            foreach ($capabilities as $capability) {
                if (current_user_can($capability)) {
                    $has_cap = true;
                    break;
                }
            }
            
            if (!$has_cap) {
                $security->log_security_event('ajax_insufficient_permissions', $action);
                return new WP_Error('insufficient_permissions', 'You do not have permission to perform this action', array('status' => 403));
            }
        }
        
        // 5. Log successful authentication for sensitive actions
        if ($is_sensitive) {
            $security->log_security_event('ajax_sensitive_action', $action, 'info');
        }
        
        return true;
    }
    
    /**
     * Check rate limiting
     * 
     * @param string $action Action being performed
     * @param bool $is_sensitive Whether this is a sensitive action
     * @return bool|WP_Error True if within limits, WP_Error if exceeded
     */
    private function check_rate_limit($action, $is_sensitive = false) {
        $user_id = get_current_user_id();
        $ip_address = $this->get_client_ip();
        
        // Use both user ID and IP for rate limiting
        $rate_key = 'hvac_rate_' . md5($user_id . '_' . $ip_address . '_' . $action);
        $sensitive_key = 'hvac_sensitive_' . md5($user_id . '_' . $ip_address);
        
        // Check general rate limit
        $requests = get_transient($rate_key);
        $limit = $is_sensitive ? self::RATE_LIMIT_SENSITIVE : self::RATE_LIMIT_REQUESTS;
        
        if (false === $requests) {
            $requests = 0;
        }
        
        if ($requests >= $limit) {
            $this->log_security_event('ajax_rate_limit_exceeded', $action, 'warning');
            return new WP_Error('rate_limit_exceeded', 'Too many requests. Please wait and try again.', array('status' => 429));
        }
        
        // Increment counter
        set_transient($rate_key, $requests + 1, self::RATE_LIMIT_WINDOW);
        
        // Additional check for sensitive actions across all endpoints
        if ($is_sensitive) {
            $sensitive_count = get_transient($sensitive_key);
            if (false === $sensitive_count) {
                $sensitive_count = 0;
            }
            
            if ($sensitive_count >= self::RATE_LIMIT_SENSITIVE) {
                $this->log_security_event('ajax_sensitive_rate_limit_exceeded', $action, 'critical');
                return new WP_Error('rate_limit_exceeded', 'Too many sensitive actions. Please wait and try again.', array('status' => 429));
            }
            
            set_transient($sensitive_key, $sensitive_count + 1, self::RATE_LIMIT_WINDOW * 5); // 5 minute window for sensitive
        }
        
        return true;
    }
    
    /**
     * Sanitize and validate input data
     * 
     * @param array $data Input data
     * @param array $rules Validation rules
     * @return array|WP_Error Sanitized data or error
     */
    public static function sanitize_input($data, $rules) {
        $sanitized = array();
        $errors = array();
        
        foreach ($rules as $field => $rule) {
            $value = isset($data[$field]) ? $data[$field] : null;
            
            // Check required fields
            if (isset($rule['required']) && $rule['required'] && empty($value)) {
                $errors[] = sprintf('Field "%s" is required', $field);
                continue;
            }
            
            // Skip optional empty fields
            if (empty($value) && (!isset($rule['required']) || !$rule['required'])) {
                continue;
            }
            
            // Apply sanitization based on type
            $type = isset($rule['type']) ? $rule['type'] : 'text';
            switch ($type) {
                case 'int':
                    $sanitized[$field] = intval($value);
                    if (isset($rule['min']) && $sanitized[$field] < $rule['min']) {
                        $errors[] = sprintf('Field "%s" must be at least %d', $field, $rule['min']);
                    }
                    if (isset($rule['max']) && $sanitized[$field] > $rule['max']) {
                        $errors[] = sprintf('Field "%s" must be at most %d', $field, $rule['max']);
                    }
                    break;
                    
                case 'email':
                    $sanitized[$field] = sanitize_email($value);
                    if (!is_email($sanitized[$field])) {
                        $errors[] = sprintf('Field "%s" must be a valid email', $field);
                    }
                    break;
                    
                case 'url':
                    $sanitized[$field] = esc_url_raw($value);
                    if (!filter_var($sanitized[$field], FILTER_VALIDATE_URL)) {
                        $errors[] = sprintf('Field "%s" must be a valid URL', $field);
                    }
                    break;
                    
                case 'textarea':
                    $sanitized[$field] = sanitize_textarea_field($value);
                    if (isset($rule['max_length']) && strlen($sanitized[$field]) > $rule['max_length']) {
                        $errors[] = sprintf('Field "%s" must be at most %d characters', $field, $rule['max_length']);
                    }
                    break;
                    
                case 'html':
                    $allowed_html = isset($rule['allowed_html']) ? $rule['allowed_html'] : wp_kses_allowed_html('post');
                    $sanitized[$field] = wp_kses($value, $allowed_html);
                    break;
                    
                case 'boolean':
                    $sanitized[$field] = filter_var($value, FILTER_VALIDATE_BOOLEAN);
                    break;
                    
                case 'array':
                    if (!is_array($value)) {
                        $errors[] = sprintf('Field "%s" must be an array', $field);
                    } else {
                        $sanitized[$field] = array_map('sanitize_text_field', $value);
                    }
                    break;
                    
                default:
                    $sanitized[$field] = sanitize_text_field($value);
                    if (isset($rule['max_length']) && strlen($sanitized[$field]) > $rule['max_length']) {
                        $errors[] = sprintf('Field "%s" must be at most %d characters', $field, $rule['max_length']);
                    }
            }
            
            // Custom validation callback
            if (isset($rule['validate']) && is_callable($rule['validate'])) {
                $validation_result = call_user_func($rule['validate'], $sanitized[$field]);
                if (is_wp_error($validation_result)) {
                    $errors[] = $validation_result->get_error_message();
                }
            }
        }
        
        if (!empty($errors)) {
            return new WP_Error('validation_failed', 'Input validation failed', array('errors' => $errors));
        }
        
        return $sanitized;
    }
    
    /**
     * Get client IP address
     * 
     * @return string
     */
    private function get_client_ip() {
        $ip_keys = array('HTTP_CF_CONNECTING_IP', 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR');
        
        foreach ($ip_keys as $key) {
            if (array_key_exists($key, $_SERVER) === true) {
                foreach (explode(',', $_SERVER[$key]) as $ip) {
                    $ip = trim($ip);
                    
                    if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) {
                        return $ip;
                    }
                }
            }
        }
        
        return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0';
    }
    
    /**
     * Log security events
     * 
     * @param string $event_type Type of security event
     * @param string $context Additional context
     * @param string $level Log level (info, warning, critical)
     */
    private function log_security_event($event_type, $context = '', $level = 'warning') {
        $user_id = get_current_user_id();
        $ip_address = $this->get_client_ip();
        
        $log_entry = array(
            'timestamp' => current_time('mysql'),
            'event_type' => $event_type,
            'user_id' => $user_id,
            'ip_address' => $ip_address,
            'context' => $context,
            'level' => $level,
            'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '',
            'request_uri' => isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : ''
        );
        
        // Use WordPress logging if available
        if (class_exists('HVAC_Logger')) {
            HVAC_Logger::log($level, 'Security: ' . $event_type, $log_entry);
        }
        
        // Store in database for audit trail
        $this->store_audit_log($log_entry);
        
        // For critical events, notify admin
        if ($level === 'critical') {
            $this->notify_admin_security_event($log_entry);
        }
    }
    
    /**
     * Store audit log in database
     * 
     * @param array $log_entry Log entry data
     */
    private function store_audit_log($log_entry) {
        global $wpdb;
        
        $table_name = $wpdb->prefix . 'hvac_security_audit';
        
        // Create table if not exists
        if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") != $table_name) {
            $charset_collate = $wpdb->get_charset_collate();
            
            $sql = "CREATE TABLE $table_name (
                id bigint(20) NOT NULL AUTO_INCREMENT,
                timestamp datetime DEFAULT CURRENT_TIMESTAMP,
                event_type varchar(100) NOT NULL,
                user_id bigint(20),
                ip_address varchar(45),
                context text,
                level varchar(20),
                user_agent text,
                request_uri text,
                PRIMARY KEY (id),
                KEY event_type (event_type),
                KEY user_id (user_id),
                KEY timestamp (timestamp)
            ) $charset_collate;";
            
            require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
            dbDelta($sql);
        }
        
        // Insert log entry
        $wpdb->insert(
            $table_name,
            $log_entry,
            array('%s', '%s', '%d', '%s', '%s', '%s', '%s', '%s')
        );
    }
    
    /**
     * Initialize audit logging system
     */
    public function init_audit_logging() {
        // Clean old audit logs (keep 30 days)
        if (!wp_next_scheduled('hvac_clean_audit_logs')) {
            wp_schedule_event(time(), 'daily', 'hvac_clean_audit_logs');
        }
        
        add_action('hvac_clean_audit_logs', array($this, 'clean_old_audit_logs'));
    }
    
    /**
     * Clean old audit logs
     */
    public function clean_old_audit_logs() {
        global $wpdb;
        $table_name = $wpdb->prefix . 'hvac_security_audit';
        
        $wpdb->query($wpdb->prepare(
            "DELETE FROM $table_name WHERE timestamp < DATE_SUB(NOW(), INTERVAL %d DAY)",
            30
        ));
    }
    
    /**
     * Notify admin of critical security events
     * 
     * @param array $log_entry Log entry data
     */
    private function notify_admin_security_event($log_entry) {
        $admin_email = get_option('admin_email');
        $site_name = get_bloginfo('name');
        
        $subject = sprintf('[%s] Critical Security Event: %s', $site_name, $log_entry['event_type']);
        
        $message = sprintf(
            "A critical security event has been detected on your site.\n\n" .
            "Event Type: %s\n" .
            "Time: %s\n" .
            "User ID: %s\n" .
            "IP Address: %s\n" .
            "Context: %s\n" .
            "User Agent: %s\n" .
            "Request URI: %s\n\n" .
            "Please review your security logs for more information.",
            $log_entry['event_type'],
            $log_entry['timestamp'],
            $log_entry['user_id'],
            $log_entry['ip_address'],
            $log_entry['context'],
            $log_entry['user_agent'],
            $log_entry['request_uri']
        );
        
        wp_mail($admin_email, $subject, $message);
    }
    
    /**
     * Block non-privileged AJAX requests
     */
    public function block_nopriv_ajax() {
        wp_send_json_error('Unauthorized access', 401);
    }
    
    /**
     * Generate secure token for sensitive operations
     * 
     * @param string $action Action identifier
     * @param int $user_id User ID
     * @return string Secure token with embedded timestamp (format: timestamp.signature)
     */
    public static function generate_secure_token($action, $user_id) {
        $salt = wp_salt('auth');
        $timestamp = time();
        $data = $action . '|' . $user_id . '|' . $timestamp;
        $signature = hash_hmac('sha256', $data, $salt);

        // Return timestamp.signature format for O(1) verification
        return $timestamp . '.' . $signature;
    }

    /**
     * Verify secure token - O(1) complexity
     *
     * SECURITY FIX (U1): Rewritten to O(1) verification instead of O(expiry) loop.
     * Previous implementation looped up to 3600 times computing HMACs, creating
     * a DoS vulnerability. Now extracts timestamp from token for single HMAC check.
     *
     * @param string $token Token to verify (format: timestamp.signature)
     * @param string $action Action identifier
     * @param int $user_id User ID
     * @param int $expiry Token expiry in seconds (default 1 hour)
     * @return bool
     */
    public static function verify_secure_token($token, $action, $user_id, $expiry = 3600) {
        // Parse token format: timestamp.signature
        $parts = explode('.', $token, 2);
        if (count($parts) !== 2) {
            return false;
        }

        list($timestamp, $signature) = $parts;
        $timestamp = (int) $timestamp;

        // Validate timestamp exists and signature is not empty
        if (!$timestamp || empty($signature)) {
            return false;
        }

        // Check if token has expired
        $current_time = time();
        if (($current_time - $timestamp) > $expiry) {
            return false;
        }

        // Single HMAC computation for verification
        $salt = wp_salt('auth');
        $data = $action . '|' . $user_id . '|' . $timestamp;
        $expected_signature = hash_hmac('sha256', $data, $salt);

        // Timing-safe comparison
        return hash_equals($expected_signature, $signature);
    }
}

// Initialize the security handler
HVAC_Ajax_Security::get_instance();