import { createHttpError } from '../../../utils/http.js'
import { logInfo, logWarn } from '../../../utils/logger.js'
import type { JsonObject } from '../../../types/json.js'
import {
  findKuaishouIndustryShopConfig,
  getKuaishouIndustrySourceConfig,
  listKuaishouIndustryShopConfigs,
  patchKuaishouIndustrySourceConfig,
  patchKuaishouIndustryShopConfig,
  type KuaishouIndustryShopConfig,
  type KuaishouIndustrySourceConfig,
} from './source-config-service.js'

const ACCESS_TOKEN_REFRESH_MARGIN_MS = 30 * 60 * 1000
const DEFAULT_ACCESS_TOKEN_TTL_MS = 47 * 60 * 60 * 1000
const DEFAULT_REFRESH_TOKEN_TTL_MS = 179 * 24 * 60 * 60 * 1000

export type KuaishouIndustryAccessTokenResult = {
  accessToken: string
  refreshed: boolean
  config: KuaishouIndustrySourceConfig
  shop: KuaishouIndustryShopConfig
}

export async function ensureKuaishouIndustryAccessToken(
  options: {
    forceRefresh?: boolean
    config?: KuaishouIndustrySourceConfig
    sellerId?: string
  } = {},
): Promise<KuaishouIndustryAccessTokenResult> {
  const config = options.config || getKuaishouIndustrySourceConfig()
  const shop = resolveTokenShop(config, options.sellerId)
  const accessToken = String(shop.accessToken || '').trim()
  const refreshToken = String(shop.refreshToken || '').trim()

  if (!options.forceRefresh && accessToken && !shouldRefreshAccessToken(shop)) {
    return { accessToken, refreshed: false, config, shop }
  }

  if (!refreshToken) {
    return { accessToken, refreshed: false, config, shop }
  }

  const sellerId = shop.sellerId || options.sellerId
  return refreshKuaishouIndustryAccessToken({
    config,
    ...(sellerId ? { sellerId } : {}),
  })
}

export async function refreshKuaishouIndustryAccessToken(
  options: {
    config?: KuaishouIndustrySourceConfig
    sellerId?: string
  } = {},
): Promise<KuaishouIndustryAccessTokenResult> {
  const config = options.config || getKuaishouIndustrySourceConfig()
  const shop = resolveTokenShop(config, options.sellerId)
  assertRefreshConfig(config, shop)

  const params = new URLSearchParams()
  params.set('app_id', config.appKey)
  params.set('app_secret', config.appSecret)
  params.set('grant_type', 'refresh_token')
  params.set('refresh_token', shop.refreshToken)

  try {
    const startedAt = Date.now()
    const tokenPayload = await requestKuaishouIndustryToken({
      config,
      path: '/oauth2/refresh_token',
      params,
      method: 'POST',
      failureMessage: '快手 accessToken 刷新失败',
      errorCode: 'kuaishou_industry_access_token_refresh_failed',
    })
    const saved = await saveTokenPayload(tokenPayload, config, shop)

    logInfo('[kuaishou-industry/token]', 'accessToken 刷新成功', {
      durationMs: Date.now() - startedAt,
      sellerId: saved.shop.sellerId || '',
      accessTokenExpiresAt: saved.shop.accessTokenExpiresAt || '',
      refreshTokenExpiresAt: saved.shop.refreshTokenExpiresAt || '',
      hasRefreshToken: Boolean(saved.shop.refreshToken),
    })

    return {
      accessToken: saved.shop.accessToken,
      refreshed: true,
      config: saved.config,
      shop: saved.shop,
    }
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error)
    await patchKuaishouIndustryShopConfig(shop.sellerId, {
      lastRefreshError: message,
    })
    await patchKuaishouIndustrySourceConfig({
      lastRefreshError: message,
    })
    logWarn('[kuaishou-industry/token]', 'accessToken 刷新失败', resolveRefreshErrorDetail(error))
    throw error
  }
}

