<?php

declare(strict_types=1);

namespace app\api\controller;

use Throwable;
use ba\Random;
use ba\Version;
use ba\Terminal;
use ba\Filesystem;
use app\common\controller\Api;
use app\admin\model\Admin as AdminModel;
use app\process\Monitor;
use support\Response;
use Webman\Http\Request;
use Phinx\Config\Config as PhinxConfig;
use Phinx\Migration\Manager as PhinxManager;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;

/**
 * 安装控制器
 */
class Install extends Api
{
    public const X64 = 'x64';

    public const X86 = 'x86';

    protected bool $useSystemSettings = false;

    /**
     * 环境检查状态
     */
    static string $ok   = 'ok';
    static string $fail = 'fail';
    static string $warn = 'warn';

    /**
     * 安装锁文件名称
     */
    static string $lockFileName = 'install.lock';

    /**
     * 配置文件
     */
    static string $dbConfigFileName    = 'thinkorm.php';
    static string $buildConfigFileName = 'buildadmin.php';

    /**
     * 自动构建的前端文件的 outDir 相对于根目录
     */
    static string $distDir = 'web' . DIRECTORY_SEPARATOR . 'dist';

    /**
     * 需要的依赖版本
     */
    static array $needDependentVersion = [
        'php'  => '8.2.0',
        'npm'  => '9.8.1',
        'cnpm' => '7.1.0',
        'node' => '20.14.0',
        'yarn' => '1.2.0',
        'pnpm' => '6.32.13',
    ];

    /**
     * 安装完成标记
     */
    static string $InstallationCompletionMark = 'install-end';

    /**
     * 命令执行窗口（exec 为 SSE 长连接，不会返回）
     * @throws Throwable
     */
    public function terminal(Request $request): Response
    {
        $this->setRequest($request);
        if ($this->isInstallComplete()) {
            return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %file% file first', ['%file%' => 'public/' . self::$lockFileName]));
        }

        (new Terminal())->exec(false);
        return $this->success(); // unreachable: exec() blocks with SSE stream
    }

    public function changePackageManager(Request $request): Response
    {
        $this->setRequest($request);
        if ($this->isInstallComplete()) {
            return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %file% file first', ['%file%' => 'public/' . self::$lockFileName]));
        }

        $newPackageManager = $request->post('manager', config('terminal.npm_package_manager'));
        if (Terminal::changeTerminalConfig()) {
            return $this->success('', [
                'manager' => $newPackageManager
            ]);
        }
        return $this->error(__('Failed to switch package manager. Please modify the configuration file manually:%s', ['config/terminal.php']));
    }

