<?php

namespace PortalPeople\NgaugeBackupSql\components;

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

/**
 * BackupManager Component
 * 
 * Handles database table backup and restore operations with compression.
 * 
 * @property Module $module The module instance
 */
class BackupManager extends Component
{
    /**
     * @var \PortalPeople\NgaugeBackupSql\Module Module instance
     */
    public $module;
    
    /**
     * @var \yii\db\Connection Database connection
     */
    private $_db;

    /**
     * @inheritdoc
     */
    public function init()
    {
        parent::init();
        
        if ($this->module === null) {
            $this->module = Yii::$app->getModule('db-backup');
        }
        
        $this->_db = $this->module->getDb();
    }

    /**
     * Get list of all tables in the database
     * 
     * @param bool $excludeSystemTables Whether to exclude configured tables
     * @return array List of table names
     */
    public function listTables($excludeSystemTables = true)
    {
        $tables = $this->_db->schema->getTableNames();
        
        if ($excludeSystemTables && !empty($this->module->excludeTables)) {
            $tables = array_diff($tables, $this->module->excludeTables);
        }
        
        sort($tables);
        return array_values($tables);
    }

    /**
     * Create a backup of a specific table
     * 
     * @param string $tableName Name of the table to backup
     * @param string|null $filename Custom filename (optional)
     * @param bool|null $compress Whether to compress (null = use module default)
     * @return string Path to the created backup file
     * @throws Exception If backup fails
     */
    public function createBackup($tableName, $filename = null, $compress = null)
    {
        // Validate table exists
        if (!in_array($tableName, $this->_db->schema->getTableNames())) {
            throw new Exception("Table '{$tableName}' does not exist.");
        }
        
        // Determine compression setting
        if ($compress === null) {
            $compress = $this->module->compressBackups;
        }
        
        // Generate filename if not provided
        if ($filename === null) {
            $timestamp = date('Ymd_Hi');
            $extension = $compress ? '.sql.gz' : '.sql';
            $filename = "{$tableName}_{$timestamp}{$extension}";
        }
        
        // Ensure filename has correct extension
        if ($compress && !preg_match('/\.(gz|zip)$/i', $filename)) {
            $filename .= '.gz';
        }
        
        // Build full path
        $backupPath = $this->module->getBackupPath();
        $fullPath = $backupPath . DIRECTORY_SEPARATOR . $filename;
        
        // Create temporary SQL file
        $tempSqlFile = $backupPath . DIRECTORY_SEPARATOR . $tableName . '_temp.sql';
        
        try {
            // Execute mysqldump
            $this->executeMysqldump($tableName, $tempSqlFile);
            
            // Compress if needed
            if ($compress) {
                if (preg_match('/\.zip$/i', $filename)) {
                    $this->compressToZip($tempSqlFile, $fullPath);
                } else {
                    $this->compressToGzip($tempSqlFile, $fullPath);
                }
                
                // Remove temporary SQL file
                @unlink($tempSqlFile);
            } else {
                // Just rename temp file
                rename($tempSqlFile, $fullPath);
            }
            
            // Encrypt if enabled
            if ($this->module->encryptionEnabled) {
                $encryptionManager = Yii::createObject([
                    'class' => 'PortalPeople\NgaugeBackupSql\components\EncryptionManager',
                    'module' => $this->module,
                ]);
                
                $encryptedPath = $fullPath . '.enc';
                $encryptionManager->encryptFile($fullPath, $encryptedPath);
                
                // Remove unencrypted file
                @unlink($fullPath);
                
                // Use encrypted file
                $fullPath = $encryptedPath;
                $filename .= '.enc';
            }
            
            // Set secure file permissions
            chmod($fullPath, 0600);
            
            // Log to history
            $this->logBackupHistory($tableName, $filename, filesize($fullPath), 'success');
            
            Yii::info("Backup created: {$filename}", __METHOD__);
            
            return $fullPath;
            
        } catch (\Exception $e) {
            // Cleanup temporary file
            @unlink($tempSqlFile);
            
            // Log failure
            $this->logBackupHistory($tableName, $filename, 0, 'failed', $e->getMessage());
            
            throw new Exception("Backup failed: " . $e->getMessage());
        }
    }