export async function exchangeKuaishouIndustryAuthorizationCode(
  code: string,
  options: {
    config?: KuaishouIndustrySourceConfig
    sellerId?: string
    shopName?: string
    customShopName?: string
  } = {},
): Promise<KuaishouIndustryAccessTokenResult> {
  const config = options.config || getKuaishouIndustrySourceConfig()
  assertAuthorizationCodeConfig(config, code)

  const params = new URLSearchParams()
  params.set('app_id', config.appKey)
  params.set('app_secret', config.appSecret)
  params.set('grant_type', 'code')
  params.set('code', code.trim())

  try {
    const startedAt = Date.now()
    const tokenPayload = await requestKuaishouIndustryToken({
      config,
      path: '/oauth2/access_token',
      params,
      method: 'GET',
      failureMessage: '快手授权码换取 accessToken 失败',
      errorCode: 'kuaishou_industry_authorization_code_exchange_failed',
    })
    const saved = await saveTokenPayload(tokenPayload, config, {
      ...createEmptyShopConfig(),
      sellerId: String(options.sellerId || '').trim(),
      shopId: String(options.sellerId || '').trim(),
      shopName: String(options.shopName || '').trim(),
      customShopName: String(options.customShopName || '').trim(),
    })

    logInfo('[kuaishou-industry/token]', '授权码换取 accessToken 成功', {
      durationMs: Date.now() - startedAt,
      sellerId: saved.shop.sellerId || '',
      accessTokenExpiresAt: saved.shop.accessTokenExpiresAt || '',
      refreshTokenExpiresAt: saved.shop.refreshTokenExpiresAt || '',
      hasRefreshToken: Boolean(saved.shop.refreshToken),
    })

    return {
      accessToken: saved.shop.accessToken,
      refreshed: true,
      config: saved.config,
      shop: saved.shop,
    }
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error)
    await patchKuaishouIndustrySourceConfig({
      lastRefreshError: message,
    })
    logWarn(
      '[kuaishou-industry/token]',
      '授权码换取 accessToken 失败',
      resolveRefreshErrorDetail(error),
    )
    throw error
  }
}

export function shouldRefreshAccessToken(
  config: Pick<KuaishouIndustryShopConfig, 'accessToken' | 'accessTokenExpiresAt'>,
): boolean {
  if (!String(config.accessToken || '').trim()) {
    return true
  }

  const expiresAt = Date.parse(String(config.accessTokenExpiresAt || '').trim())
  if (!Number.isFinite(expiresAt)) {
    return false
  }

  return expiresAt <= Date.now() + ACCESS_TOKEN_REFRESH_MARGIN_MS
}

function resolveTokenShop(
  config: KuaishouIndustrySourceConfig,
  sellerId: unknown,
): KuaishouIndustryShopConfig {
  const normalizedSellerId = String(sellerId || '').trim()
  if (normalizedSellerId) {
    const matched = findKuaishouIndustryShopConfig(normalizedSellerId, config)
    if (!matched) {
      throw createHttpError(`未找到 sellerId=${normalizedSellerId} 的快手行业电子凭证授权`, {
        statusCode: 400,
        errorCode: 'kuaishou_industry_shop_not_configured',
      })
    }

    if (matched.enabled === false) {
      throw createHttpError(`sellerId=${normalizedSellerId} 的快手行业电子凭证授权已停用`, {
        statusCode: 400,
        errorCode: 'kuaishou_industry_shop_disabled',
      })
    }

    return matched
  }

  const enabledShops = listKuaishouIndustryShopConfigs(config).filter(
    (shop) => shop.enabled !== false,
  )

  if (enabledShops.length === 1) {
    return enabledShops[0] as KuaishouIndustryShopConfig
  }

  if (enabledShops.length > 1) {
    throw createHttpError('存在多个快手行业电子凭证店铺授权，请指定 sellerId', {
      statusCode: 400,
      errorCode: 'kuaishou_industry_missing_seller_id',
    })
  }

  return createEmptyShopConfig()
}

function createEmptyShopConfig(): KuaishouIndustryShopConfig {
  return {
    enabled: true,
    sellerId: '',
    shopId: '',
    shopName: '',
    customShopName: '',
    authState: '',
    accessToken: '',
    refreshToken: '',
    accessTokenExpiresAt: '',
    refreshTokenExpiresAt: '',
    openId: '',
    grantedScopes: '',
    lastRefreshedAt: '',
    lastRefreshError: '',
  }
}

function assertRefreshConfig(
  config: KuaishouIndustrySourceConfig,
  shop: KuaishouIndustryShopConfig,
) {
  if (!config.baseUrl) {
    throw createHttpError('快手开放平台 API 地址未配置', {
      statusCode: 400,
      errorCode: 'kuaishou_industry_missing_base_url',
    })
  }

  if (!config.appKey || !config.appSecret) {
    throw createHttpError('快手 appKey/appSecret 未配置，无法刷新 accessToken', {
      statusCode: 400,
      errorCode: 'kuaishou_industry_missing_oauth_credential',
    })
  }

  if (!shop.refreshToken) {
    throw createHttpError('refreshToken 未配置，无法刷新 accessToken', {
      statusCode: 400,
      errorCode: 'kuaishou_industry_missing_refresh_token',
    })
  }
}

