import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'
import { Redis } from 'ioredis'
import { AliyunOssConfig } from 'life-helper-config'
import { RedisService } from 'nestjs-redis'
import { OssService } from 'src/shared/oss/oss.service'
import { WeixinService } from 'src/shared/weixin/weixin.service'
import { v4 as uuidv4 } from 'uuid'
import { Authentication, AuthenticationStatus, QrcodeProfile } from './qrcode.model'

@Injectable()
export class QrcodeService {
  private readonly logger = new Logger(QrcodeService.name)

  private readonly redis: Redis

  /** 存储二维码的 OSS 绑定的 URL */
  private readonly ossURL: string = AliyunOssConfig.res.url

  /** 用于存储二维码图片的目录名称 */
  private readonly dirname = 'wxacode'

  constructor(private readonly redisService: RedisService, private readonly weixinService: WeixinService, private readonly ossService: OssService) {
    this.redis = this.redisService.getClient()
  }

  /**
   * 获取检验码对应的 Redis 键名
   *
   * @param code 检验码
   */
  private getRedisKey(code: string): string {
    return `auth:authentication:code:${code}`
  }

  /**
   * 获取用于存储校验码列表的 Redis 键名
   */
  private getListRedisKey(): string {
    return 'auth:authentication:list'
  }

  /**
   * 根据校验码获取对应的二维码 URL
   *
   * @param code 检验码
   */
  private getQrcodeUrl(code: string): string {
    return this.ossURL + '/' + this.dirname + '/' + code
  }

  /**
   * 生成一份身份认证凭证信息,存于 Redis,后续操作均围绕着这个认证信息
   */
  private async createAuthentication(): Promise<string> {
    /**
     * 校验码
     *
     *
     * ### 为什么去掉短横线?
     *
     * ```markdown
     * 1. 生成小程序码环节的参数 `scene` 最多支持 32 个字符,uuid 去掉短横线后刚好 32 个字符。
     * ```
     */
    const code = uuidv4().replace(/-/gu, '')

    const authentication: Authentication = {
      status: AuthenticationStatus.Created,
      createTime: Date.now(),
    }

    const rKey = this.getRedisKey(code)

    /** 有效期 */
    const expiration = 3600 * 24 * 10

    await this.redis.set(rKey, JSON.stringify(authentication), 'EX', expiration)
    return code
  }

  /**
   * 生成用于扫码登录的二维码
   *
   * ### 备注
   *
   * ```markdown
   * 1. 目前实际上使用小程序码,非标准的二维码。
   * ```
   */
  private async generateQrcode(): Promise<string> {
    /** 小程序中定义的页面 */
    const page = 'pages/auth/login-confirm/login-confirm'

    /** 校验码 */
    const code = await this.createAuthentication()

    /** 存储于 OSS 的文件路径 */
    const filename = this.dirname + '/' + code

    const buf = await this.weixinService.getUnlimitedWxacode({ scene: code, page })
    await this.ossService.upload(filename, buf, { headers: { 'Content-Type': 'image/png' } })

    return code
  }

  /**
   * 新增随机个(1~3)二维码,并将数据添加到二维码列表中
   *
   * ### 备注
   *
   * ```markdown
   * 1. 这个方法处理除了使用资源进行存储外,无任何副作用,可任意无限次调用。
   * ```
   */
  private async addQrcode(): Promise<void> {
    const counter = Math.ceil(Math.random() * 3)

    for (let i = 0; i < counter; i++) {
      const rListKey: string = this.getListRedisKey()
      const code = await this.generateQrcode()
      await this.redis.rpush(rListKey, code)
    }
  }

  /**
   * 获取二维码信息
   */
  async getQrcode(): Promise<QrcodeProfile> {
    // 异步执行,不要加 `await`
    this.addQrcode()

    /** 校验码 */
    let code: string

    const rListKey: string = this.getListRedisKey()
    code = await this.redis.lpop(rListKey)

    if (!code) {
      code = await this.generateQrcode()
    }

    const url = this.getQrcodeUrl(code)
    return { code, url }
  }

  /**
   * 扫码操作
   *
   * @param userId 用户 ID
   * @param code 校验码
   */
  async scan(userId: number, code: string): Promise<Authentication> {
    const rKey = this.getRedisKey(code)

    const result = await this.redis.get(rKey)
    if (!result) {
      throw new HttpException({ message: '当前二维码已失效,请刷新后重新扫描!' }, HttpStatus.BAD_REQUEST)
    }

    const authen: Authentication = JSON.parse(result)
    if (authen.status === AuthenticationStatus.Created) {
      authen.status = AuthenticationStatus.Scanned
      authen.scanTime = Date.now()
      authen.scanUserId = userId

      await this.redis.set(rKey, JSON.stringify(authen), 'EX', 3600 * 2)
    }

    return authen
  }

  /**
   * 确认登录操作
   *
   * @param userId 用户 ID
   * @param code 校验码
   */
  async confirm(userId: number, code: string): Promise<Authentication> {
    const rKey = this.getRedisKey(code)

    const result = await this.redis.get(rKey)
    if (!result) {
      throw new HttpException({ message: '当前二维码已失效,请刷新后重新扫描!' }, HttpStatus.BAD_REQUEST)
    }

    const authen: Authentication = JSON.parse(result)
    if (authen.status === AuthenticationStatus.Created || authen.status === AuthenticationStatus.Scanned) {
      authen.status = AuthenticationStatus.Confirmed
      authen.confirmTime = Date.now()
      authen.confirmUserId = userId

      await this.redis.set(rKey, JSON.stringify(authen), 'EX', 60 * 10)
    }

    return authen
  }

  /**
   * 标记校验码已使用
   *
   * @param code 校验码
   *
   *
   * ### 说明
   *
   * ```markdown
   * 1. 在扫码登录控制器方法内使用,验证完成后,发放登录凭证,并标记该校验码已使用。
   * ```
   */
  async consume(code: string): Promise<Authentication> {
    const rKey = this.getRedisKey(code)

    const result = await this.redis.get(rKey)
    if (!result) {
      throw new HttpException({ message: '当前二维码已失效,请刷新后重新扫描!' }, HttpStatus.BAD_REQUEST)
    }

    const authen: Authentication = JSON.parse(result)
    if (authen.status === AuthenticationStatus.Confirmed) {
      authen.status = AuthenticationStatus.Consumed
      authen.consumeTime = Date.now()

      await this.redis.set(rKey, JSON.stringify(authen), 'EX', 60 * 10)
    }

    return authen
  }

  /**
   * 查询校验码状态
   *
   * @param code 校验码
   */
  async query(code: string): Promise<Authentication> {
    const rKey = this.getRedisKey(code)

    const result = await this.redis.get(rKey)
    if (!result) {
      throw new HttpException({ message: '当前二维码已失效,请刷新后重新扫描!' }, HttpStatus.BAD_REQUEST)
    }

    const authen: Authentication = JSON.parse(result)
    return authen
  }
}