/* eslint-disable no-nested-ternary */
import {
  IFetchOptions,
  IRequestConfig,
  IRequestMethod,
  IRequestOptions,
  ResponseObject,
  SDKRequestInterface,
  formatUrl,
} from '@cloudbase/adapter-interface'
import { ADMIN_PATH, CLIENT_AUTH_PATH } from './constants'
import { getCloudbaseContext, getSecretInfo, INIT_CONFIG } from './context'
import type { ICustomReqOpts } from './types'
import {
  getEnv,
  isFormData,
  toQueryString,
  safeParseJson,
  obj2StrRecord,
  headersInit2Indexable,
  getCurrRunEnvTag,
} from './utils'
import { ICloudbaseConfig } from '@cloudbase/types'
import { getSdkVersion } from '../../constants/common'
// 延迟加载，避免非 Node 环境打包时引入此包
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let signFn: ((...args: any[]) => any) | null = null
async function getSign() {
  if (!signFn) {
    try {
      // @ts-ignore — 该包仅在 Node 运行时存在，开发环境可能未安装
      const mod = await import('@cloudbase/signature-nodejs')
      signFn = mod.sign
    } catch (e) {
      console.error('缺少依赖 @cloudbase/signature-nodejs，请执行以下命令安装：\n\n'
        + '  npm install @cloudbase/signature-nodejs\n\n'
        + '该依赖用于 Node 环境下的请求签名。')
    }
  }
  return signFn!
}

/**
 * Node.js 环境下的 HTTP 请求实现
 * 基于原生 fetch，支持 GET/POST/PUT/上传/下载/流式请求
 * 自动处理 V3 签名和 Bearer Token 认证
 */
export class NodeRequest implements SDKRequestInterface {
  /** 请求配置，包含认证信息 */
  config: IRequestConfig & ICloudbaseConfig

  /** 请求超时时间（毫秒），默认 15000 */
  private readonly timeout: number

  /** 受超时控制的请求类型列表 */
  private readonly restrictedMethods: Array<IRequestMethod>

  constructor(config: IRequestConfig & ICloudbaseConfig) {
    const { timeout, restrictedMethods } = config
    this.timeout = timeout || 15000
    this.restrictedMethods = restrictedMethods || ['get', 'post', 'upload', 'download']
    this.config = config
  }

  /**
   * 获取客户端凭证 access_token，为管理员权限
   * 用于不支持 V3 签名的接口（如数据模型、关系型数据库、AI）
   */
  async getClientCredential(opts: { origin: string }): Promise<any> {
    const res = await this.fetch({
      url: `${opts.origin}${CLIENT_AUTH_PATH}`,
      method: 'POST',
      headers: {
        'content-type': 'application/json',
      },
      body: JSON.stringify({
        grant_type: 'client_credentials',
      }),
    })

    return res.data?.access_token
  }

  /**
   * 获取实际请求 URL
   * 当请求未携带 Authorization 且未自带 access_token 时，
   * 将 /web 路径替换为 /admin（管理端签名路径）
   */
  getRealUrl(url: string, headers: Record<string, any>, body: any) {
    if (headers.Authorization?.replace('Bearer', '')?.trim()) {
      return url
    }

    const params = typeof body === 'object' ? body : safeParseJson(body) || ''

    if (params.access_token) {
      return url
    }

    const urlObj = new URL(url)

    if (urlObj.pathname === '/web' && this.config.auth?.secretId && this.config.auth?.secretKey) {
      urlObj.pathname = ADMIN_PATH
    }
    return urlObj.toString()
  }

