/**
 * Typed API client built on openapi-fetch + generated schema.d.ts.
 *
 * Middleware contract (orchestrator-decisions.md §11):
 * 1. Always send cookies (credentials: "include"; same-origin auto-sends but explicit is clear).
 * 2. Non-GET/HEAD requests inject X-CSRF-Token from the csrf holder.
 *    Exception: POST /api/auth/login skips injection (unauthenticated endpoint).
 * 3. 401 responses → clear session state + navigate to /login.
 * 4. Other non-2xx responses → throw an ApiError carrying the parsed JSON body,
 *    so callers (e.g. SMTP test) can inspect body.result.
 */

import createClient, { type Middleware } from 'openapi-fetch'
import type { paths } from './schema.d.ts'
import { getCsrfToken } from './csrf'

// ---------------------------------------------------------------------------
// Error type
// ---------------------------------------------------------------------------

/** Error thrown for non-2xx, non-401 responses. Carries the parsed JSON body. */
export class ApiError extends Error {
  constructor(
    public readonly status: number,
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    public readonly body: any,
  ) {
    super(`API error ${status}`)
    this.name = 'ApiError'
  }
}

// ---------------------------------------------------------------------------
// Internal navigation helper (avoids React-router import at module level)
// ---------------------------------------------------------------------------

let _navigateToLogin: (() => void) | null = null

/**
 * Register a callback that the middleware calls on 401.
 * SessionProvider calls this during its setup.
 */
export function registerLoginRedirect(fn: () => void): void {
  _navigateToLogin = fn
}

// ---------------------------------------------------------------------------
// CSRF middleware
// ---------------------------------------------------------------------------

const WRITE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])
const LOGIN_PATH = '/api/auth/login'

/**
 * Endpoints where a 401 is an EXPECTED, locally-handled outcome and must NOT
 * trigger the global login redirect:
 * - GET /api/session    — the session probe; 401 means "not logged in", handled
 *   by SessionProvider's queryFn (returns null → unauthenticated state).
 * - POST /api/auth/login — bad-credentials check; 401 handled by LoginPage.
 *
 * Redirecting on these would invalidate the session query, which refetches
 * /api/session, which 401s, which redirects again → an infinite loop that
 * floods GET /api/session after logout and on the login page.
 */
const SESSION_PATH = '/api/session'
const NO_REDIRECT_ON_401 = new Set<string>([SESSION_PATH, LOGIN_PATH])

export const csrfMiddleware: Middleware = {
  async onRequest({ request }) {
    // Always include cookies (same-origin; explicit for clarity)
    // Note: credentials is set at client level; this is belt-and-suspenders doc.

    const method = request.method.toUpperCase()
    const url = new URL(request.url)

    if (WRITE_METHODS.has(method) && url.pathname !== LOGIN_PATH) {
      const token = getCsrfToken()
      if (token) {
        request.headers.set('X-CSRF-Token', token)
      }
    }

    return request
  },

  async onResponse({ schemaPath, response }) {
    if (response.status === 401) {
      // The session probe and the login endpoint own their 401s (see
      // NO_REDIRECT_ON_401). For any OTHER endpoint, a 401 means the session
      // expired mid-use → redirect to /login. Crucially, NOT redirecting on the
      // session probe breaks the refetch→401→redirect→refetch flood loop.
      if (!NO_REDIRECT_ON_401.has(schemaPath) && _navigateToLogin) {
        _navigateToLogin()
      }
      // Return the original response so callers can handle 401 if needed.
      return response
    }

    if (!response.ok) {
      // Parse body and throw; caller can catch ApiError and read .body
      let body: unknown
      try {
        body = await response.clone().json()
      } catch {
        body = null
      }
      throw new ApiError(response.status, body)
    }

    return response
  },
}

// ---------------------------------------------------------------------------
// Client instance
// ---------------------------------------------------------------------------

const apiClient = createClient<paths>({
  baseUrl: '/',
  credentials: 'include',
})

apiClient.use(csrfMiddleware)

export default apiClient
