import Bowser from 'bowser'

type RequestPrimitive = string | number | boolean | null | undefined

type RequestValue = RequestPrimitive | RequestPrimitive[]

type RequestParams = Record<string, RequestValue>

type RequestOptions = Omit<RequestInit, 'body' | 'method'> & {
  body?: BodyInit | Record<string, unknown> | null
  tokenType?: 'permanent' | 'temporary'
}

type ApiResponseLike = {
  code?: unknown
  message?: unknown
}

export type ApiResponse<T> = {
  code: number
  message: string
  data: T
}

type ClientSysInfo = {
  version?: RequestPrimitive
  platform?: RequestPrimitive
  deviceId?: RequestPrimitive
  brand?: RequestPrimitive
  model?: RequestPrimitive
  channel?: RequestPrimitive
  bd_vid?: RequestPrimitive
}

declare global {
  interface Window {
    sysInfo?: ClientSysInfo
  }
}

const SYS_INFO_STORAGE_KEY = 'sysInfo'
export const TOKEN_STORAGE_KEY = 'token'
export const TEMP_TOKEN_STORAGE_KEY = 'tempToken'

function isRecord(value: unknown): value is Record<string, RequestPrimitive> {
  return Object.prototype.toString.call(value) === '[object Object]'
}

function readStoredSysInfo(): Partial<ClientSysInfo> {
  if (typeof window === 'undefined') {
    return {}
  }

  try {
    const rawSysInfo = window.localStorage.getItem(SYS_INFO_STORAGE_KEY)
    if (!rawSysInfo) {
      return {}
    }

    const sysInfo: unknown = JSON.parse(rawSysInfo)
    return isRecord(sysInfo) ? sysInfo : {}
  } catch {
    return {}
  }
}

function writeStoredSysInfo(sysInfo: Partial<ClientSysInfo>) {
  if (typeof window === 'undefined') {
    return
  }

  try {
    window.localStorage.setItem(SYS_INFO_STORAGE_KEY, JSON.stringify(sysInfo))
  } catch {
    // localStorage can be blocked in private mode; headers can still use in-memory values.
  }
}