    /**
     * 环境基础检查
     */
    public function envBaseCheck(Request $request): Response
    {
        $this->setRequest($request);
        if ($this->isInstallComplete()) {
            return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %file% file first', ['%file%' => 'public/' . self::$lockFileName]), []);
        }
        if (($_ENV['DATABASE_TYPE'] ?? getenv('DATABASE_TYPE'))) {
            return $this->error(__('The .env file with database configuration was detected. Please clean up and try again!'));
        }

        // php版本-start
        $phpVersion        = phpversion();
        $phpBit            = PHP_INT_SIZE == 8 ? self::X64 : self::X86;
        $phpVersionCompare = Version::compare(self::$needDependentVersion['php'], $phpVersion);
        if (!$phpVersionCompare) {
            $phpVersionLink = [
                [
                    'name' => __('need') . ' >= ' . self::$needDependentVersion['php'],
                    'type' => 'text'
                ],
                [
                    'name'  => __('How to solve?'),
                    'title' => __('Click to see how to solve it'),
                    'type'  => 'faq',
                    'url'   => 'https://doc.buildadmin.com/guide/install/preparePHP.html'
                ]
            ];
        } elseif ($phpBit != self::X64) {
            $phpVersionLink = [
                [
                    'name' => __('need') . ' x64 PHP',
                    'type' => 'text'
                ],
                [
                    'name'  => __('How to solve?'),
                    'title' => __('Click to see how to solve it'),
                    'type'  => 'faq',
                    'url'   => 'https://doc.buildadmin.com/guide/install/preparePHP.html'
                ]
            ];
        }
        // php版本-end

        // 配置文件-start（分别检测目录和文件，便于定位问题）
        $configDir        = rtrim(config_path(), '/\\');
        $dbConfigFile     = $configDir . DIRECTORY_SEPARATOR . self::$dbConfigFileName;
        $configDirWritable  = Filesystem::pathIsWritable($configDir);
        $dbConfigWritable   = Filesystem::pathIsWritable($dbConfigFile);
        $configIsWritable   = $configDirWritable && $dbConfigWritable;
        if (!$configIsWritable) {
            $configIsWritableLink = [
                [
                    'name'  => __('View reason'),
                    'title' => __('Click to view the reason'),
                    'type'  => 'faq',
                    'url'   => 'https://doc.buildadmin.com/guide/install/dirNoPermission.html'
                ]
            ];
        }
        // 配置文件-end

        // public-start
        $publicIsWritable = Filesystem::pathIsWritable(public_path());
        if (!$publicIsWritable) {
            $publicIsWritableLink = [
                [
                    'name'  => __('View reason'),
                    'title' => __('Click to view the reason'),
                    'type'  => 'faq',
                    'url'   => 'https://doc.buildadmin.com/guide/install/dirNoPermission.html'
                ]
            ];
        }
        // public-end

        // PDO-start
        $phpPdo = extension_loaded("PDO") && extension_loaded('pdo_mysql');
        if (!$phpPdo) {
            $phpPdoLink = [
                [
                    'name' => __('PDO extensions need to be installed'),
                    'type' => 'text'
                ],
                [
                    'name'  => __('How to solve?'),
                    'title' => __('Click to see how to solve it'),
                    'type'  => 'faq',
                    'url'   => 'https://doc.buildadmin.com/guide/install/missingExtension.html'
                ]
            ];
        }
        // PDO-end

        // GD2和freeType-start
        $phpGd2 = extension_loaded('gd') && function_exists('imagettftext');
        if (!$phpGd2) {
            $phpGd2Link = [
                [
                    'name' => __('The gd extension and freeType library need to be installed'),
                    'type' => 'text'
                ],
                [
                    'name'  => __('How to solve?'),
                    'title' => __('Click to see how to solve it'),
                    'type'  => 'faq',
                    'url'   => 'https://doc.buildadmin.com/guide/install/gdFail.html'
                ]
            ];
        }
        // GD2和freeType-end

        // proc_open
        $phpProc = function_exists('proc_open') && function_exists('proc_close') && function_exists('proc_get_status');
        if (!$phpProc) {
            $phpProcLink = [
                [
                    'name'  => __('View reason'),
                    'title' => __('proc_open or proc_close functions in PHP Ini is disabled'),
                    'type'  => 'faq',
                    'url'   => 'https://doc.buildadmin.com/guide/install/disablement.html'
                ],
                [
                    'name'  => __('How to modify'),
                    'title' => __('Click to view how to modify'),
                    'type'  => 'faq',
                    'url'   => 'https://doc.buildadmin.com/guide/install/disablement.html'
                ],
                [
                    'name'  => __('Security assurance?'),
                    'title' => __('Using the installation service correctly will not cause any potential security problems. Click to view the details'),
                    'type'  => 'faq',
                    'url'   => 'https://doc.buildadmin.com/guide/install/senior.html'
                ],
            ];
        }
        // proc_open-end

        return $this->success('', [
            'php_version'        => [
                'describe' => $phpVersion . " ($phpBit)",
                'state'    => $phpVersionCompare && $phpBit == self::X64 ? self::$ok : self::$fail,
                'link'     => $phpVersionLink ?? [],
            ],
            'config_is_writable' => [
                'describe' => $configIsWritable
                    ? self::writableStateDescribe(true)
                    : (self::writableStateDescribe(false) . ' [' . $configDir . ']'),
                'state'    => $configIsWritable ? self::$ok : self::$fail,
                'link'     => $configIsWritableLink ?? []
            ],
            'public_is_writable' => [
                'describe' => self::writableStateDescribe($publicIsWritable),
                'state'    => $publicIsWritable ? self::$ok : self::$fail,
                'link'     => $publicIsWritableLink ?? []
            ],
            'php_pdo'            => [
                'describe' => $phpPdo ? __('already installed') : __('Not installed'),
                'state'    => $phpPdo ? self::$ok : self::$fail,
                'link'     => $phpPdoLink ?? []
            ],
            'php_gd2'            => [
                'describe' => $phpGd2 ? __('already installed') : __('Not installed'),
                'state'    => $phpGd2 ? self::$ok : self::$fail,
                'link'     => $phpGd2Link ?? []
            ],
            'php_proc'           => [
                'describe' => $phpProc ? __('Allow execution') : __('disabled'),
                'state'    => $phpProc ? self::$ok : self::$warn,
                'link'     => $phpProcLink ?? []
            ],
        ]);
    }

