<?php

namespace PortalPeople\NgaugeBackupSql\components;

use Yii;
use yii\base\Component;
use yii\base\Exception;

/**
 * EncryptionManager Component
 * 
 * Handles encryption and decryption of backup files using AES-256-GCM.
 * Implements system-specific encryption to ensure backups can only be
 * restored on authorized systems.
 * 
 * @property Module $module The module instance
 */
class EncryptionManager extends Component
{
    /**
     * @var string Magic header to identify encrypted files
     */
    const MAGIC_HEADER = 'NGBKP';
    
    /**
     * @var int Encryption version for future compatibility
     */
    const VERSION = 1;
    
    /**
     * @var string Encryption cipher method
     */
    const CIPHER_METHOD = 'aes-256-gcm';
    
    /**
     * @var int IV length for AES-256-GCM
     */
    const IV_LENGTH = 16;
    
    /**
     * @var int Authentication tag length
     */
    const TAG_LENGTH = 16;
    
    /**
     * @var \PortalPeople\NgaugeBackupSql\Module Module instance
     */
    public $module;
    
    /**
     * @inheritdoc
     */
    public function init()
    {
        parent::init();
        
        if ($this->module === null) {
            $this->module = Yii::$app->getModule('db-backup');
        }
    }
    
    /**
     * Get or generate encryption key
     * 
     * @return string Binary encryption key
     * @throws Exception If key cannot be generated
     */
    protected function getEncryptionKey()
    {
        // Use configured key if available
        if (!empty($this->module->encryptionKey)) {
            $key = $this->module->encryptionKey;
            
            // Handle base64 encoded keys
            if (strpos($key, 'base64:') === 0) {
                return base64_decode(substr($key, 7));
            }
            
            return $key;
        }
        
        // Generate system-specific key
        return $this->generateSystemKey();
    }
    
    /**
     * Generate system-specific encryption key
     * 
     * @return string Binary encryption key
     */
    protected function generateSystemKey()
    {
        // Combine multiple system identifiers for uniqueness
        $systemId = $this->getSystemId();
        $appSecret = Yii::$app->security->passwordHashStrategy ?? 'default-secret';
        
        // Derive key using PBKDF2
        return hash_pbkdf2('sha256', $systemId, $appSecret, 10000, 32, true);
    }
    
    /**
     * Get unique system identifier
     * 
     * @return string System identifier
     */
    public function getSystemId()
    {
        // Use configured system ID if available
        if (!empty($this->module->systemId)) {
            return $this->module->systemId;
        }
        
        // Otherwise, generate from system information
        $identifiers = [
            gethostname(),
            php_uname('n'),
            isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'unknown',
        ];
        
        return implode(':', $identifiers);
    }
    
    /**
     * Encrypt a file
     * 
     * @param string $inputFile Path to file to encrypt
     * @param string $outputFile Path to save encrypted file
     * @return bool Success status
     * @throws Exception If encryption fails
     */
    public function encryptFile($inputFile, $outputFile)
    {
        if (!file_exists($inputFile)) {
            throw new Exception("Input file does not exist: {$inputFile}");
        }
        
        // Read input file
        $data = file_get_contents($inputFile);
        if ($data === false) {
            throw new Exception("Failed to read input file: {$inputFile}");
        }
        
        // Encrypt data
        $encryptedData = $this->encrypt($data);
        
        // Write encrypted file
        if (file_put_contents($outputFile, $encryptedData) === false) {
            throw new Exception("Failed to write encrypted file: {$outputFile}");
        }
        
        return true;
    }
    
    /**
     * Decrypt a file
     * 
     * @param string $inputFile Path to encrypted file
     * @param string $outputFile Path to save decrypted file
     * @return bool Success status
     * @throws Exception If decryption fails
     */
    public function decryptFile($inputFile, $outputFile)
    {
        if (!file_exists($inputFile)) {
            throw new Exception("Encrypted file does not exist: {$inputFile}");
        }
        
        // Read encrypted file
        $encryptedData = file_get_contents($inputFile);
        if ($encryptedData === false) {
            throw new Exception("Failed to read encrypted file: {$inputFile}");
        }
        
        // Decrypt data
        $data = $this->decrypt($encryptedData);
        
        // Write decrypted file
        if (file_put_contents($outputFile, $data) === false) {
            throw new Exception("Failed to write decrypted file: {$outputFile}");
        }
        
        return true;
    }
    
    /**
     * Encrypt data
     * 
     * @param string $data Data to encrypt
     * @return string Encrypted data with header
     * @throws Exception If encryption fails
     */
    public function encrypt($data)
    {
        // Generate random IV
        $iv = random_bytes(self::IV_LENGTH);
        
        // Get encryption key
        $key = $this->getEncryptionKey();
        
        // Encrypt data with authentication tag
        $tag = '';
        $encryptedData = openssl_encrypt(
            $data,
            self::CIPHER_METHOD,
            $key,
            OPENSSL_RAW_DATA,
            $iv,
            $tag,
            '',
            self::TAG_LENGTH
        );
        
        if ($encryptedData === false) {
            throw new Exception("Encryption failed: " . openssl_error_string());
        }
        
        // Build encrypted package
        $systemId = $this->getSystemId();
        $systemIdLength = strlen($systemId);
        
        return pack('a5', self::MAGIC_HEADER) .     // Magic header (5 bytes)
               pack('C', self::VERSION) .            // Version (1 byte)
               pack('n', $systemIdLength) .          // System ID length (2 bytes)
               $systemId .                            // System ID (variable)
               $iv .                                  // IV (16 bytes)
               $tag .                                 // Auth tag (16 bytes)
               $encryptedData;                        // Encrypted data
    }
    
