<?php

declare(strict_types=1);

namespace app\admin\controller;

use ba\ClickCaptcha;
use ba\Random;
use app\admin\model\Admin;
use app\common\facade\Token;
use app\admin\model\AdminLog;
use app\common\library\AdminTotp;
use app\common\controller\Backend;
use support\validation\Validator;
use support\validation\ValidationException;
use Webman\Http\Request;
use support\Response;

class Index extends Backend
{
    protected array $noNeedLogin = ['logout', 'login', 'totpVerify', 'totpBindInit', 'totpBindConfirm'];
    protected array $noNeedPermission = ['index'];

    public function index(Request $request): Response
    {
        $response = $this->initializeBackend($request);
        if ($response !== null) return $response;

        $adminInfo = $this->auth->getInfo();
        $adminInfo['super'] = $this->auth->isSuperAdmin();
        unset($adminInfo['token'], $adminInfo['refresh_token']);

        $menus = $this->auth->getMenus();
        if (!$menus) {
            return $this->error(__('No background menu, please contact super administrator!'));
        }

        $apiUrl = config('buildadmin.api_url');
        if (!$apiUrl || $apiUrl === 'https://api.buildadmin.com') {
            $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
            $apiUrl = $scheme . '://' . $request->host();
        }

        return $this->success('', [
            'adminInfo' => $adminInfo,
            'menus' => $menus,
            'siteConfig' => [
                'siteName' => get_sys_config('site_name'),
                'version' => get_sys_config('version'),
                'apiUrl' => $apiUrl,
                'upload' => keys_to_camel_case(get_upload_config($request), ['max_size', 'save_name', 'allowed_suffixes', 'allowed_mime_types', 'forbidden_suffixes']),
                'cdnUrl' => full_url(),
                'cdnUrlParams' => config('buildadmin.cdn_url_params'),
                'totpEnable' => AdminTotp::isEnabled(),
            ],
        ]);
    }

    public function login(Request $request): Response
    {
        $response = $this->initializeBackend($request);
        if ($response !== null) return $response;

        if ($this->auth->isLogin()) {
            return $this->success(__('You have already logged in. There is no need to log in again~'), [
                'type' => $this->auth::LOGGED_IN
            ], $this->auth::LOGIN_RESPONSE_CODE);
        }

        $captchaSwitch = config('buildadmin.admin_login_captcha');

        if ($request->method() === 'POST') {
            $username = $request->post('username');
            $password = $request->post('password');
            $keep = $request->post('keep');

            $rules = [
                'username' => 'required|string|min:3|max:30',
                'password' => 'required|string|regex:/^(?!.*[&<>"\'\n\r]).{6,32}$/',
            ];
            $data = ['username' => $username, 'password' => $password];
            if ($captchaSwitch) {
                $rules['captchaId'] = 'required|string';
                $rules['captchaInfo'] = 'required|string';
                $data['captchaId'] = $request->post('captchaId');
                $data['captchaInfo'] = $request->post('captchaInfo');
            }

            try {
                Validator::make($data, $rules, [
                    'username.required' => __('Username'),
                    'password.required' => __('Password'),
                    'password.regex' => __('Please input correct password'),
                ])->validate();
            } catch (ValidationException $e) {
                return $this->error($e->getMessage());
            }

            if ($captchaSwitch) {
                $captchaObj = new ClickCaptcha();
                if (!$captchaObj->check($data['captchaId'], $data['captchaInfo'])) {
                    return $this->error(__('Captcha error'));
                }
            }

            AdminLog::instance($request)->setTitle(__('Login'));

            if (!$this->auth->verifyCredentials($username, $password)) {
                $msg = $this->auth->getError();
                return $this->error($msg ?: __('Incorrect user name or password!'));
            }

            if (!AdminTotp::isEnabled()) {
                if (!$this->auth->finalizeLogin((bool) $keep)) {
                    return $this->error($this->auth->getError() ?: __('Login failed'));
                }
                return $this->buildLoginSuccessResponse((bool) $keep);
            }

            if ($this->auth->hasTotpBound()) {
                $tempToken = AdminTotp::createPendingToken($this->auth->id, AdminTotp::TOKEN_TYPE_VERIFY);
                return $this->success(__('Please enter Google Authenticator code'), [
                    'type' => $this->auth::NEED_TOTP,
                    'tempToken' => $tempToken,
                    'username' => $this->auth->username,
                ]);
            }

            $tempToken = AdminTotp::createPendingToken($this->auth->id, AdminTotp::TOKEN_TYPE_BIND);
            return $this->success(__('Please bind Google Authenticator'), [
                'type' => $this->auth::NEED_BIND_TOTP,
                'tempToken' => $tempToken,
                'username' => $this->auth->username,
            ]);
        }

        return $this->success('', [
            'captcha' => $captchaSwitch,
            'totpEnable' => AdminTotp::isEnabled(),
        ]);
    }

    public function totpBindInit(Request $request): Response
    {
        $response = $this->initializeBackend($request);
        if ($response !== null) return $response;

        if (!AdminTotp::isEnabled()) {
            return $this->error(__('Google Authenticator is disabled'));
        }

        if ($request->method() !== 'POST') {
            return $this->error(__('Method not allowed'), [], 0, ['statusCode' => 405]);
        }

        $tempToken = (string) $request->post('tempToken', '');
        $adminId = AdminTotp::resolvePendingToken($tempToken, AdminTotp::TOKEN_TYPE_BIND);
        if ($adminId <= 0) {
            return $this->error(__('TOTP session expired, please login again'));
        }

        if (!$this->auth->loadAdminById($adminId)) {
            return $this->error($this->auth->getError());
        }
        if ($this->auth->hasTotpBound()) {
            return $this->error(__('Google Authenticator already bound'));
        }

        $secret = AdminTotp::generateSecret();
        $label = $this->auth->username;
        $qrCode = AdminTotp::getQrDataUri($label, $secret);

        return $this->success('', [
            'secret' => $secret,
            'qrCode' => $qrCode,
            'username' => $label,
        ]);
    }

