<?php

namespace App\Apps\Versionmgr\Services;

use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use ZipArchive;

class BackupService
{
    protected string $backupBasePath;

    protected string $codeBasePath;

    public function __construct()
    {
        $this->backupBasePath = storage_path('app_versionmgr/backups');
        $this->codeBasePath = base_path();
    }

    public function createBackup(string $version, array $upgradeJson): ?string
    {
        $timestamp = date('YmdHis');
        $backupDir = $this->backupBasePath . "/backup_{$version}_{$timestamp}";

        if (!File::isDirectory($backupDir)) {
            File::makeDirectory($backupDir, 0755, true);
        }

        $files = $upgradeJson['files'] ?? [];
        $codeBackupResult = $this->backupCodeFiles($backupDir, $files);

        $dbBackupResult = $this->backupDatabase($backupDir);

        $manifest = [
            'version' => $version,
            'timestamp' => $timestamp,
            'code_backup' => $codeBackupResult,
            'db_backup' => $dbBackupResult,
            'files' => $files,
            'created_at' => date('Y-m-d H:i:s'),
        ];

        File::put($backupDir . '/backup_manifest.json', json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));

        $zipPath = $this->packageBackup($backupDir, $version, $timestamp);

        if ($zipPath) {
            File::deleteDirectory($backupDir);
        }

        $this->cleanupOldBackups();

