import { endpoints } from './endpoints'

let accessToken: string | null = null

/**
 * Single source of truth for the in-memory bearer token.
 *
 * The bearer is intentionally held in module scope (NOT in localStorage /
 * sessionStorage / cookies) per AC-02 / restriction O2 — the only way to
 * survive a page reload is through the HttpOnly refresh cookie + the
 * `refreshToken()` round-trip in `request()` below.
 *
 * Tests override this hook to seed a bearer without going through the full
 * login flow. See `_docs/02_document/tests/test-data.md` ("Stubbed bearer /
 * cookie in test helpers"). DO NOT delete this accessor as "dead code" —
 * it is reflectively used by the test harness; the import looks dead at
 * static-grep time but is not.
 */
export function setToken(token: string | null) {
  accessToken = token
}

/** Read-side companion to {@link setToken}. */
export function getToken() {
  return accessToken
}

/**
 * Resolve the base URL prefix for every API request. Returns `''` (no
 * prefix) on production builds where the SPA and the suite share the same
 * nginx (E2); tests + alternative deployments override via the Vite env
 * var `VITE_API_BASE_URL`. A trailing slash is stripped so a value of
 * `http://host/` does not produce `http://host//api/...`.
 */
export function getApiBase(): string {
  const raw = import.meta.env.VITE_API_BASE_URL
  if (!raw) return ''
  return raw.replace(/\/+$/, '')
}

export function authenticatedApiUrl(path: string): string {
  const url = getApiBase() + path
  if (!accessToken) return url
  const separator = url.includes('?') ? '&' : '?'
  return `${url}${separator}access_token=${encodeURIComponent(accessToken)}`
}

/**
 * Indirection for the failed-refresh redirect. Default impl writes
 * `'/login'` to `window.location.href` — the production behavior. Tests
 * override via {@link setNavigateToLogin} to assert "redirect was invoked"
 * without globally stubbing `window.location`. Must be reset by the test
 * harness in teardown (see `_docs/02_document/tests/test-data.md`).
 */
let navigateToLoginImpl: () => void = () => {
  window.location.href = '/login'
}

export function setNavigateToLogin(fn: () => void) {
  navigateToLoginImpl = fn
}

async function handleResponse<T>(res: Response): Promise<T> {
  if (res.status === 204) return undefined as T
  if (!res.ok) {
    const text = await res.text().catch(() => '')
    throw new Error(`${res.status}: ${text || res.statusText}`)
  }
  return res.json()
}

async function request<T>(url: string, options: RequestInit = {}): Promise<T> {
  const headers = new Headers(options.headers)
  if (accessToken) headers.set('Authorization', `Bearer ${accessToken}`)
  if (options.body && typeof options.body === 'string') headers.set('Content-Type', 'application/json')

  const fullUrl = getApiBase() + url
  let res = await fetch(fullUrl, { ...options, headers, credentials: 'include' })

  if (res.status === 401 && accessToken) {
    const refreshed = await refreshToken()
    if (refreshed) {
      headers.set('Authorization', `Bearer ${accessToken}`)
      res = await fetch(fullUrl, { ...options, headers, credentials: 'include' })
    } else {
      setToken(null)
      navigateToLoginImpl()
      throw new Error('Session expired')
    }
  }

  return handleResponse<T>(res)
}

async function refreshToken(): Promise<boolean> {
  try {
    const res = await fetch(getApiBase() + endpoints.admin.authRefresh(), { method: 'POST', credentials: 'include' })
    if (!res.ok) return false
    const data = await res.json()
    setToken(data.token)
    return true
  } catch {
    return false
  }
}

export const api = {
  get: <T>(url: string) => request<T>(url),
  post: <T>(url: string, body?: unknown) =>
    request<T>(url, { method: 'POST', body: body ? JSON.stringify(body) : undefined }),
  put: <T>(url: string, body?: unknown) =>
    request<T>(url, { method: 'PUT', body: body ? JSON.stringify(body) : undefined }),
  patch: <T>(url: string, body?: unknown) =>
    request<T>(url, { method: 'PATCH', body: body ? JSON.stringify(body) : undefined }),
  delete: <T>(url: string) => request<T>(url, { method: 'DELETE' }),
  upload: <T>(url: string, formData: FormData) =>
    request<T>(url, { method: 'POST', body: formData }),
}
