<?php

declare(strict_types=1);

namespace App\Controllers;

use App\Models\Config;
use App\Models\InviteCode;
use App\Models\LoginIp;
use App\Models\User;
use App\Services\Auth;
use App\Services\Cache;
use App\Services\Captcha;
use App\Services\Filter;
use App\Services\Mail;
use App\Services\MFA\FIDO;
use App\Services\MFA\TOTP;
use App\Services\MFA\WebAuthn;
use App\Services\RateLimit;
use App\Services\Reward;
use App\Utils\Cookie;
use App\Utils\Hash;
use App\Utils\ResponseHelper;
use App\Utils\Tools;
use Exception;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Message\ResponseInterface;
use Ramsey\Uuid\Uuid;
use RedisException;
use Slim\Http\Response;
use Slim\Http\ServerRequest;
use function array_rand;
use function date;
use function explode;
use function strlen;
use function strtolower;
use function time;
use function trim;

final class AuthController extends BaseController
{
    /**
     * @throws Exception
     */
    public function login(ServerRequest $request, Response $response, array $args): ResponseInterface
    {
        $captcha = [];

        if (Config::obtain('enable_login_captcha')) {
            $captcha = Captcha::generate();
        }

        return $response->write($this->view()
            ->assign('base_url', $_ENV['baseUrl'])
            ->assign('captcha', $captcha)
            ->fetch('auth/login.tpl'));
    }

    public function loginHandle(ServerRequest $request, Response $response, array $args): ResponseInterface
    {
        if (Config::obtain('enable_login_captcha') && ! Captcha::verify($request->getParams())) {
            return $response->withJson([
                'ret' => 0,
                'msg' => '系统无法接受你的验证结果，请刷新页面后重试。',
            ]);
        }

        $password = $request->getParam('password');
        $rememberMe = $request->getParam('remember_me') === 'true' ? 1 : 0;
        $email = strtolower(trim($this->antiXss->xss_clean($request->getParam('email'))));
        $redir = $this->antiXss->xss_clean(Cookie::get('redir')) ?? '/user';
        $user = (new User())->where('email', $email)->first();
        $loginIp = new LoginIp();

        if ($user === null) {
            $loginIp->collectLoginIP($_SERVER['REMOTE_ADDR'], 1);

            return $response->withJson([
                'ret' => 0,
                'msg' => '邮箱或者密码错误',
            ]);
        }

        if (! Hash::checkPassword($user->pass, $password)) {
            $loginIp->collectLoginIP($_SERVER['REMOTE_ADDR'], 1, $user->id);

            return $response->withJson([
                'ret' => 0,
                'msg' => '邮箱或者密码错误',
            ]);
        }

        $mfaStatus = $user->checkMfaStatus();
        if ($mfaStatus['require']) {
            $redis = (new Cache())->initRedis();
            $redis->setex('mfa_login_' . session_id(), 300, json_encode([
                'userid' => $user->id,
                'method' => $mfaStatus,
                'redir' => $redir,
                'remember_me' => $rememberMe,
            ]));

            return $response
                ->withHeader('HX-Redirect', '/auth/mfa')
                ->withJson([
                    'ret' => 1,
                    'msg' => '请完成二步认证',
            ]);
        }

        $time = $rememberMe ? 86400 * ($_ENV['rememberMeDuration'] ?? 7) : 3600; // Cookie 过期时间

        Auth::login($user->id, $time);
        // 记录登录成功
        $loginIp->collectLoginIP($_SERVER['REMOTE_ADDR'], 0, $user->id);
        $user->last_login_time = time();
        $user->save();

        return $response->withHeader('HX-Redirect', $redir);
    }

    public function mfaPage(ServerRequest $request, Response $response, $next): ResponseInterface
    {
        $redis = (new Cache())->initRedis();
        $mfa_session = $redis->get('mfa_login_' . session_id());
        if ($mfa_session === false) {
            return $response->withStatus(302)->withHeader('Location', '/auth/login');
        }
        $mfa_session = json_decode($mfa_session, true);
        return $response->write(
            $this->view()
                ->assign('base_url', $_ENV['baseUrl'])
                ->assign('method', $mfa_session['method'])
                ->fetch('auth/mfa.tpl')
        );
    }

