/**
 * Базовый API-клиент VidConf.
 *
 * - Access-токен подставляется из authStore (память, не localStorage).
 * - На 401 выполняется один silent-refresh (POST /auth/refresh,
 *   credentials: 'include' — сессия читается из httpOnly-cookie) и повтор
 *   исходного запроса.
 * - ⚠️ Причина неудачи refresh различается (`RefreshOutcome`). Сессия
 *   сбрасывается ТОЛЬКО когда backend сказал, что она недействительна
 *   (`invalid`). Ответ 5xx или обрыв сети — это «серверу плохо», а не «вы не
 *   авторизованы»: токен сохраняется, пользователь остаётся в системе и
 *   получает обычную ошибку запроса. Раньше различия не было, и на
 *   нагрузочном тесте 07.08.2026 (когда refresh отвечал 500 из-за
 *   исчерпанного пула БД) фронтенд разлогинивал людей посреди работы, а
 *   повторный вход падал тем же 500.
 * - Параллельные 401 схлопываются в один refresh-запрос (refreshPromise).
 */
import { authStore } from '@/auth/authStore'

const API_BASE = '/api/v1'

/** Ошибка API с HTTP-статусом и разобранным телом ответа (если это JSON). */
export class ApiError extends Error {
  readonly status: number
  readonly detail: unknown

  constructor(status: number, detail: unknown, message?: string) {
    super(message ?? `Ошибка API: ${status}`)
    this.name = 'ApiError'
    this.status = status
    this.detail = detail
  }
}

/**
 * Извлекает машиночитаемый код ошибки из тела ответа FastAPI
 * (`{"detail": "some_code"}`) — используется для различения причин 403/409
 * (напр. "password_required" vs "invalid_password").
 */
export function errorDetail(err: unknown): string | undefined {
  if (err instanceof ApiError && err.detail && typeof err.detail === 'object' && 'detail' in err.detail) {
    const value = (err.detail as { detail: unknown }).detail
    return typeof value === 'string' ? value : undefined
  }
  return undefined
}

interface RequestOptions extends Omit<RequestInit, 'body'> {
  body?: unknown
  /** Не пытаться делать silent-refresh при 401 (используется самим refresh-запросом). */
  skipAuthRefresh?: boolean
}

/**
 * Итог silent-refresh.
 *
 * - `ok` — выдан новый access-токен;
 * - `invalid` — backend отверг refresh-сессию (просрочена, отозвана, reuse):
 *   единственный случай, когда пользователя правда надо разлогинить;
 * - `unavailable` — до ответа «сессия недействительна» дело не дошло: 5xx,
 *   таймаут или обрыв сети. Сессия при этом цела, `status` — HTTP-код
 *   ответа или `null`, если запрос не доехал вовсе.
 */
export type RefreshOutcome =
  | { result: 'ok' }
  | { result: 'invalid' }
  | { result: 'unavailable'; status: number | null }

let refreshPromise: Promise<RefreshOutcome> | null = null

/**
 * Выполняет silent-refresh access-токена через httpOnly refresh-cookie.
 * Параллельные вызовы переиспользуют один запрос.
 */
export async function refreshAccessToken(): Promise<RefreshOutcome> {
  if (!refreshPromise) {
    refreshPromise = (async (): Promise<RefreshOutcome> => {
      try {
        const response = await fetch(`${API_BASE}/auth/refresh`, {
          method: 'POST',
          credentials: 'include',
        })
        if (response.ok) {
          const data = (await response.json()) as { access_token: string }
          authStore.setAccessToken(data.access_token)
          return { result: 'ok' }
        }
        // Про недействительность сессии backend говорит только кодом 4xx.
        // Всё остальное (500/502/503/504) — состояние сервера, а не сессии.
        return response.status >= 500
          ? { result: 'unavailable', status: response.status }
          : { result: 'invalid' }
      } catch {
        // Сеть не доехала — про сессию мы так ничего и не узнали.
        return { result: 'unavailable', status: null }
      } finally {
        refreshPromise = null
      }
    })()
  }
  return refreshPromise
}

/** Редирект на страницу входа при невосстановимой потере сессии. */
function redirectToLogin(): void {
  authStore.setAccessToken(null)
  if (window.location.pathname !== '/login') {
    window.location.assign('/login')
  }
}

async function parseBody(response: Response): Promise<unknown> {
  const text = await response.text()
  if (!text) return null
  try {
    return JSON.parse(text)
  } catch {
    return text
  }
}

/**
 * Базовый fetch-запрос к API с автоподстановкой access-токена и
 * silent-refresh на 401.
 */
export async function apiRequest<T = unknown>(path: string, options: RequestOptions = {}): Promise<T> {
  const { body, skipAuthRefresh, headers, ...rest } = options

  const doFetch = async (): Promise<Response> => {
    const finalHeaders = new Headers(headers)
    const token = authStore.getAccessToken()
    if (token) finalHeaders.set('Authorization', `Bearer ${token}`)

    let finalBody: BodyInit | undefined
    if (body !== undefined && !(body instanceof URLSearchParams) && !(body instanceof FormData)) {
      finalHeaders.set('Content-Type', 'application/json')
      finalBody = JSON.stringify(body)
    } else if (body instanceof URLSearchParams) {
      finalHeaders.set('Content-Type', 'application/x-www-form-urlencoded')
      finalBody = body
    } else if (body instanceof FormData) {
      finalBody = body
    }

    return fetch(`${API_BASE}${path}`, {
      ...rest,
      headers: finalHeaders,
      body: finalBody,
      credentials: 'include',
    })
  }

  let response = await doFetch()

  if (response.status === 401 && !skipAuthRefresh) {
    const outcome = await refreshAccessToken()
    if (outcome.result === 'ok') {
      response = await doFetch()
    } else if (outcome.result === 'unavailable') {
      // Серверу плохо — сессию не трогаем и на /login не выкидываем:
      // как только backend оживёт, следующий запрос обновит токен сам.
      throw new ApiError(
        outcome.status ?? 0,
        null,
        'Сервер временно недоступен. Попробуйте ещё раз через минуту.',
      )
    } else {
      redirectToLogin()
      throw new ApiError(401, null, 'Сессия истекла')
    }
  }

  if (!response.ok) {
    const detail = await parseBody(response)
    throw new ApiError(response.status, detail)
  }

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