/**
 * ╔═════════════════════════════   腾讯位置服务   ═════════════════════════════════
 * ║
 * ╟── 1. 封装对 腾讯位置服务 API 接口的调用
 * ╟── 2. 对调用接口结果再做一层封装,方便内部使用
 * ║
 * ╚════════════════════════════════════════════════════════════════════════
 */

import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'
import axios from 'axios'
import { plainToClass } from 'class-transformer'
import { Redis } from 'ioredis'
import { LbsqqKeys } from 'life-helper-config'
import { RedisService } from 'nestjs-redis'
import { COMMON_SERVER_ERROR } from 'src/common/errors.constant'
import { OssService } from '../oss/oss.service'
import { GeoLocationCoderResponse, LocateIpResponse, LocationCoordinate } from './lbsqq.interface'
import { AddressInfo } from './lbsqq.model'

@Injectable()
export class LbsqqService {
  private readonly logger = new Logger(LbsqqService.name)
  private readonly redis: Redis

  /** 开发者密钥获取次数,每一次获取密钥则 +1 */
  private counter = 0

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

  /**
   * 获取一个 `腾讯位置服务` 的开发者密钥(`key`)
   *
   * @description
   * 每个开发者密钥都有请求并发限制,预定义了一个密钥数组,按照数组索引逐个获取使用。
   * @see [接口配额说明](https://lbs.qq.com/service/webService/webServiceGuide/webServiceQuota)
   */
  getKey(): string {
    const keys: string[] = LbsqqKeys
    const n: number = this.counter++ % keys.length
    return keys[n]
  }

  /**
   * 通过 IP 地址进行非精确定位
   *
   * @param ip IP 地址
   *
   * @see [文档地址](https://lbs.qq.com/service/webService/webServiceGuide/webServiceIp)
   */
  async locateIp(ip: string): Promise<LocateIpResponse> {
    if (!ip) {
      // IP 地址为空也会请求成功(默认为请求者 IP 地址,即当前服务器 IP),这里额外再加一层检验
      this.logger.error('使用 `LbsqqService.ipLocation` 方法时传入 `ip` 为空!')
      throw new HttpException(COMMON_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR)
    }

    /** Redis 键名 */
    const redisKey = `lbsqq:location:ip:${ip}`

    const result = await this.redis.get(redisKey)
    if (result) {
      return JSON.parse(result)
    }

    const key = this.getKey()
    const options = {
      url: 'https://apis.map.qq.com/ws/location/v1/ip',
      params: { ip, key },
    }
    const response = await axios.request<LocateIpResponse>(options)
    if (response.data.status !== 0) {
      this.logger.error(`[腾讯位置服务] [IP 定位] 接口请求失败,ip => \`${ip}\`,错误原因 => ${response.data.message}`)
      throw new HttpException(COMMON_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR)
    }

    /** Redis 缓存时长:10 天 */
    const expiration = 3600 * 24 * 10
    this.redis.set(redisKey, JSON.stringify(response.data), 'EX', expiration)
    return response.data
  }

  /**
   * 逆地址解析,将经纬度转换为文字地址及相关位置信息
   *
   * @param longitude 经度
   * @param latitude 纬度
   *
   * @see [文档地址](https://lbs.qq.com/service/webService/webServiceGuide/webServiceGcoder)
   */
  async geoLocationCoder(longitude: number, latitude: number): Promise<GeoLocationCoderResponse> {
    const redisKey = `lbsqq:address:location:${longitude},${latitude}`
    const result = await this.redis.get(redisKey)

    if (result) {
      return JSON.parse(result)
    }

    const key: string = this.getKey()

    /** 请求参数 */
    const options = {
      url: 'https://apis.map.qq.com/ws/geocoder/v1',
      params: { location: `${latitude},${longitude}`, key, get_poi: 0 },
    }

    const response = await axios.request<GeoLocationCoderResponse>(options)
    if (response.data.status !== 0) {
      this.logger.error(`[腾讯位置服务] [逆地址解析] 接口请求失败,params => \`${options.params}\`,错误原因 => ${response.data.message}`)
      throw new HttpException(COMMON_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR)
    }

    /** Redis 缓存时长:10 天 */
    const expiration = 3600 * 24 * 10
    this.redis.set(redisKey, JSON.stringify(response.data), 'EX', expiration)
    return response.data
  }

  /**
   * 根据经纬度生成静态地图,并返回存储路径
   *
   * @param longitude 经度
   * @param latitude 纬度
   */
  async generateStaticMap(longitude: number, latitude: number): Promise<string> {
    /** 地图视图中心点 */
    const center = `${latitude},${longitude}`

    /** 地图视图的级别设置,取值范围 4≤zoom≤18 */
    const zoom = 12

    /** 地图静态图片大小,宽*高,单位像素 */
    const size = '600*400'

    /** 是否高清,取值2为高清,取值1为普清 */
    const scale = 1

    const markers = center

    const key = this.getKey()

    const response = await axios.request({
      method: 'GET',
      url: 'https://apis.map.qq.com/ws/staticmap/v2',
      params: { center, zoom, size, scale, markers, key },
      responseType: 'arraybuffer',
    })

    const contentType = response.headers['Content-Type'] || response.headers['content-type'] || 'image/png'

    const buf = response.data
    const path = await this.ossService.save('staticmap', buf, { headers: { 'Content-Type': contentType } })

    return path
  }

  /** ═════════════════  以下方法为对原生方法的二次封装方法  ═════════════════ */

  /**
   * 通过 IP 地址获取经纬度坐标
   *
   * @param ip IP 地址
   */
  async getCoordinateByIp(ip: string): Promise<LocationCoordinate> {
    const result = await this.locateIp(ip)
    const coord = result.result.location
    return { longitude: coord.lng, latitude: coord.lat }
  }

  /**
   * 通过经纬度获取对应地点的一句话描述
   *
   * @param longitude 经度
   * @param latitude 纬度
   */
  async getRecommendAddressDescrption(longitude: number, latitude: number): Promise<string> {
    const result = await this.geoLocationCoder(longitude, latitude)
    return result.result.formatted_addresses.recommend
  }

  /**
   * 通过经纬度获取省市区等行政区划信息
   *
   * @param longitude 经度
   * @param latitude 纬度
   */
  async getAddressInfo(longitude: number, latitude: number): Promise<AddressInfo> {
    const result = await this.geoLocationCoder(longitude, latitude)
    return plainToClass(AddressInfo, result.result.ad_info)
  }

  /**
   * 获取静态地图的完整 URL
   *
   * @param longitude 经度
   * @param latitude 纬度
   * @return 静态地图的完整 URL,例如:`https://res.lifehelper.com.cn/staticmap/xxxxxx`
   */
  async getStaticMapUrl(longitude: number, latitude: number): Promise<string> {
    const key = await this.generateStaticMap(longitude, latitude)
    const url = this.ossService.getUrl(key)
    return url
  }
}