        return $zipPath;
    }

    protected function backupCodeFiles(string $backupDir, array $files): bool
    {
        $modifyFiles = $files['modify'] ?? [];
        $deleteFiles = $files['delete'] ?? [];
        $allFiles = array_merge($modifyFiles, $deleteFiles);

        if (empty($allFiles)) {
            return true;
        }

        $codeBackupDir = $backupDir . '/code';
        $backedUp = 0;

        foreach ($allFiles as $relativePath) {
            $fullPath = $this->codeBasePath . '/' . $relativePath;

            if (File::exists($fullPath)) {
                $destDir = $codeBackupDir . '/' . dirname($relativePath);
                if (!File::isDirectory($destDir)) {
                    File::makeDirectory($destDir, 0755, true);
                }
                File::copy($fullPath, $codeBackupDir . '/' . $relativePath);
                $backedUp++;
            }
        }

        return $backedUp > 0 || empty($allFiles);
    }

    protected function backupDatabase(string $backupDir): bool
    {
        try {
            $database = config('database.connections.mysql.database');
            $username = config('database.connections.mysql.username');
            $password = config('database.connections.mysql.password');
            $host = config('database.connections.mysql.host', '127.0.0.1');
            $port = config('database.connections.mysql.port', 3306);

            $sqlFile = $backupDir . '/db_backup.sql';

            $command = sprintf(
                'mysqldump -h%s -P%s -u%s %s --single-transaction --quick --no-tablespaces > "%s" 2>&1',
                escapeshellarg($host),
                escapeshellarg($port),
                escapeshellarg($username),
                escapeshellarg($database),
                $sqlFile
            );

            if ($password) {
                $command = sprintf(
                    'mysqldump -h%s -P%s -u%s -p%s %s --single-transaction --quick --no-tablespaces > "%s" 2>&1',
                    escapeshellarg($host),
                    escapeshellarg($port),
                    escapeshellarg($username),
                    escapeshellarg($password),
                    escapeshellarg($database),
                    $sqlFile
                );
            }

            if (!function_exists('exec')) {
                Log::warning('exec 函数不可用，使用 Laravel 方式备份数据库');
                return $this->backupDatabaseWithLaravel($backupDir);
            }

            \exec($command, $output, $returnCode);

            if ($returnCode !== 0 || !File::exists($sqlFile) || File::size($sqlFile) === 0) {
                Log::warning('数据库备份失败，尝试使用Laravel方式备份', ['return_code' => $returnCode]);
                return $this->backupDatabaseWithLaravel($backupDir);
            }

            return true;
        } catch (\Throwable $e) {
            Log::error('数据库备份异常', ['error' => $e->getMessage()]);
            return $this->backupDatabaseWithLaravel($backupDir);
        }
    }

    protected function backupDatabaseWithLaravel(string $backupDir): bool
    {
        try {
            $sqlFile = $backupDir . '/db_backup.sql';
            $fp = fopen($sqlFile, 'w');
            if ($fp === false) {
                Log::error('无法打开SQL备份文件进行写入', ['path' => $sqlFile]);
                return false;
            }

            // 写入文件头
            fwrite($fp, "-- Database Backup\n");
            fwrite($fp, "-- Generated: " . date('Y-m-d H:i:s') . "\n");
            fwrite($fp, "SET FOREIGN_KEY_CHECKS=0;\n\n");

            $tables = DB::select('SHOW TABLES');
            $dbName = config('database.connections.mysql.database');
            $key = "Tables_in_{$dbName}";

            foreach ($tables as $row) {
                $tableName = $row->$key;

                // 写入表结构
                $createTable = DB::selectOne("SHOW CREATE TABLE `{$tableName}`");
                $createKey = "Create Table";
                if (isset($createTable->$createKey)) {
                    fwrite($fp, "DROP TABLE IF EXISTS `{$tableName}`;\n");
                    fwrite($fp, $createTable->$createKey . ";\n\n");
                }

                // 流式分批读取数据，每批 500 行
                $total = DB::table($tableName)->count();
                if ($total === 0) {
                    continue;
                }

                $batchSize = 500;
                $batches = (int) ceil($total / $batchSize);

                for ($i = 0; $i < $batches; $i++) {
                    $rows = DB::table($tableName)
                        ->offset($i * $batchSize)
                        ->limit($batchSize)
                        ->get();

                    foreach ($rows as $row) {
                        $values = array_map(function ($val) {
                            return is_null($val) ? 'NULL' : "'" . addslashes($val) . "'";
                        }, (array) $row);
                        fwrite($fp, "INSERT INTO `{$tableName}` VALUES (" . implode(', ', $values) . ");\n");
                    }

                    // 释放当前批次内存
                    unset($rows);
                }

                fwrite($fp, "\n");
            }

            fwrite($fp, "SET FOREIGN_KEY_CHECKS=1;\n");
            fclose($fp);

            return true;
        } catch (\Throwable $e) {
            Log::error('Laravel方式数据库备份失败', ['error' => $e->getMessage()]);
            if (isset($fp) && is_resource($fp)) {
                fclose($fp);
            }
            return false;
        }
    }

    protected function packageBackup(string $backupDir, string $version, string $timestamp): ?string
    {
        $zipPath = $this->backupBasePath . "/backup_{$version}_{$timestamp}.zip";

        $zip = new ZipArchive;
        if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
            Log::error('无法创建备份压缩包', ['path' => $zipPath]);
            return null;
        }

        $files = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($backupDir, \RecursiveDirectoryIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::LEAVES_ONLY
        );

        foreach ($files as $file) {
            if (!$file->isFile()) {
                continue;
            }
            $filePath = $file->getRealPath();
            $relativePath = substr($filePath, strlen(realpath($backupDir)) + 1);
            // 标准化路径分隔符：Windows 下 getRealPath 返回反斜杠，ZIP 规范要求正斜杠
            $relativePath = str_replace('\\', '/', $relativePath);
            $zip->addFile($filePath, $relativePath);
        }

        $zip->close();

        return $zipPath;
    }

    public function restoreFromBackup(string $backupZipPath, array $upgradeJson): bool
    {
        if (!File::exists($backupZipPath)) {
            Log::error('备份文件不存在', ['path' => $backupZipPath]);
            return false;
        }

        $tempDir = storage_path('app_versionmgr/temp/restore_' . time());
        File::makeDirectory($tempDir, 0755, true);

        try {
            $zip = new ZipArchive;
            if ($zip->open($backupZipPath) !== true) {
                return false;
            }
            $zip->extractTo($tempDir);
            $zip->close();

            $this->restoreCodeFiles($tempDir . '/code', $upgradeJson);

            return true;
        } catch (\Throwable $e) {
            Log::error('恢复备份失败', ['error' => $e->getMessage()]);
            return false;
        } finally {
            if (File::isDirectory($tempDir)) {
                File::deleteDirectory($tempDir);
            }
        }
    }

    protected function restoreCodeFiles(string $codeBackupDir, array $upgradeJson): void
    {
        if (!File::isDirectory($codeBackupDir)) {
            return;
        }

        $files = $upgradeJson['files'] ?? [];
        $modifyFiles = $files['modify'] ?? [];
        $deleteFiles = $files['delete'] ?? [];
        $addFiles = $files['add'] ?? [];

        $restoreFiles = array_merge($modifyFiles, $deleteFiles);
        foreach ($restoreFiles as $relativePath) {
            $backupFile = $codeBackupDir . '/' . $relativePath;
            $targetFile = $this->codeBasePath . '/' . $relativePath;

            if (File::exists($backupFile)) {
                $destDir = dirname($targetFile);
                if (!File::isDirectory($destDir)) {
                    File::makeDirectory($destDir, 0755, true);
                }
                File::copy($backupFile, $targetFile);
            }
        }

        foreach ($addFiles as $relativePath) {
            $targetFile = $this->codeBasePath . '/' . $relativePath;
            if (File::exists($targetFile)) {
                File::delete($targetFile);
            }
        }
    }

    public function restoreDatabase(string $backupZipPath): bool
    {
        $tempDir = storage_path('app_versionmgr/temp/db_restore_' . time());
        File::makeDirectory($tempDir, 0755, true);

        try {
            $zip = new ZipArchive;
            if ($zip->open($backupZipPath) !== true) {
                return false;
            }
            $zip->extractTo($tempDir);
            $zip->close();

            $sqlFile = $tempDir . '/db_backup.sql';
            if (!File::exists($sqlFile)) {
                return false;
            }

            $database = config('database.connections.mysql.database');
            $username = config('database.connections.mysql.username');
            $password = config('database.connections.mysql.password');
            $host = config('database.connections.mysql.host', '127.0.0.1');
            $port = config('database.connections.mysql.port', 3306);

            $command = sprintf(
                'mysql -h%s -P%s -u%s %s < "%s" 2>&1',
                escapeshellarg($host),
                escapeshellarg($port),
                escapeshellarg($username),
                escapeshellarg($database),
                $sqlFile
            );

            if ($password) {
                $command = sprintf(
                    'mysql -h%s -P%s -u%s -p%s %s < "%s" 2>&1',
                    escapeshellarg($host),
                    escapeshellarg($port),
                    escapeshellarg($username),
                    escapeshellarg($password),
                    escapeshellarg($database),
                    $sqlFile
                );
            }

            if (!function_exists('exec')) {
                Log::warning('exec 函数不可用，使用 Laravel 方式恢复数据库');
                $sql = File::get($sqlFile);
                DB::unprepared($sql);
                return true;
            }

            \exec($command, $output, $returnCode);

            if ($returnCode !== 0) {
                Log::warning('mysql命令恢复失败，尝试Laravel方式', ['return_code' => $returnCode]);
                $sql = File::get($sqlFile);
                DB::unprepared($sql);
            }

            return true;
        } catch (\Throwable $e) {
            Log::error('数据库恢复失败', ['error' => $e->getMessage()]);
            return false;
        } finally {
            if (File::isDirectory($tempDir)) {
                File::deleteDirectory($tempDir);
            }
        }
    }

    protected function cleanupOldBackups(): void
    {
        $backups = File::glob($this->backupBasePath . '/backup_*.zip');
        if (count($backups) <= 5) {
            return;
        }

        usort($backups, function ($a, $b) {
            return filemtime($b) - filemtime($a);
        });

        $toDelete = array_slice($backups, 5);
        foreach ($toDelete as $file) {
            File::delete($file);
        }
    }
}
