<?php
/**
 * SumlyCMS —— 统一迁移入口（P52.1）
 *
 * 用法（在站点根目录执行）：
 *   php database/cms status              查看共享/私有 migration 的执行情况
 *   php database/cms migrate             执行所有未执行的 migration
 *   php database/cms check               与参照库（默认 demo）做 schema 一致性检查
 *   php database/cms check --ref=cmswang 换一个参照库
 *
 * 目录分层（这是本次定的架构）：
 *   database/migrations/        共享层 —— core 功能的表结构，两个站都必须一致（软链共享）
 *   resources/migrations/       实例层 —— 只有本站需要的表结构（官网的运营表 / 演示站的实验表）
 *
 * 与 Framework\Migrator 的区别：
 *   Migrator::run() 一个失败就整批中断；这里给每个 migration 单独 try/catch，
 *   失败的记下来继续跑，最后汇总 —— 补齐历史遗留库时不会被第一条错误卡死。
 */

// 站点根 = SITE_ROOT 环境变量，否则取当前工作目录。
// 【重要】不能用 __DIR__：database/ 是软链到研发站的，PHP 会把 __DIR__ realpath 解析成
// /www/wwwroot/demo.cms.wang/database，导致在官网执行时悄悄连错库（踩过）。
// 用法：cd /www/wwwroot/cms.wang && php database/cms migrate
//    或：SITE_ROOT=/www/wwwroot/cms.wang php /path/to/cms migrate
$siteRoot = getenv('SITE_ROOT') ?: getcwd();
if (!is_file($siteRoot . '/framework/Loader.php')) {
    fwrite(STDERR, "站点根定位失败：{$siteRoot} 下没有 framework/Loader.php\n"
        . "请在站点根目录执行，或设置 SITE_ROOT 环境变量。\n");
    exit(1);
}
require_once $siteRoot . '/framework/Loader.php';
Framework\Loader::register();
Framework\Config::load();
$DB_CFG = require $siteRoot . '/config/database.php';
Framework\Database::setConfig($DB_CFG);

use Framework\Database;

$SHARED_DIR = $siteRoot . '/database/migrations/';
$LOCAL_DIR  = $siteRoot . '/resources/migrations/';

$cmd = $argv[1] ?? 'status';
$ref = 'demo';
foreach (array_slice($argv, 2) as $a) {
    if (strpos($a, '--ref=') === 0) $ref = substr($a, 6);
}

// ── 基础工具 ──