function createDeviceId() {
  if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
    return crypto.randomUUID()
  }

  if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
    const bytes = crypto.getRandomValues(new Uint8Array(16))
    bytes[6] = (bytes[6] & 0x0f) | 0x40
    bytes[8] = (bytes[8] & 0x3f) | 0x80

    const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0'))
    return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex.slice(6, 8).join('')}-${hex
      .slice(8, 10)
      .join('')}-${hex.slice(10, 16).join('')}`
  }

  return `${Date.now()}-${Math.random().toString(16).slice(2)}`
}

function getDeviceId(storedSysInfo: Partial<ClientSysInfo>) {
  const storedDeviceId = storedSysInfo.deviceId
  if (storedDeviceId) {
    return String(storedDeviceId)
  }

  const deviceId = createDeviceId()
  writeStoredSysInfo({ ...storedSysInfo, deviceId })
  return deviceId
}

function mergeDefinedSysInfo(baseInfo: ClientSysInfo, overrideInfo: ClientSysInfo) {
  const mergedInfo = { ...baseInfo }

  Object.entries(overrideInfo).forEach(([key, value]) => {
    if (value !== undefined && value !== null && value !== '') {
      mergedInfo[key as keyof ClientSysInfo] = value
    }
  })

  return mergedInfo
}

export function getBrowserInfo(): ClientSysInfo {
  const storedSysInfo = readStoredSysInfo()
  const userAgent = typeof navigator === 'undefined' ? '' : navigator.userAgent
  const browserInfo = userAgent ? Bowser.parse(userAgent) : undefined
  const brand = browserInfo?.platform.vendor || browserInfo?.browser.name || ''
  const model = browserInfo?.platform.model || browserInfo?.browser.version || ''

  return {
    version: __APP_VERSION__,
    platform: 'h5',
    channel: 'h5',
    deviceId: getDeviceId(storedSysInfo),
    brand,
    model,
    bd_vid: storedSysInfo.bd_vid,
  }
}

function getSearchParams() {
  if (typeof window === 'undefined') {
    return new URLSearchParams()
  }

  return new URLSearchParams(window.location.search)
}

function getClientSysInfo() {
  if (typeof window === 'undefined') {
    return {}
  }

  return window.sysInfo ?? {}
}

function toHeaderValue(value: RequestPrimitive) {
  return value === undefined || value === null ? '' : String(value)
}

function setCommonHeaders(headers: Headers, tokenType: RequestOptions['tokenType']) {
  const params = getSearchParams()
  const sysInfo = mergeDefinedSysInfo(getBrowserInfo(), getClientSysInfo())

  headers.set('x-version', toHeaderValue(sysInfo.version ?? __APP_VERSION__))
  headers.set('x-platform', toHeaderValue(sysInfo.platform))
  headers.set('x-device-id', toHeaderValue(sysInfo.deviceId))
  headers.set('x-mobile-brand', toHeaderValue(sysInfo.brand))
  headers.set('x-mobile-model', toHeaderValue(sysInfo.model))
  headers.set('x-channel', toHeaderValue(sysInfo.channel))
  headers.set('x-click-id', toHeaderValue(params.get('bd_vid') ?? sysInfo.bd_vid ?? ''))
  headers.set('x-app-id', toHeaderValue(__APP_ID__))
  const tokenStorageKey = tokenType === 'temporary' ? TEMP_TOKEN_STORAGE_KEY : TOKEN_STORAGE_KEY
  const token = window.sessionStorage.getItem(tokenStorageKey)
  if (token) {
    headers.set('x-token', token)
  }
}

function clearStoredTokens() {
  if (typeof window === 'undefined') {
    return
  }

  window.sessionStorage.removeItem(TOKEN_STORAGE_KEY)
  window.sessionStorage.removeItem(TEMP_TOKEN_STORAGE_KEY)
}

function buildQueryString(params?: RequestParams) {
  if (!params) {
    return ''
  }

  const searchParams = new URLSearchParams()

  Object.entries(params).forEach(([key, value]) => {
    if (value === undefined || value === null) {
      return
    }

    if (Array.isArray(value)) {
      value.forEach((item) => {
        if (item !== undefined && item !== null) {
          searchParams.append(key, String(item))
        }
      })
      return
    }

    searchParams.append(key, String(value))
  })

  const queryString = searchParams.toString()
  return queryString ? `?${queryString}` : ''
}

function isPlainObject(value: RequestOptions['body']): value is Record<string, unknown> {
  return Object.prototype.toString.call(value) === '[object Object]'
}

function isApiResponseLike(value: unknown): value is ApiResponseLike {
  return Object.prototype.toString.call(value) === '[object Object]' && 'code' in (value as Record<string, unknown>)
}

let toastTimer: number | null = null

function showToast(message: string) {
  if (typeof document === 'undefined') {
    return
  }

  const existingToast = document.getElementById('request-error-toast')
  if (existingToast) {
    existingToast.remove()
  }

  if (toastTimer !== null) {
    window.clearTimeout(toastTimer)
  }

  const toast = document.createElement('div')
  toast.id = 'request-error-toast'
  toast.textContent = message
  toast.setAttribute('role', 'alert')
  toast.style.position = 'fixed'
  toast.style.left = '50%'
  toast.style.top = '50%'
  toast.style.zIndex = '9999'
  toast.style.maxWidth = 'calc(100vw - 48px)'
  toast.style.padding = '10px 16px'
  toast.style.borderRadius = '8px'
  toast.style.background = 'rgba(0, 0, 0, 0.78)'
  toast.style.color = '#fff'
  toast.style.fontSize = '14px'
  toast.style.lineHeight = '20px'
  toast.style.textAlign = 'center'
  toast.style.transform = 'translate(-50%, -50%)'
  toast.style.boxShadow = '0 8px 24px rgba(0, 0, 0, 0.18)'
  toast.style.pointerEvents = 'none'

  document.body.appendChild(toast)

  toastTimer = window.setTimeout(() => {
    toast.remove()
    toastTimer = null
  }, 2200)
}

function getBusinessErrorMessage(message: unknown) {
  if (typeof message !== 'string') {
    return '请求失败'
  }

  const errorMessage = message.split(/[,，]/)[0]?.trim()
  return errorMessage || message || '请求失败'
}

async function request<T>(url: string, method: string, options: RequestOptions = {}) {
  const { body, headers, tokenType, ...restOptions } = options
  const requestHeaders = new Headers(headers)
  setCommonHeaders(requestHeaders, tokenType)
  let requestBody: BodyInit | null | undefined

  if (isPlainObject(body)) {
    if (!requestHeaders.has('Content-Type')) {
      requestHeaders.set('Content-Type', 'application/json')
    }
    requestBody = JSON.stringify(body)
  } else {
    requestBody = body
  }

  const response = await fetch(url, {
    ...restOptions,
    method,
    headers: requestHeaders,
    body: requestBody,
  })

  if (!response.ok) {
    throw new Error(`Request failed with status ${response.status}`)
  }

  const responseData: unknown = await response.json()

  if (isApiResponseLike(responseData) && responseData.code !== 0) {
    if (responseData.code === 1001003) {
      clearStoredTokens()
    }

    const errorMessage = getBusinessErrorMessage(responseData.message)
    showToast(errorMessage)
    throw new Error(errorMessage)
  }

  return responseData as T
}

export function get<T>(url: string, params?: RequestParams, options?: Omit<RequestOptions, 'body'>) {
  return request<T>(`${url}${buildQueryString(params)}`, 'GET', options)
}

export function post<T>(url: string, body?: RequestOptions['body'], options?: Omit<RequestOptions, 'body'>) {
  return request<T>(url, 'POST', { ...options, body })
}

export function put<T>(url: string, body?: RequestOptions['body'], options?: Omit<RequestOptions, 'body'>) {
  return request<T>(url, 'PUT', { ...options, body })
}

export function del<T>(url: string, body?: RequestOptions['body'], options?: Omit<RequestOptions, 'body'>) {
  return request<T>(url, 'DELETE', { ...options, body })
}

export { del as delete }