    /**
     * npm环境检查
     */
    public function envNpmCheck(Request $request): Response
    {
        $this->setRequest($request);
        if ($this->isInstallComplete()) {
            return $this->error('', [], 2);
        }

        $packageManager = $request->post('manager', 'none');

        // npm
        $npmVersion        = Version::getVersion('npm');
        $npmVersionCompare = Version::compare(self::$needDependentVersion['npm'], $npmVersion);
        if (!$npmVersionCompare || !$npmVersion) {
            $npmVersionLink = [
                [
                    'name' => __('need') . ' >= ' . self::$needDependentVersion['npm'],
                    'type' => 'text'
                ],
                [
                    'name'  => __('How to solve?'),
                    'title' => __('Click to see how to solve it'),
                    'type'  => 'faq',
                    'url'   => 'https://doc.buildadmin.com/guide/install/prepareNpm.html'
                ]
            ];
        }

        // 包管理器
        $pmVersionLink = [];
        $pmVersion        = __('nothing');
        $pmVersionCompare = false;
        if (in_array($packageManager, ['npm', 'cnpm', 'pnpm', 'yarn'])) {
            $pmVersion        = Version::getVersion($packageManager);
            $pmVersionCompare = Version::compare(self::$needDependentVersion[$packageManager], $pmVersion);

            if (!$pmVersion) {
                $pmVersionLink[] = [
                    'name' => __('need') . ' >= ' . self::$needDependentVersion[$packageManager],
                    'type' => 'text'
                ];
                if ($npmVersionCompare) {
                    $pmVersionLink[] = [
                        'name'  => __('Click Install %s', [$packageManager]),
                        'title' => '',
                        'type'  => 'install-package-manager'
                    ];
                } else {
                    $pmVersionLink[] = [
                        'name' => __('Please install NPM first'),
                        'type' => 'text'
                    ];
                }
            } elseif (!$pmVersionCompare) {
                $pmVersionLink[] = [
                    'name' => __('need') . ' >= ' . self::$needDependentVersion[$packageManager],
                    'type' => 'text'
                ];
                $pmVersionLink[] = [
                    'name' => __('Please upgrade %s version', [$packageManager]),
                    'type' => 'text'
                ];
            }
        } elseif ($packageManager == 'ni') {
            $pmVersionCompare = true;
        }

        // nodejs
        $nodejsVersion        = Version::getVersion('node');
        $nodejsVersionCompare = Version::compare(self::$needDependentVersion['node'], $nodejsVersion);
        if (!$nodejsVersionCompare || !$nodejsVersion) {
            $nodejsVersionLink = [
                [
                    'name' => __('need') . ' >= ' . self::$needDependentVersion['node'],
                    'type' => 'text'
                ],
                [
                    'name'  => __('How to solve?'),
                    'title' => __('Click to see how to solve it'),
                    'type'  => 'faq',
                    'url'   => 'https://doc.buildadmin.com/guide/install/prepareNodeJs.html'
                ]
            ];
        }

        return $this->success('', [
            'npm_version'         => [
                'describe' => $npmVersion ?: __('Acquisition failed'),
                'state'    => $npmVersionCompare ? self::$ok : self::$warn,
                'link'     => $npmVersionLink ?? [],
            ],
            'nodejs_version'      => [
                'describe' => $nodejsVersion ?: __('Acquisition failed'),
                'state'    => $nodejsVersionCompare ? self::$ok : self::$warn,
                'link'     => $nodejsVersionLink ?? []
            ],
            'npm_package_manager' => [
                'describe' => $pmVersion ?: __('Acquisition failed'),
                'state'    => $pmVersionCompare ? self::$ok : self::$warn,
                'link'     => $pmVersionLink ?? [],
            ]
        ]);
    }