function ensureLogTable(): void
{
    $p = Database::prefix();
    Database::query(
        "CREATE TABLE IF NOT EXISTS `{$p}migrations` (
            `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
            `migration` VARCHAR(255) NOT NULL,
            `batch` INT NOT NULL,
            `executed_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
            UNIQUE KEY `uk_migration` (`migration`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
    );
}

function appliedMap(): array
{
    $p = Database::prefix();
    $map = [];
    try {
        foreach (Database::fetchAll("SELECT migration FROM `{$p}migrations`") as $r) {
            $map[$r['migration']] = true;
        }
    } catch (\Throwable $e) {}
    return $map;
}

function collectMigrations(): array
{
    global $SHARED_DIR, $LOCAL_DIR;
    $list = [];
    foreach ([['shared', $SHARED_DIR], ['local', $LOCAL_DIR]] as list($layer, $dir)) {
        if (!is_dir($dir)) continue;
        foreach (glob($dir . '*.php') ?: [] as $file) {
            $list[] = [
                'layer' => $layer,
                'name'  => basename($file, '.php'),
                'file'  => $file,
            ];
        }
    }
    // 共享层先跑（core 表要先于实例表存在），同层内按文件名排序
    usort($list, function ($a, $b) {
        if ($a['layer'] !== $b['layer']) return $a['layer'] === 'shared' ? -1 : 1;
        return strcmp($a['name'], $b['name']);
    });
    return $list;
}

function currentDb(): string
{
    return (string) (Database::fetchOne('SELECT DATABASE() db')['db'] ?? '?');
}

// ── status ──

if ($cmd === 'status') {
    ensureLogTable();
    $map  = appliedMap();
    $list = collectMigrations();
    printf("站点库: %s    共享层: %s    实例层: %s\n\n", currentDb(), $SHARED_DIR, $LOCAL_DIR);
    printf("%-6s %-46s %s\n", '层', 'MIGRATION', '状态');
    echo str_repeat('-', 72), "\n";
    $pending = 0;
    foreach ($list as $m) {
        $done = isset($map[$m['name']]);
        if (!$done) $pending++;
        printf("%-6s %-46s %s\n", $m['layer'], $m['name'], $done ? '✓ 已执行' : '· 未执行');
    }
    echo str_repeat('-', 72), "\n";
    printf("共 %d 个迁移，%d 个待执行\n", count($list), $pending);
    exit(0);
}

// ── migrate ──

if ($cmd === 'migrate') {
    ensureLogTable();
    $map    = appliedMap();
    $list   = collectMigrations();
    $batch  = (int) (Database::fetchOne('SELECT MAX(batch) b FROM `' . Database::prefix() . 'migrations`')['b'] ?? 0) + 1;

    $ok = $fail = $skip = 0;
    echo "站点库: ", currentDb(), "\n\n";
    foreach ($list as $m) {
        if (isset($map[$m['name']])) {
            $skip++;
            continue;
        }
        try {
            $mig = require $m['file'];
            if (is_array($mig) && isset($mig['up']) && is_callable($mig['up'])) {
                ($mig['up'])();
            } elseif ($mig === null || $mig === 1 || $mig === true) {
                // 脚本式 migration（历史遗留写法）：require 时就把活干完了，不返回 up 回调。
                // 它确实已执行，判为成功并记账，避免每次都重跑。
            } else {
                throw new \RuntimeException('文件未返回 up 回调，且未识别为脚本式迁移');
            }
            Database::table('migrations')->insert([
                'migration' => $m['name'],
                'batch'     => $batch,
            ]);
            $ok++;
            echo "  ✓ [{$m['layer']}] {$m['name']}\n";
        } catch (\Throwable $e) {
            $fail++;
            echo "  ✗ [{$m['layer']}] {$m['name']}  ->  ", $e->getMessage(), "\n";
        }
    }
    echo "\n完成：执行 $ok 个，跳过 $skip 个，失败 $fail 个\n";
    exit($fail > 0 ? 1 : 0);
}

// ── check：与参照库做 schema 一致性检查 ──

if ($cmd === 'check') {
    $refCfg = null;
    // 参照库连接：优先读参照站点的 config/database.php
    $guess = [
        '/www/wwwroot/' . $ref . '/config/database.php',
        '/www/wwwroot/' . $ref . '.cms.wang/config/database.php',
    ];
    foreach ($guess as $g) {
        if (is_file($g)) { $refCfg = require $g; break; }
    }
    if (!$refCfg) {
        echo "找不到参照库 {$ref} 的 config/database.php\n";
        exit(1);
    }

    $self = currentDb();

    // 护栏：确认参照库不是自己，避免自我对比得出"零差异"的假象
    if ($self === $refCfg['db']) {
        echo "参照库 {$refCfg['db']} 就是当前库，没有对比意义。请换 --ref。\n";
        exit(1);
    }

    $mkPdo = function (array $cfg, string $db) use (&$mkPdo): \PDO {
        return new \PDO(
            sprintf('mysql:host=%s;port=%d;dbname=%s;charset=%s',
                $cfg['host'], $cfg['port'] ?? 3306, $db, $cfg['charset'] ?? 'utf8mb4'),
            $cfg['user'], $cfg['pass'],
            [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]
        );
    };

    $colsOf = function (\PDO $db, string $schema): array {
        $st = $db->prepare(
            "SELECT TABLE_NAME, COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = ?"
        );
        $st->execute([$schema]);
        $out = [];
        foreach ($st->fetchAll(\PDO::FETCH_NUM) as list($t, $c)) {
            $out[$t][$c] = true;
        }
        return $out;
    };

    $refCols  = $colsOf($mkPdo($refCfg, $refCfg['db']), $refCfg['db']);
    $selfCols = $colsOf($mkPdo($DB_CFG, $self), $self);

    $missingTables = array_diff(array_keys($refCols), array_keys($selfCols));
    echo "本库: $self    参照库: {$refCfg['db']}\n\n";

    echo "【缺表】", count($missingTables), " 张\n";
    foreach ($missingTables as $t) echo "  - $t\n";

    echo "\n【同名表缺列】\n";
    $n = 0;
    foreach (array_intersect(array_keys($refCols), array_keys($selfCols)) as $t) {
        $miss = array_diff(array_keys($refCols[$t]), array_keys($selfCols[$t]));
        if ($miss) {
            $n++;
            echo "  - $t : ", implode(', ', $miss), "\n";
        }
    }
    echo $n ? '' : "  （无）\n";
    exit(0);
}

echo "未知命令: $cmd\n用法: php database/cms status|migrate|check [--ref=demo]\n";
exit(1);
