import { refreshAccessToken } from './auth'

export interface AuthSession {
  token: string
  tokenType?: string
  refreshToken?: string
  expiresIn?: number
}

export interface AuthedRequestOptions {
  method: 'GET' | 'POST'
  path: string
  auth: AuthSession
  body?: Record<string, unknown>
  onAuthUpdated?: (next: AuthSession) => void
}

export interface ApiEnvelope<T> {
  code: number
  msg: string
  data: T
}

interface ApiErrorPayload {
  code?: number
  msg?: string
  message?: string
  error?: string
}

export class AuthExpiredError extends Error {
  constructor(message = '登录状态已过期，请重新登录') {
    super(message)
    this.name = 'AuthExpiredError'
  }
}

const API_BASE_URL = (import.meta.env.VITE_API_BASE_URL ?? '').trim().replace(/\/$/, '')

function buildUrl(path: string): string {
  if (/^https?:\/\//.test(path)) {
    return path
  }

  const normalizedPath = path.startsWith('/') ? path : `/${path}`
  if (!API_BASE_URL) {
    return normalizedPath
  }

  if (API_BASE_URL.startsWith('/')) {
    const basePath = API_BASE_URL.startsWith('/') ? API_BASE_URL : `/${API_BASE_URL}`
    if (normalizedPath === basePath || normalizedPath.startsWith(`${basePath}/`)) {
      return normalizedPath
    }

    return `${basePath}${normalizedPath}`
  }

  try {
    const baseUrl = new URL(API_BASE_URL)
    const basePath = baseUrl.pathname.replace(/\/$/, '')
    if (basePath && normalizedPath.startsWith(`${basePath}/`)) {
      return `${API_BASE_URL}${normalizedPath.slice(basePath.length)}`
    }
  } catch {
    // API_BASE_URL may be a relative path.
  }

  return `${API_BASE_URL}${normalizedPath}`
}

function createAuthHeader(token: string, tokenType = 'Bearer'): string {
  const normalizedToken = token.trim()
  if (/^\S+\s+\S+/.test(normalizedToken)) {
    return normalizedToken
  }

  return `${tokenType || 'Bearer'} ${normalizedToken}`
}

async function parseEnvelope<T>(response: Response): Promise<ApiEnvelope<T>> {
  const payload = (await response.json().catch(() => ({}))) as ApiEnvelope<T> & ApiErrorPayload
  if (!response.ok) {
    throw new Error(payload.msg ?? payload.message ?? payload.error ?? '请求失败，请稍后再试')
  }

  if (typeof payload.code === 'number' && payload.code !== 0) {
    throw new Error(payload.msg ?? '接口返回失败')
  }

  return payload
}

async function runRequest<T>(
  options: Omit<AuthedRequestOptions, 'auth'> & { session: AuthSession },
): Promise<{ response: Response; parsed: ApiEnvelope<T> }> {
  const response = await fetch(buildUrl(options.path), {
    method: options.method,
    headers: {
      ...(options.body ? { 'Content-Type': 'application/json' } : undefined),
      Authorization: createAuthHeader(options.session.token, options.session.tokenType),
    },
    body: options.body ? JSON.stringify(options.body) : undefined,
  })

  if (response.status === 401) {
    return {
      response,
      parsed: { code: 401, msg: 'unauthorized', data: {} as T },
    }
  }

  const parsed = await parseEnvelope<T>(response)
  return { response, parsed }
}

export async function authedRequest<T>(options: AuthedRequestOptions): Promise<T> {
  const first = await runRequest<T>({ ...options, session: options.auth })
  if (first.response.status !== 401) {
    return first.parsed.data
  }

  if (!options.auth.refreshToken) {
    throw new AuthExpiredError()
  }

  try {
    const refreshed = await refreshAccessToken({
      token: options.auth.token,
      tokenType: options.auth.tokenType,
      refreshToken: options.auth.refreshToken,
    })

    const nextAuth: AuthSession = {
      token: refreshed.token,
      tokenType: refreshed.tokenType ?? options.auth.tokenType,
      refreshToken: refreshed.refreshToken ?? options.auth.refreshToken,
      expiresIn: refreshed.expiresIn,
    }
    options.onAuthUpdated?.(nextAuth)

    const second = await runRequest<T>({ ...options, session: nextAuth })
    if (second.response.status === 401) {
      throw new AuthExpiredError()
    }

    return second.parsed.data
  } catch (error) {
    if (error instanceof AuthExpiredError) {
      throw error
    }

    throw new AuthExpiredError()
  }
}
