<?php

namespace Acms\Services\Setup;

use Acms\Services\Setup\Http\Request;
use Acms\Services\Setup\Tool\SetupTool;
use Acms\Services\Setup\Tool\ToolErrors;
use Acms\Services\Setup\Tool\ToolView;
use Acms\Services\Update\Database\Schema;
use Acms\Services\Update\Engine;
use Acms\Services\Facades\Application;
use Acms\Services\Facades\Cache;
use Acms\Services\Facades\Database;
use Acms\Services\Facades\LocalStorage;
use Field;

/**
 * システムアップデートツール（update.php）。旧 setup/update.php の Surfacer_Update。
 *
 * DB スキーマのアップデート（GET ?step=update → 実行 → 不要カラム削除画面 drop）と、不要カラムの削除
 * （drop → complete）を行う多段ツール。旧 Surfacer は POST の hidden `step` と detectStep() の 3 分岐で
 * 状態を密輸していたが、GET の `step` と「更新実行 → その場で drop 画面」という本来の流れを素直に写す。
 * スキーマ操作は Acms\Services\Update\Database\Schema、更新本体は Acms\Services\Update\Engine に委譲する。
 * screen トークン（apply / update:error / drop / complete）は Twig を壊さないよう旧来のまま維持する。
 */
class SystemUpdate implements SetupTool
{
    public function template(): string
    {
        return '@setup/update.twig';
    }

    public function show(Request $request): ToolView
    {
        // GET: アップデート確認画面。ALTER 権限をチェックし、可否を提示する。
        ['permitted' => $permitted, 'mysqlErr' => $mysqlErr] = $this->checkAlterPermission();
        $errors = $permitted ? new ToolErrors() : (new ToolErrors())->with('alter_privilege');

        return ToolView::on('apply', $this->confirmData($permitted, $mysqlErr), $errors);
    }

    public function handle(Request $request, SetupSession $session): ToolView
    {
        @set_time_limit(0);

        // どの段階の submit か（アップデート実行 or 不要カラム削除）は GET の step で決まる。
        $stage = $request->get()->get('step');
        if ($stage === 'update') {
            return $this->runUpdate();
        }
        if ($stage === 'drop') {
            return $this->runDrop($request->post());
        }

        // 想定外の submit は確認画面へ戻す。
        return $this->show($request);
    }

    /**
     * スキーマ/config を最新へ更新する。成功したら不要カラム削除（drop）画面へ進む。
     */
    private function runUpdate(): ToolView
    {
        $errors = new ToolErrors();

        if (!LocalStorage::exists(PATH_CONFIG)) {
            $errors = $errors->with('config_exists');
        }
        if (!SetupCommon::checkPermission(PATH_CONFIG, '666')) {
            $errors = $errors->with('config_permission');
        }

        ['permitted' => $permitted, 'mysqlErr' => $mysqlErr] = $this->checkAlterPermission();
        if (!$permitted) {
            $errors = $errors->with('alter_privilege');
        }

        if (!$errors->isEmpty()) {
            return ToolView::on('update:error', $this->confirmData($permitted, $mysqlErr), $errors);
        }

        /** @var \Acms\Services\Update\LoggerFactory $loggerFactory */
        $loggerFactory = Application::make('update.logger');
        $logger = $loggerFactory->createLogger('web');

        $updateService = new Engine($logger);
        $updateService->validate(true);
        $updateService->dbUpdate();

        $this->flushCaches();

        return ToolView::on('drop', $this->dropData());
    }

    /**
     * 選択された不要カラムを削除し、アップデートを完了する。
     */
    private function runDrop(Field $post): ToolView
    {
        $Schema = new Schema(dsn());

        $tables = array_reduce(
            $post->getArray('column'),
            function (array $tables, $value) {
                list($table, $column) = explode('@', $value);
                if (!array_key_exists($table, $tables)) {
                    return array_merge($tables, [$table => [$column]]);
                }
                return array_merge($tables, [$table => array_merge($tables[$table], [$column])]);
            },
            []
        );
        foreach ($tables as $table => $columns) {
            $Schema->dropColumns($table, $columns);
        }

        $this->flushCaches();

        return ToolView::on('complete', $this->completeData());
    }

    /**
     * @return array{permitted: bool, mysqlErr: string}
     */
    private function checkAlterPermission(): array
    {
        if (SetupCommon::checkAlterPerm()) {
            return ['permitted' => true, 'mysqlErr' => ''];
        }

        return ['permitted' => false, 'mysqlErr' => Database::errorCode() . ': ' . implode(' ', Database::errorInfo())];
    }

    /**
     * apply / update:error 画面のデータ。
     *
     * @return array<string, mixed>
     */
    private function confirmData(bool $permitted, string $mysqlErr): array
    {
        return [
            'version' => VERSION,
            'setupDir' => SETUP_DIR,
            'httpRoot' => SetupCommon::fixPathSeparator(HTTP_ROOT),
            'dropTables' => [],
            'hasDrop' => false,
            'permitted' => $permitted,
            'mysqlErr' => $mysqlErr,
        ];
    }

    /**
     * drop 画面のデータ（未使用カラム一覧）。
     *
     * @return array<string, mixed>
     */
    private function dropData(): array
    {
        $Schema = new Schema(dsn());
        $Schema->setSchema();

        $dropTables = [];
        foreach ($Schema->listUp($Schema->schema) as $tb) {
            $unusedColumns = $Schema->unusedColumns($tb);
            if ($unusedColumns === []) {
                continue;
            }
            $dropTables[$tb] = $unusedColumns;
        }

        return [
            'version' => VERSION,
            'setupDir' => SETUP_DIR,
            'httpRoot' => SetupCommon::fixPathSeparator(HTTP_ROOT),
            'dropTables' => $dropTables,
            'hasDrop' => $dropTables !== [],
        ];
    }

    /**
     * complete 画面のデータ。
     *
     * @return array<string, mixed>
     */
    private function completeData(): array
    {
        return [
            'version' => VERSION,
            'setupDir' => SETUP_DIR,
            'httpRoot' => SetupCommon::fixPathSeparator(HTTP_ROOT),
        ];
    }

    private function flushCaches(): void
    {
        Cache::flush('page');
        Cache::flush('template');
        Cache::flush('config');
        Cache::flush('field');
        Cache::flush('module');
    }
}
