/**
 * BSPHP 移动端通信：MD5、AES-128-CBC、RSA PKCS#1、请求组包与响应解析。
 * 参数键按字典序排序，值按 encodeParameter 规则编码，与官方 AppEn 约定一致。
 */
import CryptoJS from 'crypto-js'
import JSEncrypt from 'jsencrypt'

export function encodeParameter(s) {
  let out = ''
  const str = s == null ? '' : String(s)
  for (const ch of str) {
    const o = ch.codePointAt(0)
    if (
      (o >= 65 && o <= 90) ||
      (o >= 97 && o <= 122) ||
      (o >= 48 && o <= 57) ||
      ch === '-' ||
      ch === '.' ||
      ch === '_' ||
      ch === '~'
    ) {
      out += ch
    } else {
      for (const b of new TextEncoder().encode(ch)) {
        out += '%' + b.toString(16).toUpperCase().padStart(2, '0')
      }
    }
  }
  return out
}

export function buildParamString(param) {
  return Object.keys(param)
    .sort()
    .map((k) => `${k}=${encodeParameter(param[k] ?? '')}`)
    .join('&')
}

export function quoteParameterPayload(payload) {
  let out = ''
  for (const ch of payload) {
    const o = ch.codePointAt(0)
    if (
      (o >= 65 && o <= 90) ||
      (o >= 97 && o <= 122) ||
      (o >= 48 && o <= 57) ||
      ch === '-' ||
      ch === '_' ||
      ch === '.' ||
      ch === '~'
    ) {
      out += ch
    } else {
      for (const b of new TextEncoder().encode(ch)) {
        out += '%' + b.toString(16).toUpperCase().padStart(2, '0')
      }
    }
  }
  return out
}

function pemPrivate(b64) {
  const body = (b64 || '').replace(/\s/g, '')
  const lines = body.match(/.{1,64}/g) || []
  return `-----BEGIN PRIVATE KEY-----\n${lines.join('\n')}\n-----END PRIVATE KEY-----`
}

function pemPublic(b64) {
  const body = (b64 || '').replace(/\s/g, '')
  const lines = body.match(/.{1,64}/g) || []
  return `-----BEGIN PUBLIC KEY-----\n${lines.join('\n')}\n-----END PUBLIC KEY-----`
}

export function md5Hex(s) {
  return CryptoJS.MD5(CryptoJS.enc.Utf8.parse(String(s))).toString(CryptoJS.enc.Hex)
}

function utf8KeyWordArray16(keyStr) {
  const enc = new TextEncoder().encode(keyStr)
  const buf = new Uint8Array(16)
  buf.set(enc.subarray(0, Math.min(16, enc.length)))
  return CryptoJS.lib.WordArray.create(buf)
}

export function aes128CbcEncryptBase64(plaintext, key16) {
  const key = utf8KeyWordArray16(key16)
  const iv = key
  const enc = CryptoJS.AES.encrypt(CryptoJS.enc.Utf8.parse(plaintext), key, {
    iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7
  })
  return enc.ciphertext.toString(CryptoJS.enc.Base64)
}

export function aes128CbcDecryptBase64(b64, key16) {
  const key = utf8KeyWordArray16(key16)
  const iv = key
  const decrypted = CryptoJS.AES.decrypt(
    { ciphertext: CryptoJS.enc.Base64.parse(b64) },
    key,
    { iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }
  )
  return CryptoJS.enc.Utf8.stringify(decrypted)
}

export function rsaEncryptPkcs1Base64(message, publicKeyB64Der) {
  const j = new JSEncrypt()
  j.setPublicKey(pemPublic(publicKeyB64Der))
  const out = j.encrypt(message)
  if (!out) throw new Error('RSA encrypt failed')
  return out
}

export function rsaDecryptPkcs1Base64(b64Cipher, privateKeyB64Der) {
  const j = new JSEncrypt()
  j.setPrivateKey(pemPrivate(privateKeyB64Der))
  const out = j.decrypt(b64Cipher)
  if (!out) throw new Error('RSA decrypt failed')
  return out
}

