import './env.mjs'
import { randomBytes } from 'node:crypto'

const defaultStateTtlMs = 10 * 60 * 1000
const oauthStates = new Map()

function oauthError(message, code = 'wechat_oauth_failed', statusCode = 400, details = {}) {
  const error = new Error(message)
  error.statusCode = statusCode
  error.code = code
  error.details = details
  return error
}

function normalizeOrigin(value, fallback) {
  return String(value || fallback || '').replace(/\/+$/, '')
}

function apiPublicOrigin() {
  return normalizeOrigin(
    process.env.API_PUBLIC_ORIGIN || process.env.BACKEND_PUBLIC_ORIGIN,
    `http://localhost:${process.env.PORT || 4000}`,
  )
}

function oauthConfig() {
  const mode = String(process.env.WECHAT_OAUTH_MODE || 'qr').trim().toLowerCase()
  const normalizedMode = mode === 'mp' || mode === 'official_account' ? 'mp' : 'qr'
  return {
    appId: process.env.WECHAT_APP_ID || process.env.WECHAT_APPID || '',
    appSecret: process.env.WECHAT_APP_SECRET || process.env.WECHAT_SECRET || '',
    mode: normalizedMode,
    scope: process.env.WECHAT_OAUTH_SCOPE || (normalizedMode === 'mp' ? 'snsapi_userinfo' : 'snsapi_login'),
    callbackUrl: process.env.WECHAT_OAUTH_CALLBACK_URL || `${apiPublicOrigin()}/api/auth/wechat/callback`,
    timeoutMs: Math.max(1000, Number(process.env.WECHAT_OAUTH_TIMEOUT_MS || 8000)),
    stateTtlMs: Math.max(60_000, Number(process.env.WECHAT_OAUTH_STATE_TTL_MS || defaultStateTtlMs)),
  }
}

function assertConfigured(config) {
  if (!config.appId || !config.appSecret) {
    throw oauthError('微信授权配置不完整，请配置 WECHAT_APP_ID 和 WECHAT_APP_SECRET', 'wechat_oauth_not_configured', 503)
  }
}

export function wechatOAuthConfigured() {
  const config = oauthConfig()
  return Boolean(config.appId && config.appSecret)
}

function sanitizeRedirect(value) {
  const redirect = String(value || '/').trim()
  if (!redirect || !redirect.startsWith('/') || redirect.startsWith('//')) return '/'
  return redirect.slice(0, 512)
}

function cleanupExpiredStates(now = Date.now()) {
  for (const [state, record] of oauthStates.entries()) {
    if (record.expiresAt <= now) oauthStates.delete(state)
  }
}

function createOAuthState(redirect, ttlMs) {
  cleanupExpiredStates()
  const state = randomBytes(16).toString('hex')
  oauthStates.set(state, {
    redirect: sanitizeRedirect(redirect),
    expiresAt: Date.now() + ttlMs,
  })
  return state
}

export function consumeWechatOAuthState(state) {
  const normalized = String(state || '').trim()
  if (!/^[a-f0-9]{32}$/i.test(normalized)) {
    throw oauthError('微信授权状态无效，请重新发起登录', 'wechat_oauth_invalid_state', 400)
  }

  const record = oauthStates.get(normalized)
  oauthStates.delete(normalized)
  if (!record || record.expiresAt <= Date.now()) {
    throw oauthError('微信授权已过期，请重新发起登录', 'wechat_oauth_state_expired', 400)
  }
  return record
}

export function createWechatOAuthUrl(redirect = '/') {
  const config = oauthConfig()
  assertConfigured(config)

  const endpoint = config.mode === 'mp'
    ? 'https://open.weixin.qq.com/connect/oauth2/authorize'
    : 'https://open.weixin.qq.com/connect/qrconnect'
  const params = new URLSearchParams({
    appid: config.appId,
    redirect_uri: config.callbackUrl,
    response_type: 'code',
    scope: config.scope,
    state: createOAuthState(redirect, config.stateTtlMs),
  })
  return `${endpoint}?${params.toString()}#wechat_redirect`
}

