<?php
// +----------------------------------------------------------------------
// | 萤火商城系统 [ 致力于通过产品和服务,帮助商家高效化开拓市场 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2017~2023 https://www.yiovo.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed 这不是一个自由软件,不允许对程序代码以任何形式任何目的的再发行
// +----------------------------------------------------------------------
// | Author: 萤火科技 <admin@yiovo.com>
// +----------------------------------------------------------------------
declare (strict_types=1);

namespace app\api\service\passport;

use app\api\model\{dealer\Referee as RefereeModel,
    Setting as SettingModel,
    UploadFile as UploadFileModel,
    User as UserModel};
use app\api\service\{passport\Party as PartyService, user\Oauth as OauthService};
use app\api\validate\passport\Login as ValidateLogin;
use app\common\enum\Client as ClientEnum;
use app\common\enum\Setting as SettingEnum;
use app\common\service\BaseService;
use cores\exception\BaseException;
use think\facade\Cache;
use yiovo\captcha\facade\CaptchaApi;

/**
 * 服务类:用户登录
 * Class Login
 * @package app\api\service\passport
 */
class Login extends BaseService
{
    /**
     * 用户信息 (登录成功后才记录)
     * @var UserModel|null $userInfo
     */
    private ?UserModel $userInfo;

    // 用于生成token的自定义盐
    const TOKEN_SALT = 'user_salt';

    /**
     * 执行用户登录
     * @param array $data
     * @return bool
     * @throws BaseException
     * @throws \think\Exception
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    public function login(array $data): bool
    {
        // 数据验证
        $this->validate($data);
        // 自动登录注册
        $this->register($data);
        // 保存第三方用户信息
        $this->createUserOauth($this->getUserId(), $data['isParty'], $data['partyData']);
        // 记录登录态
        return $this->setSession();
    }

    /**
     * 快捷登录:微信小程序用户
     * @param array $form
     * @return bool
     * @throws BaseException
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     * @throws \think\Exception
     */
    public function loginMpWx(array $form): bool
    {
        // 获取微信小程序登录态(session)
        $wxSession = PartyService::getMpWxSession($form['partyData']['code']);
        // 判断openid是否存在
        $userId = OauthService::getUserIdByOauthId($wxSession['openid'], ClientEnum::MP_WEIXIN);
        // 获取用户信息
        $userInfo = !empty($userId) ? UserModel::detail($userId) : null;
        // 用户信息存在, 更新登录信息
        if (!empty($userInfo)) {
            // 更新用户登录信息
            $this->updateUser($userInfo, true, $form['partyData']);
            // 记录登录态
            return $this->setSession();
        }
        // 用户信息不存在 => 注册新用户 或者 跳转到绑定手机号页
        $setting = SettingModel::getItem(SettingEnum::REGISTER);
        // 后台设置了需强制绑定手机号, 返回前端isBindMobile, 跳转到手机号验证页
        if ($setting['isForceBindMpweixin']) {
            throwError('当前用户未绑定手机号', null, ['isBindMobile' => true]);
        }
        // 后台未开启强制绑定手机号, 直接保存新用户
        if (!$setting['isForceBindMpweixin']) {
            // 推荐人ID
            $refereeId = $form['refereeId'] ?? null;
            // 用户不存在: 创建一个新用户
            $this->createUser('', true, $form['partyData'], (int)$refereeId);
            // 保存第三方用户信息
            $this->createUserOauth($this->getUserId(), true, $form['partyData']);
        }
        // 记录登录态
        return $this->setSession();
    }

    /**
     * 是否需要填写昵称头像 (微信小程序端)
     * @param string $code
     * @return bool
     * @throws BaseException
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     * @throws \think\Exception
     */
    public function isPersonalMpweixin(string $code): bool
    {
        // 后台需开启填写微信头像和昵称
        $setting = SettingModel::getItem(SettingEnum::REGISTER);
        if (!$setting['isPersonalMpweixin']) {
            return false;
        }
        // 获取微信小程序登录态 (session)
        $wxSession = PartyService::getMpWxSession($code);
        // 判断用户是否存在 (openid)
        return !OauthService::getUserIdByOauthId($wxSession['openid'], ClientEnum::MP_WEIXIN);
    }

