<?php

namespace app\common\library;

use Throwable;
use ba\Random;
use support\think\Db;
use app\common\model\User;
use app\common\facade\Token;
use app\common\service\GameHotDataRedis;
use Webman\Http\Request;

/**
 * 公共权限类（会员权限类）
 */
class Auth extends \ba\Auth
{
    public const LOGIN_RESPONSE_CODE = 303;
    public const NEED_LOGIN = 'need login';
    public const LOGGED_IN = 'logged in';
    public const TOKEN_TYPE = 'user';

    protected bool $loginEd = false;
    protected string $error = '';
    protected ?User $model = null;
    protected string $token = '';
    protected string $refreshToken = '';
    protected int $keepTime = 86400;
    protected int $refreshTokenKeepTime = 2592000;

    /** 注册失败原因：`username`/`email` 表示账号已占用，供接口返回友好提示 */
    protected string $registerDuplicateKind = '';

    protected array $allowFields = [
        'id', 'username', 'nickname', 'email', 'phone', 'avatar', 'gender', 'birthday',
        'coin', 'channel_id', 'risk_flags', 'uuid',
        'join_time', 'motto', 'last_login_time', 'last_login_ip',
    ];

    public function __construct(array $config = [])
    {
        parent::__construct(array_merge([
            'auth_group'        => 'user_group',
            'auth_group_access' => '',
            'auth_rule'         => 'user_rule',
        ], $config));

        $this->setKeepTime((int)config('buildadmin.user_token_keep_time', 86400));
    }

    public function __get($name): mixed
    {
        return $this->model?->$name;
    }

    public static function instance(array $options = []): Auth
    {
        $request = $options['request'] ?? (function_exists('request') ? request() : null);
        unset($options['request']);
        if ($request && !isset($request->userAuth)) {
            $request->userAuth = new static($options);
        }
        return $request && isset($request->userAuth) ? $request->userAuth : new static($options);
    }

    public function init($token): bool
    {
        $tokenData = Token::get($token);
        if ($tokenData) {
            Token::tokenExpirationCheck($tokenData);
            $userId = $tokenData['user_id'];
            if ($tokenData['type'] == self::TOKEN_TYPE && $userId > 0) {
                $this->model = GameHotDataRedis::userModelFromCacheOrDb($userId);
                if (!$this->model) {
                    $this->setError('Account not exist');
                    return false;
                }
                $status = $this->model->status ?? '';
                if ($status === 'disable' || $status === 0 || $status === '0') {
                    $this->setError('Account disabled');
                    return false;
                }
                $channelId = intval($this->model->channel_id ?? 0);
                if ($channelId > 0 && !$this->isChannelEnabled($channelId)) {
                    $this->setError('Channel disabled');
                    return false;
                }
                $this->token = $token;
                $this->loginEd = true;
                return true;
            }
        }
        $this->setError('Token login failed');
        $this->reset();
        return false;
    }