export function createWechatOAuthWidgetConfig(redirect = '/') {
  const config = oauthConfig()
  assertConfigured(config)
  if (config.mode !== 'qr') {
    throw oauthError('当前微信授权模式不支持网页登录二维码，请将 WECHAT_OAUTH_MODE 设置为 qr', 'wechat_qr_mode_required', 400)
  }

  return {
    appId: config.appId,
    scope: config.scope,
    redirectUri: config.callbackUrl,
    state: createOAuthState(redirect, config.stateTtlMs),
    selfRedirect: false,
    style: process.env.WECHAT_OAUTH_QR_STYLE || 'black',
    href: process.env.WECHAT_OAUTH_QR_CSS_URL || '',
  }
}

async function fetchWechatJson(url, timeoutMs) {
  const controller = new AbortController()
  const timer = setTimeout(() => controller.abort(), timeoutMs)
  let response
  try {
    response = await fetch(url, { signal: controller.signal })
  } catch (error) {
    const timedOut = error.name === 'AbortError'
    throw oauthError(timedOut ? '微信授权网关请求超时' : '微信授权网关连接失败', timedOut ? 'wechat_oauth_timeout' : 'wechat_oauth_unreachable', 502, {
      cause: error.message,
    })
  } finally {
    clearTimeout(timer)
  }

  const text = await response.text()
  let payload
  try {
    payload = text ? JSON.parse(text) : {}
  } catch {
    throw oauthError('微信授权网关返回异常', 'wechat_oauth_bad_response', 502, { status: response.status })
  }

  if (!response.ok) {
    throw oauthError('微信授权网关请求失败', 'wechat_oauth_gateway_error', 502, { status: response.status })
  }
  return payload
}

function providerMessage(payload, fallback) {
  if (payload?.errmsg) return `微信授权失败：${payload.errmsg}`
  if (payload?.errcode) return `微信授权失败：${payload.errcode}`
  return fallback
}

export async function exchangeWechatCodeForProfile(code) {
  const config = oauthConfig()
  assertConfigured(config)

  const normalizedCode = String(code || '').trim()
  if (!normalizedCode) {
    throw oauthError('微信没有返回授权 code，请重新发起登录', 'wechat_oauth_missing_code', 400)
  }

  const tokenUrl = new URL('https://api.weixin.qq.com/sns/oauth2/access_token')
  tokenUrl.searchParams.set('appid', config.appId)
  tokenUrl.searchParams.set('secret', config.appSecret)
  tokenUrl.searchParams.set('code', normalizedCode)
  tokenUrl.searchParams.set('grant_type', 'authorization_code')

  const tokenPayload = await fetchWechatJson(tokenUrl, config.timeoutMs)
  if (tokenPayload.errcode || !tokenPayload.openid) {
    throw oauthError(providerMessage(tokenPayload, '微信授权失败，无法获取 openid'), 'wechat_oauth_token_failed', 401, {
      providerCode: tokenPayload.errcode || '',
    })
  }

  let userPayload = {}
  if (tokenPayload.access_token && tokenPayload.openid && tokenPayload.scope !== 'snsapi_base') {
    const userInfoUrl = new URL('https://api.weixin.qq.com/sns/userinfo')
    userInfoUrl.searchParams.set('access_token', tokenPayload.access_token)
    userInfoUrl.searchParams.set('openid', tokenPayload.openid)
    userInfoUrl.searchParams.set('lang', 'zh_CN')
    try {
      const fetchedUser = await fetchWechatJson(userInfoUrl, config.timeoutMs)
      if (!fetchedUser.errcode) userPayload = fetchedUser
    } catch {
      userPayload = {}
    }
  }

  return {
    openid: tokenPayload.openid,
    unionid: userPayload.unionid || tokenPayload.unionid || '',
    nickname: userPayload.nickname || '',
    avatarUrl: userPayload.headimgurl || '',
    scope: tokenPayload.scope || config.scope,
  }
}