    /**
     * 测试数据库连接
     */
    public function testDatabase(Request $request): Response
    {
        $this->setRequest($request);
        $database = [
            'hostname' => $request->post('hostname'),
            'username' => $request->post('username'),
            'password' => $request->post('password'),
            'hostport' => $request->post('hostport'),
            'database' => '',
        ];

        $conn = $this->connectDb($database);
        if ($conn['code'] == 0) {
            return $this->error($conn['msg']);
        }
        return $this->success('', [
            'databases' => $conn['databases']
        ]);
    }

    /**
     * 系统基础配置
     * post请求=开始安装
     */
    public function baseConfig(Request $request): Response
    {
        $this->setRequest($request);
        if ($this->isInstallComplete()) {
            return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %file% file first', ['%file%' => 'public/' . self::$lockFileName]));
        }

        $envOk          = $this->commandExecutionCheck();
        $rootPath       = str_replace('\\', '/', root_path());
        $migrateCommand = 'php vendor/bin/phinx migrate';
        if ($request->isGet()) {
            return $this->success('', [
                'rootPath'            => $rootPath,
                'executionWebCommand' => $envOk,
                'migrateCommand'      => $migrateCommand,
            ]);
        }

        // Windows 下 php windows.php 会每秒检测监控目录；写入 config/.env 等会触发 taskkill 整进程，导致 POST 被中断（ERR_CONNECTION_RESET）
        Monitor::pause();
        try {
            return $this->baseConfigPost($request, $envOk, $rootPath, $migrateCommand);
        } finally {
            Monitor::resume();
        }
    }

