<?php
declare(strict_types=1);

/**
 * RC Developments - Client OTA Updater
 * Handles downloading, extracting, and database migrations for remote software updates.
 * Contact: info@rcdevelopments.co.za | +27 61 523 9126
 */
class OtaUpdater {
    private PDO $db;
    private string $appRoot;
    private string $tempZipPath;

    public function __construct(string $appRoot) {
        // Initialize an independent PDO connection using the global config constants
        $dsn = 'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8mb4';
        $this->db = new PDO($dsn, DB_USER, DB_PASS, [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ
        ]);
        
        $this->appRoot = rtrim($appRoot, '/');
        $this->tempZipPath = $this->appRoot . '/public/uploads/_temp_ota_update_' . time() . '.zip';
    }

    /**
     * Executes the full update lifecycle.
     */
    public function executeUpdate(string $downloadUrl, int $currentDbVersion = 0): array {
        try {
            // Step 1: Download the Payload
            $this->downloadZip($downloadUrl);

            // Step 2: Extract the Files (Overwriting old files)
            $this->extractZip();

            // Step 3: Run Database Migrations
            $newDbVersion = $this->runMigrations($currentDbVersion);

            // Step 4: Cleanup
            $this->cleanup();

            return [
                'success' => true,
                'message' => 'Update installed successfully.',
                'new_db_version' => $newDbVersion
            ];

        } catch (Exception $e) {
            // If anything fails, clean up the temp zip to save space
            $this->cleanup();
            
            return [
                'success' => false,
                'message' => 'Update Failed: ' . $e->getMessage()
            ];
        }
    }

    private function downloadZip(string $url): void {
        $fp = fopen($this->tempZipPath, 'w+');
        if ($fp === false) {
            throw new Exception("Cannot create temporary update file at {$this->tempZipPath}. Check folder permissions.");
        }

        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_FILE, $fp);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 300); // Allow 5 minutes for large updates
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 
        
        curl_exec($ch);
        $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error = curl_error($ch);
        curl_close($ch);
        fclose($fp);

        if ($statusCode !== 200 || $error) {
            throw new Exception("Failed to download update package. HTTP Code: {$statusCode}. Error: {$error}");
        }
    }

    private function extractZip(): void {
        $zip = new ZipArchive();
        if ($zip->open($this->tempZipPath) === true) {
            // Extract directly into the app root, overwriting existing files safely
            if (!$zip->extractTo($this->appRoot)) {
                $zip->close();
                throw new Exception("Failed to extract files. Check folder write permissions on your server.");
            }
            $zip->close();
        } else {
            throw new Exception("The downloaded update package is corrupted or not a valid ZIP archive.");
        }
    }

    private function runMigrations(int $currentDbVersion): int {
        $migrationsDir = $this->appRoot . '/migrations';
        $highestVersionRun = $currentDbVersion;

        // If no migrations folder exists in the update, skip this step
        if (!is_dir($migrationsDir)) {
            return $highestVersionRun;
        }

        // Find all .sql files and sort them alphabetically/numerically
        $migrationFiles = glob($migrationsDir . '/*.sql');
        if (!$migrationFiles) {
            return $highestVersionRun;
        }
        
        sort($migrationFiles);

        // Lock the database state
        $this->db->beginTransaction();

        try {
            foreach ($migrationFiles as $file) {
                // Expecting filename format: "001_create_users.sql"
                preg_match('/^(\d+)_/', basename($file), $matches);
                $fileVersion = isset($matches[1]) ? (int)$matches[1] : 0;

                // Only run scripts that are strictly newer than the client's current DB
                if ($fileVersion > $currentDbVersion || $currentDbVersion === 0) {
                    $sql = file_get_contents($file);
                    
                    // Execute the raw SQL schema changes
                    $this->db->exec($sql);
                    
                    $highestVersionRun = $fileVersion;
                }
            }

            // THE FIX: MySQL implicitly commits on DDL statements (like CREATE TABLE).
            // We MUST check if the transaction is still active before trying to commit it.
            if ($this->db->inTransaction()) {
                $this->db->commit();
            }

            // Delete the migration files from the server so they can't be triggered manually
            foreach ($migrationFiles as $file) {
                @unlink($file);
            }
            @rmdir($migrationsDir);

            return $highestVersionRun;

        } catch (Exception $e) {
            // THE FIX: Check if transaction is active before rolling back
            if ($this->db->inTransaction()) {
                $this->db->rollBack();
            }
            throw new Exception("Database migration failed on file version {$highestVersionRun}. DB Error: " . $e->getMessage());
        }
    }

    private function cleanup(): void {
        if (file_exists($this->tempZipPath)) {
            @unlink($this->tempZipPath);
        }
    }
}