    public function register(string $username, string $password = '', string $phone = '', string $email = '', int $group = 1, array $extend = []): bool
    {
        $this->registerDuplicateKind = '';
        $request = function_exists('request') ? request() : null;
        $ip = $request ? $request->getRealIp() : '0.0.0.0';

        if ($email && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
            $this->setError(__('Email'));
            return false;
        }
        $isMobileUsername = preg_match('/^1[3-9]\d{9}$/', $username) === 1;
        $isNormalUsername = preg_match('/^[a-zA-Z][a-zA-Z0-9_]{2,15}$/', $username) === 1;
        if ($username && !$isMobileUsername && !$isNormalUsername) {
            $this->setError(__('Username'));
            return false;
        }
        if (User::where('email', $email)->find() && $email) {
            $this->registerDuplicateKind = 'email';
            $this->setError(__('Email') . ' ' . __('already exists'));
            return false;
        }
        if (User::where('username', $username)->find()) {
            $this->registerDuplicateKind = 'username';
            $this->setError(__('Username') . ' ' . __('already exists'));
            return false;
        }
        if ($phone !== '' && User::where('phone', $phone)->find()) {
            $this->registerDuplicateKind = 'phone';
            $this->setError(__('Mobile') . ' ' . __('already exists'));
            return false;
        }

        $nickname = preg_replace_callback('/1[3-9]\d{9}/', fn($m) => substr($m[0], 0, 3) . '****' . substr($m[0], 7), $username);
        $time = time();
        $data = [
            'group_id' => $group,
            'nickname' => $nickname,
            'join_ip' => $ip,
            'join_time' => $time,
            'last_login_ip' => $ip,
            'last_login_time' => $time,
            'status' => 1,
            'uuid' => User::generateUniquePublicCode10(),
            'remark' => User::formatLoginRemark($time, $ip),
        ];
        $data = array_merge(compact('username', 'password', 'phone', 'email'), $data, $extend);
        $channelIdForRegister = isset($data['channel_id']) ? intval($data['channel_id']) : 0;
        if ($channelIdForRegister > 0 && !$this->isChannelEnabled($channelIdForRegister)) {
            $this->setError('Channel disabled');
            return false;
        }

        Db::startTrans();
        try {
            $this->model = User::create($data);
            $this->token = Random::uuid();
            Token::set($this->token, self::TOKEN_TYPE, $this->model->id, $this->keepTime);
            Db::commit();

            if ($password) {
                $this->model->resetPassword($this->model->id, $password);
            }

            event_trigger('userRegisterSuccess', $this->model);
        } catch (Throwable $e) {
            $this->setError($e->getMessage());
            Db::rollback();
            return false;
        }
        return true;
    }

    public function login(string $username, string $password, bool $keep): bool
    {
        $accountType = false;
        if (preg_match('/^1[3-9]\d{9}$/', $username)) $accountType = 'phone';
        elseif (filter_var($username, FILTER_VALIDATE_EMAIL)) $accountType = 'email';
        elseif (preg_match('/^[a-zA-Z][a-zA-Z0-9_]{2,15}$/', $username)) $accountType = 'username';
        if (!$accountType) {
            $this->setError('Account not exist');
            return false;
        }

        $this->model = User::where($accountType, $username)->find();
        if (!$this->model) {
            $this->setError('Account not exist');
            return false;
        }
        $status = $this->model->status ?? '';
        if ($status === 'disable' || $status === 0 || $status === '0') {
            $this->setError('Account disabled');
            return false;
        }
        $channelId = intval($this->model->channel_id ?? 0);
        if ($channelId > 0 && !$this->isChannelEnabled($channelId)) {
            $this->setError('Channel disabled');
            return false;
        }

        $userLoginRetry = config('buildadmin.user_login_retry');
        if ($userLoginRetry && $this->model->last_login_time) {
            $lastLoginTs = is_numeric($this->model->last_login_time) ? (int)$this->model->last_login_time : strtotime($this->model->last_login_time);
            if ($this->model->login_failure > 0 && $lastLoginTs > 0 && time() - $lastLoginTs >= 86400) {
                $this->model->login_failure = 0;
                $this->model->save();
                $this->model = User::where($accountType, $username)->find();
            }
            if ($this->model->login_failure >= $userLoginRetry) {
                $this->setError('Please try again after 1 day');
                return false;
            }
        }

        if (!verify_password($password, $this->model->password, ['salt' => $this->model->salt ?? ''])) {
            $this->loginFailed();
            $this->setError('Password is incorrect');
            return false;
        }

        if (config('buildadmin.user_sso')) {
            Token::clear(self::TOKEN_TYPE, $this->model->id);
            Token::clear(self::TOKEN_TYPE . '-refresh', $this->model->id);
        }

        if ($keep) $this->setRefreshToken($this->refreshTokenKeepTime);
        return $this->loginSuccessful();
    }

    public function direct(int $userId): bool
    {
        $this->model = User::find($userId);
        if (!$this->model) return false;
        if (config('buildadmin.user_sso')) {
            Token::clear(self::TOKEN_TYPE, $this->model->id);
            Token::clear(self::TOKEN_TYPE . '-refresh', $this->model->id);
        }
        return $this->loginSuccessful();
    }