function assertAuthorizationCodeConfig(config: KuaishouIndustrySourceConfig, code: string) {
  if (!config.baseUrl) {
    throw createHttpError('快手开放平台 API 地址未配置', {
      statusCode: 400,
      errorCode: 'kuaishou_industry_missing_base_url',
    })
  }

  if (!config.appKey || !config.appSecret) {
    throw createHttpError('快手 appKey/appSecret 未配置，无法换取 accessToken', {
      statusCode: 400,
      errorCode: 'kuaishou_industry_missing_oauth_credential',
    })
  }

  if (!String(code || '').trim()) {
    throw createHttpError('授权 code 未填写，无法换取 accessToken', {
      statusCode: 400,
      errorCode: 'kuaishou_industry_missing_authorization_code',
    })
  }
}

async function requestKuaishouIndustryToken({
  config,
  path,
  params,
  method,
  failureMessage,
  errorCode,
}: {
  config: KuaishouIndustrySourceConfig
  path: string
  params: URLSearchParams
  method: 'GET' | 'POST'
  failureMessage: string
  errorCode: string
}) {
  const baseUrl = String(config.baseUrl || '')
    .trim()
    .replace(/\/+$/, '')
  const url = new URL(`${baseUrl}${path}`)
  const response =
    method === 'GET'
      ? await fetch(appendSearchParams(url, params))
      : await fetch(url, {
          method: 'POST',
          headers: { 'content-type': 'application/x-www-form-urlencoded' },
          body: params.toString(),
        })
  const text = await response.text()
  const json = parseJsonObject(text)
  const tokenPayload = normalizeTokenResponse(json, config)

  if (!response.ok || !isTokenResponseSuccess(json) || !tokenPayload.accessToken) {
    const message = resolveTokenErrorMessage(json, response.status, failureMessage)
    await patchKuaishouIndustrySourceConfig({
      lastRefreshError: message,
    })
    throw createHttpError(message, {
      statusCode: 502,
      errorCode,
    })
  }

  return tokenPayload
}

async function saveTokenPayload(
  tokenPayload: ReturnType<typeof normalizeTokenResponse>,
  config: KuaishouIndustrySourceConfig,
  shop: KuaishouIndustryShopConfig,
) {
  const nextSellerId = tokenPayload.sellerId || shop.sellerId
  if (!nextSellerId) {
    throw createHttpError('授权结果缺少 sellerId，请在店铺授权中填写 sellerId 后重试', {
      statusCode: 502,
      errorCode: 'kuaishou_industry_missing_token_seller_id',
    })
  }

  const nextRefreshToken = tokenPayload.refreshToken || shop.refreshToken
  const savedConfig = await patchKuaishouIndustryShopConfig(nextSellerId, {
    ...shop,
    enabled: shop.enabled !== false,
    sellerId: nextSellerId,
    shopId: shop.shopId || nextSellerId,
    accessToken: tokenPayload.accessToken,
    refreshToken: nextRefreshToken,
    accessTokenExpiresAt: tokenPayload.accessTokenExpiresAt,
    refreshTokenExpiresAt:
      tokenPayload.refreshTokenExpiresAt ||
      (tokenPayload.refreshToken
        ? new Date(Date.now() + DEFAULT_REFRESH_TOKEN_TTL_MS).toISOString()
        : shop.refreshTokenExpiresAt),
    openId: tokenPayload.openId || shop.openId,
    grantedScopes: tokenPayload.grantedScopes || shop.grantedScopes,
    lastRefreshedAt: new Date().toISOString(),
    lastRefreshError: '',
  })

  return {
    config: savedConfig,
    shop: resolveTokenShop(savedConfig, nextSellerId),
  }
}