  /**
   * 为请求添加签名或认证信息
   *
   * 处理逻辑优先级：
   * 1. 已有 Authorization header → 直接返回
   * 2. /auth/ 路径 → SDK 认证接口，无需签名
   * 3. 不支持 V3 签名的路径（/v1/model/ 等）→ 用 clientCredential Bearer Token
   * 4. 参数含 access_token → 无需签名
   * 5. 其他 → 使用 V3 签名（TC3-HMAC-SHA256）
   */
  public getReqOptions = async (
    url: string,
    options: {
      method?: any
      headers: any
      body?: any
      credentials?: string
      signal?: AbortSignal
      url?: any
    },
  ) => {
    const urlObj = new URL(url)

    const { TCB_SOURCE } = getCloudbaseContext()
    // Note: 云函数被调用时可能调用端未传递 SOURCE，TCB_SOURCE 可能为空
    const SOURCE = `${INIT_CONFIG.context?.extendedContext?.source || TCB_SOURCE || ''},${await getCurrRunEnvTag()}`

    options.headers = {
      ...options.headers,
      'User-Agent': `adapter-node/${options.headers?.['X-SDK-Version'] || `@cloudbase/js-sdk/${getSdkVersion()}`}`,
      'X-TCB-Source': SOURCE,
      'X-Client-Timestamp': new Date().valueOf(),
      'X-TCB-Region': INIT_CONFIG.region || getEnv('TENCENTCLOUD_REGION') || '',
      Host: urlObj.host,
    }

    // 已携带有效 Authorization，直接返回
    if (options.headers.Authorization?.replace('Bearer', '')?.trim()) {
      return options
    }

    // auth 相关接口不需要签名
    if (urlObj.pathname.startsWith('/auth/') && urlObj.pathname !== CLIENT_AUTH_PATH) {
      return options
    }

    const isAdminPath = urlObj.pathname === ADMIN_PATH

    // 这些路径不支持 V3 签名，改用 clientCredential 获取 Bearer Token
    const UNSUPPORTED_V3_SIGN_PATH = ['/v1/model/', '/v1/rdb/', '/v1/ai/']

    if (UNSUPPORTED_V3_SIGN_PATH.some(v => urlObj.pathname.startsWith(v))) {
      const token = await this.getClientCredential({ origin: urlObj.origin })
      options.headers.Authorization = `Bearer ${token}`
      return options
    }

    let { secretId, secretKey, sessionToken } = this.config?.auth || {}
    const { secretType } = this.config?.auth || {}
    const { method, body } = options
    const headers = JSON.parse(JSON.stringify(options.headers))
    let params = typeof body === 'object' ? body : safeParseJson(body) || {}

    params.envName = params?.env || this.config.env || getSecretInfo()?.env || ''

    // 参数自带 access_token，无需签名
    if (params.access_token) {
      return options
    }

    // GET 请求参数已在 URL 中，签名时 params 置空
    if (method.toLowerCase() === 'get') {
      params = undefined
    }

    // SESSION_SECRET 类型或无密钥时，从环境变量获取临时密钥
    if (!secretId || !secretKey || secretType === 'SESSION_SECRET') {
      const secretInfo = getSecretInfo()
      secretId = secretInfo.secretId
      secretKey = secretInfo.secretKey
      sessionToken = secretInfo.sessionToken
    }

    // 仍无密钥则跳过签名
    if (!secretId || !secretKey) {
      return options
    }

    delete headers.Authorization

    // content-type 需与服务端签名验证保持大小写一致
    if (headers['content-type']) {
      headers['content-type'] = headers['content-type'].toLowerCase()
    } else if (headers['Content-Type']) {
      headers['Content-Type'] = headers['Content-Type'].toLowerCase()
    }

    if (isAdminPath && !!sessionToken) {
      params.sessionToken = sessionToken
    }

    const signedParams = {
      secretId,
      secretKey,
      method,
      url,
      params,
      headers,
      withSignedParams: isAdminPath,
      timestamp: Math.floor(new Date().getTime() / 1000) - 1,
      isCloudApi: !isAdminPath,
    }

    if (!headers['content-type'] && !headers['Content-Type']) {
      return options
    }

    const sign = await getSign()
    if (!sign) return options

    const { authorization, timestamp } = sign(signedParams)

    if (typeof sessionToken === 'string' && sessionToken !== '') {
      if (isAdminPath) {
        headers.Authorization = `${authorization}`
        headers['X-Timestamp'] = timestamp
        headers['X-Signature-Expires'] = 600
      } else  {
        // 临时密钥需要额外附带 Token
        headers.Authorization = `${authorization}, Timestamp=${timestamp}, Token=${sessionToken}`
      }
    } else {
      headers.Authorization = `${authorization}, Timestamp=${timestamp}`

      // admin 路径需要额外的时间戳和过期头
      if (isAdminPath) {
        headers['X-Timestamp'] = timestamp
        headers['X-Signature-Expires'] = 600
      }
    }

    return {
      ...options,
      headers,
      body: typeof params === 'object' ? JSON.stringify(params) : params,
    }
  }

  public get = (options: IRequestOptions): Promise<ResponseObject> => this.request(
    {
      ...options,
      method: 'get',
    },
    this.restrictedMethods.includes('get'),
  )

  public post = (options: IRequestOptions): Promise<ResponseObject> => this.request(
    {
      ...options,
      method: 'post',
    },
    this.restrictedMethods.includes('post'),
  )

  public put = (options: IRequestOptions): Promise<ResponseObject> => this.request({
    ...options,
    method: 'put',
  })

  /**
   * 文件上传
   * POST 方式：构建 FormData 上传
   * PUT 方式：直接将文件作为 body 上传
   */
  public upload = (options: IRequestOptions): Promise<ResponseObject> => {
    const { data: _data, file, name, method, headers = {} } = options
    if (file === undefined || name == undefined) {
      throw new Error('file and name is required')
    }
    const data = obj2StrRecord(_data ?? {})
    const loweredMethod = method?.toLowerCase()
    const reqMethod = ['post', 'put'].find(m => m === loweredMethod) ?? 'put'
    const formData = new FormData()
    if (reqMethod === 'post') {
      Object.keys(data).forEach((key) => {
        formData.append(key, data[key])
      })
      formData.append('key', name)
      formData.append('file', file)
      return this.request(
        {
          ...options,
          data: formData,
          method: reqMethod,
        },
        this.restrictedMethods.includes('upload'),
      )
    }

    return this.request(
      {
        ...options,
        method: 'put',
        headers,
        body: file,
      },
      this.restrictedMethods.includes('upload'),
    )
  }

