import { config } from '@/lib/config'
import { AppError, ErrorType } from '@/lib/errors'
import type { ApiResponse, RequestOptions } from '@/types'
import {
  clearRefreshPromise,
  clearSession,
  endRefreshing,
  getAccessToken,
  getRefreshPromise,
  isAccessTokenExpired,
  isRefreshing,
  setAccessToken,
  setRefreshPromise,
  startRefreshing,
} from './auth-session'
import { clearRefreshToken, getRefreshToken, setRefreshToken } from '../storage'
import { CSRF_PROTECTED_METHODS, getCSRFHeaders } from './csrf'
import type { TokenBundle } from '@/types'

const DEFAULT_TIMEOUT = 30_000
let inFlightRefreshBundle: Promise<TokenBundle> | null = null

function isFormDataBody(body: unknown): body is FormData {
  return typeof FormData !== 'undefined' && body instanceof FormData
}

function serializeBody(body: unknown): BodyInit | undefined {
  if (body === undefined || body === null) {
    return undefined
  }

  if (isFormDataBody(body)) {
    return body
  }

  return JSON.stringify(body)
}

function resolveApiBaseUrl(): URL {
  const origin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost'
  const rawBaseUrl = /^https?:\/\//i.test(config.apiBaseUrl)
    ? config.apiBaseUrl
    : config.apiBaseUrl.startsWith('/')
      ? config.apiBaseUrl
      : `/${config.apiBaseUrl}`

  const baseUrl = new URL(rawBaseUrl, origin)
  if (!baseUrl.pathname.endsWith('/')) {
    baseUrl.pathname = `${baseUrl.pathname}/`
  }
  return baseUrl
}

function buildUrl(path: string, params?: Record<string, string | number | boolean | undefined>): string {
  const url = new URL(path.replace(/^\/+/, ''), resolveApiBaseUrl())

  if (params) {
    for (const [key, value] of Object.entries(params)) {
      if (value !== undefined) {
        url.searchParams.append(key, String(value))
      }
    }
  }

  return url.toString()
}

function cleanupSessionOnAuthFailure(): never {
  clearRefreshToken()
  clearSession()
  throw AppError.auth('会话已过期，请重新登录')
}

function createTimeoutSignal(signal?: AbortSignal): { signal: AbortSignal; cleanup: () => void } {
  const controller = new AbortController()
  const timeoutId = window.setTimeout(() => controller.abort(), DEFAULT_TIMEOUT)

  if (signal) {
    signal.addEventListener('abort', () => controller.abort(), { once: true })
  }

  return {
    signal: controller.signal,
    cleanup: () => window.clearTimeout(timeoutId),
  }
}

async function parseJsonResponse<T>(response: Response): Promise<ApiResponse<T>> {
  const raw = await response.json()

  // 运行时验证响应结构
  if (!isApiResponse(raw)) {
    throw new Error('Invalid API response structure: missing required fields')
  }

  return raw as ApiResponse<T>
}

/**
 * 运行时验证 API 响应结构
 * 防止后端返回异常格式时导致运行时错误
 */
function isApiResponse(obj: unknown): obj is ApiResponse<unknown> {
  if (typeof obj !== 'object' || obj === null) {
    return false
  }

  const r = obj as Record<string, unknown>

  // 必须有 code 字段且为数字
  if (typeof r.code !== 'number') {
    return false
  }

  // 必须有 message 字段且为字符串
  if (typeof r.message !== 'string') {
    return false
  }

  // 如果有 data 字段，应该存在
  // (data 可以是 undefined/null/任何类型，但我们允许这些值)

  return true
}

async function refreshAccessToken(): Promise<TokenBundle> {
  const refreshToken = getRefreshToken()
  const body = refreshToken ? JSON.stringify({ refresh_token: refreshToken }) : undefined

  const response = await fetch(buildUrl('/auth/refresh'), {
    method: 'POST',
    credentials: 'include',
    headers: body ? { 'Content-Type': 'application/json' } : undefined,
    body,
  })

  if (!response.ok) {
    return cleanupSessionOnAuthFailure()
  }

  const result = await parseJsonResponse<TokenBundle>(response)
  if (result.code !== 0) {
    return cleanupSessionOnAuthFailure()
  }

  return result.data as TokenBundle
}

async function performTokenRefresh(): Promise<TokenBundle> {
  if (inFlightRefreshBundle) {
    return inFlightRefreshBundle
  }

  startRefreshing()
  const promise = (async () => {
    try {
      const tokenBundle = await refreshAccessToken()
      setAccessToken(tokenBundle.access_token, tokenBundle.expires_in)
      setRefreshToken(tokenBundle.refresh_token)
      return tokenBundle
    } finally {
      endRefreshing()
      clearRefreshPromise()
      inFlightRefreshBundle = null
    }
  })()

  inFlightRefreshBundle = promise
  setRefreshPromise(
    promise.then(
      () => undefined,
      () => undefined,
    ),
  )

  return promise
}

export async function refreshSessionBundle(): Promise<TokenBundle> {
  return await performTokenRefresh()
}

async function performRefresh(): Promise<string> {
  if (isRefreshing()) {
    const promise = getRefreshPromise()
    if (promise) {
      await promise
    }

    const token = getAccessToken()
    if (!token) {
      return cleanupSessionOnAuthFailure()
    }

    return token
  }

  const tokenBundle = await performTokenRefresh()
  return tokenBundle.access_token
}