function pad2(n) {
  return n < 10 ? '0' + n : '' + n
}

export function formatDateFull(d) {
  return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(
    d.getMinutes()
  )}:${pad2(d.getSeconds())}`
}

export function formatDateHash(d) {
  return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}#${pad2(d.getHours())}:${pad2(
    d.getMinutes()
  )}:${pad2(d.getSeconds())}`
}

export function getMachineCode(storageKey) {
  const k = storageKey || 'bsphp_mc_v1'
  let v = uni.getStorageSync(k)
  if (v) return String(v)
  v = 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
    const r = (Math.random() * 16) | 0
    return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)
  })
  uni.setStorageSync(k, v)
  return v
}

export function apiDataMessage(data) {
  if (data == null) return ''
  if (typeof data === 'string') return data
  if (Array.isArray(data)) return data.map((x) => String(x)).join('\n')
  return String(data)
}

export function coerceCode(v) {
  if (v == null || typeof v === 'boolean') return null
  const n = parseInt(v, 10)
  return Number.isNaN(n) ? null : n
}

export function bsphpPost(url, bodyString) {
  return new Promise((resolve) => {
    uni.request({
      url,
      method: 'POST',
      header: { 'Content-Type': 'application/x-www-form-urlencoded' },
      data: bodyString,
      timeout: 60000,
      success(res) {
        if (res.statusCode !== 200) {
          resolve(null)
          return
        }
        const d = res.data
        resolve(typeof d === 'string' ? d : d != null ? String(d) : '')
      },
      fail() {
        resolve(null)
      }
    })
  })
}

function parseResponseBody(rawText, serverPrivateKey, appsafecode) {
  let text = (rawText || '').trim()
  if (!text) return null
  if (text.includes('%')) {
    try {
      text = decodeURIComponent(text.replace(/\+/g, ' '))
    } catch (e) {
      /* keep */
    }
  }
  const parts = text.split('|')
  if (parts.length < 3) return null
  const respEnc = parts[1].trim()
  const respRsa = parts[2].trim()
  let sig
  try {
    sig = rsaDecryptPkcs1Base64(respRsa, serverPrivateKey)
  } catch (e) {
    return null
  }
  const sigParts = sig.split('|')
  if (sigParts.length < 4) return null
  const respAesKey = sigParts[2].substring(0, 16)
  let decrypted
  try {
    decrypted = aes128CbcDecryptBase64(respEnc, respAesKey)
  } catch (e) {
    return null
  }
  let j
  try {
    j = JSON.parse(decrypted)
  } catch (e) {
    return null
  }
  const response = j.response
  if (!response || typeof response !== 'object') return null
  if (response.appsafecode !== appsafecode) {
    return { ...response, data: 'appsafecode 安全参数验证不通过' }
  }
  return response
}

