import axios from 'axios'
import wxCloudClient, { IMySqlClient, IMySqlOptions, OrmClient, OrmRawQueryClient } from '@cloudbase/wx-cloud-client-sdk'

import {
  ICloudBaseConfig,

  ISCFContext,
  IContextParam,
  ICompleteCloudbaseContext,

  ICustomReqOpts,

  ICallFunctionOptions,
  ICallContainerOptions,

  IUploadFileOptions,
  IUploadFileResult,
  IDownloadFileOptions,
  IDownloadFileResult,
  ICopyFileOptions,
  ICopyFileResult,
  IDeleteFileOptions,
  IDeleteFileResult,
  IGetFileUrlOptions,
  IGetFileUrlResult,
  IGetFileInfoOptions,
  IGetFileInfoResult,
  IGetUploadMetadataOptions,
  IGetUploadMetadataResult,
  IGetFileAuthorityOptions,
  IGetFileAuthorityResult,

  ICallWxOpenApiOptions,
  ICallWxOpenApiResult,

  IReportData,

  Extension,
  ICallApisOptions,
  ICloudBaseDBConfig
} from '../types'

import { auth } from './auth'
import { callApis, callFunction } from './functions'
import { callContainer } from './cloudrun'
import { newDb } from './database'
import { uploadFile, deleteFile, getTempFileURL, getFileInfo, downloadFile, getUploadMetadata, getFileAuthority, copyFile } from './storage'
import { callWxOpenApi, callCompatibleWxOpenApi, callWxPayApi, wxCallContainerApi } from './wx'
import { analytics } from './analytics'
import { createAI } from './ai'
import { AI } from '@cloudbase/ai'

import { Logger, logger } from './logger'

import { ERROR } from './const/code'
import * as utils from './utils/utils'

import { preflightRuntimeCloudPlatform } from './utils/cloudplatform'
import { parseContext, getCloudbaseContext } from './utils/tcbcontext'
import { sendNotification, ITemplateNotifyReq } from './notification'
import * as openapicommonrequester from './utils/tcbopenapicommonrequester'
import { IFetchOptions } from '@cloudbase/adapter-interface'
import { buildCommonOpenApiUrlWithPath } from './utils/tcbopenapiendpoint'
import { SYMBOL_CURRENT_ENV } from './const/symbol'
import { IncomingHttpHeaders } from 'http'
import { normalizeConfig } from './utils/utils'
export class CloudBase {
  public static scfContext: ISCFContext

  public static parseContext(context: IContextParam): ISCFContext {
    const parseResult = parseContext(context)
    CloudBase.scfContext = parseResult
    return parseResult
  }

  public static getCloudbaseContext(context?: IContextParam): ICompleteCloudbaseContext {
    return getCloudbaseContext(context)
  }

  public config: ICloudBaseConfig

  private clsLogger: Logger

  private extensionMap: Map<string, Extension>

  private aiInstance: AI

  public models: OrmClient & OrmRawQueryClient

  public mysql: IMySqlClient
  public rdb: IMySqlClient

  public constructor(config?: ICloudBaseConfig) {
    this.init(config)
  }

