// Single typed fetch client for the ERPCore API (docs/20-FRONTEND.md §1). Per-endpoint
// methods live in lib/api/*.ts — no scattered fetch() in components.
//
// Transport: the API is reached SAME-ORIGIN through the Next rewrite in next.config.ts
// (/api/* -> BACKEND_ORIGIN). That is why API_BASE is relative and why no CORS setup
// exists on the backend: there is no cross-origin request to allow.
//
// Auth: the session is an httpOnly `erp_at` cookie issued by POST /auth/login (docs/11
// §2.0) — there is no bearer token to read, and by design JS cannot read the cookie.
// `credentials: "include"` is what actually authenticates every call.
import { ApiResult, ProblemDetails } from "@/types/common"

const API_BASE = "/api/v1"

/** Normalized RFC 7807 error (docs/11-BACKEND-PHASE1.md §1.8) thrown on any non-2xx response. */
export class ApiError extends Error {
  status: number
  code?: string
  detail?: string
  errors?: Record<string, string[]>
  traceId?: string

  constructor(problem: ProblemDetails) {
    super(problem.title || "Request failed")
    this.status = problem.status
    this.code = problem.code
    this.detail = problem.detail
    this.errors = problem.errors
    this.traceId = problem.traceId
  }
}

export interface RequestOptions extends Omit<RequestInit, "body"> {
  body?: unknown
  /** Sent as If-Match for concurrency-guarded PUT/PATCH (docs/11 §1.6). Echo the ETag verbatim, quotes included. */
  ifMatch?: string
  /** Sent as Idempotency-Key for transactional POSTs (e.g. GRN confirm, docs/11 §1.6). */
  idempotencyKey?: string
  /** Sent as X-XSRF-TOKEN. Only the [ValidateCsrf] actions on AuthController need it (docs/11 §2.0). */
  csrf?: boolean
}

/** Reads the non-httpOnly CSRF cookie. It rotates on every session write, so read it per call. */
export function readCsrfToken(): string | null {
  if (typeof document === "undefined") return null
  const match = document.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]*)/)
  return match ? decodeURIComponent(match[1]) : null
}

async function rawRequest(path: string, options: RequestOptions = {}): Promise<Response> {
  const { body, ifMatch, idempotencyKey, csrf, headers, ...rest } = options
  const csrfToken = csrf ? readCsrfToken() : null
  // Multipart uploads (attendance files, staff documents) pass a FormData body — the
  // browser sets its own Content-Type (with boundary), and it must never be JSON-encoded.
  const isFormData = typeof FormData !== "undefined" && body instanceof FormData

  const finalHeaders: Record<string, string> = {
    Accept: "application/json",
    ...(body !== undefined && !isFormData ? { "Content-Type": "application/json" } : {}),
    ...(ifMatch ? { "If-Match": ifMatch } : {}),
    ...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}),
    ...(csrfToken ? { "X-XSRF-TOKEN": csrfToken } : {}),
    ...((headers as Record<string, string> | undefined) ?? {}),
  }

  const response = await fetch(`${API_BASE}${path}`, {
    ...rest,
    credentials: "include", // sends erp_at; the whole auth story depends on this
    headers: finalHeaders,
    body: isFormData ? (body as FormData) : body !== undefined ? JSON.stringify(body) : undefined,
  })

  if (!response.ok) {
    let problem: ProblemDetails
    try {
      problem = (await response.json()) as ProblemDetails
    } catch {
      // e.g. a proxy/network failure with a non-JSON body.
      problem = { title: response.statusText || "Request failed", status: response.status }
    }
    if (!problem.status) problem.status = response.status
    throw new ApiError(problem)
  }

  return response
}

/** Fire a request and decode the JSON body only (no ETag needed). */
export async function apiRequest<T>(path: string, options?: RequestOptions): Promise<T> {
  const response = await rawRequest(path, options)
  if (response.status === 204) return undefined as T
  return (await response.json()) as T
}

/** Fire a request and also surface the ETag header, for resources that support If-Match. */
export async function apiRequestWithETag<T>(path: string, options?: RequestOptions): Promise<ApiResult<T>> {
  const response = await rawRequest(path, options)
  const etag = response.headers.get("ETag")
  const data = response.status === 204 ? (undefined as T) : ((await response.json()) as T)
  return { data, etag }
}

/** Build a `?a=1&b=2` query string, dropping null/undefined/empty values. */
export function buildQuery(params: object): string {
  const search = new URLSearchParams()
  for (const [key, value] of Object.entries(params) as [string, string | number | boolean | null | undefined][]) {
    if (value === null || value === undefined || value === "") continue
    search.set(key, String(value))
  }
  const qs = search.toString()
  return qs ? `?${qs}` : ""
}