  /** 文件下载（浏览器环境，通过创建 <a> 标签触发下载） */
  public download = async (options: IRequestOptions): Promise<unknown> => {
    const { data } = await this.get({
      ...options,
      headers: {},
      responseType: 'blob',
    })
    const url = window.URL.createObjectURL(new Blob([data]))
    const fileName = decodeURIComponent(new URL(options?.url ?? '').pathname.split('/').pop() || '')
    const link = document.createElement('a')

    link.href = url
    link.setAttribute('download', fileName)
    link.style.display = 'none'

    document.body.appendChild(link)
    link.click()
    window.URL.revokeObjectURL(url)
    document.body.removeChild(link)
    return new Promise((resolve) => {
      resolve({
        statusCode: 200,
        tempFilePath: options.url,
      })
    })
  }

  /**
   * 底层 fetch 请求，支持流式响应
   * 超时通过 AbortController + setTimeout 实现
   */
  public fetch = async (options: Omit<IFetchOptions, 'signal'> & { signal?: AbortSignal; customReqOpts?: ICustomReqOpts }): Promise<ResponseObject> => {
    const { enableAbort = false, stream = false, signal, customReqOpts } = options
    const url = this.getRealUrl(options.url, options.headers || {}, options.body)
    const abortController = new AbortController()
    const timeout = customReqOpts?.timeout || this.timeout

    // 桥接外部 signal 到内部 AbortController
    if (signal) {
      if (signal.aborted) {
        abortController.abort()
      } else {
        signal.addEventListener('abort', () => abortController.abort())
      }
    }

    // 超时自动中断
    let timer = undefined
    if (enableAbort || timeout) {
      timer = setTimeout(() => {
        const timeoutMsg = `请求在${timeout / 1000}s内未完成，已中断`
        console.warn(timeoutMsg)
        abortController.abort(new Error(timeoutMsg))
      }, timeout)
    }

    const headers = options.headers ? headersInit2Indexable(options.headers) : undefined

    const fetchOptions = await this.getReqOptions(url, {
      ...options,
      headers,
      body: options.body as any as NodeJS.ReadableStream,
      signal: abortController.signal,
    })

    const res = await fetch(url, fetchOptions as RequestInit)
      .then((x) => {
        clearTimeout(timer)
        return x
      })
      .catch((x) => {
        clearTimeout(timer)
        return Promise.reject(x)
      })

    const ret = {
      data:
        +res.status === 204
          ? '' // 204 No Content
          : stream
            ? res.body
            : await res.json(),
      statusCode: res.status,
      header: res.headers,
    }
    return ret
  }

  /**
   * 通用请求方法，被 get/post/put/upload 等调用
   * 负责构建 payload、设置超时、解析响应
   *
   * @param options - 请求选项
   * @param enableAbort - 是否启用超时中断（由 restrictedMethods 决定）
   */
  public request = async (options: IRequestOptions, enableAbort = false): Promise<ResponseObject> => {
    const {
      url,
      headers: _headers = {},
      data,
      responseType,
      withCredentials,
      body,
      method: _method,
      customReqOpts,
    } = options
    const headers = obj2StrRecord(_headers)
    const method = String(_method).toLowerCase() || 'get'
    const abortController = new AbortController()
    const { signal } = abortController
    const timeout = customReqOpts?.timeout || this.timeout

    // 构建请求 payload：FormData > urlencoded > raw body > JSON
    let payload
    if (isFormData(data)) {
      payload = data
    } else if (headers['content-type'] === 'application/x-www-form-urlencoded') {
      payload = toQueryString(data ?? {})
    } else if (body) {
      payload = body
    } else {
      payload = data ? JSON.stringify(data) : undefined
    }

    const realUrl = this.getRealUrl(
      formatUrl('https', url ?? '', method === 'get' ? data : {}),
      options.headers || {},
      payload,
    )

    // 超时通过 AbortController + setTimeout 实现
    let timer

    if (enableAbort || timeout) {
      timer = setTimeout(() => {
        const timeoutMsg = `请求在${timeout / 1000}s内未完成，已中断`
        console.warn(timeoutMsg)
        abortController.abort(new Error(timeoutMsg))
      }, timeout)
    }

    const requestOptions = await this.getReqOptions(realUrl, {
      method,
      headers,
      body: payload,
      credentials: withCredentials ? 'include' : 'same-origin',
      signal,
    })

    try {
      const response = await fetch(realUrl, requestOptions as RequestInit)
      const result: ResponseObject = {
        header: {},
        statusCode: response.status,
      }

      try {
        result.data = responseType === 'blob' ? await response.blob() : safeParseJson(await response.text())
      } catch (e) {
        // 上传 POST 请求可能返回 XML 等非 JSON 格式，容错处理
        console.log('catch an error', e)
        result.data = responseType === 'blob' ? await response.blob() : await response.text()
      }

      // 将响应头统一转为小写 key
      const { headers } = response
      headers.forEach((val, key) => (result.header[key.toLowerCase()] = val))

      return result
    } finally {
      clearTimeout(timer)
    }
  }
}