    /**
     * 快捷登录:微信公众号用户
     * @param array $form
     * @return bool
     * @throws BaseException
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     * @throws \think\Exception
     */
    public function loginWxOfficial(array $form): bool
    {
        // 解密encryptedData -> 拿到openid
        $plainData = OauthService::wxDecryptData($form['partyData']['encryptedData'], $form['partyData']['iv']);
        // 判断openid是否存在
        $userId = OauthService::getUserIdByOauthId($plainData['openid'], ClientEnum::WXOFFICIAL);
        // 获取用户信息
        $userInfo = !empty($userId) ? UserModel::detail($userId) : null;
        // 用户信息存在, 更新登录信息
        if (!empty($userInfo)) {
            // 更新用户登录信息
            $this->updateUser($userInfo, true, $form['partyData']);
            // 记录登录态
            return $this->setSession();
        }
        // 用户信息不存在 => 注册新用户 或者 跳转到绑定手机号页
        $setting = SettingModel::getItem(SettingEnum::REGISTER);
        // 后台设置了需强制绑定手机号, 返回前端isBindMobile, 跳转到手机号验证页
        if ($setting['isForceBindWxofficial']) {
            throwError('当前用户未绑定手机号', null, ['isBindMobile' => true]);
        }
        // 后台未开启强制绑定手机号, 直接保存新用户
        if (!$setting['isForceBindWxofficial']) {
            // 推荐人ID
            $refereeId = $form['refereeId'] ?? null;
            // 用户不存在: 创建一个新用户
            $this->createUser('', true, $form['partyData'], (int)$refereeId);
            // 保存第三方用户信息
            $this->createUserOauth($this->getUserId(), true, $form['partyData']);
        }
        // 记录登录态
        return $this->setSession();
    }

    /**
     * 快捷登录:微信小程序用户
     * @param array $form
     * @return bool
     * @throws BaseException
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     * @throws \think\Exception
     */
    public function loginMpWxMobile(array $form): bool
    {
        // 获取微信小程序登录态(session)
        $wxSession = PartyService::getMpWxSession($form['code']);
        // 解密encryptedData -> 拿到手机号
        $plainData = OauthService::wxDecryptData($form['encryptedData'], $form['iv'], $wxSession['session_key']);
        // 整理登录注册数据
        if (empty($form['partyData']['oauth'])) {
            $form['partyData']['oauth'] = 'MP-WEIXIN';
            $form['partyData']['code'] = $form['code'];
        }
        $loginData = [
            'mobile' => $plainData['purePhoneNumber'],
            'isParty' => $form['isParty'],
            'partyData' => $form['partyData'],
            'refereeId' => $form['refereeId'] ?? null,
        ];
        // 自动登录注册
        $this->register($loginData);
        // 保存第三方用户信息
        $this->createUserOauth($this->getUserId(), $loginData['isParty'], $loginData['partyData']);
        // 记录登录态
        return $this->setSession();
    }

    /**
     * 快捷登录:支付宝小程序用户
     * @param array $form
     * @return bool
     * @throws BaseException
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     * @throws \think\Exception
     */
    public function loginMpAlipay(array $form): bool
    {
        // 获取支付宝小程序登录态(session)
        $mpAlipayOauth = PartyService::getMpAlipayOauth($form['partyData']['code']);
        // 判断openid是否存在
        $userId = OauthService::getUserIdByOauthId($mpAlipayOauth['user_id'], ClientEnum::MP_ALIPAY);
        // 获取用户信息
        $userInfo = !empty($userId) ? UserModel::detail($userId) : null;
        // 用户信息存在, 更新登录信息
        if (!empty($userInfo)) {
            // 更新用户登录信息
            $this->updateUser($userInfo, true, $form['partyData']);
            // 记录登录态
            return $this->setSession();
        }
        // 用户信息不存在 => 注册新用户 或者 跳转到绑定手机号页
        $setting = SettingModel::getItem(SettingEnum::REGISTER);
        // 后台设置了需强制绑定手机号, 返回前端isBindMobile, 跳转到手机号验证页
        if ($setting['isForceBindMpAlipay']) {
            throwError('当前用户未绑定手机号', null, ['isBindMobile' => true]);
        }
        // 后台未开启强制绑定手机号, 直接保存新用户
        if (!$setting['isForceBindMpAlipay']) {
            // 推荐人ID
            $refereeId = $form['refereeId'] ?? null;
            // 用户不存在: 创建一个新用户
            $this->createUser('', true, $form['partyData'], (int)$refereeId);
            // 保存第三方用户信息
            $this->createUserOauth($this->getUserId(), true, $form['partyData']);
        }
        // 记录登录态
        return $this->setSession();
    }

    /**
     * 绑定推荐关系
     * @param int $userId 当前用户ID
     * @param int $refereeId 推荐人ID
     * @return void
     * @throws \think\Exception
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    private function bindRelation(int $userId, int $refereeId): void
    {
        $refereeId > 0 && RefereeModel::createRelation($userId, $refereeId);
    }

    /**
     * 保存oauth信息(第三方用户信息)
     * @param int $userId 用户ID
     * @param bool $isParty 是否为第三方用户
     * @param array $partyData 第三方用户数据
     * @return void
     * @throws BaseException
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    private function createUserOauth(int $userId, bool $isParty, array $partyData = []): void
    {
        if ($isParty) {
            $Oauth = new PartyService;
            $Oauth->createUserOauth($userId, $partyData);
        }
    }

    /**
     * 当前登录的用户信息
     * @return UserModel
     */
    public function getUserInfo(): UserModel
    {
        return $this->userInfo;
    }

    /**
     * 当前登录的用户ID
     * @return int
     */
    private function getUserId(): int
    {
        return (int)$this->getUserInfo()['user_id'];
    }