    /**
     * List available restore points for a table
     * 
     * @param string|null $tablePrefix Filter by table name prefix (null = all backups)
     * @return array List of backup files with metadata
     */
    public function listRestorePoints($tablePrefix = null)
    {
        $backupPath = $this->module->getBackupPath();
        
        if (!is_dir($backupPath)) {
            return [];
        }
        
        $pattern = $tablePrefix 
            ? $backupPath . DIRECTORY_SEPARATOR . $tablePrefix . '_*'
            : $backupPath . DIRECTORY_SEPARATOR . '*';
        
        $files = glob($pattern . '.{sql,sql.gz,zip,enc}', GLOB_BRACE);
        
        if (empty($files)) {
            return [];
        }
        
        $restorePoints = [];
        
        // Create encryption manager if needed
        $encryptionManager = null;
        if ($this->module->encryptionEnabled) {
            $encryptionManager = Yii::createObject([
                'class' => 'PortalPeople\NgaugeBackupSql\components\EncryptionManager',
                'module' => $this->module,
            ]);
        }
        
        foreach ($files as $file) {
            $filename = basename($file);
            
            $metadata = null;
            if ($encryptionManager && $encryptionManager->isEncrypted($file)) {
                $metadata = $encryptionManager->getMetadata($file);
            }
            
            $restorePoints[] = [
                'filename' => $filename,
                'filepath' => $file,
                'size' => filesize($file),
                'size_formatted' => $this->formatBytes(filesize($file)),
                'created' => filemtime($file),
                'created_formatted' => date('Y-m-d H:i:s', filemtime($file)),
                'table' => $this->extractTableName($filename),
                'encrypted' => $metadata !== null,
                'encryption_metadata' => $metadata,
            ];
        }
        
        // Sort by creation time (newest first)
        usort($restorePoints, function($a, $b) {
            return $b['created'] - $a['created'];
        });
        
        return $restorePoints;
    }

    /**
     * Restore database from a backup file
     * 
     * @param string $filename Backup filename (basename only)
     * @return bool Success status
     * @throws Exception If restore fails
     */
    public function restore($filename)
    {
        $backupPath = $this->module->getBackupPath();
        $fullPath = $backupPath . DIRECTORY_SEPARATOR . $filename;
        
        if (!file_exists($fullPath)) {
            throw new Exception("Backup file not found: {$filename}");
        }
        
        $tableName = $this->extractTableName($filename);
        $tempSqlFile = $backupPath . DIRECTORY_SEPARATOR . 'restore_temp.sql';
        $workingFile = $fullPath;
        
        try {
            // Decrypt if encrypted
            if (preg_match('/\.enc$/i', $filename)) {
                $encryptionManager = Yii::createObject([
                    'class' => 'PortalPeople\NgaugeBackupSql\components\EncryptionManager',
                    'module' => $this->module,
                ]);
                
                $decryptedPath = $backupPath . DIRECTORY_SEPARATOR . 'restore_decrypted.tmp';
                $encryptionManager->decryptFile($fullPath, $decryptedPath);
                
                $workingFile = $decryptedPath;
                $filename = str_replace('.enc', '', $filename);
            }
            
            // Decompress if needed
            if (preg_match('/\.gz$/i', $filename)) {
                $this->decompressGzip($workingFile, $tempSqlFile);
                if ($workingFile !== $fullPath) {
                    @unlink($workingFile); // Remove decrypted temp file
                }
            } elseif (preg_match('/\.zip$/i', $filename)) {
                $this->decompressZip($workingFile, $tempSqlFile);
                if ($workingFile !== $fullPath) {
                    @unlink($workingFile); // Remove decrypted temp file
                }
            } else {
                $tempSqlFile = $workingFile;
            }
            
            // Execute mysql restore
            $this->executeMysqlRestore($tempSqlFile);
            
            // Cleanup temp files
            if ($tempSqlFile !== $fullPath && $tempSqlFile !== $workingFile) {
                @unlink($tempSqlFile);
            }
            if ($workingFile !== $fullPath) {
                @unlink($workingFile);
            }
            
            // Log to history
            $this->logBackupHistory($tableName, $filename, 0, 'restored');
            
            Yii::info("Backup restored: {$filename}", __METHOD__);
            
            return true;
            
        } catch (\Exception $e) {
            // Cleanup temp file
            if ($tempSqlFile !== $fullPath && file_exists($tempSqlFile)) {
                @unlink($tempSqlFile);
            }
            
            // Log failure
            $this->logBackupHistory($tableName, $filename, 0, 'restore_failed', $e->getMessage());
            
            throw new Exception("Restore failed: " . $e->getMessage());
        }
    }