    /**
     * 系统基础配置 POST：写入配置并执行迁移
     */
    private function baseConfigPost(Request $request, bool $envOk, string $rootPath, string $migrateCommand): Response
    {
        $connectData = $databaseParam = $request->only(['hostname', 'username', 'password', 'hostport', 'database', 'prefix']);

        // 数据库配置测试
        $connectData['database'] = '';
        $connect                 = $this->connectDb($connectData, true);
        if ($connect['code'] == 0) {
            return $this->error($connect['msg']);
        }

        // 建立数据库
        if (!in_array($databaseParam['database'], $connect['databases'])) {
            $sql = "CREATE DATABASE IF NOT EXISTS `{$databaseParam['database']}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci";
            $connect['pdo']->exec($sql);
        }

        // 写入数据库配置文件（thinkorm.php 使用 $env('database.xxx', 'default') 格式）
        $dbConfigFile    = config_path(self::$dbConfigFileName);
        $dbConfigContent = @file_get_contents($dbConfigFile);
        if ($dbConfigContent === false || $dbConfigContent === '') {
            return $this->error(__('File has no write permission:%s', ['config/' . self::$dbConfigFileName]));
        }
        $callback       = function ($matches) use ($databaseParam) {
            $key   = $matches[1];
            $value = (string) ($databaseParam[$key] ?? '');
            return "\$env('database.{$key}', '" . addslashes($value) . "')";
        };
        $dbConfigText = preg_replace_callback("/\\\$env\('database\.(hostname|database|username|password|hostport|prefix)',\s*'[^']*'\)/", $callback, $dbConfigContent);
        if ($dbConfigText === null) {
            return $this->error(__('Failed to update database config file:%s', ['config/' . self::$dbConfigFileName]));
        }
        $result       = @file_put_contents($dbConfigFile, $dbConfigText);
        if (!$result) {
            return $this->error(__('File has no write permission:%s', ['config/' . self::$dbConfigFileName]));
        }

        // 写入 .env 和 .env-example（仅使用 Dotenv 可解析的 DATABASE_XXX 格式，避免 [DATABASE] 导致解析失败）
        $databaseBlock = "\n# Database\n"
            . 'DATABASE_TYPE = mysql' . "\n"
            . 'DATABASE_HOSTNAME = ' . $databaseParam['hostname'] . "\n"
            . 'DATABASE_DATABASE = ' . $databaseParam['database'] . "\n"
            . 'DATABASE_USERNAME = ' . $databaseParam['username'] . "\n"
            . 'DATABASE_PASSWORD = ' . $databaseParam['password'] . "\n"
            . 'DATABASE_HOSTPORT = ' . $databaseParam['hostport'] . "\n"
            . 'DATABASE_CHARSET = utf8mb4' . "\n"
            . 'DATABASE_PREFIX = ' . ($databaseParam['prefix'] ?? '') . "\n";
        foreach (['.env', '.env-example'] as $envName) {
            $envFile = root_path() . $envName;
            $envFileContent = is_file($envFile) ? @file_get_contents($envFile) : '';
            if ($envFileContent !== false) {
                $cutPos = strlen($envFileContent);
                foreach (['[DATABASE]', "\n# Database\n", "\n# 数据库", "\nDATABASE_DRIVER", "\nDATABASE_TYPE"] as $marker) {
                    $pos = stripos($envFileContent, $marker);
                    if ($pos !== false && $pos < $cutPos) {
                        $cutPos = $pos;
                    }
                }
                $envFileContent = rtrim(substr($envFileContent, 0, $cutPos)) . $databaseBlock;
                $result = @file_put_contents($envFile, $envFileContent);
                if (!$result && is_file($envFile)) {
                    return $this->error(__('File has no write permission:%s', ['%s' => $envName]));
                }
            }
        }

        // 设置新的Token随机密钥key
        $oldTokenKey        = config('buildadmin.token.key');
        $newTokenKey        = Random::build('alnum', 32);
        $buildConfigFile    = config_path(self::$buildConfigFileName);
        $buildConfigContent = @file_get_contents($buildConfigFile);
        if ($buildConfigContent === false || $buildConfigContent === '') {
            return $this->error(__('File has no write permission:%s', ['config/' . self::$buildConfigFileName]));
        }
        $buildConfigContent = preg_replace("/'key'(\s+)=>(\s+)'$oldTokenKey'/", "'key'\$1=>\$2'$newTokenKey'", $buildConfigContent);
        $result             = @file_put_contents($buildConfigFile, $buildConfigContent);
        if (!$result) {
            return $this->error(__('File has no write permission:%s', ['config/' . self::$buildConfigFileName]));
        }

        // 建立安装锁文件
        $result = @file_put_contents(public_path(self::$lockFileName), date('Y-m-d H:i:s'));
        if (!$result) {
            return $this->error(__('File has no write permission:%s', ['public/' . self::$lockFileName]));
        }

        // 自动执行数据库迁移（无需手动运行 phinx 命令）
        $migrateResult = $this->runPhinxMigrate($databaseParam);
        if ($migrateResult !== true) {
            return $this->error($migrateResult);
        }

        return $this->success('', [
            'rootPath'             => $rootPath,
            'executionWebCommand'  => $envOk,
            'migrateCommand'       => $migrateCommand,
            'migrationCompleted'   => true,
        ]);
    }

