<?php
/**
 * Descomplicar® Crescimento Digital
 * https://descomplicar.pt
 * 
 * AES-256-GCM Encryption Helper for Desk-Moloni v3.0
 * 
 * Provides secure encryption/decryption for OAuth tokens and sensitive configuration
 * Uses industry-standard AES-256-GCM with authenticated encryption
 * 
 * @package DeskMoloni\Libraries
 * @author  Descomplicar.pt
 * @version 3.0.0
 */

namespace DeskMoloni;

use Exception;

class Encryption
{
    const CIPHER_METHOD = 'aes-256-gcm';
    const KEY_LENGTH = 32; // 256 bits
    const IV_LENGTH = 12;  // 96 bits (recommended for GCM)
    const TAG_LENGTH = 16; // 128 bits authentication tag
    
    private string $encryption_key;
    private string $key_version;
    
    /**
     * Initialize encryption with application key
     * 
     * @param string|null $app_key Application encryption key (auto-generated if null)
     * @param string $key_version Key version for rotation support
     * @throws Exception If OpenSSL extension not available
     */
    public function __construct(?string $app_key = null, string $key_version = '1')
    {
        if (!extension_loaded('openssl')) {
            throw new Exception('OpenSSL extension is required for encryption');
        }
        
        if (!in_array(self::CIPHER_METHOD, openssl_get_cipher_methods())) {
            throw new Exception('AES-256-GCM cipher method not available');
        }
        
        $this->key_version = $key_version;
        
        // Generate or use provided encryption key
        if ($app_key === null) {
            $this->encryption_key = $this->generateEncryptionKey();
        } else {
            $this->encryption_key = $this->deriveKey($app_key, $key_version);
        }
    }
    
    /**
     * Encrypt data using AES-256-GCM
     * 
     * @param string $plaintext Data to encrypt
     * @param string $additional_data Additional authenticated data (optional)
     * @return string Base64-encoded encrypted data with metadata
     * @throws Exception On encryption failure
     */
    public function encrypt(string $plaintext, string $additional_data = ''): string
    {
        try {
            // Generate random IV for each encryption
            $iv = random_bytes(self::IV_LENGTH);
            
            // Initialize authentication tag
            $tag = '';
            
            // Encrypt the data
            $ciphertext = openssl_encrypt(
                $plaintext,
                self::CIPHER_METHOD,
                $this->encryption_key,
                OPENSSL_RAW_DATA,
                $iv,
                $tag,
                $additional_data,
                self::TAG_LENGTH
            );
            
            if ($ciphertext === false) {
                throw new Exception('Encryption failed: ' . openssl_error_string());
            }
            
            // Combine IV, tag, and ciphertext for storage
            $encrypted_data = [
                'version' => $this->key_version,
                'iv' => base64_encode($iv),
                'tag' => base64_encode($tag),
                'data' => base64_encode($ciphertext),
                'aad' => base64_encode($additional_data)
            ];
            
            return base64_encode(json_encode($encrypted_data));
            
        } catch (Exception $e) {
            throw new Exception('Encryption error: ' . $e->getMessage());
        }
    }
    
    /**
     * Decrypt data using AES-256-GCM
     * 
     * @param string $encrypted_data Base64-encoded encrypted data with metadata
     * @return string Decrypted plaintext
     * @throws Exception On decryption failure or invalid data
     */
    public function decrypt(string $encrypted_data): string
    {
        try {
            // Decode the encrypted data structure
            $data = json_decode(base64_decode($encrypted_data), true);
            
            if (!$data || !$this->validateEncryptedDataStructure($data)) {
                throw new Exception('Invalid encrypted data structure');
            }
            
            // Extract components
            $iv = base64_decode($data['iv']);
            $tag = base64_decode($data['tag']);
            $ciphertext = base64_decode($data['data']);
            $additional_data = base64_decode($data['aad']);
            
            // Handle key version compatibility
            $decryption_key = $this->getKeyForVersion($data['version']);
            
            // Decrypt the data
            $plaintext = openssl_decrypt(
                $ciphertext,
                self::CIPHER_METHOD,
                $decryption_key,
                OPENSSL_RAW_DATA,
                $iv,
                $tag,
                $additional_data
            );
            
            if ($plaintext === false) {
                throw new Exception('Decryption failed: Invalid data or authentication failed');
            }
            
            return $plaintext;
            
        } catch (Exception $e) {
            throw new Exception('Decryption error: ' . $e->getMessage());
        }
    }
    
