<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\DB;
use ZipArchive;
use Illuminate\Support\Str;

class UpdateController extends Controller
{
    public function index()
    {
        return view('update.index');
    }

    public function run(Request $request)
    {
        $request->validate([
            'package' => 'nullable|file|mimes:zip',
        ]);

        $messages = [];

        // Create backups before applying update
        $backupDir = storage_path('app/backups');
        if (!file_exists($backupDir)) {
            @mkdir($backupDir, 0755, true);
        }

        // File system backup: zip the current application files (excluding storage/backups to avoid recursion)
        $timestamp = now()->format('Ymd_His');
        $fileBackupPath = $backupDir . DIRECTORY_SEPARATOR . 'app_backup_' . $timestamp . '.zip';
        $zip = new ZipArchive();
        if ($zip->open($fileBackupPath, ZipArchive::CREATE) === true) {
            $base = base_path();
            $files = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($base));
            foreach ($files as $file) {
                if (!$file->isFile()) continue;
                $filePath = $file->getRealPath();
                // Skip backup dir and vendor composer cache and node_modules to keep backup size reasonable
                if (Str::contains($filePath, DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'backups')) continue;
                if (Str::contains($filePath, DIRECTORY_SEPARATOR . 'node_modules')) continue;
                if (Str::contains($filePath, DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'composer')) continue;
                $localPath = substr($filePath, strlen($base) + 1);
                $zip->addFile($filePath, $localPath);
            }
            $zip->close();
            $messages[] = __('app.update_backup_created', ['path' => $fileBackupPath]);
        } else {
            $messages[] = __('app.update_backup_failed_files');
        }

        // Attempt a database dump backup if connection is MySQL and mysqldump available
        $dbDriver = config('database.default');
        if ($dbDriver === 'mysql') {
            $dbConfig = config('database.connections.mysql');
            $dbHost = $dbConfig['host'] ?? env('DB_HOST');
            $dbPort = $dbConfig['port'] ?? env('DB_PORT', '3306');
            $dbDatabase = $dbConfig['database'] ?? env('DB_DATABASE');
            $dbUsername = $dbConfig['username'] ?? env('DB_USERNAME');
            $dbPassword = $dbConfig['password'] ?? env('DB_PASSWORD');

            $dbDumpPath = $backupDir . DIRECTORY_SEPARATOR . 'db_backup_' . $timestamp . '.sql';
            // Build mysqldump command safely
            $mysqldump = 'mysqldump';
            $cmd = "$mysqldump --host={$dbHost} --port={$dbPort} --user={$dbUsername} --single-transaction --quick --lock-tables=false {$dbDatabase} > " . escapeshellarg($dbDumpPath);
            // Set MYSQL_PWD env var for password (avoid exposing on the commandline)
            $envCmd = '';
            if ($dbPassword !== null && $dbPassword !== '') {
                $envCmd = 'set MYSQL_PWD=' . escapeshellarg($dbPassword) . ' & ';
            }

            @exec($envCmd . $cmd, $output, $returnVar);
            if ($returnVar === 0 && file_exists($dbDumpPath)) {
                $messages[] = __('app.update_db_backup_created', ['path' => $dbDumpPath]);
            } else {
                $messages[] = __('app.update_db_backup_skipped');
            }
        }