    /**
     * Delete old backups based on retention policy
     * 
     * @param int|null $days Override retention days (null = use module setting)
     * @return int Number of files deleted
     */
    public function cleanupOldBackups($days = null)
    {
        if ($days === null) {
            $days = $this->module->retentionDays;
        }
        
        if ($days <= 0) {
            return 0; // Retention disabled
        }
        
        $backupPath = $this->module->getBackupPath();
        $cutoffTime = time() - ($days * 86400);
        $deleted = 0;
        
        $files = glob($backupPath . DIRECTORY_SEPARATOR . '*.{sql,sql.gz,zip}', GLOB_BRACE);
        
        foreach ($files as $file) {
            if (filemtime($file) < $cutoffTime) {
                if (@unlink($file)) {
                    $deleted++;
                    Yii::info("Deleted old backup: " . basename($file), __METHOD__);
                }
            }
        }
        
        return $deleted;
    }

    /**
     * Execute mysqldump command
     * 
     * @param string $tableName Table to dump
     * @param string $outputFile Output SQL file path
     * @throws Exception If mysqldump fails
     */
    protected function executeMysqldump($tableName, $outputFile)
    {
        // Parse DSN for connection details
        $dsn = $this->_db->dsn;
        preg_match('/host=([^;]+)/', $dsn, $hostMatch);
        preg_match('/dbname=([^;]+)/', $dsn, $dbnameMatch);
        preg_match('/port=([^;]+)/', $dsn, $portMatch);
        
        $host = $hostMatch[1] ?? 'localhost';
        $dbname = $dbnameMatch[1] ?? '';
        $port = $portMatch[1] ?? '3306';
        $username = $this->_db->username;
        $password = $this->_db->password;
        
        // Determine backup dump tool path (prefer mariadb-dump, fallback to mysqldump)
        $mysqldump = $this->module->mysqldumpPath;
        if (!$mysqldump) {
            // Try mariadb-dump first (MariaDB), then mysqldump (MySQL)
            $mysqldump = shell_exec('which mariadb-dump 2>/dev/null') ? 'mariadb-dump' : 'mysqldump';
            $mysqldump = trim($mysqldump) ?: 'mysqldump';
        }
        
        // Build command (secure - using config file for password)
        $configFile = tempnam(sys_get_temp_dir(), 'mysql_');
        file_put_contents($configFile, "[client]\npassword=\"{$password}\"\n");
        chmod($configFile, 0600);
        
        // Create temp file for stderr to capture errors separately from SQL output
        $stderrFile = tempnam(sys_get_temp_dir(), 'mysqldump_err_');
        
        $command = sprintf(
            '%s --defaults-extra-file=%s --host=%s --port=%s --user=%s --single-transaction --skip-lock-tables %s %s > %s 2>%s',
            escapeshellcmd($mysqldump),
            escapeshellarg($configFile),
            escapeshellarg($host),
            escapeshellarg($port),
            escapeshellarg($username),
            escapeshellarg($dbname),
            escapeshellarg($tableName),
            escapeshellarg($outputFile),
            escapeshellarg($stderrFile)
        );
        
        exec($command, $output, $returnCode);
        
        // Read stderr output
        $stderrOutput = file_exists($stderrFile) ? file_get_contents($stderrFile) : '';
        
        // Remove temp files
        @unlink($configFile);
        @unlink($stderrFile);
        
        if ($returnCode !== 0) {
            throw new Exception("Backup failed: " . ($stderrOutput ?: 'Unknown error'));
        }
        
        if (!file_exists($outputFile) || filesize($outputFile) === 0) {
            throw new Exception("mysqldump produced no output");
        }
    }

