<?php

namespace App\Services;

use App\Http\Responses\ApiResponse;
use App\Models\AppBackup;
use App\Models\AppModel;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
use ZipArchive;
use App\Helpers\DatabaseBackupHelper;

class AppBackupService
{
    protected string $backupBasePath;

    public function __construct()
    {
        $this->backupBasePath = storage_path('app_backups');
    }

    /**
     * 获取应用的备份列表
     */
    public function getBackupList(string $appId): array
    {
        $backups = AppBackup::where('app_id', $appId)
            ->orderBy('id', 'desc')
            ->get();

        return ApiResponse::success($backups);
    }

    /**
     * 创建应用备份（数据表 + 程序包）
     * 内部拆分为 prepareBackup / backupTable / finishBackup 三步，
     * 便于前端 AJAX 分步调用来展示进度。
     */
    public function createBackup(string $appId, int $volumeSizeMB = 2): array
    {
        set_time_limit(0);

        // Step 1: 准备备份环境
        $prepResult = $this->prepareBackup($appId, $volumeSizeMB);
        if ($prepResult['code'] !== 0) {
            return $prepResult;
        }

        $sessionId = $prepResult['data']['session_id'];
        $tables = $prepResult['data']['tables'];

        // Step 2: 逐个备份数据表（每张表可能有多页）
        foreach ($tables as $table) {
            $lastId = null;
            do {
                $tableResult = $this->backupTable($appId, $sessionId, $table, $volumeSizeMB, $lastId);
                if ($tableResult['code'] !== 0) {
                    $backupDir = $this->backupBasePath . "/{$appId}/backup_{$sessionId}";
                    if (File::isDirectory($backupDir)) {
                        File::deleteDirectory($backupDir);
                    }
                    Log::error('应用备份失败', ['app_id' => $appId, 'table' => $table, 'error' => $tableResult['message'] ?? '']);
                    return ApiResponse::error(50020, '备份失败：' . ($tableResult['message'] ?? ''));
                }
                $lastId = $tableResult['data']['last_id'] ?? null;
            } while ($tableResult['data']['has_more'] ?? false);
        }

        // Step 3: 完成打包
        return $this->finishBackup($appId, $sessionId);
    }

    /**
     * 第一步：准备备份环境
     *
     * 验证应用是否存在、获取数据表列表、创建临时备份目录。
     * 返回会话 ID（session_id），前端后续步骤需携带此 ID。
     */
    public function prepareBackup(string $appId, int $volumeSizeMB = 2): array
    {
        $app = AppModel::where('app_id', $appId)->first();
        if (! $app) {
            return ApiResponse::error(50006, '应用未安装');
        }

        $appPath = $this->resolveAppPath($app);
        if (! File::isDirectory($appPath)) {
            return ApiResponse::error(50001, '应用目录不存在');
        }

        // 获取应用关联的数据表
        $dbTables = $this->getAppTables($appId, $appPath);
        if (empty($dbTables)) {
            return ApiResponse::error(50020, '未找到应用关联的数据表，无法备份');
        }

        $sessionId = date('YmdHis');
        $backupDir = $this->backupBasePath . "/{$appId}/backup_{$sessionId}";

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

        return ApiResponse::success([
            'session_id' => $sessionId,
            'tables' => $dbTables,
            'total_tables' => count($dbTables),
        ], '备份准备完成');
    }

    /**
     * 第二步：备份单个数据表（分页流式写入）
     *
     * 使用 DatabaseBackupHelper::streamTableDataPage 分页备份一张表的一页数据。
     * lastId=null 表示第一页（写入 DDL + 第一页数据），
     * lastId=上一页返回的 last_id 表示后续页（追加写入数据）。
     * 当 has_more=false 时自动将表记录到 .backed_up_tables.json。
     */
    public function backupTable(string $appId, string $sessionId, string $table, int $volumeSizeMB = 2, ?int $lastId = null): array
    {
        $backupDir = $this->backupBasePath . "/{$appId}/backup_{$sessionId}";

        if (! File::isDirectory($backupDir)) {
            return ApiResponse::error(50025, '备份会话已过期或目录不存在');
        }

        if (! Schema::hasTable($table)) {
            return ApiResponse::error(50026, "数据表 {$table} 不存在");
        }

        try {
            // 使用 helper 分页备份该表数据
            $result = DatabaseBackupHelper::streamTableDataPage($table, $backupDir, $volumeSizeMB, $lastId);

            // 该表所有页已完成 → 记录到跟踪文件
            if (! $result['has_more']) {
                $this->markTableBackedUp($appId, $sessionId, $table, $result['files']);
            }

            return ApiResponse::success([
                'table' => $table,
                'files' => $result['files'],
                'last_id' => $result['last_id'],
                'total_rows' => $result['total_rows'],
                'page' => $result['page'],
                'has_more' => $result['has_more'],
            ], $result['has_more']
                ? "数据表 {$table} 第 {$result['page']} 页备份完成，继续..."
                : "数据表 {$table} 备份完成");
        } catch (\Throwable $e) {
            Log::error('数据表备份失败', ['table' => $table, 'error' => $e->getMessage()]);
            return ApiResponse::error(50026, "数据表 {$table} 备份失败：" . $e->getMessage());
        }
    }