async function resolveAuthorizationHeader(auth: boolean): Promise<string | null> {
  if (!auth) {
    return null
  }

  let token = getAccessToken()
  if (isRefreshing()) {
    const promise = getRefreshPromise()
    if (promise) {
      await promise
      token = getAccessToken()
    }
  }

  if (token && isAccessTokenExpired()) {
    token = await performRefresh()
  }

  return token
}

async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
  const {
    method = 'GET',
    headers = {},
    body,
    params,
    auth = true,
    credentials = 'include',
    signal,
  } = options

  const url = buildUrl(path, params)
  const requestHeaders: Record<string, string> = { ...headers }

  if (body !== undefined && body !== null && !isFormDataBody(body) && !requestHeaders['Content-Type']) {
    requestHeaders['Content-Type'] = 'application/json'
  }

  if (CSRF_PROTECTED_METHODS.includes(method)) {
    Object.assign(requestHeaders, getCSRFHeaders())
  }

  const authToken = await resolveAuthorizationHeader(auth)
  if (authToken) {
    requestHeaders.Authorization = `Bearer ${authToken}`
  }

  const timeout = createTimeoutSignal(signal)

  try {
    let response = await fetch(url, {
      method,
      headers: requestHeaders,
      body: serializeBody(body),
      credentials,
      signal: timeout.signal,
    })

    if (response.status === 401 && auth) {
      const refreshedToken = await performRefresh()
      requestHeaders.Authorization = `Bearer ${refreshedToken}`

      response = await fetch(url, {
        method,
        headers: requestHeaders,
        body: serializeBody(body),
        credentials,
        signal: timeout.signal,
      })
    }

    if (response.status === 401) {
      return cleanupSessionOnAuthFailure()
    }

    if (!response.ok) {
      if (response.status === 403) {
        throw AppError.forbidden()
      }
      if (response.status === 404) {
        throw new AppError(404, '请求的资源不存在', {
          status: 404,
          type: ErrorType.NOT_FOUND,
        })
      }
      throw AppError.network(`请求失败: ${response.status}`)
    }

    const result = await parseJsonResponse<T>(response)
    if (result.code !== 0) {
      throw AppError.fromResponse(result, response.status)
    }

    return result.data!
  } catch (error) {
    if (error instanceof DOMException && error.name === 'AbortError') {
      throw AppError.network('请求超时，请稍后重试')
    }
    throw error
  } finally {
    timeout.cleanup()
  }
}

export function get<T>(
  path: string,
  params?: Record<string, string | number | boolean | undefined>,
  options?: Omit<RequestOptions, 'method' | 'params' | 'body'>,
): Promise<T> {
  return request<T>(path, { ...options, method: 'GET', params })
}

export function post<T>(
  path: string,
  body?: unknown,
  options?: Omit<RequestOptions, 'method' | 'body'>,
): Promise<T> {
  return request<T>(path, { ...options, method: 'POST', body })
}

export function put<T>(
  path: string,
  body?: unknown,
  options?: Omit<RequestOptions, 'method' | 'body'>,
): Promise<T> {
  return request<T>(path, { ...options, method: 'PUT', body })
}

export function del<T>(
  path: string,
  options?: Omit<RequestOptions, 'method'>,
): Promise<T> {
  return request<T>(path, { ...options, method: 'DELETE' })
}

async function resolveAuthorizedHeaders(options?: Omit<RequestOptions, 'method' | 'params' | 'body'>): Promise<Record<string, string>> {
  const headers: Record<string, string> = { ...(options?.headers ?? {}) }

  if (options?.auth !== false) {
    const token = await resolveAuthorizationHeader(true)
    if (token) {
      headers.Authorization = `Bearer ${token}`
    }
  }

  return headers
}

export async function download(
  path: string,
  params?: Record<string, string | number | boolean | undefined>,
  options?: Omit<RequestOptions, 'method' | 'params'>,
): Promise<Blob> {
  const url = buildUrl(path, params)
  const headers = await resolveAuthorizedHeaders(options)
  const timeout = createTimeoutSignal(options?.signal)

  try {
    let response = await fetch(url, {
      headers,
      credentials: options?.credentials ?? 'include',
      signal: timeout.signal,
    })

    if (response.status === 401 && options?.auth !== false) {
      const refreshedToken = await performRefresh()
      headers.Authorization = `Bearer ${refreshedToken}`
      response = await fetch(url, {
        headers,
        credentials: options?.credentials ?? 'include',
        signal: timeout.signal,
      })
    }

    if (response.status === 401) {
      return cleanupSessionOnAuthFailure()
    }
    if (!response.ok) {
      throw AppError.network(`下载失败: ${response.status}`)
    }

    return response.blob()
  } catch (error) {
    if (error instanceof DOMException && error.name === 'AbortError') {
      throw AppError.network('下载超时，请稍后重试')
    }
    throw error
  } finally {
    timeout.cleanup()
  }
}

export async function upload<T>(
  path: string,
  file: File,
  fieldName: string = 'file',
  additionalData?: Record<string, string>,
  options?: Omit<RequestOptions, 'method' | 'body'>,
): Promise<T> {
  const formData = new FormData()
  formData.append(fieldName, file)

  if (additionalData) {
    for (const [key, value] of Object.entries(additionalData)) {
      formData.append(key, value)
    }
  }

  return request<T>(path, {
    ...options,
    method: 'POST',
    body: formData,
  })
}

export { request }
