<?php

declare(strict_types=1);

namespace App\Helpers;

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

class DatabaseBackupHelper
{
    /**
     * 从文件句柄中流式读取并提取 SQL 语句
     * 支持引号跟踪、转义字符，不一次加载整个文件到内存
     *
     * @param resource $fh 文件句柄
     * @return \Generator 逐条产生 SQL 语句
     */
    public static function streamParseSql($fh): \Generator
    {
        $buffer = '';
        $inSingleQuote = false;
        $inDoubleQuote = false;
        $prevChar = '';

        while (!feof($fh)) {
            $chunk = fread($fh, 1048576); // 每次读 1MB
            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)) {
                        yield $statement;
                    }
                    $buffer = '';
                    $prevChar = '';
                    continue;
                }

                $buffer .= $char;
                $prevChar = $char;
            }
        }

        // 最后一段（文件末尾可能无分号）
        $statement = trim($buffer);
        if (!empty($statement)) {
            yield $statement;
        }
    }

    /**
     * 检查当前文件大小是否达到分卷阈值，是则自动切换文件
     *
     * @param resource &$fh 当前文件句柄（引用，可能被替换）
     * @param int &$partCounter 分卷计数器（引用）
     * @param string $dirPath 目录路径
     * @param int $volumeSizeMB 分卷大小（MB）
     * @return bool 是否切换了文件
     */
    public static function checkSplit(&$fh, int &$partCounter, string $dirPath, int $volumeSizeMB): bool
    {
        $threshold = $volumeSizeMB * 1048576;

        if (fstat($fh)['size'] >= $threshold) {
            fclose($fh);

            $partCounter++;
            $partFilename = 'db_backup.part' . $partCounter . '.sql';
            $partPath = $dirPath . '/' . $partFilename;

            $fh = fopen($partPath, 'w');
            if ($fh === false) {
                throw new \RuntimeException("无法创建分卷文件: {$partPath}");
            }

            return true;
        }

        return false;
    }

    /**
     * 将行数据构建为 INSERT 语句
     *
     * @param string $table 表名
     * @param array $columns 列名列表
     * @param array $row 行数据
     * @return string
     */
    public static function buildInsertSql(string $table, array $columns, array $row): string
    {
        $columnList = '`' . implode('`, `', $columns) . '`';
        $values = array_map(function ($val) {
            if (is_null($val)) {
                return 'NULL';
            }
            return "'" . addslashes((string) $val) . "'";
        }, $row);

        return "INSERT INTO `{$table}` ({$columnList}) VALUES (" . implode(', ', $values) . ");\n";
    }

    /**
     * 将行数据（stdClass）转为数组
     *
     * @param mixed $row 行数据
     * @return array
     */
    public static function rowToArray($row): array
    {
        return array_map(function ($val) {
            return is_null($val) ? null : (is_string($val) ? $val : $val);
        }, (array) $row);
    }

    /**
     * 以流式方式将表数据写入 SQL 文件，支持分卷
     *
     * @param string $table 表名
     * @param string $dirPath 写入目录
     * @param int $volumeSizeMB 分卷大小（MB）
     * @return array 写入的文件列表 [db_backup.sql, db_backup.part2.sql, ...]
     */
    public static function streamTableData(string $table, string $dirPath, int $volumeSizeMB = 2): array
    {
        // 保留原有方法，内部委托给分页版：逐页执行直到完成
        $allFiles = [];
        $lastId = null;
        do {
            $result = self::streamTableDataPage($table, $dirPath, $volumeSizeMB, $lastId, 50000);
            $allFiles = $result['files'];
            $lastId = $result['last_id'];
        } while ($result['has_more']);
        return $allFiles;
    }

    /**
     * 分页备份单表数据（一页），支持游标分页
     *
     * 使用 WHERE id > lastId ORDER BY id LIMIT pageSize 逐页读取，
     * 避免单张大表在单个 HTTP 请求中耗时过长。
     * 每页默认 50000 行，每页独立请求，前端可展示精确进度。
     *
     * - 第一页（lastId=null）：写入 DROP+CREATE 表结构，再写入数据
     * - 后续页（lastId=上一页最后 ID）：追加写入数据到最新 SQL 文件
     * - 自动分卷检查：写入后检查文件大小，超限自动切分
     * - 总行数在首次请求时 COUNT 并缓存到 .count_{table_hash}.json
     * - 页码持久化到 .page_{table_hash}.json
     *
     * @param string $table 表名
     * @param string $dirPath 写入目录
     * @param int $volumeSizeMB 分卷大小（MB）
     * @param int|null $lastId 上一页最后一条记录的 ID（null=第一页）
     * @param int $pageSize 每页行数（默认 50000）
     * @return array [
     *   'files' => 当前目录下所有 .sql 文件路径列表,
     *   'last_id' => int|null 本页最后一条 ID（null=无数据或无更多）,
     *   'total_rows' => int 该表总行数,
     *   'page' => int 当前页码,
     *   'has_more' => bool 是否还有下一页,
     * ]
     */
    public static function streamTableDataPage(
        string $table,
        string $dirPath,
        int $volumeSizeMB = 2,
        ?int $lastId = null,
        int $pageSize = 50000
    ): array {
        // 统计总行数（首次请求缓存到文件，避免重复 COUNT）
        $countFile = $dirPath . '/.count_' . md5($table) . '.json';
        $totalRows = 0;
        if (file_exists($countFile)) {
            $countData = json_decode(file_get_contents($countFile), true);
            $totalRows = $countData['total'] ?? 0;
        } else {
            try {
                $totalRows = DB::table($table)->count();
            } catch (\Throwable $e) {
                throw new \RuntimeException("无法统计表 {$table} 的行数：" . $e->getMessage());
            }
            file_put_contents($countFile, json_encode(['total' => $totalRows]));
        }

        // 页码持久化（跨请求跟踪当前页码）
        $pageFile = $dirPath . '/.page_' . md5($table) . '.json';
        $page = 1;
        if ($lastId !== null && file_exists($pageFile)) {
            $pagePrev = json_decode(file_get_contents($pageFile), true);
            $page = ($pagePrev['page'] ?? 0) + 1;
        }
        file_put_contents($pageFile, json_encode(['page' => $page]));

        // --- 确定要写入的文件句柄 ---
        $partCounter = 1;

        if ($lastId === null) {
            // 第一页：写入表结构
            // 多表备份时，所有表共用同一个 db_backup.sql
            // 第一张表创建文件，后续表追加到已有文件
            $existingSqlFiles = glob($dirPath . '/*.sql');
            if (!empty($existingSqlFiles)) {
                // 已有SQL文件（其他表已备份），追加到最新文件
                usort($existingSqlFiles, function ($a, $b) {
                    return filemtime($b) - filemtime($a);
                });
                $basePath = $existingSqlFiles[0];
                $fh = fopen($basePath, 'a');
                if (preg_match('/\.part(\d+)\.sql$/', $basePath, $m)) {
                    $partCounter = (int) $m[1];
                }
                fwrite($fh, "\n");
            } else {
                // 首张表，创建新文件
                $basePath = $dirPath . '/db_backup.sql';
                $fh = fopen($basePath, 'w');
            }
            if ($fh === false) {
                throw new \RuntimeException("无法创建备份文件: {$basePath}");
            }

            $createTable = DB::selectOne("SHOW CREATE TABLE `{$table}`");
            $createKey = 'Create Table';
            fwrite($fh, "DROP TABLE IF EXISTS `{$table}`;\n");
            fwrite($fh, $createTable->$createKey . ";\n\n");

            if ($totalRows === 0) {
                fclose($fh);
                return [
                    'files' => glob($dirPath . '/*.sql') ?: [$basePath],
                    'last_id' => null,
                    'total_rows' => 0,
                    'page' => 1,
                    'has_more' => false,
                ];
            }

            fwrite($fh, "-- Data for table `{$table}`\n");
        } else {
            // 后续页：查找最新的 .sql 文件，追加写入
            $sqlFiles = glob($dirPath . '/*.sql');
            if (empty($sqlFiles)) {
                throw new \RuntimeException("未找到 SQL 文件，无法追加数据");
            }
            // 按修改时间倒序取最新文件
            usort($sqlFiles, function ($a, $b) {
                return filemtime($b) - filemtime($a);
            });
            $lastFilePath = $sqlFiles[0];
            $fh = fopen($lastFilePath, 'a');
            if ($fh === false) {
                throw new \RuntimeException("无法追加写入文件: {$lastFilePath}");
            }

            if (preg_match('/\.part(\d+)\.sql$/', $lastFilePath, $m)) {
                $partCounter = (int) $m[1];
            }
        }

        // --- 分页查询并写入数据（内层小批量，防 OOM） ---
        $cursor = $lastId ?? 0;
        $actualLastId = null;
        $columns = null;
        $processedCount = 0;

        // 外层循环：最多处理 pageSize 行
        while ($processedCount < $pageSize) {
            $rows = DB::table($table)
                ->where('id', '>', $cursor)
                ->orderBy('id')
                ->limit(200)
                ->get();

            if ($rows->isEmpty()) {
                break;
            }

            foreach ($rows as $row) {
                $rowArr = self::rowToArray($row);
                if ($columns === null) {
                    $columns = array_keys($rowArr);
                }
                $sql = self::buildInsertSql($table, $columns, $rowArr);
                fwrite($fh, $sql);

                // 写入后检查分卷
                if (self::checkSplit($fh, $partCounter, $dirPath, $volumeSizeMB)) {
                    // 文件已切换，无需额外处理
                }

                $cursor = $row->id;
                $actualLastId = $row->id;
                $processedCount++;

                if ($processedCount >= $pageSize) {
                    break;
                }
            }

            // 如果本次查询不足 200 行，说明已无更多数据，结束循环
            if (count($rows) < 200) {
                break;
            }
        }

        if (is_resource($fh)) {
            fclose($fh);
        }

        // 扫描所有 .sql 文件作为返回值
        $allFiles = glob($dirPath . '/*.sql');
        $allFiles = $allFiles !== false ? array_values($allFiles) : [];

        return [
            'files' => $allFiles,
            'last_id' => $actualLastId,
            'total_rows' => $totalRows,
            'page' => $page,
            'has_more' => $processedCount >= $pageSize && $totalRows > 0,
        ];
    }

    /**
     * 流式恢复 SQL 文件（支持分卷）
     *
     * 恢复策略：
     * - 表已存在：先清空数据（TRUNCATE），然后只执行 INSERT 语句，跳过 DROP/CREATE
     * - 表不存在：执行全部语句（DROP + CREATE + INSERT），由 SQL 自行建表
     *
     * @param array $sqlFiles 文件路径列表（按顺序）
     * @param array $expectedTables 期望恢复的表列表（用于清空数据）
     */
    public static function streamRestore(array $sqlFiles, array $expectedTables = []): void
    {
        // 检测哪些表已存在，只清空数据不 DROP
        $existingTables = [];
        $missingTables = [];
        if (!empty($expectedTables)) {
            DB::statement('SET FOREIGN_KEY_CHECKS = 0');
            foreach ($expectedTables as $tableName) {
                if (DB::getSchemaBuilder()->hasTable($tableName)) {
                    // 表已存在，清空数据保留表结构
                    DB::statement("TRUNCATE TABLE `{$tableName}`");
                    $existingTables[$tableName] = true;
                } else {
                    // 表不存在，需要通过 SQL 重建
                    $missingTables[$tableName] = true;
                }
            }
            DB::statement('SET FOREIGN_KEY_CHECKS = 1');
        }

        // 逐个文件流式恢复
        foreach ($sqlFiles as $filePath) {
            if (!file_exists($filePath)) {
                throw new \RuntimeException("分卷文件缺失: {$filePath}");
            }

            $fh = fopen($filePath, 'r');
            if ($fh === false) {
                throw new \RuntimeException("无法读取文件: {$filePath}");
            }

            try {
                foreach (self::streamParseSql($fh) as $statement) {
                    try {
                        // 判断语句类型，已存在的表跳过 DROP/CREATE
                        $type = self::detectStatementType($statement);
                        if ($type && isset($existingTables[$type['table']]) && $type['type'] !== 'insert') {
                            // 表已存在，跳过 DROP TABLE 和 CREATE TABLE
                            Log::info('恢复时跳过DDL语句（表已存在）', [
                                'type' => $type['type'],
                                'table' => $type['table'],
                            ]);
                            continue;
                        }
                        DB::unprepared($statement);
                    } catch (\Throwable $e) {
                        Log::warning('SQL 执行失败（已跳过）', [
                            'error' => $e->getMessage(),
                            'sql' => mb_substr($statement, 0, 100),
                        ]);
                    }
                }
            } finally {
                fclose($fh);
            }
        }
    }

    /**
     * 检测 SQL 语句的类型和目标表名
     *
     * @param string $statement SQL 语句
     * @return array|null ['type' => 'drop'|'create'|'insert', 'table' => string] 或 null
     */
    public static function detectStatementType(string $statement): ?array
    {
        // DROP TABLE
        if (preg_match('/DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?`?(\w+)`?/i', $statement, $m)) {
            return ['type' => 'drop', 'table' => $m[1]];
        }
        // CREATE TABLE
        if (preg_match('/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?`?(\w+)`?/i', $statement, $m)) {
            return ['type' => 'create', 'table' => $m[1]];
        }
        // INSERT INTO
        if (preg_match('/INSERT\s+INTO\s+`?(\w+)`?/i', $statement, $m)) {
            return ['type' => 'insert', 'table' => $m[1]];
        }
        return null;
    }

    /**
     * 生成分卷清单
     *
     * @param string $dirPath 目录路径
     * @param int $volumeSizeMB 分卷大小（MB）
     * @param array $tables 表名列表
     * @param array $sqlFiles SQL 文件路径列表
     */
    public static function writeVolumeManifest(string $dirPath, int $volumeSizeMB, array $tables, array $sqlFiles): void
    {
        $manifest = [
            'volume_size_mb' => $volumeSizeMB,
            'total_volumes' => count($sqlFiles),
            'tables' => $tables,
            'sql_files' => array_map('basename', $sqlFiles),
            'created_at' => date('Y-m-d H:i:s'),
        ];

        file_put_contents(
            $dirPath . '/volume_manifest.json',
            json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
        );
    }

    /**
     * 获取分卷文件列表
     *
     * @param string $dirPath 解压后的临时目录
     * @return array
     */
    public static function resolveSqlFiles(string $dirPath): array
    {
        $manifest = self::getVolumeInfo($dirPath);
        if ($manifest && isset($manifest['sql_files'])) {
            $files = [];
            foreach ($manifest['sql_files'] as $basename) {
                $files[] = $dirPath . '/' . $basename;
            }
            return $files;
        }

        // 旧版单文件兼容
        $legacyFile = $dirPath . '/db_backup.sql';
        if (file_exists($legacyFile)) {
            return [$legacyFile];
        }

        throw new \RuntimeException('未找到备份 SQL 文件');
    }

    /**
     * 获取分卷清单信息
     *
     * @param string $dirPath 解压后的临时目录
     * @return array|null
     */
    public static function getVolumeInfo(string $dirPath): ?array
    {
        $manifestPath = $dirPath . '/volume_manifest.json';
        if (!file_exists($manifestPath)) {
            return null;
        }
        return json_decode(file_get_contents($manifestPath), true);
    }
}