    /**
     * 标记某表已完整备份到跟踪文件 .backed_up_tables.json
     */
    protected function markTableBackedUp(string $appId, string $sessionId, string $table, array $files): void
    {
        $backupDir = $this->backupBasePath . "/{$appId}/backup_{$sessionId}";
        $trackFile = $backupDir . '/.backed_up_tables.json';
        $backedUp = [];
        if (File::exists($trackFile)) {
            $backedUp = json_decode(File::get($trackFile), true) ?: [];
        }
        $backedUp[] = [
            'table' => $table,
            'files' => $files,
            'backed_at' => date('Y-m-d H:i:s'),
        ];
        File::put($trackFile, json_encode($backedUp, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
    }

    /**
     * 第三步：完成备份打包
     *
     * 读取 .backed_up_tables.json 获取已备份表列表，扫描 SQL 文件，
     * 生成分卷清单和备份清单，备份程序文件，打包 ZIP，记录备份记录到数据库。
     */
    public function finishBackup(string $appId, string $sessionId): array
    {
        $backupDir = $this->backupBasePath . "/{$appId}/backup_{$sessionId}";

        if (! File::isDirectory($backupDir)) {
            return ApiResponse::error(50025, '备份会话已过期或目录不存在');
        }

        $app = AppModel::where('app_id', $appId)->first();
        if (! $app) {
            return ApiResponse::error(50006, '应用未安装');
        }

        $appPath = $this->resolveAppPath($app);

        // 读取已备份的表跟踪文件
        $trackFile = $backupDir . '/.backed_up_tables.json';
        $tables = [];
        $allSqlFiles = [];
        if (File::exists($trackFile)) {
            $backedUp = json_decode(File::get($trackFile), true) ?: [];
            foreach ($backedUp as $entry) {
                $tables[] = $entry['table'];
                $allSqlFiles = array_merge($allSqlFiles, $entry['files']);
            }
        }

        // 扫描所有 .sql 文件作为兜底（兼容无跟踪文件的场景）
        if (empty($allSqlFiles)) {
            $sqlFiles = File::glob($backupDir . '/*.sql');
            if (! empty($sqlFiles)) {
                $allSqlFiles = $sqlFiles;
            }
        }

        // 去重
        $allSqlFiles = array_values(array_unique($allSqlFiles));

        try {
            // 写入分卷清单
            $volumeSizeMB = 2;
            DatabaseBackupHelper::writeVolumeManifest($backupDir, $volumeSizeMB, $tables, $allSqlFiles);

            // 写入备份清单
            $manifest = [
                'app_id' => $appId,
                'app_name' => $app->name,
                'app_version' => $app->version,
                'timestamp' => $sessionId,
                'db_tables' => $tables,
                'created_at' => date('Y-m-d H:i:s'),
            ];
            File::put($backupDir . '/backup_manifest.json', json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));

            // 备份程序文件（应用目录不存在时仍可打包数据库备份）
            if (File::isDirectory($appPath)) {
                $codeResult = $this->backupCodeFiles($backupDir, $appPath);
                if (! $codeResult) {
                    Log::warning('程序文件备份失败，仅保留数据库备份', ['app_id' => $appId]);
                }
            } else {
                Log::warning('应用目录不存在，仅打包数据库备份', ['app_id' => $appId, 'app_path' => $appPath]);
            }

            // 打包为ZIP
            $zipPath = $this->packageBackup($backupDir, $appId, $sessionId);
            if (! $zipPath) {
                throw new \RuntimeException('备份打包失败');
            }

            // 清理临时目录
            File::deleteDirectory($backupDir);

            $fileSize = File::size($zipPath);

            // 记录到数据库
            $backupRecord = AppBackup::create([
                'app_id' => $appId,
                'app_name' => $app->name,
                'app_version' => $app->version,
                'backup_type' => 'full',
                'file_path' => $zipPath,
                'file_size' => $fileSize,
                'db_tables' => $tables,
                'status' => 1,
                'operator_id' => Auth::guard('admin')->id(),
            ]);

            // 清理旧备份，每个应用最多保留10份
            $this->cleanupOldBackups($appId);

            return ApiResponse::success([
                'backup_id' => $backupRecord->id,
                'file_size' => $fileSize,
            ], '备份完成');
        } catch (\Throwable $e) {
            // 清理临时目录
            if (File::isDirectory($backupDir)) {
                File::deleteDirectory($backupDir);
            }

            Log::error('应用备份完成失败', ['app_id' => $appId, 'error' => $e->getMessage()]);

            // 记录失败日志
            AppBackup::create([
                'app_id' => $appId,
                'app_name' => $app->name,
                'app_version' => $app->version,
                'backup_type' => 'full',
                'file_path' => '',
                'file_size' => 0,
                'db_tables' => $tables,
                'status' => 0,
                'operator_id' => Auth::guard('admin')->id(),
                'error_message' => $e->getMessage(),
            ]);

            return ApiResponse::error(50020, '备份完成失败：' . $e->getMessage());
        }
    }

    /**
     * 从备份恢复应用
     */
    public function restoreBackup(int $backupId): array
    {
        set_time_limit(0);

        $backup = AppBackup::find($backupId);
        if (! $backup) {
            return ApiResponse::error(50021, '备份记录不存在');
        }

        if ($backup->status !== 1) {
            return ApiResponse::error(50022, '该备份状态异常，无法恢复');
        }

        if (! File::exists($backup->file_path)) {
            return ApiResponse::error(50023, '备份文件不存在，可能已被清理');
        }

        $app = AppModel::where('app_id', $backup->app_id)->first();
        if (! $app) {
            return ApiResponse::error(50006, '应用未安装，无法恢复');
        }

        $tempDir = $this->backupBasePath . "/{$backup->app_id}/temp_restore_" . time();
        File::makeDirectory($tempDir, 0755, true);

        try {
            // 解压备份包
            $zip = new ZipArchive;
            if ($zip->open($backup->file_path) !== true) {
                throw new \RuntimeException('无法打开备份文件');
            }
            $zip->extractTo($tempDir);
            $zip->close();

            // 恢复数据库
            $sqlFile = $tempDir . '/db_backup.sql';
            if (File::exists($sqlFile)) {
                $dbResult = $this->restoreDatabase($sqlFile, $backup->db_tables ?? [], $backup->app_id);
                if (! $dbResult) {
                    throw new \RuntimeException('数据库恢复失败');
                }
            }

            // 恢复程序文件
            $codeDir = $tempDir . '/code';
            $appPath = $this->resolveAppPath($app);
            if (File::isDirectory($codeDir) && File::isDirectory($appPath)) {
                $this->restoreCodeFiles($codeDir, $appPath);
            }

            // 清理临时目录
            File::deleteDirectory($tempDir);

            return ApiResponse::success(null, '恢复成功');
        } catch (\Throwable $e) {
            if (File::isDirectory($tempDir)) {
                File::deleteDirectory($tempDir);
            }

            Log::error('应用备份恢复失败', ['backup_id' => $backupId, 'error' => $e->getMessage()]);

            return ApiResponse::error(50024, '恢复失败：' . $e->getMessage());
        }
    }

    /**
     * 删除备份记录及文件
     */
    public function deleteBackup(int $backupId): array
    {
        $backup = AppBackup::find($backupId);
        if (! $backup) {
            return ApiResponse::error(50021, '备份记录不存在');
        }

        if ($backup->file_path && File::exists($backup->file_path)) {
            File::delete($backup->file_path);
        }

        $backup->delete();

        return ApiResponse::success(null, '删除成功');
    }

    /**
     * 获取应用关联的数据表
     * 通过解析 Migrations 文件中的 Schema::create 获取表名
     */
    protected function getAppTables(string $appId, string $appPath): array
    {
        $tables = [];

        // 方式1：从 Migrations 目录解析
        $migrationPath = $appPath . '/Migrations';
        if (File::isDirectory($migrationPath)) {
            $files = File::glob($migrationPath . '/*.php');
            foreach ($files as $file) {
                $content = file_get_contents($file);
                if (preg_match_all("/Schema::create\(\s*'(\w+)'/", $content, $matches)) {
                    $tables = array_merge($tables, $matches[1]);
                }
            }
        }

        // 方式2：通过表前缀匹配
        $prefix = app_id_to_table_prefix($appId);
        if (! empty($prefix)) {
            $dbName = config('database.connections.mysql.database');
            $likePrefix = str_replace('_', '\\_', $prefix) . '%';
            $prefixedTables = DB::select(
                "SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME LIKE ?",
                [$dbName, $likePrefix]
            );
            foreach ($prefixedTables as $row) {
                $tableName = $row->TABLE_NAME;
                if (! in_array($tableName, $tables)) {
                    $tables[] = $tableName;
                }
            }
        }

        // 过滤：仅保留实际存在的表
        $tables = array_filter($tables, fn($table) => Schema::hasTable($table));

        return array_values(array_unique($tables));
    }

    /**
     * 备份应用程序文件
     */
    protected function backupCodeFiles(string $backupDir, string $appPath): bool
    {
        try {
            $codeDir = $backupDir . '/code';
            if (! File::isDirectory($codeDir)) {
                File::makeDirectory($codeDir, 0755, true);
            }

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

            $appPathReal = realpath($appPath);
            foreach ($files as $file) {
                if (! $file->isFile()) {
                    continue;
                }
                $filePath = $file->getRealPath();
                $relativePath = substr($filePath, strlen($appPathReal) + 1);
                $relativePath = str_replace('\\', '/', $relativePath);

                $destDir = $codeDir . '/' . dirname($relativePath);
                if (! File::isDirectory($destDir)) {
                    File::makeDirectory($destDir, 0755, true);
                }
                File::copy($filePath, $codeDir . '/' . $relativePath);
            }

            return true;
        } catch (\Throwable $e) {
            Log::error('程序文件备份失败', ['app_path' => $appPath, 'error' => $e->getMessage()]);

            return false;
        }
    }

    /**
     * 打包备份为ZIP
     */
    protected function packageBackup(string $backupDir, string $appId, string $timestamp): ?string
    {
        $appBackupDir = $this->backupBasePath . "/{$appId}";
        if (! File::isDirectory($appBackupDir)) {
            File::makeDirectory($appBackupDir, 0755, true);
        }

        $zipPath = $appBackupDir . "/backup_{$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);
            $relativePath = str_replace('\\', '/', $relativePath);
            $zip->addFile($filePath, $relativePath);
        }

        $zip->close();

        return $zipPath;
    }