        // If a package is uploaded, apply it with safe steps: extract to temp, validate, atomic swap, run migrations, rollback on failure
        if ($request->hasFile('package')) {
            $file = $request->file('package');
            $path = $file->getRealPath();
            $zip = new ZipArchive();
            if ($zip->open($path) === true) {
                // Extract into a temporary directory
                $tmpDir = storage_path('app/backups/update_tmp_' . $timestamp);
                if (!file_exists($tmpDir)) {
                    @mkdir($tmpDir, 0755, true);
                }

                $zip->extractTo($tmpDir);
                $zip->close();

                // Basic validation: ensure extracted contains at least one of expected top-level dirs (app, routes, resources or public)
                $expected = ['app', 'routes', 'resources', 'public', 'bootstrap'];
                $hasExpected = false;
                foreach ($expected as $dir) {
                    if (file_exists($tmpDir . DIRECTORY_SEPARATOR . $dir)) {
                        $hasExpected = true;
                        break;
                    }
                }

                if (!$hasExpected) {
                    $messages[] = __('app.update_extract_failed') . ' (' . __('app.update_validation_failed') . ')';
                    // clean tmp
                    @exec('rd /s /q ' . escapeshellarg($tmpDir));
                } else {
                    // Version check: detect version in package and compare with current
                    $uploadedVersion = $this->detectPackageVersion($tmpDir);
                    $currentVersion = $this->getCurrentVersion();
                    if (!$uploadedVersion) {
                        $messages[] = __('app.update_no_version_found');
                        @exec('rd /s /q ' . escapeshellarg($tmpDir));
                        // skip applying
                    } elseif ($currentVersion !== null && version_compare($uploadedVersion, $currentVersion, '<=')) {
                        $messages[] = __('app.update_version_too_old', ['uploaded' => $uploadedVersion, 'current' => $currentVersion]);
                        @exec('rd /s /q ' . escapeshellarg($tmpDir));
                    } else {
                        // proceed with swap/apply
                    // Perform atomic swap: move current app to .old and move new files into place
                    $base = base_path();
                    $oldDir = storage_path('app/backups/app_old_' . $timestamp);
                    // Move current files to oldDir (use robocopy on Windows for reliability)
                    // We'll copy, then on success remove originals to avoid data loss on failure
                    $copySuccess = false;
                    if (PHP_OS_FAMILY === 'Windows') {
                        // Use robocopy to copy excluding storage\app\backups to avoid recursion
                        $exclude = "storage\\app\\backups";
                        $robocopy = sprintf('robocopy %s %s /MIR /XD %s', escapeshellarg($base), escapeshellarg($oldDir), escapeshellarg($exclude));
                        @exec($robocopy, $out, $r);
                        // robocopy returns 0-8 as success codes; accept <=8
                        $copySuccess = ($r !== null && $r <= 8);
                    } else {
                        // Unix-like: use rsync
                        @mkdir($oldDir, 0755, true);
                        $rsync = sprintf('rsync -a --delete --exclude="storage/app/backups" %s/ %s/', escapeshellarg($base), escapeshellarg($oldDir));
                        @exec($rsync, $out, $r);
                        $copySuccess = ($r === 0);
                    }

                    if (!$copySuccess) {
                        $messages[] = __('app.update_backup_failed_files');
                        // cleanup tmp
                        @exec('rd /s /q ' . escapeshellarg($tmpDir));
                    } else {
                        // Now copy new files into place (rsync or robocopy from tmp to base)
                        if (PHP_OS_FAMILY === 'Windows') {
                            $robocopy2 = sprintf('robocopy %s %s /MIR', escapeshellarg($tmpDir), escapeshellarg($base));
                            @exec($robocopy2, $out2, $r2);
                            $swapOk = ($r2 !== null && $r2 <= 8);
                        } else {
                            $rsync2 = sprintf('rsync -a --delete %s/ %s/', escapeshellarg($tmpDir), escapeshellarg($base));
                            @exec($rsync2, $out2, $r2);
                            $swapOk = ($r2 === 0);
                        }

                        if (!$swapOk) {
                            $messages[] = __('app.update_extract_failed') . ' (' . __('app.update_swap_failed') . ')';
                            // attempt restore by copying oldDir back
                            if (PHP_OS_FAMILY === 'Windows') {
                                $robocopyRestore = sprintf('robocopy %s %s /MIR', escapeshellarg($oldDir), escapeshellarg($base));
                                @exec($robocopyRestore, $out3, $r3);
                            } else {
                                $rsyncRestore = sprintf('rsync -a --delete %s/ %s/', escapeshellarg($oldDir), escapeshellarg($base));
                                @exec($rsyncRestore, $out3, $r3);
                            }
                        } else {
                            $messages[] = __('app.update_extracted');
                            // Run migrations
                            try {
                                Artisan::call('migrate', ['--force' => true]);
                                $messages[] = __('app.update_migrations_success');
                                // If we detected an uploaded version, persist it
                                if (!empty($uploadedVersion)) {
                                    $this->setCurrentVersion($uploadedVersion);
                                    $messages[] = __('app.update_version_updated', ['version' => $uploadedVersion]);
                                }
                                // On success, cleanup oldDir and tmpDir
                                @exec('rd /s /q ' . escapeshellarg($oldDir));
                                @exec('rd /s /q ' . escapeshellarg($tmpDir));
                            } catch (\Exception $e) {
                                $messages[] = __('app.update_migrations_failed') . ': ' . $e->getMessage();
                                // Rollback: restore files from oldDir
                                if (PHP_OS_FAMILY === 'Windows') {
                                    $robocopyRestore = sprintf('robocopy %s %s /MIR', escapeshellarg($oldDir), escapeshellarg($base));
                                    @exec($robocopyRestore, $out3, $r3);
                                } else {
                                    $rsyncRestore = sprintf('rsync -a --delete %s/ %s/', escapeshellarg($oldDir), escapeshellarg($base));
                                    @exec($rsyncRestore, $out3, $r3);
                                }

                                // Attempt to restore DB dump if present
                                $dbDump = $backupDir . DIRECTORY_SEPARATOR . 'db_backup_' . $timestamp . '.sql';
                                if (file_exists($dbDump) && $dbDriver === 'mysql') {
                                    $dbConfig = config('database.connections.mysql');
                                    $dbHost = $dbConfig['host'] ?? env('DB_HOST');
                                    $dbPort = $dbConfig['port'] ?? env('DB_PORT', '3306');
                                    $dbDatabase = $dbConfig['database'] ?? env('DB_DATABASE');
                                    $dbUsername = $dbConfig['username'] ?? env('DB_USERNAME');
                                    $dbPassword = $dbConfig['password'] ?? env('DB_PASSWORD');

                                    $importCmd = '';
                                    if ($dbPassword !== null && $dbPassword !== '') {
                                        $importCmd = 'set MYSQL_PWD=' . escapeshellarg($dbPassword) . ' & ';
                                    }
                                    $importCmd .= sprintf('mysql --host=%s --port=%s --user=%s %s < %s', escapeshellarg($dbHost), escapeshellarg($dbPort), escapeshellarg($dbUsername), escapeshellarg($dbDatabase), escapeshellarg($dbDump));
                                    @exec($importCmd, $out4, $r4);
                                    if (isset($r4) && $r4 === 0) {
                                        $messages[] = __('app.update_db_restored');
                                    } else {
                                        $messages[] = __('app.update_db_restore_failed');
                                    }
                                }
                            }
                        }
                    }
                }
            } else {
                $messages[] = __('app.update_extract_failed');
            }
        }

        // Clear caches
        Artisan::call('cache:clear');
        Artisan::call('config:clear');
        Artisan::call('route:clear');
        Artisan::call('view:clear');
        $messages[] = __('app.update_cache_cleared');

        return view('update.result', ['messages' => $messages]);
    }