    /**
     * Encrypt OAuth token with expiration metadata
     * 
     * @param string $token OAuth token
     * @param int $expires_at Unix timestamp when token expires
     * @return string Encrypted token with metadata
     * @throws Exception On encryption failure
     */
    public function encryptToken(string $token, int $expires_at): string
    {
        $token_data = [
            'token' => $token,
            'expires_at' => $expires_at,
            'created_at' => time(),
            'type' => 'oauth_token'
        ];
        
        $additional_data = 'oauth_token_v' . $this->key_version;
        
        return $this->encrypt(json_encode($token_data), $additional_data);
    }
    
    /**
     * Decrypt OAuth token and validate expiration
     * 
     * @param string $encrypted_token Encrypted token data
     * @return array Token data with expiration info
     * @throws Exception If token invalid or expired
     */
    public function decryptToken(string $encrypted_token): array
    {
        $decrypted_data = $this->decrypt($encrypted_token);
        $token_data = json_decode($decrypted_data, true);
        
        if (!$token_data || $token_data['type'] !== 'oauth_token') {
            throw new Exception('Invalid token data structure');
        }
        
        // Check if token is expired (with 5-minute buffer)
        if ($token_data['expires_at'] <= (time() + 300)) {
            throw new Exception('Token has expired');
        }
        
        return $token_data;
    }
    
    /**
     * Generate secure encryption key
     * 
     * @return string Random 256-bit encryption key
     * @throws Exception If random generation fails
     */
    private function generateEncryptionKey(): string
    {
        try {
            return random_bytes(self::KEY_LENGTH);
        } catch (Exception $e) {
            throw new Exception('Failed to generate encryption key: ' . $e->getMessage());
        }
    }
    
    /**
     * Derive encryption key from application key and version
     * 
     * @param string $app_key Base application key
     * @param string $version Key version for rotation
     * @return string Derived encryption key
     */
    private function deriveKey(string $app_key, string $version): string
    {
        // Use PBKDF2 for key derivation with version-specific salt
        $salt = hash('sha256', 'desk_moloni_v3.0_' . $version, true);
        
        return hash_pbkdf2('sha256', $app_key, $salt, 10000, self::KEY_LENGTH, true);
    }
    
    /**
     * Get encryption key for specific version (supports key rotation)
     * 
     * @param string $version Key version
     * @return string Encryption key for version
     * @throws Exception If version not supported
     */
    private function getKeyForVersion(string $version): string
    {
        if ($version === $this->key_version) {
            return $this->encryption_key;
        }
        
        // Handle legacy versions if needed
        switch ($version) {
            case '1':
                // Default version, use current key
                return $this->encryption_key;
            default:
                throw new Exception("Unsupported key version: {$version}");
        }
    }
    
    /**
     * Validate encrypted data structure
     * 
     * @param array $data Decoded encrypted data
     * @return bool True if structure is valid
     */
    private function validateEncryptedDataStructure(array $data): bool
    {
        $required_fields = ['version', 'iv', 'tag', 'data', 'aad'];
        
        foreach ($required_fields as $field) {
            if (!isset($data[$field])) {
                return false;
            }
        }
        
        // Validate base64 encoding
        foreach (['iv', 'tag', 'data', 'aad'] as $field) {
            if (base64_decode($data[$field], true) === false) {
                return false;
            }
        }
        
        // Validate IV length
        if (strlen(base64_decode($data['iv'])) !== self::IV_LENGTH) {
            return false;
        }
        
        // Validate tag length
        if (strlen(base64_decode($data['tag'])) !== self::TAG_LENGTH) {
            return false;
        }
        
        return true;
    }
    
    /**
     * Securely generate encryption key for application
     * 
     * @return string Base64-encoded application key
     * @throws Exception If key generation fails
     */
    public static function generateApplicationKey(): string
    {
        try {
            $key = random_bytes(64); // 512-bit master key
            return base64_encode($key);
        } catch (Exception $e) {
            throw new Exception('Failed to generate application key: ' . $e->getMessage());
        }
    }
    
    /**
     * Test encryption system integrity
     * 
     * @return bool True if encryption system is working correctly
     */
    public function testIntegrity(): bool
    {
        try {
            $test_data = 'Desk-Moloni v3.0 Encryption Test - ' . microtime(true);
            $encrypted = $this->encrypt($test_data);
            $decrypted = $this->decrypt($encrypted);
            
            return $decrypted === $test_data;
        } catch (Exception $e) {
            return false;
        }
    }
    
    /**
     * Get encryption system information
     * 
     * @return array System information
     */
    public function getSystemInfo(): array
    {
        return [
            'cipher_method' => self::CIPHER_METHOD,
            'key_length' => self::KEY_LENGTH,
            'iv_length' => self::IV_LENGTH,
            'tag_length' => self::TAG_LENGTH,
            'key_version' => $this->key_version,
            'openssl_version' => OPENSSL_VERSION_TEXT,
            'available_methods' => openssl_get_cipher_methods(),
            'integrity_test' => $this->testIntegrity()
        ];
    }
}