import { defineEventHandler, getQuery, setResponseHeader, createError } from 'h3'
import { createHash, randomBytes } from 'node:crypto'

type AccessTokenResponse = {
  access_token: string
  expires_in: number
  errcode?: number
  errmsg?: string
}

type JsapiTicketResponse = {
  errcode: number
  errmsg: string
  ticket: string
  expires_in: number
}

type WxConfigPayload = {
  appId: string
  timestamp: number
  nonceStr: string
  signature: string
}

/**
 * GET /api/wechat/jssdk-config?url=<encoded_url>
 *
 * 仅新增本文件，不修改其他任何代码。
 * 从环境变量读取：
 * - WECHAT_APP_ID
 * - WECHAT_APP_SECRET
 *
 * 按微信官方文档生成 JS-SDK 所需签名配置：appId、timestamp、nonceStr、signature。
 * 会在内存中缓存 access_token 与 jsapi_ticket，缓存有效期比官方返回略短（预留安全缓冲）。
 *
 * 参考文档：
 * - JS-SDK 使用步骤（签名计算与 ticket 缓存）：https://developers.weixin.qq.com/doc/service/guide/h5/jssdk.html#JSSDK%E4%BD%BF%E7%94%A8%E6%AD%A5%E9%AA%A4
 */

const TOKEN_SAFETY_BUFFER_SECONDS = 300 // 5 分钟安全缓冲

let cachedAccessToken: { value: string, expiresAt: number } | null = null
let cachedJsapiTicket: { value: string, expiresAt: number } | null = null

function isExpired(cache: { expiresAt: number } | null): boolean {
  if (!cache) return true
  return Date.now() >= cache.expiresAt
}

async function fetchAccessToken(appId: string, appSecret: string): Promise<string> {
  if (!isExpired(cachedAccessToken)) {
    return cachedAccessToken!.value
  }

  const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${encodeURIComponent(appId)}&secret=${encodeURIComponent(appSecret)}`
  const response = await $fetch<AccessTokenResponse>(url, { method: 'GET' })

  if (!response || !response.access_token) {
    throw createError({
      statusCode: 502,
      statusMessage: `Failed to obtain access_token: ${response?.errcode ?? ''} ${response?.errmsg ?? ''}`.trim()
    })
  }

  const expiresInSeconds = Math.max(0, (response.expires_in ?? 7200) - TOKEN_SAFETY_BUFFER_SECONDS)
  cachedAccessToken = {
    value: response.access_token,
    expiresAt: Date.now() + expiresInSeconds * 1000
  }

  return response.access_token
}

async function fetchJsapiTicket(accessToken: string): Promise<string> {
  if (!isExpired(cachedJsapiTicket)) {
    return cachedJsapiTicket!.value
  }

  const url = `https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${encodeURIComponent(accessToken)}&type=jsapi`
  const response = await $fetch<JsapiTicketResponse>(url, { method: 'GET' })

  if (!response || response.errcode !== 0 || !response.ticket) {
    throw createError({
      statusCode: 502,
      statusMessage: `Failed to obtain jsapi_ticket: ${response?.errcode ?? ''} ${response?.errmsg ?? ''}`.trim()
    })
  }

  const expiresInSeconds = Math.max(0, (response.expires_in ?? 7200) - TOKEN_SAFETY_BUFFER_SECONDS)
  cachedJsapiTicket = {
    value: response.ticket,
    expiresAt: Date.now() + expiresInSeconds * 1000
  }

  return response.ticket
}

function generateNonceStr(bytes: number = 16): string {
  // 生成由 [a-z0-9] 组成的随机字符串
  return randomBytes(bytes).toString('hex')
}

function generateTimestamp(): number {
  return Math.floor(Date.now() / 1000)
}

function buildSignature(jsapiTicket: string, nonceStr: string, timestamp: number, url: string): string {
  // 注意：参数名必须为全小写，且按字典序构造字符串
  // 官方示例顺序：jsapi_ticket, noncestr, timestamp, url
  const rawString = `jsapi_ticket=${jsapiTicket}&noncestr=${nonceStr}&timestamp=${timestamp}&url=${url}`
  return createHash('sha1').update(rawString).digest('hex')
}

export default defineEventHandler(async (event): Promise<WxConfigPayload> => {
  // 避免代理缓存
  setResponseHeader(event, 'Cache-Control', 'no-store')

  const appId = process.env.WECHAT_APP_ID || ''
  const appSecret = process.env.WECHAT_APP_SECRET || ''

  if (!appId || !appSecret) {
    throw createError({ statusCode: 500, statusMessage: 'Missing WECHAT_APP_ID or WECHAT_APP_SECRET in environment.' })
  }

  // 限制可签名的 URL 域名：从环境变量中获取站点基准地址，仅允许该主域或其子域
  const siteBaseUrl = process.env.NUXT_PUBLIC_SITE_URL || process.env.SITE_BASE_URL || process.env.BASE_URL || ''
  if (!siteBaseUrl) {
    throw createError({ statusCode: 500, statusMessage: 'Missing site base URL in environment (NUXT_PUBLIC_SITE_URL/SITE_BASE_URL/BASE_URL).' })
  }

  let allowedHostname = ''
  try {
    allowedHostname = new URL(siteBaseUrl).hostname
  } catch {
    throw createError({ statusCode: 500, statusMessage: 'Invalid site base URL in environment. Expecting an absolute URL.' })
  }

  const query = getQuery(event)
  let pageUrlRaw = typeof query.url === 'string' ? query.url : ''
  // 若未传入 url，则使用当前请求完整地址（去除 hash），通常建议前端传入当前页面 URL
  if (!pageUrlRaw) {
    const requestUrl = event.node.req.headers['x-forwarded-proto'] && event.node.req.headers['x-forwarded-host']
      ? `${event.node.req.headers['x-forwarded-proto']}://${event.node.req.headers['x-forwarded-host']}${event.node.req.url ?? ''}`
      : new URL(event.node.req.url ?? '/', `http://${event.node.req.headers.host}`).toString()
    pageUrlRaw = requestUrl
  }

  const pageUrlNoHash = pageUrlRaw.includes('#')
    ? pageUrlRaw.slice(0, pageUrlRaw.indexOf('#'))
    : pageUrlRaw
  const pageUrl = decodeURIComponent(pageUrlNoHash)
  if (!/^https?:\/\//i.test(pageUrl)) {
    throw createError({ statusCode: 400, statusMessage: 'Invalid url parameter. Expecting an absolute http/https URL.' })
  }

  // 校验传入 URL 的域名是否在允许范围（同主域或其子域）
  let pageHostname = ''
  try {
    pageHostname = new URL(pageUrl).hostname
  } catch {
    throw createError({ statusCode: 400, statusMessage: 'Invalid url parameter. Expecting an absolute http/https URL.' })
  }
  const isSameHost = pageHostname === allowedHostname
  const isSubdomain = pageHostname.endsWith('.' + allowedHostname)
  if (!isSameHost && !isSubdomain) {
    throw createError({ statusCode: 400, statusMessage: 'The url hostname is not allowed.' })
  }

  const accessToken = await fetchAccessToken(appId, appSecret)
  const jsapiTicket = await fetchJsapiTicket(accessToken)

  const timestamp = generateTimestamp()
  const nonceStr = generateNonceStr(16)
  const signature = buildSignature(jsapiTicket, nonceStr, timestamp, pageUrl)

  return {
    appId,
    timestamp,
    nonceStr,
    signature
  }
})