    public function loginSuccessful(): bool
    {
        if (!$this->model) return false;
        $request = function_exists('request') ? request() : null;
        $ip = $request ? $request->getRealIp() : '0.0.0.0';
        if (!$this->token) {
            $this->token = Random::uuid();
            Token::set($this->token, self::TOKEN_TYPE, $this->model->id, $this->keepTime);
        }
        $this->model->startTrans();
        try {
            $this->model->login_failure = 0;
            $this->model->last_login_time = time();
            $this->model->last_login_ip = $ip;
            $this->model->remark = User::formatLoginRemark(time(), $ip);
            $this->model->save();
            $this->loginEd = true;
            $this->model->commit();
        } catch (Throwable $e) {
            $this->model->rollback();
            if ($this->token) {
                Token::delete($this->token);
                $this->token = '';
            }
            $this->setError($e->getMessage());
            return false;
        }
        return true;
    }

    public function loginFailed(): bool
    {
        if (!$this->model) return false;
        $request = function_exists('request') ? request() : null;
        $ip = $request ? $request->getRealIp() : '0.0.0.0';
        $this->model->startTrans();
        try {
            $this->model->login_failure++;
            $this->model->last_login_time = time();
            $this->model->last_login_ip = $ip;
            $this->model->save();
            $this->model->commit();
        } catch (Throwable $e) {
            $this->model->rollback();
        }
        return $this->reset();
    }

    public function logout(): bool
    {
        if (!$this->loginEd) {
            $this->setError('You are not logged in');
            return false;
        }
        return $this->reset();
    }

    public function isLogin(): bool
    {
        return $this->loginEd;
    }

    public function getUser(): User
    {
        return $this->model;
    }

    public function getToken(): string
    {
        return $this->token;
    }

    public function setRefreshToken(int $keepTime = 0): void
    {
        $this->refreshToken = Random::uuid();
        Token::set($this->refreshToken, self::TOKEN_TYPE . '-refresh', $this->model->id, $keepTime);
    }

    public function getRefreshToken(): string
    {
        return $this->refreshToken;
    }

    public function getUserInfo(): array
    {
        if (!$this->model) return [];
        $info = $this->model->toArray();
        $info = array_intersect_key($info, array_flip($this->getAllowFields()));
        $info['token'] = $this->getToken();
        $info['refresh_token'] = $this->getRefreshToken();
        return $info;
    }

    public function getAllowFields(): array
    {
        return $this->allowFields;
    }

    public function setAllowFields($fields): void
    {
        $this->allowFields = $fields;
    }

    public function setKeepTime(int $keepTime = 0): void
    {
        $this->keepTime = $keepTime;
    }

    public function check(string $name, int $uid = 0, string $relation = 'or', string $mode = 'url'): bool
    {
        return parent::check($name, $uid ?: $this->id, $relation, $mode);
    }

    public function getRuleList(int $uid = 0): array
    {
        return parent::getRuleList($uid ?: $this->id);
    }

    public function getRuleIds(int $uid = 0): array
    {
        return parent::getRuleIds($uid ?: $this->id);
    }

    public function getMenus(int $uid = 0): array
    {
        return parent::getMenus($uid ?: $this->id);
    }

    public function isSuperUser(): bool
    {
        return in_array('*', $this->getRuleIds());
    }

    public function setError(string $error): Auth
    {
        $this->error = $error;
        return $this;
    }

    public function getError(): string
    {
        return $this->error ? __($this->error) : '';
    }

    public function getRegisterDuplicateKind(): string
    {
        return $this->registerDuplicateKind;
    }

    protected function reset(bool $deleteToken = true): bool
    {
        if ($deleteToken && $this->token) {
            Token::delete($this->token);
        }
        $this->token = '';
        $this->loginEd = false;
        $this->model = null;
        $this->refreshToken = '';
        $this->registerDuplicateKind = '';
        $this->setError('');
        $this->setKeepTime((int)config('buildadmin.user_token_keep_time', 86400));
        return true;
    }

    private function isChannelEnabled(int $channelId): bool
    {
        $status = Db::name('channel')->where('id', $channelId)->value('status');
        if ($status === null || $status === '') {
            return false;
        }
        return intval($status) === 1;
    }
}
