import {Config, Inject, Provide, Scope, ScopeEnum} from '@midwayjs/core';
import {UserService} from '../../sys/authority/service/user-service.js';
import jwt from 'jsonwebtoken';
import {AuthException, CommonException, Need2FAException} from "@certd/lib-server";
import {RoleService} from '../../sys/authority/service/role-service.js';
import {UserEntity} from '../../sys/authority/entity/user.js';
import {SysSettingsService} from '@certd/lib-server';
import {SysPrivateSettings} from '@certd/lib-server';
import {cache, utils} from '@certd/basic';
import {LoginErrorException} from '@certd/lib-server/dist/basic/exception/login-error-exception.js';
import {CodeService} from '../../basic/service/code-service.js';
import {TwoFactorService} from "../../mine/service/two-factor-service.js";
import {UserSettingsService} from '../../mine/service/user-settings-service.js';
import {isPlus} from "@certd/plus-core";

/**
 * 系统用户
 */
@Provide()
@Scope(ScopeEnum.Request, {allowDowngrade: true})
export class LoginService {
  @Inject()
  userService: UserService;
  @Inject()
  roleService: RoleService;

  @Inject()
  codeService: CodeService;
  @Config('auth.jwt')
  private jwt: any;

  @Inject()
  sysSettingsService: SysSettingsService;
  @Inject()
  userSettingsService: UserSettingsService;
  @Inject()
  twoFactorService: TwoFactorService;

  checkIsBlocked(username: string) {
    const blockDurationKey = `login_block_duration:${username}`;
    const value = cache.get(blockDurationKey);
    if (value) {
      const ttl = cache.getRemainingTTL(blockDurationKey)
      const leftMin = Math.ceil(ttl / 1000 / 60);
      throw new CommonException(`账号被锁定，请${leftMin}分钟后重试`);
    }
  }

  clearCacheOnSuccess(username: string) {
    cache.delete(`login_error_times:${username}`);
    cache.delete(`login_block_times:${username}`);
    cache.delete(`login_block_duration:${username}`);
  }

  addErrorTimes(username: string, errorMessage: string) {
    const errorTimesKey = `login_error_times:${username}`;
    const blockTimesKey = `login_block_times:${username}`;
    const blockDurationKey = `login_block_duration:${username}`;
    let blockTimes = cache.get(blockTimesKey);
    // let maxWaitMin = 2;
    const maxRetryTimes = blockTimes > 1 ? 3 : 5;
    if (blockTimes == null) {
      blockTimes = 0;
    }
    // maxWaitMin = maxWaitMin * blockTimes;
    // let ttl = maxWaitMin * 60 * 1000;

    let errorTimes = cache.get(errorTimesKey);

    if (errorTimes == null) {
      errorTimes = 0;
    }
    errorTimes += 1;
    const ttl24H = 24 * 60 * 60 * 1000;
    cache.set(errorTimesKey, errorTimes, {
      ttl: ttl24H,
    });
    if (errorTimes > maxRetryTimes) {
      blockTimes += 1;
      cache.set(blockTimesKey, blockTimes, {
        ttl: ttl24H,
      });
      //按照block次数指数递增，最长24小时
      const ttl = Math.min(blockTimes * blockTimes * 60 * 1000, ttl24H);
      const leftMin = Math.ceil(ttl / 1000 / 60);
      cache.set(blockDurationKey, 1, {
        ttl: ttl,
      })
      // 清除error次数
      cache.delete(errorTimesKey);
      throw new LoginErrorException(`登录失败次数过多，请${leftMin}分钟后重试`, 0);
    }
    const leftTimes = maxRetryTimes - errorTimes;
    if (leftTimes < 3) {
      throw new LoginErrorException(`登录失败(${errorMessage})，剩余尝试次数：${leftTimes}`, leftTimes);
    }
    throw new LoginErrorException(errorMessage, leftTimes);
  }


  async loginBySmsCode(req: { mobile: string; phoneCode: string; smsCode: string; randomStr: string }) {

    this.checkIsBlocked(req.mobile)

    const smsChecked = await this.codeService.checkSmsCode({
      mobile: req.mobile,
      phoneCode: req.phoneCode,
      smsCode: req.smsCode,
      randomStr: req.randomStr,
      throwError: false,
    });

    const {mobile, phoneCode} = req;
    if (!smsChecked) {
      this.addErrorTimes(mobile, '验证码错误');
    }
    let info = await this.userService.findOne({phoneCode, mobile: mobile});
    if (info == null) {
      //用户不存在，注册
      info = await this.userService.register('mobile', {
        phoneCode,
        mobile,
        password: '',
      } as any);
    }
    this.clearCacheOnSuccess(mobile);
    return this.onLoginSuccess(info);
  }

  async loginByPassword(req: { username: string; password: string; phoneCode: string }) {
    this.checkIsBlocked(req.username)
    const {username, password, phoneCode} = req;
    const info = await this.userService.findOne([{username: username}, {email: username}, {
      phoneCode,
      mobile: username
    }]);
    if (info == null) {
      throw new CommonException('用户名或密码错误');
    }
    const right = await this.userService.checkPassword(password, info.password, info.passwordVersion);
    if (!right) {
      this.addErrorTimes(username, '用户名或密码错误');
    }
    this.clearCacheOnSuccess(username);
    return this.onLoginSuccess(info);
  }

  async checkTwoFactorEnabled(userId: number) {
    //检查是否开启多重认证
    if (!isPlus()) {
      return true
    }

    const twoFactorSetting = await this.twoFactorService.getSetting(userId)

    const authenticatorSetting = twoFactorSetting.authenticator
    if (authenticatorSetting.enabled) {
      //要检查
      const randomKey = utils.id.simpleNanoId(12)
      cache.set(`login_2fa_code:${randomKey}`, userId, {
        ttl: 60 * 1000 * 2,
      })
      throw new Need2FAException('已开启多重认证，请在2分钟内输入OPT验证码',randomKey)
    }

  }

  async loginByTwoFactor(req: { loginId: string; verifyCode: string }) {
    //检查是否开启多重认证
    if (!isPlus()) {
      throw new Error('本功能需要开通专业版')
    }
    const userId = cache.get(`login_2fa_code:${req.loginId}`)
    if (!userId) {
      throw new AuthException('已超时，请返回重新登录')
    }
    await this.twoFactorService.verifyAuthenticatorCode(userId, req.verifyCode)

    const user = await this.userService.info(userId);
    if (!user) {
      throw new AuthException('用户不存在')
    }
    return this.generateToken(user)
  }

  private async onLoginSuccess(info: UserEntity) {
    if (info.status === 0) {
      throw new CommonException('用户已被禁用');
    }
    await this.checkTwoFactorEnabled(info.id)
    return this.generateToken(info);
  }


  /**
   * 生成token
   * @param user 用户对象
   * @param roleIds
   */
  async generateToken(user: UserEntity) {
    const roleIds = await this.roleService.getRoleIdsByUserId(user.id);
    const tokenInfo = {
      username: user.username,
      id: user.id,
      roles: roleIds,
    };
    const expire = this.jwt.expire;

    const setting = await this.sysSettingsService.getSetting<SysPrivateSettings>(SysPrivateSettings);
    const jwtSecret = setting.jwtKey;

    const token = jwt.sign(tokenInfo, jwtSecret, {
      expiresIn: expire,
    });

    return {
      token,
      expire,
    };
  }
}