    /**
     * 程序化执行 Phinx 数据库迁移
     * @param array $databaseParam 数据库连接参数
     * @return true|string 成功返回 true，失败返回错误信息
     */
    private function runPhinxMigrate(array $databaseParam): true|string
    {
        try {
            $baseDir = root_path();
            $phinxConfigPath = $baseDir . 'phinx.php';

            if (!is_file($phinxConfigPath)) {
                return __('Failed to install SQL execution:%msg%', ['%msg%' => 'phinx.php not found']);
            }

            // 临时设置环境变量，供 phinx 读取数据库配置
            $_ENV['DATABASE_HOSTNAME'] = $databaseParam['hostname'] ?? '127.0.0.1';
            $_ENV['DATABASE_DATABASE'] = $databaseParam['database'] ?? '';
            $_ENV['DATABASE_USERNAME'] = $databaseParam['username'] ?? 'root';
            $_ENV['DATABASE_PASSWORD'] = $databaseParam['password'] ?? '';
            $_ENV['DATABASE_HOSTPORT'] = $databaseParam['hostport'] ?? '3306';
            $_ENV['DATABASE_PREFIX'] = $databaseParam['prefix'] ?? '';
            putenv('DATABASE_HOSTNAME=' . $_ENV['DATABASE_HOSTNAME']);
            putenv('DATABASE_DATABASE=' . $_ENV['DATABASE_DATABASE']);
            putenv('DATABASE_USERNAME=' . $_ENV['DATABASE_USERNAME']);
            putenv('DATABASE_PASSWORD=' . $_ENV['DATABASE_PASSWORD']);
            putenv('DATABASE_HOSTPORT=' . $_ENV['DATABASE_HOSTPORT']);
            putenv('DATABASE_PREFIX=' . $_ENV['DATABASE_PREFIX']);

            $config = PhinxConfig::fromPhp($phinxConfigPath);
            $input = new ArrayInput([]);
            $output = new NullOutput();
            $manager = new PhinxManager($config, $input, $output);

            $environment = $config->getDefaultEnvironment();
            $manager->migrate($environment);
            return true;
        } catch (Throwable $e) {
            $msg = $e->getMessage();
            if ($e->getPrevious()) {
                $msg .= ' | ' . $e->getPrevious()->getMessage();
            }
            return __('Failed to install SQL execution:%msg%', ['%msg%' => $msg]);
        }
    }

    protected function isInstallComplete(): bool
    {
        if (is_file(public_path(self::$lockFileName))) {
            $contents = @file_get_contents(public_path(self::$lockFileName));
            if ($contents == self::$InstallationCompletionMark) {
                return true;
            }
        }
        return false;
    }

    /**
     * 标记命令执行完毕
     * @throws Throwable
     */
    public function commandExecComplete(Request $request): Response
    {
        $this->setRequest($request);
        if ($this->isInstallComplete()) {
            return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %file% file first', ['%file%' => 'public/' . self::$lockFileName]));
        }

        $param = $request->only(['type', 'adminname', 'adminpassword', 'sitename']);
        if ($param['type'] == 'web') {
            $result = @file_put_contents(public_path(self::$lockFileName), self::$InstallationCompletionMark);
            if (!$result) {
                return $this->error(__('File has no write permission:%s', ['public/' . self::$lockFileName]));
            }
        } else {
            // 管理员配置入库
            $adminModel             = new AdminModel();
            $defaultAdmin           = $adminModel->where('username', 'admin')->find();
            $defaultAdmin->username = $param['adminname'];
            $defaultAdmin->nickname = ucfirst($param['adminname']);
            $defaultAdmin->save();

            if (isset($param['adminpassword']) && $param['adminpassword']) {
                $adminModel->resetPassword($defaultAdmin->id, $param['adminpassword']);
            }

            // 修改站点名称
            if (class_exists(\app\admin\model\Config::class)) {
                \app\admin\model\Config::where('name', 'site_name')->update([
                    'value' => $param['sitename']
                ]);
            }
        }
        return $this->success();
    }

    /**
     * 获取命令执行检查的结果
     * @return bool 是否拥有执行命令的条件
     */
    private function commandExecutionCheck(): bool
    {
        $pm = config('terminal.npm_package_manager');
        if ($pm == 'none') {
            return false;
        }
        $check['phpPopen']             = function_exists('proc_open') && function_exists('proc_close');
        $check['npmVersionCompare']    = Version::compare(self::$needDependentVersion['npm'], Version::getVersion('npm'));
        $check['pmVersionCompare']     = Version::compare(self::$needDependentVersion[$pm], Version::getVersion($pm));
        $check['nodejsVersionCompare'] = Version::compare(self::$needDependentVersion['node'], Version::getVersion('node'));

        $envOk = true;
        foreach ($check as $value) {
            if (!$value) {
                $envOk = false;
                break;
            }
        }
        return $envOk;
    }