    /**
     * Execute mysql/mariadb restore command
     * 
     * @param string $sqlFile SQL file to restore
     * @throws Exception If restore fails
     */
    protected function executeMysqlRestore($sqlFile)
    {
        // Parse DSN for connection details
        $dsn = $this->_db->dsn;
        preg_match('/host=([^;]+)/', $dsn, $hostMatch);
        preg_match('/dbname=([^;]+)/', $dsn, $dbnameMatch);
        preg_match('/port=([^;]+)/', $dsn, $portMatch);
        
        $host = $hostMatch[1] ?? 'localhost';
        $dbname = $dbnameMatch[1] ?? '';
        $port = $portMatch[1] ?? '3306';
        $username = $this->_db->username;
        $password = $this->_db->password;
        
        // Determine restore tool path (prefer mariadb, fallback to mysql)
        $mysql = $this->module->mysqlPath;
        if (!$mysql) {
            // Try mariadb first (MariaDB), then mysql (MySQL)
            $mysql = shell_exec('which mariadb 2>/dev/null') ? 'mariadb' : 'mysql';
            $mysql = trim($mysql) ?: 'mysql';
        }
        
        // Build command (secure - using config file for password)
        $configFile = tempnam(sys_get_temp_dir(), 'mysql_');
        file_put_contents($configFile, "[client]\npassword=\"{$password}\"\n");
        chmod($configFile, 0600);
        
        // Create temp file for stderr to capture errors separately
        $stderrFile = tempnam(sys_get_temp_dir(), 'mysql_err_');
        
        $command = sprintf(
            '%s --defaults-extra-file=%s --host=%s --port=%s --user=%s %s < %s 2>%s',
            escapeshellcmd($mysql),
            escapeshellarg($configFile),
            escapeshellarg($host),
            escapeshellarg($port),
            escapeshellarg($username),
            escapeshellarg($dbname),
            escapeshellarg($sqlFile),
            escapeshellarg($stderrFile)
        );
        
        exec($command, $output, $returnCode);
        
        // Read stderr output
        $stderrOutput = file_exists($stderrFile) ? file_get_contents($stderrFile) : '';
        
        // Remove temp files
        @unlink($configFile);
        @unlink($stderrFile);
        
        if ($returnCode !== 0) {
            throw new Exception("mysql restore failed: " . $stderrOutput);
        }
    }

    /**
     * Compress SQL file to gzip
     * 
     * @param string $sqlFile Source SQL file
     * @param string $gzFile Target GZ file
     */
    protected function compressToGzip($sqlFile, $gzFile)
    {
        $gz = gzopen($gzFile, 'w9'); // Maximum compression
        $fp = fopen($sqlFile, 'r');
        
        if (!$gz || !$fp) {
            throw new Exception("Failed to open files for compression");
        }
        
        while (!feof($fp)) {
            gzwrite($gz, fread($fp, 1024 * 512));
        }
        
        fclose($fp);
        gzclose($gz);
    }