  public init(config: ICloudBaseConfig = {}): void {
    // 预检运行环境，调用与否并不影响后续逻辑
    // 注意：该函数为异步函数，这里并不等待检查结果
    /* eslint-disable-next-line */
    preflightRuntimeCloudPlatform()

    // 所有的鉴权，参数塑形都在 normalizeConfig 中处理
    // 后续其他模块获取 config 都通过 CloudBase 实例的 config 获取
    // 禁止在业务模块中直接修改 config 配置
    this.config = normalizeConfig(config)
    this.extensionMap = new Map()

    // NOTE：try-catch 为防止 init 报错
    try {
      // 初始化数据模型等 SDK 方法
      const envId = this.config.envName === SYMBOL_CURRENT_ENV
        ? openapicommonrequester.getEnvIdFromContext()
        : (this.config.envName as string)
      const httpClient = wxCloudClient.generateHTTPClient(this.callFunction.bind(this), async (options: IFetchOptions) => {
        const result = await openapicommonrequester.request({
          config: this.config,
          data: safeParseJSON(options.body),
          method: options.method?.toUpperCase(),
          url: options.url,
          headers: {
            'Content-Type': 'application/json',
            ...headersInitToRecord(options.headers)
          },
          /**
           * 既然 openapicommonrequester.request 的参数里的 token 获取也是通过 openapicommonrequester.request 方法去获取的
           * 为什么不把这里的 token 去掉，全部放在 openapicommonrequester.request 中去统一处理 token 获取的逻辑
           */
          token: (await this.auth().getClientCredential()).access_token
        })
        return result.body
      }, buildCommonOpenApiUrlWithPath({ serviceUrl: this.config.serviceUrl, envId, path: '/v1/model', region: this.config.region }), {
        sqlBaseUrl: buildCommonOpenApiUrlWithPath({ serviceUrl: this.config.serviceUrl, envId, path: '/v1/sql', region: this.config.region })
      })

      this.models = httpClient
    } catch (e) {
      // ignore
    }

    try {
      const getEntity = (options: IMySqlOptions) => {
        const envId
          = this.config.envName === SYMBOL_CURRENT_ENV
            ? openapicommonrequester.getEnvIdFromContext()
            : (this.config.envName as string)

        const { instance = 'default', database = envId } = options || {}

        const mysqlClient = wxCloudClient.generateMySQLClient(this, {
          mysqlBaseUrl: buildCommonOpenApiUrlWithPath({
            serviceUrl: this.config.serviceUrl,
            envId,
            path: '/v1/rdb/rest',
            region: this.config.region
          }),

          fetch: async (url: RequestInfo | URL, options: RequestInit) => {
            let headers = {}
            if (options.headers instanceof Headers) {
              options.headers.forEach((value, key) => {
                headers[key] = value
              })
            } else {
              headers = options.headers || {}
            }

            const result = await openapicommonrequester.request({
              config: this.config,
              data: safeParseJSON(options.body),
              method: options.method?.toUpperCase(),
              url: url instanceof URL ? url.href : String(url),
              headers: {
                'Content-Type': 'application/json',
                ...headersInitToRecord({
                  'X-Db-Instance': instance,
                  'Accept-Profile': database,
                  'Content-Profile': database,
                  ...headers
                })
              },
              token: (await this.auth().getClientCredential()).access_token
            })

            const data = result.body

            const res = {
              ok: result?.statusCode >= 200 && result?.statusCode < 300,
              status: result?.statusCode || 200,
              statusText: (result as unknown as { statusMessage: string })?.statusMessage || 'OK',
              json: async () => await Promise.resolve(data || {}),
              text: async () => await Promise.resolve(typeof data === 'string' ? data : JSON.stringify(data || {})),
              headers: new Headers(incomingHttpHeadersToHeadersInit(result?.headers || {}))
            }

            return res as Response
          }
        })

        return mysqlClient
      }

      this.mysql = (options: IMySqlOptions) => {
        return getEntity(options)(options)
      }
      this.rdb = (options: IMySqlOptions) => {
        return getEntity(options)(options)
      }
    } catch (e) {
      // ignore
    }
  }

  public logger(): Logger {
    if (!this.clsLogger) {
      this.clsLogger = logger()
    }
    return this.clsLogger
  }

  public auth() {
    return auth(this)
  }

  public database(dbConfig: ICloudBaseDBConfig = {}) {
    return newDb(this, dbConfig)
  }

  public ai(): AI {
    if (!this.aiInstance) {
      this.aiInstance = createAI(this)
    }
    return this.aiInstance
  }

  public async callFunction<ParaT, ResultT>(callFunctionOptions: ICallFunctionOptions<ParaT>, opts?: ICustomReqOpts) {
    return await callFunction<ParaT, ResultT>(this, callFunctionOptions, opts)
  }

  public async callContainer<ParaT, ResultT>(callContainerOptions: ICallContainerOptions<ParaT>, opts?: ICustomReqOpts) {
    return await callContainer<ParaT, ResultT>(this, callContainerOptions, opts)
  }

  public async callApis<ParaT>(callApiOptions: ICallApisOptions<ParaT>, opts?: ICustomReqOpts) {
    return await callApis<ParaT>(this, callApiOptions, opts)
  }

  public async callWxOpenApi(wxOpenApiOptions: ICallWxOpenApiOptions, opts?: ICustomReqOpts): Promise<ICallWxOpenApiResult> {
    return await callWxOpenApi(this, wxOpenApiOptions, opts)
  }

  public async callWxPayApi(wxOpenApiOptions: ICallWxOpenApiOptions, opts?: ICustomReqOpts): Promise<any> {
    return await callWxPayApi(this, wxOpenApiOptions, opts)
  }

  public async wxCallContainerApi(wxOpenApiOptions: ICallWxOpenApiOptions, opts?: ICustomReqOpts): Promise<any> {
    return await wxCallContainerApi(this, wxOpenApiOptions, opts)
  }