    /**
     * 获取安装完成后的访问地址（根据请求来源区分 API 与前端开发模式）
     * - 通过 API 访问（7979）：index#/admin、index#/（无 index 与 # 之间的斜杠）
     * - 通过前端开发服务访问（1818）：/#/admin、/#/
     */
    public function accessUrls(Request $request): Response
    {
        $this->setRequest($request);
        $host = $request->header('host', '127.0.0.1:7979');
        $port = '7979';
        if (str_contains($host, ':')) {
            $port = substr($host, strrpos($host, ':') + 1);
        }
        $scheme = $request->header('x-forwarded-proto', 'http');
        $basePath = $request instanceof \support\Request ? $request->publicBasePath() : '';
        $base = rtrim($scheme . '://' . $host, '/') . $basePath;

        if ($port === '1818') {
            $adminUrl = $base . '/#/admin';
            $frontUrl = $base . '/#/';
        } else {
            $adminUrl = $base . '/index.html#/admin';
            $frontUrl = $base . '/index.html#/';
        }

        return $this->success('', [
            'adminUrl'  => $adminUrl,
            'frontUrl'  => $frontUrl,
        ]);
    }

    /**
     * 安装指引
     */
    public function manualInstall(Request $request): Response
    {
        $this->setRequest($request);
        return $this->success('', [
            'webPath' => str_replace('\\', '/', root_path() . 'web')
        ]);
    }

    public function mvDist(Request $request): Response
    {
        $this->setRequest($request);
        if (!is_file(root_path() . self::$distDir . DIRECTORY_SEPARATOR . 'index.html')) {
            return $this->error(__('No built front-end file found, please rebuild manually!'));
        }

        if (Terminal::mvDist()) {
            return $this->success();
        }
        return $this->error(__('Failed to move the front-end file, please move it manually!'));
    }

    /**
     * 目录是否可写
     * @param $writable
     * @return string
     */
    private static function writableStateDescribe($writable): string
    {
        return $writable ? __('Writable') : __('No write permission');
    }

    /**
     * 数据库连接-获取数据表列表（使用 raw PDO）
     * @param array $database hostname, hostport, username, password, database
     * @param bool  $returnPdo
     * @return array
     */
    private function connectDb(array $database, bool $returnPdo = false): array
    {
        $host = $database['hostname'] ?? '127.0.0.1';
        $port = $database['hostport'] ?? '3306';
        $user = $database['username'] ?? '';
        $pass = $database['password'] ?? '';
        $db   = $database['database'] ?? '';

        $dsn = "mysql:host={$host};port={$port};charset=utf8mb4";
        if ($db) {
            $dsn .= ";dbname={$db}";
        }

        try {
            $pdo = new \PDO($dsn, $user, $pass, [
                \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
                \PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,
            ]);
            $pdo->query("SELECT 1")->fetchAll(\PDO::FETCH_ASSOC);
        } catch (\PDOException $e) {
            $errorMsg = mb_convert_encoding($e->getMessage() ?: 'unknown', 'UTF-8', 'UTF-8,GBK,GB2312,BIG5');
            $template = __('Database connection failed:%s');
            return [
                'code' => 0,
                'msg'  => strpos($template, '%s') !== false ? sprintf($template, $errorMsg) : $template . $errorMsg,
            ];
        }

        $databases = [];
        $databasesExclude = ['information_schema', 'mysql', 'performance_schema', 'sys'];
        $stmt = $pdo->query("SHOW DATABASES");
        $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
        $stmt->closeCursor();
        foreach ($rows as $row) {
            $dbName = $row['Database'] ?? $row['database'] ?? '';
            if ($dbName && !in_array($dbName, $databasesExclude)) {
                $databases[] = $dbName;
            }
        }

        return [
            'code'      => 1,
            'msg'       => '',
            'databases' => $databases,
            'pdo'       => $returnPdo ? $pdo : null,
        ];
    }
}