    /**
     * @throws Exception
     */
    public function register(ServerRequest $request, Response $response, $next): ResponseInterface
    {
        $captcha = [];

        if (Config::obtain('enable_reg_captcha')) {
            $captcha = Captcha::generate();
        }

        $invite_code = $this->antiXss->xss_clean($request->getParam('code'));

        return $response->write(
            $this->view()
                ->assign('invite_code', $invite_code)
                ->assign('base_url', $_ENV['baseUrl'])
                ->assign('captcha', $captcha)
                ->fetch('auth/register.tpl')
        );
    }

    /**
     * @throws RedisException
     */
    public function sendVerify(ServerRequest $request, Response $response, $next): ResponseInterface
    {
        if (Config::obtain('reg_email_verify')) {
            $email = strtolower(trim($this->antiXss->xss_clean($request->getParam('email'))));

            if ($email === '') {
                return ResponseHelper::error($response, '未填写邮箱');
            }

            // check email format
            $email_check = Filter::checkEmailFilter($email);

            if (! $email_check) {
                return ResponseHelper::error($response, '无效的邮箱');
            }

            if (! (new RateLimit())->checkRateLimit('email_request_ip', $request->getServerParam('REMOTE_ADDR')) ||
                ! (new RateLimit())->checkRateLimit('email_request_address', $email)
            ) {
                return ResponseHelper::error($response, '你的请求过于频繁，请稍后再试');
            }

            $user = (new User())->where('email', $email)->first();

            if ($user !== null) {
                return ResponseHelper::error($response, '此邮箱已经注册');
            }

            $email_code = Tools::genRandomChar(6);
            $redis = (new Cache())->initRedis();
            $redis->setex('email_verify:' . $email_code, Config::obtain('email_verify_code_ttl'), $email);

            try {
                Mail::send(
                    $email,
                    $_ENV['appName'] . '- 验证邮件',
                    'verify_code.tpl',
                    [
                        'code' => $email_code,
                        'expire' => date('Y-m-d H:i:s', time() + Config::obtain('email_verify_code_ttl')),
                    ]
                );
            } catch (Exception|ClientExceptionInterface) {
                return ResponseHelper::error($response, '邮件发送失败，请联系网站管理员。');
            }

            return ResponseHelper::success($response, '验证码发送成功，请查收邮件。');
        }

        return ResponseHelper::error($response, '站点未启用邮件验证');
    }

    /**
     * @throws Exception
     */
    public function registerHelper(
        Response $response,
        $name,
        $email,
        $password,
        $invite_code,
        $imtype,
        $imvalue,
        $money,
        $is_admin_reg
    ): ResponseInterface {
        $redir = $this->antiXss->xss_clean(Cookie::get('redir')) ?? '/user';
        $configs = Config::getClass('reg');
        // do reg user
        $user = new User();

        $user->user_name = $name;
        $user->email = $email;
        $user->remark = '';
        $user->pass = Hash::passwordHash($password);
        $user->passwd = Tools::genRandomChar(16);
        $user->uuid = Uuid::uuid4();
        $user->api_token = Tools::genRandomChar(32);
        $user->port = Tools::getSsPort();
        $user->u = 0;
        $user->d = 0;
        $user->method = $configs['reg_method'];
        $user->im_type = $imtype;
        $user->im_value = $imvalue;
        $user->transfer_enable = Tools::gbToB($configs['reg_traffic']);
        $user->auto_reset_day = Config::obtain('free_user_reset_day');
        $user->auto_reset_bandwidth = Config::obtain('free_user_reset_bandwidth');
        $user->daily_mail_enable = $configs['reg_daily_report'];

        if ($money > 0) {
            $user->money = $money;
        } else {
            $user->money = 0;
        }

        $user->ref_by = 0;

        if ($invite_code !== '') {
            $invite = (new InviteCode())->where('code', $invite_code)->first();

            if ($invite !== null) {
                $user->ref_by = $invite->user_id;
            }
        }

        $user->class = $configs['reg_class'];
        $user->class_expire = date('Y-m-d H:i:s', time() + (int) $configs['reg_class_time'] * 86400);
        $user->node_iplimit = $configs['reg_ip_limit'];
        $user->node_speedlimit = $configs['reg_speed_limit'];
        $user->reg_date = date('Y-m-d H:i:s');
        $user->reg_ip = $_SERVER['REMOTE_ADDR'];
        $user->theme = $_ENV['theme'];
        $user->locale = $_ENV['locale'];
        $random_group = Config::obtain('random_group');

        if ($random_group === '') {
            $user->node_group = 0;
        } else {
            $user->node_group = $random_group[array_rand(explode(',', $random_group))];
        }

        $user->last_login_time = time();

        if ($user->save() && ! $is_admin_reg) {
            if ($user->ref_by !== 0) {
                Reward::issueRegReward($user->id, $user->ref_by);
            }

            Auth::login($user->id, 3600);
            (new LoginIp())->collectLoginIP($_SERVER['REMOTE_ADDR'], 0, $user->id);

            return $response->withHeader('HX-Redirect', $redir);
        }

        return ResponseHelper::error($response, '未知错误');
    }