  public async callCompatibleWxOpenApi(wxOpenApiOptions: ICallWxOpenApiOptions, opts?: ICustomReqOpts): Promise<any> {
    return await callCompatibleWxOpenApi(this, wxOpenApiOptions, opts)
  }

  public async uploadFile({ cloudPath, fileContent }: IUploadFileOptions, opts?: ICustomReqOpts): Promise<IUploadFileResult> {
    return await uploadFile(this, { cloudPath, fileContent }, opts)
  }

  public async downloadFile({ fileID, urlType, tempFilePath }: IDownloadFileOptions, opts?: ICustomReqOpts): Promise<IDownloadFileResult> {
    return await downloadFile(this, { fileID, urlType, tempFilePath }, opts)
  }

  /**
   * 复制文件
   *
   * @param fileList 复制列表
   * @param fileList.srcPath 源文件路径
   * @param fileList.dstPath 目标文件路径
   * @param fileList.overwrite 当目标文件已经存在时，是否允许覆盖已有文件，默认 true
   * @param fileList.removeOriginal 复制文件后是否删除源文件，默认不删除
   * @param opts
   */
  public async copyFile({ fileList }: ICopyFileOptions, opts?: ICustomReqOpts): Promise<ICopyFileResult> {
    return await copyFile(this, { fileList }, opts)
  }

  public async deleteFile({ fileList }: IDeleteFileOptions, opts?: ICustomReqOpts): Promise<IDeleteFileResult> {
    return await deleteFile(this, { fileList }, opts)
  }

  public async getTempFileURL({ fileList }: IGetFileUrlOptions, opts?: ICustomReqOpts): Promise<IGetFileUrlResult> {
    return await getTempFileURL(this, { fileList }, opts)
  }

  public async getFileInfo({ fileList }: IGetFileInfoOptions, opts?: ICustomReqOpts): Promise<IGetFileInfoResult> {
    return await getFileInfo(this, { fileList }, opts)
  }

  public async getUploadMetadata({ cloudPath }: IGetUploadMetadataOptions, opts?: ICustomReqOpts): Promise<IGetUploadMetadataResult> {
    return await getUploadMetadata(this, { cloudPath }, opts)
  }

  public async getFileAuthority({ fileList }: IGetFileAuthorityOptions, opts?: ICustomReqOpts): Promise<IGetFileAuthorityResult> {
    return await getFileAuthority(this, { fileList }, opts)
  }

  /**
   * @deprecated
   */
  public async analytics(reportData: IReportData): Promise<void> {
    await analytics(this, reportData)
  }

  public registerExtension(ext: Extension): void {
    this.extensionMap.set(ext.name, ext)
  }

  public async invokeExtension<OptsT>(name: string, opts: OptsT) {
    const ext = this.extensionMap.get(name)
    if (!ext) {
      throw Error(`Please register '${name}' extension before invoke.`)
    }

    return ext.invoke(opts, this)
  }

  // SDK推送消息（对外API：sendTemplateNotification）
  public async sendTemplateNotification(params: ITemplateNotifyReq, opts?: ICustomReqOpts) {
    return await sendNotification(this, params, opts)
  }

  /**
   * shim for tcb extension ci
   */
  public get requestClient() {
    return {
      get: axios,
      post: axios,
      put: axios,
      delete: axios
    }
  }
}

function headersInitToRecord(headers: HeadersInit): Record<string, string> {
  if (!headers) {
    return {}
  }
  const ret: Record<string, string> = {}
  if (Array.isArray(headers)) {
    headers.forEach(([key, value]) => {
      ret[key] = value
    })
  } else if (typeof headers.forEach === 'function') {
    headers.forEach(([key, value]) => {
      ret[key] = value
    })
  } else {
    Object.keys(headers).forEach((key) => {
      ret[key] = headers[key]
    })
  }
  return ret
}

function safeParseJSON(x: unknown) {
  try {
    return JSON.parse(x as string)
  } catch (e) {
    return x
  }
}

function incomingHttpHeadersToHeadersInit(headers: IncomingHttpHeaders): HeadersInit {
  const result: Array<[string, string]> = []
  for (const [key, value] of Object.entries(headers)) {
    if (value === undefined) {
      continue
    }
    if (Array.isArray(value)) {
      for (const v of value) {
        result.push([key, v])
      }
    } else {
      result.push([key, value])
    }
  }
  return result
}