export function createUserBsphpClient(cfg, getMachineCodeFn) {
  let bsPhpSeSsL = ''
  const mc = () => (getMachineCodeFn ? getMachineCodeFn() : getMachineCode())

  async function send(api, params = {}) {
    const now = new Date()
    const appsafecode = md5Hex(formatDateFull(now))
    const param = {
      api,
      BSphpSeSsL: bsPhpSeSsL,
      date: formatDateHash(now),
      md5: '',
      mutualkey: cfg.mutualKey,
      appsafecode,
      ...params
    }
    const dataStr = buildParamString(param)
    const aesKey = md5Hex(cfg.serverPrivateKey + appsafecode).substring(0, 16)
    const encB64 = aes128CbcEncryptBase64(dataStr, aesKey)
    const sigMd5 = md5Hex(encB64)
    const signatureContent = `0|AES-128-CBC|${aesKey}|${sigMd5}|json`
    const rsaB64 = rsaEncryptPkcs1Base64(signatureContent, cfg.clientPublicKey)
    const payload = `${encB64}|${rsaB64}`
    const encoded = quoteParameterPayload(payload)
    const raw = await bsphpPost(cfg.url, 'parameter=' + encoded)
    if (raw == null) return null
    return parseResponseBody(raw, cfg.serverPrivateKey, appsafecode)
  }

  function toResult(resp) {
    if (!resp) return { data: null, code: null }
    return { data: resp.data, code: coerceCode(resp.code) }
  }

  return {
    get bsPhpSeSsL() {
      return bsPhpSeSsL
    },
    set bsPhpSeSsL(v) {
      bsPhpSeSsL = v || ''
    },
    codeImageURL() {
      if (!cfg.codeUrlPrefix) return ''
      return cfg.codeUrlPrefix + bsPhpSeSsL
    },
    machineCode: mc,
    async connect() {
      const r = await send('internet.in', {})
      return r && String(r.data) === '1'
    },
    async getSeSsL() {
      const r = await send('BSphpSeSsL.in', {})
      if (!r) return false
      const d = r.data
      if (typeof d === 'string' && d) {
        bsPhpSeSsL = d
        return true
      }
      return false
    },
    async bootstrap() {
      if (!(await this.connect())) throw new Error('连接失败')
      if (!(await this.getSeSsL())) throw new Error('获取 BSphpSeSsL 失败')
    },
    async logout() {
      const r = await send('cancellation.lg', {})
      const res = toResult(r)
      bsPhpSeSsL = ''
      await this.getSeSsL()
      return res
    },
    getNotice: () => send('gg.in', {}).then(toResult),
    getVersion: () => send('v.in', {}).then(toResult),
    getSoftInfo: () => send('miao.in', {}).then(toResult),
    getServerDate: () => send('date.in', {}).then(toResult),
    getPresetURL: () => send('url.in', {}).then(toResult),
    getWebURL: () => send('weburl.in', {}).then(toResult),
    getGlobalInfo: () => send('globalinfo.in', {}).then(toResult),
    getCodeEnabled(type) {
      const p = type ? { type } : {}
      return send('getsetimag.in', p).then(toResult)
    },
    getEndTime: () => send('vipdate.lg', {}).then(toResult),
    getUserInfo(info) {
      const p = info ? { info } : {}
      return send('getuserinfo.lg', p).then(toResult)
    },
    perfectUserInfo(params) {
      return send('Perfect.lg', params).then(toResult)
    },
    async login(user, password, coode) {
      const key = mc()
      const r = await send('login.lg', {
        user,
        pwd: password,
        coode: coode || '',
        key,
        maxoror: key
      })
      if (!r) return { data: '系统错误，登录失败！', code: null }
      const ci = coerceCode(r.code)
      if ((ci === 1011 || ci === 9908) && typeof r.SeSsL === 'string' && r.SeSsL) {
        bsPhpSeSsL = r.SeSsL
      }
      return { data: r.data, code: ci }
    },
    async loginSms(mobile, area, smsCode, coode) {
      const key = mc()
      const r = await send('login_sms.lg', {
        mobile,
        area,
        sms_code: smsCode,
        key,
        maxoror: key,
        coode: coode || ''
      })
      if (!r) return { data: '系统错误，短信验证码登录失败！', code: null }
      const ci = coerceCode(r.code)
      if ((ci === 1011 || ci === 9908) && typeof r.SeSsL === 'string' && r.SeSsL) {
        bsPhpSeSsL = r.SeSsL
      }
      return { data: r.data, code: ci }
    },
    async loginEmail(email, emailCode, coode) {
      const key = mc()
      const r = await send('login_email.lg', {
        email,
        email_code: emailCode,
        key,
        maxoror: key,
        coode: coode || ''
      })
      if (!r) return { data: '系统错误，邮箱验证码登录失败！', code: null }
      return { data: r.data, code: coerceCode(r.code) }
    },
    sendSmsCode(scene, mobile, area, coode) {
      return send('send_sms.lg', {
        scene,
        mobile,
        area,
        coode: coode || ''
      }).then(toResult)
    },
    sendEmailCode(scene, email, coode) {
      return send('send_email.lg', { scene, email, coode: coode || '' }).then(toResult)
    },
    registerSms(user, mobile, area, smsCode, pwd, pwdb, coode) {
      return send('register_sms.lg', {
        user,
        mobile,
        area,
        sms_code: smsCode,
        pwd,
        pwdb,
        key: mc(),
        coode: coode || ''
      }).then(toResult)
    },
    registerEmail(user, email, emailCode, pwd, pwdb, coode) {
      return send('register_email.lg', {
        user,
        email,
        email_code: emailCode,
        pwd,
        pwdb,
        key: mc(),
        coode: coode || ''
      }).then(toResult)
    },
    reg(user, pwd, pwdb, coode, mobile, mibaoWenti, mibaoDaan, qq, mail, extension) {
      return send('registration.lg', {
        user,
        pwd,
        pwdb,
        coode: coode || '',
        mobile: mobile || '',
        mibao_wenti: mibaoWenti || '',
        mibao_daan: mibaoDaan || '',
        qq: qq || '',
        mail: mail || '',
        extension: extension || ''
      }).then(toResult)
    },
    resetSmsPwd(mobile, area, smsCode, pwd, pwdb, coode) {
      return send('resetpwd_sms.lg', {
        mobile,
        area,
        sms_code: smsCode,
        pwd,
        pwdb,
        coode: coode || ''
      }).then(toResult)
    },
    resetEmailPwd(email, emailCode, pwd, pwdb, coode) {
      return send('resetpwd_email.lg', {
        email,
        email_code: emailCode,
        pwd,
        pwdb,
        coode: coode || ''
      }).then(toResult)
    },
    pay(user, userpwd, userset, ka, pwd) {
      return send('chong.lg', {
        user,
        userpwd: userpwd || '',
        userset: userset ? '1' : '0',
        ka,
        pwd: pwd || ''
      }).then(toResult)
    },
    unbind(user, pwd) {
      return send('jiekey.lg', { user, pwd: pwd || '' }).then(toResult)
    },
    feedback(user, pwd, table, qq, leix, text, coode) {
      return send('liuyan.in', {
        user,
        pwd: pwd || '',
        table,
        qq: qq || '',
        leix,
        txt: text,
        coode: coode || ''
      }).then(toResult)
    }
  }
}

