<?php
/**
 * Security Service
 * 
 * Handles encryption, rate limiting, and security utilities
 *
 * @package Freelance_Studio
 * @since 2.12.14
 */

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

class FS_Security_Service {

    /**
     * Encryption key option name
     */
    const ENCRYPTION_KEY_OPTION = 'fs_encryption_key';

    /**
     * Rate limit transient prefix
     */
    const RATE_LIMIT_PREFIX = 'fs_rate_limit_';

    /**
     * Initialize security service
     */
    public static function init() {
        // Ensure encryption key exists
        self::get_or_create_encryption_key();
        
        // Add security headers
        add_action('send_headers', array(__CLASS__, 'add_security_headers'));
    }

    /**
     * Get or create encryption key
     * 
     * @return string
     */
    private static function get_or_create_encryption_key() {
        $key = get_option(self::ENCRYPTION_KEY_OPTION);
        
        if (empty($key)) {
            // Generate a secure random key
            if (function_exists('sodium_crypto_secretbox_keygen')) {
                $key = base64_encode(sodium_crypto_secretbox_keygen());
            } else {
                $key = base64_encode(random_bytes(32));
            }
            update_option(self::ENCRYPTION_KEY_OPTION, $key, false);
        }
        
        return base64_decode($key);
    }

    /**
     * Encrypt sensitive data
     * 
     * @param string $data Data to encrypt
     * @return string|false Encrypted data or false on failure
     */
    public static function encrypt($data) {
        if (empty($data)) {
            return $data;
        }

        $key = self::get_or_create_encryption_key();
        
        // Use libsodium if available (PHP 7.2+)
        if (function_exists('sodium_crypto_secretbox')) {
            try {
                $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
                $ciphertext = sodium_crypto_secretbox($data, $nonce, $key);
                return base64_encode($nonce . $ciphertext);
            } catch (Exception $e) {
                error_log('FS Security: Encryption failed - ' . $e->getMessage());
                return false;
            }
        }
        
        // Fallback to OpenSSL
        if (function_exists('openssl_encrypt')) {
            $iv = random_bytes(16);
            $encrypted = openssl_encrypt($data, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
            if ($encrypted === false) {
                return false;
            }
            return base64_encode($iv . $encrypted);
        }
        
        // No encryption available - log warning
        error_log('FS Security: No encryption method available. Storing data unencrypted.');
        return $data;
    }

    /**
     * Decrypt sensitive data
     * 
     * @param string $data Encrypted data
     * @return string|false Decrypted data or false on failure
     */
    public static function decrypt($data) {
        if (empty($data)) {
            return $data;
        }

        $key = self::get_or_create_encryption_key();
        $decoded = base64_decode($data);
        
        if ($decoded === false) {
            // Data might not be encrypted (legacy)
            return $data;
        }
        
        // Try libsodium first
        if (function_exists('sodium_crypto_secretbox_open')) {
            try {
                $nonce = substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
                $ciphertext = substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
                
                if (strlen($nonce) !== SODIUM_CRYPTO_SECRETBOX_NONCEBYTES) {
                    // Not sodium encrypted, try OpenSSL or return raw
                    return self::decrypt_openssl($decoded, $key) ?: $data;
                }
                
                $plaintext = sodium_crypto_secretbox_open($ciphertext, $nonce, $key);
                if ($plaintext === false) {
                    // Might be OpenSSL encrypted or unencrypted
                    return self::decrypt_openssl($decoded, $key) ?: $data;
                }
                return $plaintext;
            } catch (Exception $e) {
                // Try OpenSSL fallback
                return self::decrypt_openssl($decoded, $key) ?: $data;
            }
        }
        
        // Fallback to OpenSSL
        return self::decrypt_openssl($decoded, $key) ?: $data;
    }

    /**
     * Decrypt with OpenSSL
     * 
     * @param string $decoded Base64 decoded data
     * @param string $key Encryption key
     * @return string|false
     */
    private static function decrypt_openssl($decoded, $key) {
        if (!function_exists('openssl_decrypt')) {
            return false;
        }
        
        $iv = substr($decoded, 0, 16);
        $ciphertext = substr($decoded, 16);
        
        if (strlen($iv) !== 16) {
            return false;
        }
        
        return openssl_decrypt($ciphertext, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
    }

    /**
     * Store encrypted option
     * 
     * @param string $option_name Option name
     * @param string $value Value to encrypt and store
     * @return bool
     */
    public static function update_encrypted_option($option_name, $value) {
        $encrypted = self::encrypt($value);
        if ($encrypted === false) {
            return false;
        }
        return update_option($option_name, $encrypted, false);
    }

    /**
     * Get decrypted option
     * 
     * @param string $option_name Option name
     * @param string $default Default value
     * @return string
     */
    public static function get_encrypted_option($option_name, $default = '') {
        $value = get_option($option_name, $default);
        if ($value === $default) {
            return $default;
        }
        $decrypted = self::decrypt($value);
        return $decrypted !== false ? $decrypted : $default;
    }

    /**
     * Check rate limit
     * 
     * @param string $action Action identifier
     * @param string $identifier User identifier (IP, token, etc.)
     * @param int $max_attempts Maximum attempts allowed
     * @param int $window_seconds Time window in seconds
     * @return bool True if within limit, false if exceeded
     */
    public static function check_rate_limit($action, $identifier, $max_attempts = 5, $window_seconds = 300) {
        $key = self::RATE_LIMIT_PREFIX . md5($action . $identifier);
        $attempts = get_transient($key);
        
        if ($attempts === false) {
            set_transient($key, 1, $window_seconds);
            return true;
        }
        
        if ($attempts >= $max_attempts) {
            return false;
        }
        
        set_transient($key, $attempts + 1, $window_seconds);
        return true;
    }

    /**
     * Get remaining rate limit attempts
     * 
     * @param string $action Action identifier
     * @param string $identifier User identifier
     * @param int $max_attempts Maximum attempts allowed
     * @return int Remaining attempts
     */
    public static function get_remaining_attempts($action, $identifier, $max_attempts = 5) {
        $key = self::RATE_LIMIT_PREFIX . md5($action . $identifier);
        $attempts = get_transient($key);
        
        if ($attempts === false) {
            return $max_attempts;
        }
        
        return max(0, $max_attempts - $attempts);
    }

    /**
     * Reset rate limit for an action/identifier
     * 
     * @param string $action Action identifier
     * @param string $identifier User identifier
     */
    public static function reset_rate_limit($action, $identifier) {
        $key = self::RATE_LIMIT_PREFIX . md5($action . $identifier);
        delete_transient($key);
    }

    /**
     * Validate and sanitize base64 signature data
     * 
     * @param string $signature Base64 encoded signature
     * @return string|false Sanitized signature or false if invalid
     */
    public static function sanitize_signature($signature) {
        // Must start with data:image
        if (strpos($signature, 'data:image/') !== 0) {
            return false;
        }
        
        // Extract mime type and data
        if (!preg_match('/^data:image\/(png|jpeg|jpg|gif);base64,([A-Za-z0-9+\/=]+)$/', $signature, $matches)) {
            return false;
        }
        
        $mime_type = $matches[1];
        $base64_data = $matches[2];
        
        // Validate base64
        $decoded = base64_decode($base64_data, true);
        if ($decoded === false) {
            return false;
        }
        
        // Check file size (max 500KB for signature)
        if (strlen($decoded) > 512000) {
            return false;
        }
        
        // Verify it's actually an image
        $finfo = new finfo(FILEINFO_MIME_TYPE);
        $detected_mime = $finfo->buffer($decoded);
        
        $allowed_mimes = array('image/png', 'image/jpeg', 'image/gif');
        if (!in_array($detected_mime, $allowed_mimes)) {
            return false;
        }
        
        return $signature;
    }

    /**
     * Generate secure random token
     * 
     * @param int $length Token length
     * @return string
     */
    public static function generate_token($length = 32) {
        return bin2hex(random_bytes($length));
    }

    /**
     * Hash a token for storage comparison
     * 
     * @param string $token Plain token
     * @return string Hashed token
     */
    public static function hash_token($token) {
        return hash('sha256', $token . wp_salt('auth'));
    }

    /**
     * Verify a token against stored hash
     * 
     * @param string $token Plain token to verify
     * @param string $hash Stored hash
     * @return bool
     */
    public static function verify_token($token, $hash) {
        return hash_equals($hash, self::hash_token($token));
    }

    /**
     * Get client IP address (with proxy support)
     * 
     * @return string
     */
    public static function get_client_ip() {
        $ip = '';
        
        // Check for CloudFlare
        if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])) {
            $ip = $_SERVER['HTTP_CF_CONNECTING_IP'];
        }
        // Check for proxy
        elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
            // Can contain multiple IPs, get the first one
            $ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
            $ip = trim($ips[0]);
        }
        elseif (!empty($_SERVER['HTTP_X_REAL_IP'])) {
            $ip = $_SERVER['HTTP_X_REAL_IP'];
        }
        elseif (!empty($_SERVER['HTTP_CLIENT_IP'])) {
            $ip = $_SERVER['HTTP_CLIENT_IP'];
        }
        elseif (!empty($_SERVER['REMOTE_ADDR'])) {
            $ip = $_SERVER['REMOTE_ADDR'];
        }
        
        // Validate IP format
        if (filter_var($ip, FILTER_VALIDATE_IP)) {
            return $ip;
        }
        
        return 'unknown';
    }

    /**
     * Add security headers
     */
    public static function add_security_headers() {
        if (headers_sent()) {
            return;
        }
        
        // Only add headers on our pages
        if (!isset($_GET['fs_portal']) && !isset($_GET['fs_proposal']) && 
            !isset($_GET['fs_contract']) && !isset($_GET['fs_project_portal'])) {
            return;
        }
        
        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');
    }

    /**
     * Sanitize array of data recursively
     * 
     * @param array $data Data to sanitize
     * @param array $rules Field-specific sanitization rules
     * @return array
     */
    public static function sanitize_array($data, $rules = array()) {
        $sanitized = array();
        
        foreach ($data as $key => $value) {
            $key = sanitize_key($key);
            
            if (isset($rules[$key])) {
                $sanitized[$key] = self::apply_sanitization($value, $rules[$key]);
            } elseif (is_array($value)) {
                $sanitized[$key] = self::sanitize_array($value, $rules);
            } elseif (is_string($value)) {
                $sanitized[$key] = sanitize_text_field($value);
            } else {
                $sanitized[$key] = $value;
            }
        }
        
        return $sanitized;
    }

    /**
     * Apply specific sanitization rule
     * 
     * @param mixed $value Value to sanitize
     * @param string $rule Sanitization rule
     * @return mixed
     */
    private static function apply_sanitization($value, $rule) {
        switch ($rule) {
            case 'int':
                return intval($value);
            case 'float':
                return floatval($value);
            case 'email':
                return sanitize_email($value);
            case 'url':
                return esc_url_raw($value);
            case 'textarea':
                return sanitize_textarea_field($value);
            case 'html':
                return wp_kses_post($value);
            case 'key':
                return sanitize_key($value);
            case 'filename':
                return sanitize_file_name($value);
            case 'bool':
                return (bool) $value;
            case 'json':
                $decoded = json_decode($value, true);
                return $decoded !== null ? $value : '';
            default:
                return sanitize_text_field($value);
        }
    }

    /**
     * Check if current user has required capability
     * 
     * @param string $capability Required capability
     * @return bool
     */
    public static function current_user_can($capability = 'edit_posts') {
        if (!is_user_logged_in()) {
            return false;
        }
        return current_user_can($capability);
    }

    /**
     * Verify user capability for AJAX requests
     * 
     * @param string $capability Required capability
     * @return void Dies with JSON error if unauthorized
     */
    public static function verify_capability($capability = 'edit_posts') {
        if (!self::current_user_can($capability)) {
            wp_send_json_error(array(
                'message' => __('You do not have permission to perform this action.', 'freelance-studio')
            ));
            exit;
        }
    }

    /**
     * Log security event
     * 
     * @param string $event Event type
     * @param string $message Event message
     * @param array $context Additional context
     */
    public static function log_security_event($event, $message, $context = array()) {
        $log_entry = array(
            'event' => $event,
            'message' => $message,
            'ip' => self::get_client_ip(),
            'user_id' => get_current_user_id(),
            'timestamp' => current_time('mysql'),
            'context' => $context
        );
        
        // Get existing log
        $log = get_option('fs_security_log', array());
        
        // Keep only last 1000 entries
        if (count($log) >= 1000) {
            $log = array_slice($log, -999);
        }
        
        $log[] = $log_entry;
        update_option('fs_security_log', $log, false);
        
        // Also log to error_log for critical events
        if (in_array($event, array('rate_limit_exceeded', 'invalid_token', 'unauthorized_access'))) {
            error_log('FS Security [' . $event . ']: ' . $message . ' - IP: ' . self::get_client_ip());
        }
    }
}
