/**
 * api/index.ts — HTTP client with JWT + HttpOnly refresh cookie.
 */
import { useAuthStore } from '@/stores/auth'

const BASE_URL = import.meta.env.VITE_API_BASE || '/api'

const AUTH_SKIP_REFRESH = new Set(['/auth/login', '/auth/refresh', '/auth/logout'])

function normalizeDetail(detail: unknown, fallback: string): string {
  if (typeof detail === 'string') return detail
  if (Array.isArray(detail)) {
    return detail.map((x: { msg?: string }) => x?.msg || JSON.stringify(x)).join('; ')
  }
  if (detail && typeof detail === 'object') return JSON.stringify(detail)
  return fallback
}

let refreshInFlight: Promise<boolean> | null = null

async function tryRefreshSession(): Promise<boolean> {
  if (refreshInFlight) return refreshInFlight
  refreshInFlight = (async () => {
    try {
      const res = await fetch(`${BASE_URL}/auth/refresh`, {
        method: 'POST',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: '{}',
      })
      if (!res.ok) return false
      const data = await res.json()
      if (data?.access_token) {
        useAuthStore().setAccessToken(data.access_token)
        return true
      }
      return false
    } catch {
      return false
    } finally {
      refreshInFlight = null
    }
  })()
  return refreshInFlight
}

function buildApiUrl(path: string, params?: Record<string, unknown>): string {
  let url = `${BASE_URL}${path}`
  if (params) {
    const sp = new URLSearchParams()
    for (const [k, v] of Object.entries(params)) {
      if (v !== undefined && v !== null) sp.set(k, String(v))
    }
    const qs = sp.toString()
    if (qs) url += `?${qs}`
  }
  return url
}

function buildAuthHeaders(init: RequestInit): Record<string, string> {
  const auth = useAuthStore()
  const headers: Record<string, string> = {
    ...(init.headers as Record<string, string> || {}),
  }
  if (!(init.body instanceof FormData)) {
    headers['Content-Type'] = 'application/json'
  }
  if (auth.token) {
    headers['Authorization'] = `Bearer ${auth.token}`
  }
  return headers
}

/** Authenticated fetch with JWT refresh retry; returns raw Response. */
export async function fetchAuthed(
  path: string,
  opts: RequestInit & { params?: Record<string, unknown>; _retry?: boolean } = {},
): Promise<Response> {
  const { params, _retry, ...init } = opts
  const url = buildApiUrl(path, params)
  const auth = useAuthStore()
  const res = await fetch(url, {
    ...init,
    headers: buildAuthHeaders(init),
    credentials: 'include',
  })

  if (res.status === 401) {
    if (!AUTH_SKIP_REFRESH.has(path) && !_retry) {
      const refreshed = await tryRefreshSession()
      if (refreshed) {
        return fetchAuthed(path, { ...opts, _retry: true })
      }
      auth.forceLogout()
      throw new ApiError(401, '登录已过期，请重新登录')
    }
    const errText = await res.text()
    let errBody: { detail?: unknown } = {}
    try { errBody = JSON.parse(errText) } catch { errBody = { detail: errText } }
    const msg = normalizeDetail(
      errBody.detail,
      AUTH_SKIP_REFRESH.has(path) ? '认证失败' : '登录已过期，请重新登录',
    )
    throw new ApiError(401, msg)
  }

  return res
}

async function throwApiErrorFromResponse(res: Response): Promise<never> {
  const text = await res.text()
  let data: unknown
  try {
    data = JSON.parse(text)
  } catch {
    data = text
  }
  const body = data as { detail?: unknown; message?: string }
  const msg = normalizeDetail(body?.detail ?? body?.message, res.statusText)
  const err = new ApiError(res.status, msg)
  err.detail = body?.detail
  throw err
}

function filenameFromContentDisposition(header: string | null): string | null {
  if (!header) return null
  const star = header.match(/filename\*=UTF-8''([^;]+)/i)
  if (star?.[1]) {
    try {
      return decodeURIComponent(star[1])
    } catch {
      return star[1]
    }
  }
  const plain = header.match(/filename="?([^";]+)"?/i)
  return plain?.[1] ?? null
}

/** POST binary download (e.g. /sync/download StreamingResponse). */
export async function downloadPost(
  path: string,
  body?: unknown,
): Promise<{ blob: Blob; filename: string }> {
  const res = await fetchAuthed(path, {
    method: 'POST',
    body: JSON.stringify(body ?? {}),
  })
  if (res.status === 202) {
    throw new TotpRequiredError('请输入 TOTP 验证码')
  }
  if (res.status === 429) {
    throw new ApiError(429, '登录尝试过多，账户已锁定 15 分钟')
  }
  if (!res.ok) {
    await throwApiErrorFromResponse(res)
  }
  const blob = await res.blob()
  const filename = filenameFromContentDisposition(res.headers.get('Content-Disposition')) || 'download'
  return { blob, filename }
}

/** Raw fetch wrapper with JSON + JWT + refresh retry */
export async function api<T = any>(
  path: string,
  opts: RequestInit & { params?: Record<string, any>; _retry?: boolean } = {},
): Promise<T> {
  const res = await fetchAuthed(path, opts)

  if (res.status === 202) {
    const text = await res.text()
    let data: unknown
    try { data = JSON.parse(text) } catch { data = { message: text } }
    const msg = (data as { detail?: string; message?: string })?.detail
      || (data as { message?: string })?.message
      || '请输入 TOTP 验证码'
    throw new TotpRequiredError(msg)
  }

  if (res.status === 429) {
    throw new ApiError(429, '登录尝试过多，账户已锁定 15 分钟')
  }

  if (res.status === 204) return undefined as T

  const text = await res.text()
  let data: unknown
  try {
    data = JSON.parse(text)
  } catch {
    data = text
  }

  if (!res.ok) {
    const body = data as { detail?: unknown; message?: string }
    const msg = normalizeDetail(body?.detail ?? body?.message, res.statusText)
    const err = new ApiError(res.status, msg)
    err.detail = body?.detail
    throw err
  }

  return data as T
}

export const http = {
  get: <T = any>(path: string, params?: Record<string, any>) =>
    api<T>(path, { method: 'GET', params }),

  getList: async <T = any>(path: string, params?: Record<string, any>) => {
    const data = await api<unknown>(path, { method: 'GET', params })
    if (Array.isArray(data)) {
      return { items: data as T[], total: data.length }
    }
    if (data && typeof data === 'object') {
      const obj = data as { items?: T[]; total?: number }
      if (Array.isArray(obj.items)) {
        return { items: obj.items, total: obj.total ?? obj.items.length }
      }
    }
    return { items: [] as T[], total: 0 }
  },

  post: <T = any>(path: string, body?: unknown) =>
    api<T>(path, { method: 'POST', body: body ? JSON.stringify(body) : undefined }),

  put: <T = any>(path: string, body?: unknown) =>
    api<T>(path, { method: 'PUT', body: body ? JSON.stringify(body) : undefined }),

  delete: <T = any>(path: string) =>
    api<T>(path, { method: 'DELETE' }),

  upload: <T = any>(path: string, formData: FormData) =>
    api<T>(path, { method: 'POST', body: formData }),

  download: (path: string, body?: unknown) => downloadPost(path, body),
}

export class ApiError extends Error {
  detail?: unknown
  constructor(public status: number, message: string) {
    super(message)
    this.name = 'ApiError'
  }
}

export class TotpRequiredError extends Error {
  constructor(message: string) {
    super(message)
    this.name = 'TotpRequiredError'
  }
}