    /**
     * @throws RedisException
     * @throws Exception
     */
    public function registerHandle(ServerRequest $request, Response $response, array $args): ResponseInterface
    {
        if (Config::obtain('reg_mode') === 'close') {
            return ResponseHelper::error($response, '未开放注册。');
        }

        if (Config::obtain('enable_reg_captcha') && ! Captcha::verify($request->getParams())) {
            return ResponseHelper::error($response, '系统无法接受你的验证结果，请刷新页面后重试。');
        }

        $tos = $request->getParam('tos') === 'true' ? 1 : 0;
        $email = strtolower(trim($this->antiXss->xss_clean($request->getParam('email'))));
        $name = $this->antiXss->xss_clean($request->getParam('name'));
        $password = $request->getParam('password');
        $confirm_password = $request->getParam('confirm_password');
        $invite_code = $this->antiXss->xss_clean(trim($request->getParam('invite_code')));

        if (! $tos) {
            return ResponseHelper::error($response, '请同意服务条款');
        }

        if (strlen($password) < 8) {
            return ResponseHelper::error($response, '密码请大于8位');
        }

        if ($password !== $confirm_password) {
            return ResponseHelper::error($response, '两次密码输入不符');
        }

        if ($invite_code === '' && Config::obtain('reg_mode') === 'invite') {
            return ResponseHelper::error($response, '邀请码不能为空');
        }

        if ($invite_code !== '') {
            $invite = (new InviteCode())->where('code', $invite_code)->first();

            if ($invite === null) {
                return ResponseHelper::error($response, '邀请码无效');
            }

            $ref_user = (new User())->where('id', $invite->user_id)->first();

            if ($ref_user === null) {
                return ResponseHelper::error($response, '邀请码无效');
            }
        }

        $imtype = 0;
        $imvalue = '';

        // check email format
        $email_check = Filter::checkEmailFilter($email);

        if (! $email_check) {
            return ResponseHelper::error($response, '无效的邮箱');
        }
        // check email
        $user = (new User())->where('email', $email)->first();

        if ($user !== null) {
            return ResponseHelper::error($response, '无效的邮箱');
        }

        if (Config::obtain('reg_email_verify')) {
            $redis = (new Cache())->initRedis();
            $email_verify_code = trim($this->antiXss->xss_clean($request->getParam('emailcode')));
            $email_verify = $redis->get('email_verify:' . $email_verify_code);

            if (! $email_verify) {
                return ResponseHelper::error($response, '你的邮箱验证码不正确');
            }

            $redis->del('email_verify:' . $email_verify_code);
        }

        return $this->registerHelper($response, $name, $email, $password, $invite_code, $imtype, $imvalue, 0, 0);
    }

    public function logout(ServerRequest $request, Response $response, $next): Response
    {
        Auth::logout();

        return $response->withStatus(302)->withHeader('Location', '/auth/login');
    }

    public function webauthnRequest(ServerRequest $request, Response $response, $next): ResponseInterface
    {
        return $response->withJson(WebAuthn::AssertRequest());
    }

