/**
 * Typed fetch wrapper for the CannaManage API.
 *
 * Routes through the Next.js rewrite proxy:
 *   /api/backend/... → BACKEND_URL/api/v1/...
 */

export class ApiError extends Error {
  constructor(
    public status: number,
    public code: string,
    message: string
  ) {
    super(message)
    this.name = "ApiError"
  }

  /** True for 401/403 — session likely expired or insufficient permissions */
  get isAuthError(): boolean {
    return this.status === 401 || this.status === 403
  }

  /** True for 5xx — server-side failure */
  get isServerError(): boolean {
    return this.status >= 500
  }

  /** True for network failures (status 0) */
  get isNetworkError(): boolean {
    return this.status === 0
  }
}

export interface ApiClientOptions extends Omit<RequestInit, "body"> {
  token?: string
  body?: unknown
  params?: Record<string, string | number | boolean | undefined>
}

/**
 * Core fetch wrapper that targets the Next.js rewrite proxy.
 *
 * @param endpoint - API path without the prefix, e.g. `/members` or `/members/123`
 * @param options  - Fetch options + optional token and typed body
 * @returns Parsed JSON response of type T
 * @throws ApiError on non-2xx responses
 */
export async function apiClient<T>(
  endpoint: string,
  options: ApiClientOptions = {}
): Promise<T> {
  const {
    token,
    body,
    params,
    headers: extraHeaders,
    ...fetchOptions
  } = options

  // Build URL with optional query params
  let url = `/api/backend${endpoint}`
  if (params) {
    const searchParams = new URLSearchParams()
    for (const [key, value] of Object.entries(params)) {
      if (value !== undefined) {
        searchParams.set(key, String(value))
      }
    }
    const qs = searchParams.toString()
    if (qs) url += `?${qs}`
  }

  const headers: Record<string, string> = {
    "Content-Type": "application/json",
    ...(token && { Authorization: `Bearer ${token}` }),
    ...(extraHeaders as Record<string, string>),
  }

  // Don't set Content-Type for GET/HEAD (no body)
  const method = (fetchOptions.method || "GET").toUpperCase()
  if (method === "GET" || method === "HEAD") {
    delete headers["Content-Type"]
  }

  try {
    const res = await fetch(url, {
      ...fetchOptions,
      method,
      headers,
      body: body ? JSON.stringify(body) : undefined,
    })

    if (!res.ok) {
      // Try to parse application/problem+json or standard error body
      const errorBody = await res.json().catch(() => ({
        code: `HTTP_${res.status}`,
        message: res.statusText || "Request failed",
      }))

      throw new ApiError(
        res.status,
        errorBody.code || `HTTP_${res.status}`,
        errorBody.message || errorBody.detail || res.statusText
      )
    }

    // 204 No Content — return undefined as T
    if (res.status === 204) {
      return undefined as T
    }

    return res.json() as Promise<T>
  } catch (error) {
    if (error instanceof ApiError) {
      throw error
    }
    // Network errors (fetch throws TypeError for network failures)
    throw new ApiError(
      0,
      "NETWORK_ERROR",
      "Network error — unable to reach the server."
    )
  }
}

/**
 * Download a file (PDF, CSV) from the API as a Blob.
 * Used for report downloads where we don't parse JSON.
 */
export async function apiDownload(
  endpoint: string,
  options: ApiClientOptions = {}
): Promise<{ blob: Blob; filename: string }> {
  const {
    token,
    body,
    params,
    headers: extraHeaders,
    ...fetchOptions
  } = options

  let url = `/api/backend${endpoint}`
  if (params) {
    const searchParams = new URLSearchParams()
    for (const [key, value] of Object.entries(params)) {
      if (value !== undefined) {
        searchParams.set(key, String(value))
      }
    }
    const qs = searchParams.toString()
    if (qs) url += `?${qs}`
  }

  const headers: Record<string, string> = {
    Accept: "application/octet-stream",
    ...(token && { Authorization: `Bearer ${token}` }),
    ...(extraHeaders as Record<string, string>),
  }

  try {
    const res = await fetch(url, {
      ...fetchOptions,
      headers,
      body: body ? JSON.stringify(body) : undefined,
    })

    if (!res.ok) {
      const errorBody = await res.json().catch(() => ({
        code: `HTTP_${res.status}`,
        message: res.statusText,
      }))
      throw new ApiError(
        res.status,
        errorBody.code || `HTTP_${res.status}`,
        errorBody.message || res.statusText
      )
    }

    const blob = await res.blob()
    const disposition = res.headers.get("Content-Disposition") || ""
    const filenameMatch = disposition.match(/filename="?([^";\n]+)"?/)
    const filename = filenameMatch?.[1] || "download"

    return { blob, filename }
  } catch (error) {
    if (error instanceof ApiError) throw error
    throw new ApiError(
      0,
      "NETWORK_ERROR",
      "Network error — unable to reach the server."
    )
  }
}