export function demoResult(r, fallback) {
  if (!r) return { code: null, message: fallback || '请求失败' }
  let m = apiDataMessage(r.data)
  if (m == null || m === '') m = fallback || ''
  return { code: r.code, message: m }
}

export function createMobileApi(cfg) {
  const client = createUserBsphpClient(cfg)
  let ready = false
  const codeCache = {}

  async function ensureReady() {
    if (ready) return null
    try {
      if (!(await client.connect())) return { code: null, message: '服务器连接失败' }
      if (!(await client.getSeSsL())) return { code: null, message: '获取会话失败' }
      ready = true
      return null
    } catch (e) {
      return { code: null, message: '初始化失败：' + (e.message || e) }
    }
  }

  return {
    client,
    clearCodeCache() {
      Object.keys(codeCache).forEach((k) => delete codeCache[k])
    },
    async captchaURL() {
      const e = await ensureReady()
      if (e && e.code == null) return { url: null, err: e.message }
      const u = client.codeImageURL()
      if (!u) return { url: null, err: '验证码地址无效' }
      return { url: u, err: null }
    },
    async isCodeEnabled(scene) {
      if (codeCache[scene] != null) return codeCache[scene]
      const e = await ensureReady()
      if (e && e.code == null) return false
      try {
        const type =
          scene === 'login' ? 'INGES_LOGIN' : scene === 'register' ? 'INGES_RE' : 'INGES_MACK'
        const r = await client.getCodeEnabled(type)
        const msg = demoResult(r, '').message
        const enabled = msg && msg.trim().toLowerCase() === 'checked'
        codeCache[scene] = enabled
        return enabled
      } catch (err) {
        return false
      }
    },
    wrap(r, fallback) {
      return demoResult(r, fallback)
    },
    ensureReady
  }
}