    /**
     * 自动登录注册
     * @param array $data
     * @throws BaseException
     * @throws \think\Exception
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    private function register(array $data): void
    {
        // 查询用户是否已存在
        // 用户存在: 更新用户登录信息
        $userInfo = UserModel::detail(['mobile' => $data['mobile']]);
        if ($userInfo) {
            $this->updateUser($userInfo, $data['isParty'], $data['partyData']);
            return;
        }
        // 推荐人ID
        $refereeId = (int)$data['refereeId'] ?? null;
        // 用户不存在: 创建一个新用户
        $this->createUser($data['mobile'], $data['isParty'], $data['partyData'], $refereeId);
    }

    /**
     * 新增用户
     * @param string $mobile 手机号
     * @param bool $isParty 是否存在第三方用户信息
     * @param array $partyData 用户信息(第三方)
     * @param int|null $refereeId 推荐人ID
     * @return void
     * @throws \think\Exception
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    private function createUser(string $mobile, bool $isParty, array $partyData = [], ?int $refereeId = null): void
    {
        // 用户信息
        $data = [
            'mobile' => $mobile,
            'nick_name' => !empty($mobile) ? \hide_mobile($mobile) : '',
            'platform' => \getPlatform(),
            'last_login_time' => \time(),
            'store_id' => $this->storeId
        ];
        // 写入用户信息(第三方)
        if ($isParty === true && !empty($partyData)) {
            $partyUserInfo = PartyService::partyUserInfo($partyData, true);
            $data = array_merge($data, $partyUserInfo);
        }
        // 新增用户记录
        $model = new UserModel;
        $model->save($data);
        // 将微信用户昵称添加编号便于后台管理, 例如:微信用户_10001
        if (\in_array($data['nick_name'], ['微信用户', '支付宝用户'])) {
            $model->save(['nick_name' => "{$data['nick_name']}_{$model['user_id']}"]);
        }
        // 记录头像文件上传者
        if (isset($data['avatar_id']) && $data['avatar_id'] > 0) {
            UploadFileModel::setUploaderId($data['avatar_id'], (int)$model['user_id']);
        }
        // 记录用户信息
        $this->userInfo = $model;
        // 记录推荐人关系
        $this->bindRelation($this->getUserId(), (int)$refereeId);
    }

    /**
     * 更新用户登录信息
     * @param UserModel $userInfo
     * @param bool $isParty 是否存在第三方用户信息
     * @param array $partyData 用户信息(第三方)
     */
    private function updateUser(UserModel $userInfo, bool $isParty, array $partyData = []): void
    {
        // 用户信息
        $data = [
            'last_login_time' => \time(),
            'store_id' => $this->storeId
        ];
        // 写入用户信息(第三方)
        // 如果不需要每次登录都更新微信用户头像昵称, 下面几行代码可以屏蔽掉
//        if ($isParty === true && !empty($partyData)) {
//            $partyUserInfo = PartyService::partyUserInfo($partyData);
//            $data = array_merge($data, $partyUserInfo);
//        }
//        // 记录头像文件上传者
//        if (isset($data['avatar_id']) && $data['avatar_id'] > 0) {
//            UploadFileModel::setUploaderId($data['avatar_id'], $userInfo['user_id']);
//        }
        // 更新用户记录
        $userInfo->save($data);
        // 记录用户信息
        $this->userInfo = $userInfo;
    }

    /**
     * 记录登录态
     * @return bool
     * @throws BaseException
     */
    private function setSession(): bool
    {
        empty($this->userInfo) && \throwError('未找到用户信息');
        // 登录的token
        $token = $this->getToken($this->getUserId());
        // 记录缓存, 30天
        Cache::set($token, [
            'user' => $this->userInfo,
            'store_id' => $this->storeId,
            'is_login' => true,
        ], 86400 * 30);
        return true;
    }

    /**
     * 数据验证
     * @param array $data
     * @return void
     * @throws BaseException
     */
    private function validate(array $data): void
    {
        // 数据验证
        $validate = new ValidateLogin;
        if (!$validate->check($data)) {
            throwError($validate->getError());
        }
        // 验证短信验证码是否匹配
        try {
            CaptchaApi::checkSms($data['smsCode'], $data['mobile']);
        } catch (\Exception $e) {
            throwError($e->getMessage() ?: '短信验证码不正确');
        }
    }

    /**
     * 获取登录的token
     * @param int $userId
     * @return string
     */
    public function getToken(int $userId): string
    {
        static $token = '';
        if (empty($token)) {
            $token = $this->makeToken($userId);
        }
        return $token;
    }

    /**
     * 生成用户认证的token
     * @param int $userId
     * @return string
     */
    private function makeToken(int $userId): string
    {
        $storeId = $this->storeId;
        // 生成一个不会重复的随机字符串
        $guid = \get_guid_v4();
        // 当前时间戳 (精确到毫秒)
        $timeStamp = \microtime(true);
        // 自定义一个盐
        $salt = self::TOKEN_SALT;
        return md5("{$storeId}_{$timeStamp}_{$userId}_{$guid}_{$salt}");
    }
}