    /**
     * 恢复数据库（使用流式分卷恢复）
     */
    protected function restoreDatabase(string $sqlFile, array $expectedTables, string $appId = ''): bool
    {
        try {
            $dirPath = dirname($sqlFile);

            // 获取分卷文件列表（自动兼容旧版单文件）
            $sqlFiles = DatabaseBackupHelper::resolveSqlFiles($dirPath);

            // 流式恢复：streamRestore 内部会根据表是否存在决定策略
            // - 表已存在：清空数据 + 只执行 INSERT（保留表结构）
            // - 表不存在：执行全部 DDL + DML（由 SQL 自行建表）
            DatabaseBackupHelper::streamRestore($sqlFiles, $expectedTables);

            // 恢复后验证：缺失的表尝试通过应用迁移重建
            if (! empty($appId)) {
                $this->ensureTablesExist($appId, $expectedTables);
            }

            // 最终验证
            foreach ($expectedTables as $tableName) {
                if (! Schema::hasTable($tableName)) {
                    Log::error('数据库恢复后验证失败：表不存在', ['table' => $tableName]);
                    return false;
                }
            }

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

    /**
     * 恢复程序文件
     */
    protected function restoreCodeFiles(string $codeDir, string $appPath): void
    {
        // 先清空现有应用目录
        File::deleteDirectory($appPath);
        File::makeDirectory($appPath, 0755, true);

        // 从备份目录复制文件
        $files = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($codeDir, \RecursiveDirectoryIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::LEAVES_ONLY
        );

        foreach ($files as $file) {
            if (! $file->isFile()) {
                continue;
            }
            $filePath = $file->getRealPath();
            $codeDirReal = realpath($codeDir);
            $relativePath = substr($filePath, strlen($codeDirReal) + 1);
            $relativePath = str_replace('\\', '/', $relativePath);

            $destDir = $appPath . '/' . dirname($relativePath);
            if (! File::isDirectory($destDir)) {
                File::makeDirectory($destDir, 0755, true);
            }
            File::copy($filePath, $appPath . '/' . $relativePath);
        }
    }

    /**
     * 清理旧备份，每个应用最多保留10份
     */
    protected function cleanupOldBackups(string $appId): void
    {
        $backups = AppBackup::where('app_id', $appId)
            ->where('status', 1)
            ->orderBy('id', 'desc')
            ->get();

        if ($backups->count() <= 10) {
            return;
        }

        $toDelete = $backups->slice(10);
        foreach ($toDelete as $backup) {
            if ($backup->file_path && File::exists($backup->file_path)) {
                File::delete($backup->file_path);
            }
            $backup->delete();
        }
    }

    /**
     * 获取备份文件路径（用于下载导出）
     */
    public function getBackupFilePath(int $backupId): ?string
    {
        $backup = AppBackup::find($backupId);
        if (! $backup || $backup->status !== 1) {
            return null;
        }

        if (! $backup->file_path || ! File::exists($backup->file_path)) {
            return null;
        }

        return $backup->file_path;
    }

    /**
     * 从上传的备份包恢复应用（跨站点导入）
     *
     * @param string $appId 目标应用ID
     * @param \Illuminate\Http\UploadedFile $file 上传的备份ZIP文件
     */
    public function importBackup(string $appId, \Illuminate\Http\UploadedFile $file): array
    {
        set_time_limit(0);

        // 验证目标应用存在
        $app = AppModel::where('app_id', $appId)->first();
        if (! $app) {
            return ApiResponse::error(50006, '目标应用未安装');
        }

        // 验证上传文件格式
        if (! $file->isValid() || strtolower($file->getClientOriginalExtension()) !== 'zip') {
            return ApiResponse::error(40001, '仅支持 .zip 格式的备份包');
        }

        $tempDir = $this->backupBasePath . "/{$appId}/temp_import_" . time();
        File::makeDirectory($tempDir, 0755, true);

        try {
            // 解压上传的备份包
            $zipPath = $file->path();
            $zip = new ZipArchive;
            if ($zip->open($zipPath) !== true) {
                throw new \RuntimeException('无法打开备份文件，可能已损坏或不是有效的ZIP文件');
            }
            $zip->extractTo($tempDir);
            $zip->close();

            // 验证备份包结构：必须包含 backup_manifest.json 或 db_backup.sql
            $hasManifest = File::exists($tempDir . '/backup_manifest.json');
            $hasDbSql = File::exists($tempDir . '/db_backup.sql');

            if (! $hasDbSql) {
                throw new \RuntimeException('备份包无效：缺少 db_backup.sql 文件');
            }

            // 读取备份清单获取表列表
            $importTables = [];
            if ($hasManifest) {
                $manifest = json_decode(File::get($tempDir . '/backup_manifest.json'), true);
                $importTables = $manifest['db_tables'] ?? [];
            }

            // 恢复数据库
            $sqlFile = $tempDir . '/db_backup.sql';
            $dbResult = $this->restoreDatabaseFromImport($sqlFile, $importTables, $appId);

            // 恢复程序文件
            $codeDir = $tempDir . '/code';
            $appPath = $this->resolveAppPath($app);
            if (File::isDirectory($codeDir)) {
                $this->restoreCodeFiles($codeDir, $appPath);
            }

            // 清理临时目录
            File::deleteDirectory($tempDir);

            // 记录导入日志
            AppBackup::create([
                'app_id' => $appId,
                'app_name' => $app->name,
                'app_version' => $app->version,
                'backup_type' => 'import',
                'file_path' => '',
                'file_size' => $file->getSize(),
                'db_tables' => $importTables,
                'status' => 1,
                'operator_id' => Auth::guard('admin')->id(),
            ]);

            return ApiResponse::success(null, '从备份包恢复成功');
        } catch (\Throwable $e) {
            if (File::isDirectory($tempDir)) {
                File::deleteDirectory($tempDir);
            }
            Log::error('导入备份失败', ['app_id' => $appId, 'error' => $e->getMessage()]);

            return ApiResponse::error(50024, '导入失败：' . $e->getMessage());
        }
    }

    /**
     * 分步恢复 - 第一步：准备
     *
     * 解压备份包、读取清单获取表列表，按表拆分SQL文件，
     * 返回 session_id 供后续步骤使用。
     */
    public function prepareRestore(string $appId, ?int $backupId = null, ?string $localFileName = null): array
    {
        set_time_limit(0);

        // 验证目标应用存在
        $app = AppModel::where('app_id', $appId)->first();
        if (! $app) {
            return ApiResponse::error(50006, '目标应用未安装');
        }

        // 确定备份文件路径
        $zipPath = null;
        if ($backupId) {
            $backup = AppBackup::find($backupId);
            if (! $backup || $backup->status !== 1) {
                return ApiResponse::error(50022, '备份记录不存在或状态异常');
            }
            if (! File::exists($backup->file_path)) {
                return ApiResponse::error(50023, '备份文件不存在');
            }
            $zipPath = $backup->file_path;
        } elseif ($localFileName) {
            if (str_contains($localFileName, '/') || str_contains($localFileName, '\\') || str_contains($localFileName, '..')) {
                return ApiResponse::error(40001, '文件名不合法');
            }
            $filePath = $this->backupBasePath . "/{$appId}/{$localFileName}";
            if (! File::exists($filePath)) {
                return ApiResponse::error(50023, '备份文件不存在');
            }
            if (strtolower(pathinfo($localFileName, PATHINFO_EXTENSION)) !== 'zip') {
                return ApiResponse::error(40001, '仅支持 .zip 格式的备份包');
            }
            $zipPath = $filePath;
        } else {
            return ApiResponse::error(40001, '未指定恢复来源');
        }

        // 解压到临时目录
        $sessionId = 'restore_' . time() . '_' . mt_rand(1000, 9999);
        $tempDir = $this->backupBasePath . "/{$appId}/temp_{$sessionId}";
        File::makeDirectory($tempDir, 0755, true);

        $zip = new ZipArchive;
        if ($zip->open($zipPath) !== true) {
            File::deleteDirectory($tempDir);
            return ApiResponse::error(50024, '无法打开备份文件，可能已损坏');
        }
        $zip->extractTo($tempDir);
        $zip->close();

        // 验证备份包结构
        if (! File::exists($tempDir . '/db_backup.sql')) {
            File::deleteDirectory($tempDir);
            return ApiResponse::error(50024, '备份包无效：缺少 db_backup.sql 文件');
        }

        // 读取清单获取表列表
        $tables = [];
        if (File::exists($tempDir . '/backup_manifest.json')) {
            $manifest = json_decode(File::get($tempDir . '/backup_manifest.json'), true);
            $tables = $manifest['db_tables'] ?? [];
        }
        if (empty($tables)) {
            $tables = $this->parseTablesFromSqlFiles($tempDir);
        }

        // 按表拆分SQL文件到独立目录，避免 restoreTableStep 重复扫描大文件
        $tableLineCounts = $this->splitSqlByTable($tempDir);

        // 调试日志：记录prepareRestore结果
        $sqlFiles = DatabaseBackupHelper::resolveSqlFiles($tempDir);
        $sqlFileSizes = [];
        foreach ($sqlFiles as $sf) {
            $sqlFileSizes[basename($sf)] = file_exists($sf) ? filesize($sf) : 0;
        }
        Log::info('prepareRestore 准备完成', [
            'app_id' => $appId,
            'session_id' => $sessionId,
            'manifest_tables' => $tables,
            'table_line_counts' => $tableLineCounts,
            'sql_files' => $sqlFileSizes,
            'has_code' => File::isDirectory($tempDir . '/code'),
        ]);

        // 保存恢复会话信息
        $sessionInfo = [
            'app_id' => $appId,
            'backup_id' => $backupId,
            'local_file_name' => $localFileName,
            'zip_path' => $zipPath,
            'temp_dir' => $tempDir,
            'tables' => $tables,
            'table_line_counts' => $tableLineCounts,
            'has_code' => File::isDirectory($tempDir . '/code'),
            'created_at' => date('Y-m-d H:i:s'),
        ];
        File::put($tempDir . '/.restore_session.json', json_encode($sessionInfo, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));

        return ApiResponse::success([
            'session_id' => $sessionId,
            'tables' => $tables,
            'total_tables' => count($tables),
            'has_code' => $sessionInfo['has_code'],
        ], '恢复准备完成');
    }

    /**
     * 按表拆分SQL文件
     *
     * 将 db_backup.sql 等大文件按表拆分为 tables/{table_name}.sql，
     * 每个 INSERT 语句单独一行，方便 restoreTableStep 按行读取执行。
     *
     * @return array 表名 => 行数 的映射
     */
    protected function splitSqlByTable(string $tempDir): array
    {
        $tablesDir = $tempDir . '/tables';
        if (! File::isDirectory($tablesDir)) {
            File::makeDirectory($tablesDir, 0755, true);
        }

        $sqlFiles = DatabaseBackupHelper::resolveSqlFiles($tempDir);
        $tableFiles = []; // [table_name => resource]

        foreach ($sqlFiles as $sqlFilePath) {
            if (! file_exists($sqlFilePath)) {
                continue;
            }

            $fh = fopen($sqlFilePath, 'r');
            if ($fh === false) {
                continue;
            }

            try {
                $buffer = '';
                $inSingleQuote = false;
                $inDoubleQuote = false;
                $prevChar = '';
                $currentTable = null;

                while (! feof($fh)) {
                    $chunk = fread($fh, 1048576);
                    if ($chunk === false || $chunk === '') {
                        break;
                    }

                    for ($i = 0; $i < strlen($chunk); $i++) {
                        $char = $chunk[$i];

                        // 处理转义
                        if ($prevChar === '\\' && ($char === "'" || $char === '"' || $char === '\\')) {
                            $buffer .= $char;
                            $prevChar = '';
                            continue;
                        }

                        // 跟踪引号状态
                        if ($char === "'" && ! $inDoubleQuote) {
                            $inSingleQuote = ! $inSingleQuote;
                        } elseif ($char === '"' && ! $inSingleQuote) {
                            $inDoubleQuote = ! $inDoubleQuote;
                        }

                        // 遇到语句结束符
                        if ($char === ';' && ! $inSingleQuote && ! $inDoubleQuote) {
                            $buffer .= $char;
                            $statement = trim($buffer);

                            if (! empty($statement)) {
                                $detectedTable = $this->detectTableFromStatement($statement);
                                if ($detectedTable) {
                                    $currentTable = $detectedTable;

                                    // 懒创建文件句柄
                                    if (! isset($tableFiles[$currentTable])) {
                                        $safeName = preg_replace('/[^a-zA-Z0-9_]/', '_', $currentTable);
                                        $tableFiles[$currentTable] = fopen($tablesDir . '/' . $safeName . '.sql', 'w');
                                    }

                                    // 直接写入完整语句，streamParseSql 会正确解析多行语句
                                    fwrite($tableFiles[$currentTable], $statement . "\n");
                                } elseif ($currentTable) {
                                    // 属于当前表的语句（如 INSERT 续行），追加到该表文件
                                    if (isset($tableFiles[$currentTable])) {
                                        fwrite($tableFiles[$currentTable], $statement . "\n");
                                    }
                                }
                            }

                            $buffer = '';
                            $prevChar = '';
                            continue;
                        }

                        $buffer .= $char;
                        $prevChar = $char;
                    }
                }
            } finally {
                fclose($fh);
            }
        }

        // 关闭所有文件句柄，统计每个表的语句数
        // 用 streamParseSql 统计语句数（而非 fgets 行数），因为语句可能跨多行
        $tableLineCounts = [];
        foreach ($tableFiles as $tableName => $fh) {
            if (is_resource($fh)) {
                fclose($fh);
            }
            $safeName = preg_replace('/[^a-zA-Z0-9_]/', '_', $tableName);
            $countFile = $tablesDir . '/' . $safeName . '.sql';
            $stmtCount = 0;
            $cfh = fopen($countFile, 'r');
            if ($cfh !== false) {
                foreach (DatabaseBackupHelper::streamParseSql($cfh) as $stmt) {
                    $stmtCount++;
                }
                fclose($cfh);
            }
            $tableLineCounts[$tableName] = $stmtCount;
        }

        // 调试日志：记录拆分结果
        Log::info('splitSqlByTable 拆分结果', [
            'tables' => $tableLineCounts,
            'total_statements' => array_sum($tableLineCounts),
        ]);

        return $tableLineCounts;
    }

    /**
     * 从SQL语句检测表名
     */
    protected function detectTableFromStatement(string $statement): ?string
    {
        // DROP TABLE `table_name` / DROP TABLE IF EXISTS `table_name`
        if (preg_match('/DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?`?(\w+)`?/i', $statement, $m)) {
            return $m[1];
        }
        // CREATE TABLE `table_name` / CREATE TABLE IF NOT EXISTS `table_name`
        if (preg_match('/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?`?(\w+)`?/i', $statement, $m)) {
            return $m[1];
        }
        // INSERT INTO `table_name`
        if (preg_match('/INSERT\s+INTO\s+`?(\w+)`?/i', $statement, $m)) {
            return $m[1];
        }
        return null;
    }

    /**
     * 分步恢复 - 第二步：恢复单个数据表
     *
     * 从拆分后的表级SQL文件中用 streamParseSql 流式解析完整SQL语句执行，
     * 每次最多执行 max_statements 条SQL（默认200），返回 offset 供下次续传。
     * 使用 streamParseSql 而非 fgets 逐行读取，确保多行SQL语句（如 CREATE TABLE）
     * 不会被拆分成碎片。
     */
    public function restoreTableStep(string $appId, string $sessionId, string $table, int $offset = 0, int $maxStatements = 200): array
    {
        set_time_limit(0);

        $tempDir = $this->backupBasePath . "/{$appId}/temp_{$sessionId}";
        if (! File::isDirectory($tempDir)) {
            return ApiResponse::error(50025, '恢复会话已过期或目录不存在');
        }

        // 从会话信息获取预计算的行数
        $sessionFile = $tempDir . '/.restore_session.json';
        $totalLines = 0;
        if (File::exists($sessionFile)) {
            $sessionInfo = json_decode(File::get($sessionFile), true);
            $totalLines = $sessionInfo['table_line_counts'][$table] ?? 0;
        }

        try {
            // 读取拆分后的表级SQL文件
            $safeName = preg_replace('/[^a-zA-Z0-9_]/', '_', $table);
            $tableSqlFile = $tempDir . '/tables/' . $safeName . '.sql';

            if (! File::exists($tableSqlFile)) {
                $this->markTableRestored($appId, $sessionId, $table);
                return ApiResponse::success([
                    'table' => $table,
                    'offset' => 0,
                    'has_more' => false,
                ], "数据表 {$table} 无数据，跳过");
            }

            // 首次恢复该表（offset=0）：先清空数据
            // 放在 SQL 文件存在性检查之后，避免清空了表却没有数据可恢复
            if ($offset === 0) {
                DB::statement('SET FOREIGN_KEY_CHECKS = 0');
                if (Schema::hasTable($table)) {
                    // 表已存在，清空数据保留表结构（只恢复数据）
                    DB::statement("TRUNCATE TABLE `{$table}`");
                }
                DB::statement('SET FOREIGN_KEY_CHECKS = 1');
            }

            // 判断目标表是否已存在（用于跳过 DDL 语句）
            $tableExists = Schema::hasTable($table);

            // 用 streamParseSql 流式解析完整SQL语句，避免 fgets 把多行语句拆碎
            $fh = fopen($tableSqlFile, 'r');
            if ($fh === false) {
                return ApiResponse::error(50024, "无法打开表 {$table} 的SQL文件");
            }

            $executed = 0;
            $skipped = 0;
            $currentOffset = $offset;
            $stmtIndex = 0;

            try {
                foreach (DatabaseBackupHelper::streamParseSql($fh) as $statement) {
                    // 跳过 offset 之前的语句
                    if ($stmtIndex < $offset) {
                        $stmtIndex++;
                        continue;
                    }

                    // 已达到批次上限，直接结束
                    if ($executed >= $maxStatements) {
                        break;
                    }

                    // 表已存在时，跳过 DROP TABLE 和 CREATE TABLE 语句
                    if ($tableExists && preg_match('/^\s*(DROP\s+TABLE|CREATE\s+TABLE)/i', $statement)) {
                        $stmtIndex++;
                        $skipped++;
                        continue;
                    }

                    try {
                        DB::unprepared($statement);
                    } catch (\Throwable $e) {
                        Log::warning('恢复SQL执行失败（已跳过）', [
                            'table' => $table,
                            'stmt_index' => $stmtIndex,
                            'error' => $e->getMessage(),
                            'sql' => mb_substr($statement, 0, 200),
                        ]);
                    }
                    $executed++;
                    $currentOffset = $stmtIndex + 1;
                    $stmtIndex++;
                }
            } finally {
                fclose($fh);
            }

            // 调试日志：记录每批次执行情况
            Log::info('restoreTableStep 执行批次', [
                'table' => $table,
                'offset' => $offset,
                'total_lines' => $totalLines,
                'executed' => $executed,
                'skipped_ddl' => $skipped,
                'current_offset' => $currentOffset,
                'has_more' => $currentOffset < $totalLines,
                'table_exists' => $tableExists,
                'sql_file' => $tableSqlFile,
                'sql_file_size' => file_exists($tableSqlFile) ? filesize($tableSqlFile) : 0,
            ]);

            $hasMore = $currentOffset < $totalLines;

            if (! $hasMore) {
                $this->markTableRestored($appId, $sessionId, $table);
            }

            return ApiResponse::success([
                'table' => $table,
                'offset' => $currentOffset,
                'total' => $totalLines,
                'executed' => $executed,
                'has_more' => $hasMore,
            ], $hasMore ? "数据表 {$table} 恢复中 ({$currentOffset}/{$totalLines})" : "数据表 {$table} 恢复完成");
        } catch (\Throwable $e) {
            Log::error('数据表恢复失败', ['table' => $table, 'error' => $e->getMessage()]);
            return ApiResponse::error(50026, "数据表 {$table} 恢复失败：" . $e->getMessage());
        }
    }

    /**
     * 分步恢复 - 第三步：完成恢复
     *
     * 恢复程序文件、清理临时目录、记录日志。
     *
     * @param string $appId 目标应用ID
     * @param string $sessionId 恢复会话ID
     */
    public function finishRestore(string $appId, string $sessionId): array
    {
        set_time_limit(0);

        $tempDir = $this->backupBasePath . "/{$appId}/temp_{$sessionId}";
        if (! File::isDirectory($tempDir)) {
            return ApiResponse::error(50025, '恢复会话已过期或目录不存在');
        }

        $sessionFile = $tempDir . '/.restore_session.json';
        if (! File::exists($sessionFile)) {
            File::deleteDirectory($tempDir);
            return ApiResponse::error(50025, '恢复会话信息丢失');
        }

        $sessionInfo = json_decode(File::get($sessionFile), true);

        try {
            $app = AppModel::where('app_id', $appId)->first();

            // 恢复程序文件
            $codeDir = $tempDir . '/code';
            if (File::isDirectory($codeDir) && $app) {
                $appPath = $this->resolveAppPath($app);
                if (File::isDirectory($appPath)) {
                    $this->restoreCodeFiles($codeDir, $appPath);
                }
            }

            // 恢复后验证：检查所有期望的表是否存在，缺失则运行应用迁移建表
            $this->ensureTablesExist($appId, $sessionInfo['tables'] ?? []);

            // 清理临时目录
            File::deleteDirectory($tempDir);

            // 记录恢复日志
            if ($app) {
                $backupId = $sessionInfo['backup_id'] ?? null;
                $localFileName = $sessionInfo['local_file_name'] ?? null;
                $fileSize = 0;

                if ($backupId) {
                    $backup = AppBackup::find($backupId);
                    $fileSize = $backup ? $backup->file_size : 0;
                } elseif ($localFileName) {
                    $filePath = $this->backupBasePath . "/{$appId}/{$localFileName}";
                    if (File::exists($filePath)) {
                        $fileSize = File::size($filePath);
                    }
                }

                AppBackup::create([
                    'app_id' => $appId,
                    'app_name' => $app->name,
                    'app_version' => $app->version,
                    'backup_type' => 'import',
                    'file_path' => '',
                    'file_size' => $fileSize,
                    'db_tables' => $sessionInfo['tables'] ?? [],
                    'status' => 1,
                    'operator_id' => Auth::guard('admin')->id(),
                ]);
            }

            return ApiResponse::success(null, '恢复完成');
        } catch (\Throwable $e) {
            if (File::isDirectory($tempDir)) {
                File::deleteDirectory($tempDir);
            }
            Log::error('恢复完成步骤失败', ['app_id' => $appId, 'error' => $e->getMessage()]);
            return ApiResponse::error(50024, '恢复完成失败：' . $e->getMessage());
        }
    }

    /**
     * 标记表已恢复到跟踪文件
     */
    protected function markTableRestored(string $appId, string $sessionId, string $table): void
    {
        $tempDir = $this->backupBasePath . "/{$appId}/temp_{$sessionId}";
        $trackFile = $tempDir . '/.restored_tables.json';
        $restored = [];
        if (File::exists($trackFile)) {
            $restored = json_decode(File::get($trackFile), true) ?: [];
        }
        $restored[] = [
            'table' => $table,
            'restored_at' => date('Y-m-d H:i:s'),
        ];
        File::put($trackFile, json_encode($restored, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
    }

    /**
     * 从SQL文件解析表名（兜底方案）
     */
    protected function parseTablesFromSqlFiles(string $dir): array
    {
        $tables = [];
        $sqlFiles = glob($dir . '/*.sql');
        foreach ($sqlFiles as $sqlFile) {
            $content = file_get_contents($sqlFile);
            if (preg_match_all("/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?`?(\w+)`?/i", $content, $matches)) {
                $tables = array_merge($tables, $matches[1]);
            }
        }
        return array_values(array_unique($tables));
    }

    /**
     * 上传统份文件到服务器本地目录
     *
     * 仅将上传的 .zip 文件保存到 storage/app_backups/{appId}/ 目录，
     * 不执行恢复，返回保存后的文件名供分步恢复使用。
     */
    public function uploadBackupFile(string $appId, \Illuminate\Http\UploadedFile $file): array
    {
        if (! $file->isValid() || strtolower($file->getClientOriginalExtension()) !== 'zip') {
            return ApiResponse::error(40001, '仅支持 .zip 格式的备份包');
        }

        $dir = $this->backupBasePath . "/{$appId}";
        if (! File::isDirectory($dir)) {
            File::makeDirectory($dir, 0755, true);
        }

        // 生成唯一文件名避免冲突
        $originalName = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
        $fileName = $originalName . '_' . date('YmdHis') . '.zip';
        $file->move($dir, $fileName);

        return ApiResponse::success([
            'file_name' => $fileName,
            'file_size' => File::size($dir . '/' . $fileName),
        ], '文件上传成功');
    }

    /**
     * 列出服务器本地备份目录中的 .zip 文件
     *
     * 扫描 storage/app_backups/{appId}/ 目录下的所有 .zip 文件，
     * 返回文件名、大小、修改时间等信息，供前端选择后直接恢复。
     */
    public function listLocalBackupFiles(string $appId): array
    {
        $dir = $this->backupBasePath . "/{$appId}";

        if (! File::isDirectory($dir)) {
            return ApiResponse::success([], '目录不存在');
        }

        $files = [];
        $zipFiles = File::glob($dir . '/*.zip');

        foreach ($zipFiles as $filePath) {
            $fileName = basename($filePath);
            $files[] = [
                'file_name' => $fileName,
                'file_path' => $filePath,
                'file_size' => File::size($filePath),
                'modified_time' => date('Y-m-d H:i:s', File::lastModified($filePath)),
            ];
        }

        // 按修改时间倒序
        usort($files, function ($a, $b) {
            return strcmp($b['modified_time'], $a['modified_time']);
        });

        return ApiResponse::success($files);
    }

    /**
     * 从服务器本地备份文件恢复应用
     *
     * 直接读取 storage/app_backups/{appId}/ 下的 .zip 文件进行恢复，
     * 无需上传，规避 413 Request Entity Too Large 问题。
     *
     * @param string $appId 目标应用ID
     * @param string $fileName 备份文件名（仅文件名，不含路径）
     */
    public function importLocalBackup(string $appId, string $fileName): array
    {
        set_time_limit(0);

        // 安全检查：文件名不允许包含路径分隔符
        if (str_contains($fileName, '/') || str_contains($fileName, '\\') || str_contains($fileName, '..')) {
            return ApiResponse::error(40001, '文件名不合法');
        }

        // 验证目标应用存在
        $app = AppModel::where('app_id', $appId)->first();
        if (! $app) {
            return ApiResponse::error(50006, '目标应用未安装');
        }

        // 验证文件存在
        $filePath = $this->backupBasePath . "/{$appId}/{$fileName}";
        if (! File::exists($filePath)) {
            return ApiResponse::error(50023, '备份文件不存在');
        }

        // 验证文件格式
        if (strtolower(pathinfo($fileName, PATHINFO_EXTENSION)) !== 'zip') {
            return ApiResponse::error(40001, '仅支持 .zip 格式的备份包');
        }

        $tempDir = $this->backupBasePath . "/{$appId}/temp_import_" . time();
        File::makeDirectory($tempDir, 0755, true);

        try {
            // 解压备份包
            $zip = new ZipArchive;
            if ($zip->open($filePath) !== true) {
                throw new \RuntimeException('无法打开备份文件，可能已损坏或不是有效的ZIP文件');
            }
            $zip->extractTo($tempDir);
            $zip->close();

            // 验证备份包结构：必须包含 db_backup.sql
            $hasManifest = File::exists($tempDir . '/backup_manifest.json');
            $hasDbSql = File::exists($tempDir . '/db_backup.sql');

            if (! $hasDbSql) {
                throw new \RuntimeException('备份包无效：缺少 db_backup.sql 文件');
            }

            // 读取备份清单获取表列表
            $importTables = [];
            if ($hasManifest) {
                $manifest = json_decode(File::get($tempDir . '/backup_manifest.json'), true);
                $importTables = $manifest['db_tables'] ?? [];
            }

            // 恢复数据库
            $sqlFile = $tempDir . '/db_backup.sql';
            $dbResult = $this->restoreDatabaseFromImport($sqlFile, $importTables, $appId);

            // 恢复程序文件
            $codeDir = $tempDir . '/code';
            $appPath = $this->resolveAppPath($app);
            if (File::isDirectory($codeDir)) {
                $this->restoreCodeFiles($codeDir, $appPath);
            }

            // 清理临时目录
            File::deleteDirectory($tempDir);

            // 记录导入日志
            AppBackup::create([
                'app_id' => $appId,
                'app_name' => $app->name,
                'app_version' => $app->version,
                'backup_type' => 'import',
                'file_path' => '',
                'file_size' => File::size($filePath),
                'db_tables' => $importTables,
                'status' => 1,
                'operator_id' => Auth::guard('admin')->id(),
            ]);

            return ApiResponse::success(null, '从本地备份文件恢复成功');
        } catch (\Throwable $e) {
            if (File::isDirectory($tempDir)) {
                File::deleteDirectory($tempDir);
            }
            Log::error('本地备份导入失败', ['app_id' => $appId, 'file' => $fileName, 'error' => $e->getMessage()]);

            return ApiResponse::error(50024, '导入失败：' . $e->getMessage());
        }
    }

    /**
     * 从导入的SQL文件恢复数据库（委托 restoreDatabase 处理）
     */
    protected function restoreDatabaseFromImport(string $sqlFile, array $expectedTables, string $appId = ''): bool
    {
        return $this->restoreDatabase($sqlFile, $expectedTables, $appId);
    }

    /**
     * 解析应用路径
     */
    protected function resolveAppPath(AppModel $app): string
    {
        $path = $app->path;
        if (! str_starts_with($path, '/') && ! str_starts_with($path, '\\') && ! preg_match('/^[A-Za-z]:/', $path)) {
            return base_path($path);
        }

        return $path;
    }

    /**
     * 恢复后验证：检查期望的表是否存在，缺失则运行应用迁移建表
     *
     * 如果部分表在恢复过程中未能通过 SQL 创建成功（如 MySQL 版本差异、
     * 字符集不兼容等），则通过应用的 Install::runMigrations() 重建缺失的表。
     * 已存在的表不受影响（迁移内部会检查表是否已存在）。
     */
    protected function ensureTablesExist(string $appId, array $expectedTables): void
    {
        if (empty($expectedTables)) {
            return;
        }

        // 检查缺失的表
        $missingTables = [];
        foreach ($expectedTables as $tableName) {
            if (! Schema::hasTable($tableName)) {
                $missingTables[] = $tableName;
            }
        }

        if (empty($missingTables)) {
            return;
        }

        Log::warning('恢复后检测到缺失的表，尝试通过应用迁移重建', [
            'app_id' => $appId,
            'missing_tables' => $missingTables,
        ]);

        // 尝试通过应用的 Install 类运行迁移
        $app = AppModel::where('app_id', $appId)->first();
        if (! $app) {
            Log::error('无法运行迁移：应用未找到', ['app_id' => $appId]);
            return;
        }

        $appPath = $this->resolveAppPath($app);
        $manifest = $app->manifest ?? [];
        $className = $manifest['install'] ?? 'Install';
        $fullClass = 'App\\Apps\\' . app_id_to_class_name($appId) . "\\{$className}";

        if (class_exists($fullClass)) {
            $instance = new $fullClass;
            // 优先调用 upgrade 方法（它会执行迁移），其次调用 install
            if (method_exists($instance, 'upgrade')) {
                $instance->upgrade($app->version ?? '', $app->version ?? '');
            } elseif (method_exists($instance, 'install')) {
                $instance->install();
            }
        }

        // 再次验证
        $stillMissing = [];
        foreach ($missingTables as $tableName) {
            if (! Schema::hasTable($tableName)) {
                $stillMissing[] = $tableName;
            }
        }

        if (! empty($stillMissing)) {
            Log::error('迁移重建后表仍然缺失', [
                'app_id' => $appId,
                'still_missing' => $stillMissing,
            ]);
        } else {
            Log::info('缺失的表已通过迁移成功重建', [
                'app_id' => $appId,
                'tables' => $missingTables,
            ]);
        }
    }
}