    public function totpBindConfirm(Request $request): Response
    {
        $response = $this->initializeBackend($request);
        if ($response !== null) return $response;

        if (!AdminTotp::isEnabled()) {
            return $this->error(__('Google Authenticator is disabled'));
        }

        if ($request->method() !== 'POST') {
            return $this->error(__('Method not allowed'), [], 0, ['statusCode' => 405]);
        }

        $tempToken = (string) $request->post('tempToken', '');
        $secret = (string) $request->post('secret', '');
        $code = (string) $request->post('code', '');
        $keep = (bool) $request->post('keep');

        if ($tempToken === '' || $secret === '' || $code === '') {
            return $this->error(__('Parameter %s can not be empty', ['']));
        }

        $adminId = AdminTotp::resolvePendingToken($tempToken, AdminTotp::TOKEN_TYPE_BIND);
        if ($adminId <= 0) {
            return $this->error(__('TOTP session expired, please login again'));
        }

        if (!AdminTotp::verifyCode($secret, $code)) {
            return $this->error(__('Google Authenticator code error'));
        }

        if (!$this->auth->loadAdminById($adminId)) {
            return $this->error($this->auth->getError());
        }
        if ($this->auth->hasTotpBound()) {
            AdminTotp::deletePendingToken($tempToken);
            return $this->error(__('Google Authenticator already bound'));
        }

        $encrypted = AdminTotp::encryptSecret($secret);
        if ($encrypted === '') {
            return $this->error(__('Google Authenticator bind failed'));
        }

        Admin::where('id', $adminId)->update([
            'totp_secret' => $encrypted,
            'totp_bind_time' => time(),
        ]);

        AdminTotp::deletePendingToken($tempToken);

        if (!$this->auth->finalizeLogin($keep)) {
            return $this->error($this->auth->getError() ?: __('Google Authenticator bind failed'));
        }

        return $this->buildLoginSuccessResponse($keep, __('Google Authenticator bound successfully'));
    }

    public function totpVerify(Request $request): Response
    {
        $response = $this->initializeBackend($request);
        if ($response !== null) return $response;

        if (!AdminTotp::isEnabled()) {
            return $this->error(__('Google Authenticator is disabled'));
        }

        if ($request->method() !== 'POST') {
            return $this->error(__('Method not allowed'), [], 0, ['statusCode' => 405]);
        }

        $tempToken = (string) $request->post('tempToken', '');
        $code = (string) $request->post('code', '');
        $keep = (bool) $request->post('keep');

        if ($tempToken === '' || $code === '') {
            return $this->error(__('Parameter %s can not be empty', ['']));
        }

        $adminId = AdminTotp::resolvePendingToken($tempToken, AdminTotp::TOKEN_TYPE_VERIFY);
        if ($adminId <= 0) {
            return $this->error(__('TOTP session expired, please login again'));
        }

        if (!$this->auth->loadAdminById($adminId)) {
            return $this->error($this->auth->getError());
        }
        if (!$this->auth->hasTotpBound()) {
            AdminTotp::deletePendingToken($tempToken);
            return $this->error(__('Google Authenticator not bound'));
        }

        $encrypted = $this->auth->getAdmin()->getData('totp_secret');
        if (!AdminTotp::verifyStoredCode($encrypted, $code)) {
            $this->auth->loginFailed();
            return $this->error(__('Google Authenticator code error'));
        }

        AdminTotp::deletePendingToken($tempToken);

        if (!$this->auth->finalizeLogin($keep)) {
            return $this->error($this->auth->getError() ?: __('Login failed'));
        }

        return $this->buildLoginSuccessResponse($keep);
    }

    public function logout(Request $request): Response
    {
        $response = $this->initializeBackend($request);
        if ($response !== null) return $response;

        if ($request->method() === 'POST') {
            $refreshToken = $request->post('refreshToken', '');
            if ($refreshToken) {
                Token::delete((string) $refreshToken);
            }
            $this->auth->logout();
            return $this->success();
        }
        return $this->error(__('Method not allowed'), [], 0, ['statusCode' => 405]);
    }

    private function buildLoginSuccessResponse(bool $keep, ?string $message = null): Response
    {
        $userInfo = $this->auth->getInfo();
        $adminId = $this->auth->id;
        $keepTime = (int) config('buildadmin.admin_token_keep_time', 86400 * 3);
        if (empty($userInfo['token']) && $adminId) {
            $userInfo['token'] = Random::uuid();
            Token::set($userInfo['token'], \app\admin\library\Auth::TOKEN_TYPE, $adminId, $keepTime);
        }
        if (empty($userInfo['refresh_token']) && $keep && $adminId) {
            $userInfo['refresh_token'] = Random::uuid();
            Token::set($userInfo['refresh_token'], \app\admin\library\Auth::TOKEN_TYPE . '-refresh', $adminId, 2592000);
        }
        return $this->success($message ?: __('Login succeeded!'), [
            'userInfo' => $userInfo,
        ]);
    }
}