function normalizeTokenResponse(json: JsonObject, _currentConfig: KuaishouIndustrySourceConfig) {
  const payload = isPlainObject(json.data) ? json.data : json
  const accessToken = pickFirstString(payload, ['access_token', 'accessToken', 'accessTokenValue'])
  const refreshToken = pickFirstString(payload, [
    'refresh_token',
    'refreshToken',
    'refreshTokenValue',
  ])
  const sellerId = pickFirstString(payload, ['seller_id', 'sellerId'])
  const openId = pickFirstString(payload, ['open_id', 'openId'])
  const grantedScopes = pickScopes(payload)

  return {
    accessToken,
    refreshToken,
    sellerId,
    openId,
    grantedScopes,
    accessTokenExpiresAt: resolveExpiresAt(
      payload,
      [
        'access_token_expires_at',
        'accessTokenExpiresAt',
        'accessTokenExpireAt',
        'expires_at',
        'expireAt',
      ],
      ['expires_in', 'expiresIn', 'access_token_expires_in', 'accessTokenExpiresIn'],
      DEFAULT_ACCESS_TOKEN_TTL_MS,
    ),
    refreshTokenExpiresAt: resolveExpiresAt(
      payload,
      ['refresh_token_expires_at', 'refreshTokenExpiresAt', 'refreshTokenExpireAt'],
      ['refresh_token_expires_in', 'refreshTokenExpiresIn'],
      0,
    ),
  }
}

function appendSearchParams(url: URL, params: URLSearchParams): URL {
  for (const [key, value] of params.entries()) {
    url.searchParams.set(key, value)
  }

  return url
}

function resolveExpiresAt(
  payload: JsonObject,
  dateKeys: string[],
  durationKeys: string[],
  fallbackDurationMs: number,
): string {
  for (const key of dateKeys) {
    const value = payload[key]
    const timestamp = parseTimestampLike(value)
    if (Number.isFinite(timestamp) && timestamp > 0) {
      return new Date(timestamp).toISOString()
    }
  }

  for (const key of durationKeys) {
    const seconds = Number(payload[key])
    if (Number.isFinite(seconds) && seconds > 0) {
      return new Date(Date.now() + seconds * 1000).toISOString()
    }
  }

  return fallbackDurationMs > 0 ? new Date(Date.now() + fallbackDurationMs).toISOString() : ''
}

function parseTimestampLike(value: unknown): number {
  const raw = typeof value === 'number' ? value : Number(String(value || '').trim())

  if (Number.isFinite(raw) && raw > 0) {
    return raw > 10_000_000_000 ? raw : raw * 1000
  }

  return Date.parse(String(value || '').trim())
}

function isTokenResponseSuccess(json: JsonObject): boolean {
  const payload = isPlainObject(json.data) ? json.data : json
  const result = payload.result ?? json.result
  if (result === undefined || result === null || result === '') {
    return true
  }

  return Number(result) === 1 || result === true || String(result).toLowerCase() === 'true'
}

function resolveTokenErrorMessage(json: JsonObject, status: number, fallback: string): string {
  const payload = isPlainObject(json.data) ? json.data : json
  return (
    pickFirstString(payload, [
      'error_msg',
      'errorMsg',
      'message',
      'msg',
      'error_description',
      'errorDescription',
    ]) || `${fallback}，HTTP ${status}`
  )
}

function parseJsonObject(text: string): JsonObject {
  try {
    const parsed = JSON.parse(text || '{}')
    return isPlainObject(parsed) ? parsed : { raw: text }
  } catch {
    return { raw: text }
  }
}

function pickFirstString(payload: JsonObject, keys: string[]): string {
  for (const key of keys) {
    const text = String(payload[key] || '').trim()
    if (text) {
      return text
    }
  }

  return ''
}

function pickScopes(payload: JsonObject): string {
  const value = payload.scopes ?? payload.scope
  if (Array.isArray(value)) {
    return value
      .map((item) => String(item || '').trim())
      .filter(Boolean)
      .join(',')
  }

  return String(value || '').trim()
}

function resolveRefreshErrorDetail(error: unknown): JsonObject {
  const detail: JsonObject = {
    error: error instanceof Error ? error.message : String(error),
  }
  const cause =
    error && typeof error === 'object' && 'cause' in error
      ? (error as { cause?: unknown }).cause
      : null
  if (!cause || typeof cause !== 'object') {
    return detail
  }

  const current = cause as Record<string, unknown>
  for (const [key, value] of [
    ['causeMessage', current.message],
    ['causeCode', current.code],
    ['causeSyscall', current.syscall],
    ['causeHostname', current.hostname],
  ] as Array<[string, unknown]>) {
    const text = String(value || '').trim()
    if (text) {
      detail[key] = text
    }
  }

  return detail
}

function isPlainObject(value: unknown): value is JsonObject {
  return Object.prototype.toString.call(value) === '[object Object]'
}