    /**
     * List available backups
     */
    public function backups()
    {
        $backupDir = storage_path('app/backups');
        $files = [];
        if (file_exists($backupDir)) {
            $it = new \DirectoryIterator($backupDir);
            foreach ($it as $fileinfo) {
                if ($fileinfo->isFile()) {
                    $files[] = [
                        'name' => $fileinfo->getFilename(),
                        'path' => $fileinfo->getRealPath(),
                        'size' => $fileinfo->getSize(),
                        'mtime' => date('Y-m-d H:i:s', $fileinfo->getMTime()),
                    ];
                }
            }
        }

        return view('update.backups', ['files' => $files]);
    }

    /**
     * Restore a selected backup (file backup zip and optional DB sql)
     */
    public function restoreBackup(Request $request)
    {
        $request->validate([
            'backup' => ['required', 'string'],
        ]);

        $backupDir = storage_path('app/backups');
        $backupFile = $backupDir . DIRECTORY_SEPARATOR . $request->input('backup');
        $messages = [];

        if (!file_exists($backupFile) || pathinfo($backupFile, PATHINFO_EXTENSION) !== 'zip') {
            return back()->withErrors(['backup' => __('app.invalid_backup_selected')]);
        }

        // Extract backup into tmp and swap
        $tmpDir = storage_path('app/backups/restore_tmp_' . time());
        @mkdir($tmpDir, 0755, true);
        $zip = new ZipArchive();
        if ($zip->open($backupFile) === true) {
            $zip->extractTo($tmpDir);
            $zip->close();
            // swap similar to update: copy current to old, copy tmp to base
            $base = base_path();
            $oldDir = storage_path('app/backups/restore_old_' . time());
            @mkdir($oldDir, 0755, true);
            // Use simple PHP copy for portability (may be slower)
            $this->recursiveCopy($base, $oldDir, ['storage/app/backups']);
            $this->recursiveCopy($tmpDir, $base, ['storage/app/backups']);
            $messages[] = __('app.update_restore_files_success');

            // If there's a matching db backup (.sql) with same timestamp prefix, try restore
            $sqlCandidate = preg_replace('/\.zip$/', '.sql', $backupFile);
            if (file_exists($sqlCandidate)) {
                $dbConfig = config('database.connections.mysql');
                $dbHost = $dbConfig['host'] ?? env('DB_HOST');
                $dbPort = $dbConfig['port'] ?? env('DB_PORT', '3306');
                $dbDatabase = $dbConfig['database'] ?? env('DB_DATABASE');
                $dbUsername = $dbConfig['username'] ?? env('DB_USERNAME');
                $dbPassword = $dbConfig['password'] ?? env('DB_PASSWORD');

                $importCmd = '';
                if ($dbPassword !== null && $dbPassword !== '') {
                    $importCmd = 'set MYSQL_PWD=' . escapeshellarg($dbPassword) . ' & ';
                }
                $importCmd .= sprintf('mysql --host=%s --port=%s --user=%s %s < %s', escapeshellarg($dbHost), escapeshellarg($dbPort), escapeshellarg($dbUsername), escapeshellarg($dbDatabase), escapeshellarg($sqlCandidate));
                @exec($importCmd, $out, $r);
                if (isset($r) && $r === 0) {
                    $messages[] = __('app.update_db_restored');
                } else {
                    $messages[] = __('app.update_db_restore_failed');
                }
            }

            // cleanup tmp
            @exec('rd /s /q ' . escapeshellarg($tmpDir));
        } else {
            return back()->withErrors(['backup' => __('app.update_extract_failed')]);
        }

        return view('update.result', ['messages' => $messages]);
    }

