<?php

namespace Acms\Services\Setup;

use Acms\Services\Facades\Application;
use Acms\Services\Facades\Common;
use Acms\Services\Facades\Database;
use Acms\Services\Facades\RateLimiter;
use Acms\Services\Login\LockKeyPolicy;
use Acms\Services\Setup\Http\Request;
use Acms\Services\Setup\Http\Response;
use SQL;

/**
 * setup メンテナンスメニュー（index.php）の Action。
 *
 * ログイン（管理者パスワード照合・レート制限 W3）、ログアウト、各メンテナンスツールへの導線描画を担う。
 * インストール済み判定は InstalledGuardMiddleware（REQUIRE_INSTALLED）へ、セッション開始は
 * SessionMiddleware へ移した。描画は setup 専用 Twig（`@setup/index/*.twig`）。
 */
class Detector
{
    // W3: setup ログインの総当たりロック閾値。サイト側の config に依存させず setup 固有の固定値。
    private const LOGIN_TRIAL_TIME = 5;    // 分: 失敗回数を数える時間窓
    private const LOGIN_TRIAL_NUMBER = 5;  // この回数連続で失敗するとロック
    private const LOGIN_LOCK_TIME = 15;    // 分: ロックの継続時間

    public function __construct(private readonly SetupSession $session)
    {
    }

    public function __invoke(Request $request): Response
    {
        $post = $request->post();
        $error = [];

        /**
         * try login（W3: 管理者アカウントへの総当たり対策）
         *
         * 本体ログインと同じ Common::validateLockPost / logLockPost（lock / lock_source テーブル＋
         * 接続元 IP キーの永続ロック）を使う。閾値はサイト config に依存させず setup 固有の固定値。
         * ログインは認証境界のため CSRF は課さない（CsrfMiddleware も index には掛けない）。
         */
        $tryingLogin = $post->get('action') === 'login' && !$this->session->isAuthenticated();
        if ($tryingLogin) {
            // 他のログインハンドラ（Member/Signin.php 等）と同じく、lockKey 計算前に空白を除去する。
            // 除去しないと "admin" と "admin " が別バケットになり、末尾空白は SQL 側の
            // PAD SPACE 照合で無視される（user_code/user_mail の一致判定と揃わない）ため。
            $inputId = (string) preg_replace("/(\s|　)/", "", $post->get('id'));
            $lockKey = LockKeyPolicy::forInputId('Setup_Login', $inputId);

            if (RateLimiter::validateLockPost($lockKey, self::LOGIN_TRIAL_TIME, self::LOGIN_TRIAL_NUMBER, self::LOGIN_LOCK_TIME) === false) {
                $error[] = 'login_locked';
            } elseif ($userId = $this->loginAuth($inputId, $post->get('pass'))) {
                $this->session->authenticate($userId);
            } else {
                RateLimiter::logLockPost($lockKey);
                $error[] = 'login_auth';
            }
        }

        /**
         * logout
         */
        if ($post->get('action') === 'logout') {
            $this->session->destroy();
            return Response::redirect('./index.php');
        }

        // メニュー/ログアウト等のフォームへ CSRF トークンを注入する（ログインは認証境界のため検証しない）。
        $token = $this->session->ensureFormToken();
        /** @var \Acms\Services\Template\Twig $twig */
        $twig = Application::make('template.setup.twig');

        if ($this->session->isAuthenticated()) {
            $version = $this->currentSystemVersion();
            // DB のほうが新しい（setup が古い）場合はアップデートを進めさせず、差し替えを促して停止する。
            if (version_compare($version, VERSION, '>')) {
                return Response::html("<p>Setupプログラムがデータベースのバージョンよりも古い可能性があります。最新のsetupに差し替えてから実行してください。(Setup: " . VERSION . ", Database: $version )</p>"); // phpcs:ignore
            }
            return Response::html($twig->renderTemplate('@setup/index/menu.twig', [
                'setupFormToken' => $token,
                'copyrightYear' => (int)date('Y'),
                'version' => VERSION,
                'setupDir' => SETUP_DIR,
                // version < VERSION（要更新）と取得不能（空）＝旧バージョン扱いは true。version = VERSION は false。
                'needsUpdate' => version_compare($version, VERSION, '<'),
            ]));
        }

        return Response::html($twig->renderTemplate('@setup/index/curtain.twig', [
            'setupFormToken' => $token,
            'copyrightYear' => (int)date('Y'),
            'version' => VERSION,
            'errors' => $error,
        ]));
    }

    private function currentSystemVersion(): string
    {
        $SQL = SQL::newSelect('sequence');
        $SQL->addSelect('sequence_system_version');
        return (string)Database::query($SQL->get(dsn()), 'one');
    }

    private function loginAuth(string $id, string $pass): false|int
    {
        $SQL = SQL::newSelect('user');
        $SQL->addWhereOpr('user_auth', 'administrator');
        $W  = SQL::newWhere();
        $W->addWhereOpr('user_code', $id, '=', 'OR');
        $W->addWhereOpr('user_mail', $id, '=', 'OR');
        $SQL->addWhere($W);

        $all = Database::query($SQL->get(dsn()), 'all');
        // array_filter は元配列のキーを保持するため、パスワード一致行が先頭でないと $all[0] が
        // 未定義になり正当なログインを取りこぼす。array_values で 0 起点に振り直してから先頭を取る。
        $matched = array_values(array_filter($all, function ($user) use ($pass) {
            return acmsUserPasswordVerify($pass, $user['user_pass'], getPasswordGeneration($user));
        }));
        if ($matched === []) {
            return false;
        }
        return (int) $matched[0]['user_id'];
    }
}