    /**
     * Compress SQL file to zip
     * 
     * @param string $sqlFile Source SQL file
     * @param string $zipFile Target ZIP file
     */
    protected function compressToZip($sqlFile, $zipFile)
    {
        $zip = new \ZipArchive();
        
        if ($zip->open($zipFile, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
            throw new Exception("Failed to create ZIP archive");
        }
        
        $zip->addFile($sqlFile, basename($sqlFile));
        $zip->close();
    }

    /**
     * Decompress gzip file
     * 
     * @param string $gzFile Source GZ file
     * @param string $sqlFile Target SQL file
     */
    protected function decompressGzip($gzFile, $sqlFile)
    {
        $gz = gzopen($gzFile, 'r');
        $fp = fopen($sqlFile, 'w');
        
        if (!$gz || !$fp) {
            throw new Exception("Failed to open files for decompression");
        }
        
        while (!gzeof($gz)) {
            fwrite($fp, gzread($gz, 1024 * 512));
        }
        
        gzclose($gz);
        fclose($fp);
    }

    /**
     * Decompress zip file
     * 
     * @param string $zipFile Source ZIP file
     * @param string $sqlFile Target SQL file
     */
    protected function decompressZip($zipFile, $sqlFile)
    {
        $zip = new \ZipArchive();
        
        if ($zip->open($zipFile) !== true) {
            throw new Exception("Failed to open ZIP archive");
        }
        
        // Extract first file (should be the SQL file)
        $zip->extractTo(dirname($sqlFile), $zip->getNameIndex(0));
        $zip->close();
        
        // Rename extracted file to target name
        $extractedFile = dirname($sqlFile) . DIRECTORY_SEPARATOR . $zip->getNameIndex(0);
        if ($extractedFile !== $sqlFile) {
            rename($extractedFile, $sqlFile);
        }
    }

    /**
     * Extract table name from backup filename
     * 
     * @param string $filename Backup filename
     * @return string Table name
     */
    protected function extractTableName($filename)
    {
        // Remove extensions
        $name = preg_replace('/\.(sql|gz|zip)$/i', '', $filename);
        $name = preg_replace('/\.sql$/i', '', $name);
        
        // Extract table name (everything before last underscore + timestamp pattern)
        if (preg_match('/^(.+?)_\d{8}_\d{4}$/', $name, $matches)) {
            return $matches[1];
        }
        
        // Fallback: return everything before first underscore
        $parts = explode('_', $name);
        return $parts[0];
    }

    /**
     * Format bytes to human-readable size
     * 
     * @param int $bytes File size in bytes
     * @return string Formatted size
     */
    protected function formatBytes($bytes)
    {
        $units = ['B', 'KB', 'MB', 'GB', 'TB'];
        $bytes = max($bytes, 0);
        $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
        $pow = min($pow, count($units) - 1);
        $bytes /= pow(1024, $pow);
        
        return round($bytes, 2) . ' ' . $units[$pow];
    }

    /**
     * Log backup operation to history
     * 
     * @param string $tableName Table name
     * @param string $filename Backup filename
     * @param int $fileSize File size in bytes
     * @param string $status Operation status
     * @param string|null $errorMessage Error message if any
     */
    protected function logBackupHistory($tableName, $filename, $fileSize, $status, $errorMessage = null)
    {
        try {
            $modelClass = 'PortalPeople\NgaugeBackupSql\models\BackupHistory';
            
            if (!class_exists($modelClass)) {
                return; // History tracking not available
            }
            
            $history = new $modelClass();
            $history->table_name = $tableName;
            $history->filename = $filename;
            $history->file_size = $fileSize;
            $history->status = $status;
            $history->error_message = $errorMessage;
            $history->created_at = time();
            $history->created_by = Yii::$app->has('user') && !Yii::$app->user->isGuest 
                ? Yii::$app->user->id 
                : null;
            $history->save(false);
            
        } catch (\Exception $e) {
            // Silently fail if history logging fails
            Yii::warning("Failed to log backup history: " . $e->getMessage(), __METHOD__);
        }
    }
}
