import type { ApiError } from './types'

let accessToken: string | null = null
let refreshInFlight: Promise<boolean> | null = null
let onUnauthorized: (() => void) | null = null

export function setAccessToken(token: string | null) {
  accessToken = token
}

export function getAccessToken() {
  return accessToken
}

/** Вызывается, когда refresh-токен недействителен — обычно очищает стор авторизации и шлёт на /login. */
export function setUnauthorizedHandler(handler: (() => void) | null) {
  onUnauthorized = handler
}

type RequestOptions = {
  method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
  body?: unknown
  /** Не пытаться освежить токен на 401 (используется самим refresh-запросом, чтобы не зациклиться). */
  skipRefresh?: boolean
}

export async function refreshAccessToken(): Promise<boolean> {
  refreshInFlight ??= (async () => {
    try {
      const response = await fetch('/api/auth/refresh', {
        method: 'POST',
        credentials: 'include',
      })
      if (!response.ok) return false
      const data = (await response.json()) as { accessToken?: unknown }
      if (typeof data?.accessToken !== 'string') return false
      setAccessToken(data.accessToken)
      return true
    } catch {
      return false
    } finally {
      refreshInFlight = null
    }
  })()
  return refreshInFlight
}

export class HttpError extends Error implements ApiError {
  title: string
  detail: string
  status: number

  constructor(problem: Partial<ApiError>, status: number) {
    super(problem.detail ?? problem.title ?? `HTTP ${status}`)
    this.title = problem.title ?? 'Error'
    this.detail = problem.detail ?? this.message
    this.status = status
  }
}

async function parseError(response: Response): Promise<HttpError> {
  try {
    const problem = (await response.json()) as Partial<ApiError>
    return new HttpError(problem, response.status)
  } catch {
    return new HttpError({ title: response.statusText }, response.status)
  }
}

/**
 * Скачивание файла с той же авторизацией, что и обычные запросы. Отдельно от {@link apiRequest}:
 * тот разбирает ответ как JSON, а здесь нужен сам поток и имя файла из Content-Disposition.
 */
export async function apiDownload(
  path: string,
  fallbackName: string,
  options: { method?: 'GET' | 'POST'; skipRefresh?: boolean } = {},
): Promise<{ blob: Blob; fileName: string }> {
  const headers: Record<string, string> = {}
  if (accessToken) headers.Authorization = `Bearer ${accessToken}`

  const response = await fetch(`/api${path}`, {
    method: options.method ?? 'GET',
    headers,
    credentials: 'include',
  })

  if (response.status === 401 && !options.skipRefresh && (await refreshAccessToken()))
    return apiDownload(path, fallbackName, { ...options, skipRefresh: true })

  if (!response.ok) throw await parseError(response)

  const disposition = response.headers.get('Content-Disposition') ?? ''
  const match = /filename="?([^";]+)"?/.exec(disposition)
  return { blob: await response.blob(), fileName: match?.[1] ?? fallbackName }
}

export async function apiRequest<T>(path: string, options: RequestOptions = {}): Promise<T> {
  const headers: Record<string, string> = {}
  if (accessToken) headers.Authorization = `Bearer ${accessToken}`
  if (options.body !== undefined) headers['Content-Type'] = 'application/json'

  const response = await fetch(`/api${path}`, {
    method: options.method ?? 'GET',
    headers,
    credentials: 'include',
    body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
  })

  if (response.status === 401) {
    if (!options.skipRefresh) {
      const refreshed = await refreshAccessToken()
      if (refreshed) return apiRequest<T>(path, { ...options, skipRefresh: true })
    }
    // 401 и освежить токен нельзя/не помогло (включая повторный 401 уже после успешного refresh —
    // токен приняли, но прав нет / он тут же отозван): сессия мертва, чистим авторизацию.
    onUnauthorized?.()
    throw await parseError(response)
  }

  if (!response.ok) throw await parseError(response)

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

  const text = await response.text()
  return (text ? JSON.parse(text) : undefined) as T
}