    /**
     * Decrypt data
     * 
     * @param string $encryptedPackage Encrypted data with header
     * @return string Decrypted data
     * @throws Exception If decryption fails or file is invalid
     */
    public function decrypt($encryptedPackage)
    {
        // Minimum package size
        $minSize = 5 + 1 + 2 + 1 + self::IV_LENGTH + self::TAG_LENGTH;
        if (strlen($encryptedPackage) < $minSize) {
            throw new Exception("Invalid encrypted file: too small");
        }
        
        $offset = 0;
        
        // Extract and verify magic header
        $magic = substr($encryptedPackage, $offset, 5);
        $offset += 5;
        if ($magic !== self::MAGIC_HEADER) {
            throw new Exception("Invalid encrypted file: bad magic header");
        }
        
        // Extract version
        $version = unpack('C', substr($encryptedPackage, $offset, 1))[1];
        $offset += 1;
        if ($version !== self::VERSION) {
            throw new Exception("Unsupported encryption version: {$version}");
        }
        
        // Extract system ID
        $systemIdLength = unpack('n', substr($encryptedPackage, $offset, 2))[1];
        $offset += 2;
        $fileSystemId = substr($encryptedPackage, $offset, $systemIdLength);
        $offset += $systemIdLength;
        
        // Verify system authorization
        $this->verifySystemAuthorization($fileSystemId);
        
        // Extract IV
        $iv = substr($encryptedPackage, $offset, self::IV_LENGTH);
        $offset += self::IV_LENGTH;
        
        // Extract authentication tag
        $tag = substr($encryptedPackage, $offset, self::TAG_LENGTH);
        $offset += self::TAG_LENGTH;
        
        // Extract encrypted data
        $encryptedData = substr($encryptedPackage, $offset);
        
        // Get encryption key
        $key = $this->getEncryptionKey();
        
        // Decrypt data
        $decryptedData = openssl_decrypt(
            $encryptedData,
            self::CIPHER_METHOD,
            $key,
            OPENSSL_RAW_DATA,
            $iv,
            $tag
        );
        
        if ($decryptedData === false) {
            throw new Exception("Decryption failed: Invalid key or corrupted data");
        }
        
        return $decryptedData;
    }
    
    /**
     * Verify if current system is authorized to decrypt
     * 
     * @param string $fileSystemId System ID that created the backup
     * @throws Exception If system is not authorized
     */
    protected function verifySystemAuthorization($fileSystemId)
    {
        $currentSystemId = $this->getSystemId();
        
        // Check if current system created the backup
        if ($fileSystemId === $currentSystemId) {
            return;
        }
        
        // Check authorized systems list
        $authorizedSystems = $this->module->authorizedSystems ?? [];
        
        if (empty($authorizedSystems)) {
            // If no authorized systems configured, only creator can decrypt
            throw new Exception("This backup can only be restored on the system that created it: {$fileSystemId}");
        }
        
        // Extract primary identifier from system ID
        $fileSystemParts = explode(':', $fileSystemId);
        $currentSystemParts = explode(':', $currentSystemId);
        
        // Check if any part matches authorized systems
        foreach ($fileSystemParts as $part) {
            if (in_array($part, $authorizedSystems)) {
                return;
            }
        }
        
        foreach ($currentSystemParts as $part) {
            if (in_array($part, $authorizedSystems)) {
                return;
            }
        }
        
        throw new Exception("This system is not authorized to restore this backup. Created on: {$fileSystemId}");
    }
    
    /**
     * Check if a file is encrypted
     * 
     * @param string $filePath Path to file
     * @return bool True if encrypted
     */
    public function isEncrypted($filePath)
    {
        if (!file_exists($filePath)) {
            return false;
        }
        
        $handle = fopen($filePath, 'rb');
        if (!$handle) {
            return false;
        }
        
        $magic = fread($handle, 5);
        fclose($handle);
        
        return $magic === self::MAGIC_HEADER;
    }
    
    /**
     * Get metadata from encrypted file
     * 
     * @param string $filePath Path to encrypted file
     * @return array|null Metadata or null if not encrypted
     */
    public function getMetadata($filePath)
    {
        if (!$this->isEncrypted($filePath)) {
            return null;
        }
        
        $handle = fopen($filePath, 'rb');
        if (!$handle) {
            return null;
        }
        
        fread($handle, 5); // Skip magic header
        $version = unpack('C', fread($handle, 1))[1];
        $systemIdLength = unpack('n', fread($handle, 2))[1];
        $systemId = fread($handle, $systemIdLength);
        
        fclose($handle);
        
        return [
            'version' => $version,
            'system_id' => $systemId,
            'encrypted' => true,
        ];
    }
}