    public function webauthnHandle(ServerRequest $request, Response $response, $next): ResponseInterface
    {
        $data = $this->antiXss->xss_clean((array) $request->getParsedBody());
        $redir = $this->antiXss->xss_clean(Cookie::get('redir')) ?? '/user';
        $result = WebAuthn::AssertHandle($data);
        if ($result['ret'] === 1) {
            $user = $result['user'];
            if ($user === null) {
                return $response->withJson([
                    'ret' => 0,
                    'msg' => '用户不存在',
                ]);
            }
            $rememberMe = $request->getParam('remember_me') === 'true';
            $time = $rememberMe ? 86400 * ($_ENV['rememberMeDuration'] ?? 7) : 3600;
            Auth::login($user->id, $time);
            $loginIp = new LoginIp();
            $loginIp->collectLoginIP($_SERVER['REMOTE_ADDR'], 0, $user->id);
            $user->last_login_time = time();
            $user->save();
            return $response->withJson([
                'ret' => 1,
                'msg' => '登录成功',
                'redir' => $redir,
            ]);
        }
        return $response->withJson($result);
    }

    public function totpHandle(ServerRequest $request, Response $response, $next): ResponseInterface
    {
        $redis = (new Cache())->initRedis();
        $login_session = $redis->get('mfa_login_' . session_id());
        if ($login_session === false) {
            return $response->withJson(['ret' => 0, 'msg' => '登录会话已过期'])->withHeader('HX-Redirect', '/auth/login');
        }
        $login_session = json_decode($login_session, true);
        $code = $this->antiXss->xss_clean($request->getParam('code'));
        $user = (new User())->where('id', $login_session['userid'])->first();
        if ($user === null) {
            return $response->withJson(['ret' => 0, 'msg' => '用户不存在'])->withHeader('HX-Redirect', '/auth/login');
        }
        $result = TOTP::AssertHandle($user, $code);
        if ($result['ret'] === 1) {
            $redis->del('mfa_login_' . session_id());
            $rememberMe = $login_session['remember_me'];
            $time = $rememberMe ? 86400 * ($_ENV['rememberMeDuration'] ?? 7) : 3600;
            Auth::login($user->id, $time);
            $loginIp = new LoginIp();
            $loginIp->collectLoginIP($_SERVER['REMOTE_ADDR'], 0, $user->id);
            $user->last_login_time = time();
            $user->save();
            return $response
                ->withHeader('HX-Redirect', $login_session['redir'])
                ->withJson(['ret' => 1, 'msg' => '登录成功']);
        }
        return $response->withJson($result);
    }

    public function fidoRequest(ServerRequest $request, Response $response, $next): ResponseInterface
    {
        $redis = (new Cache())->initRedis();
        $login_session = $redis->get('mfa_login_' . session_id());
        if ($login_session === false) {
            return $response->withJson(['ret' => 0, 'msg' => '登录会话已过期'])->withHeader('HX-Redirect', '/auth/login');
        }
        $login_session = json_decode($login_session, true);
        $user = (new User())->where('id', $login_session['userid'])->first();
        if ($user === null) {
            return $response->withJson(['ret' => 0, 'msg' => '用户不存在'])->withHeader('HX-Redirect', '/auth/login');
        }
        return $response->withJson(FIDO::AssertRequest($user));
    }

    public function fidoHandle(ServerRequest $request, Response $response, $next): ResponseInterface
    {
        $redis = (new Cache())->initRedis();
        $login_session = $redis->get('mfa_login_' . session_id());
        if ($login_session === false) {
            return $response->withJson(['ret' => 0, 'msg' => '登录会话已过期'])->withHeader('HX-Redirect', '/auth/login');
        }
        $login_session = json_decode($login_session, true);
        $data = $this->antiXss->xss_clean((array) $request->getParsedBody());
        $user = (new User())->where('id', $login_session['userid'])->first();
        if ($user === null) {
            return $response->withJson(['ret' => 0, 'msg' => '用户不存在'])->withHeader('HX-Redirect', '/auth/login');
        }
        $result = FIDO::AssertHandle($user, $data);
        if ($result['ret'] === 1) {
            $redis->del('mfa_login_' . session_id());
            $rememberMe = $login_session['remember_me'];
            $time = $rememberMe ? 86400 * ($_ENV['rememberMeDuration'] ?? 7) : 3600;
            Auth::login($user->id, $time);
            $loginIp = new LoginIp();
            $loginIp->collectLoginIP($_SERVER['REMOTE_ADDR'], 0, $user->id);
            $user->last_login_time = time();
            $user->save();
            return $response->withJson(['ret' => 1, 'msg' => '登录成功', 'redir' => $login_session['redir']]);
        }
        return $response->withJson($result);
    }
}