    // Simple PHP recursive copy helper
    protected function recursiveCopy($src, $dst, $exclude = [])
    {
        $dir = opendir($src);
        @mkdir($dst, 0755, true);
        while(false !== ($file = readdir($dir))) {
            if (($file !== '.') && ($file !== '..')) {
                $skip = false;
                foreach ($exclude as $ex) {
                    if (strpos($src . DIRECTORY_SEPARATOR . $file, base_path($ex)) === 0) {
                        $skip = true; break;
                    }
                }
                if ($skip) continue;
                if (is_dir($src . DIRECTORY_SEPARATOR . $file)) {
                    $this->recursiveCopy($src . DIRECTORY_SEPARATOR . $file, $dst . DIRECTORY_SEPARATOR . $file, $exclude);
                } else {
                    copy($src . DIRECTORY_SEPARATOR . $file, $dst . DIRECTORY_SEPARATOR . $file);
                }
            }
        }
        closedir($dir);
    }

    /**
     * Try to detect a version string inside the extracted package.
     * Look for a VERSION file at top-level or composer.json version entry.
     */
    protected function detectPackageVersion(string $tmpDir): ?string
    {
        // Check VERSION file
        $versionFile = $tmpDir . DIRECTORY_SEPARATOR . 'VERSION';
        if (file_exists($versionFile)) {
            $v = trim(file_get_contents($versionFile));
            return $v ?: null;
        }

        // Check composer.json
        $composer = $tmpDir . DIRECTORY_SEPARATOR . 'composer.json';
        if (file_exists($composer)) {
            $json = json_decode(file_get_contents($composer), true);
            if (!empty($json['version'])) return trim($json['version']);
        }

        return null;
    }

    protected function getCurrentVersion(): ?string
    {
        $vFile = base_path('VERSION');
        if (file_exists($vFile)) {
            $v = trim(file_get_contents($vFile));
            return $v ?: null;
        }
        return null;
    }

    protected function setCurrentVersion(string $version): void
    {
        @file_put_contents(base_path('VERSION'), $version . PHP_EOL);
    }
}
