import { getAuthHeaders, getJsonHeaders } from './api-headers';
import { base } from '$app/paths';
import { API_ABSOLUTE_URL_PROTOCOLS, ERROR_MESSAGES, HTTP_CODE_TO_STRING } from '$lib/constants';

/**
 * API Fetch Utilities
 *
 * Provides common fetch patterns used across services:
 * - Automatic JSON headers
 * - Error handling with proper error messages
 * - Base path resolution
 */

/**
 * Error thrown when an API request fails, carrying the HTTP status code
 * so callers can distinguish e.g. a 503 "still loading" response from a
 * genuine failure.
 */
export class ApiError extends Error {
	status: number;

	constructor(message: string, status: number) {
		super(message);
		this.name = 'ApiError';
		this.status = status;
	}
}

export interface ApiFetchOptions extends Omit<RequestInit, 'headers'> {
	/**
	 * Use auth-only headers (no Content-Type).
	 * Default: false (uses JSON headers with Content-Type: application/json)
	 */
	authOnly?: boolean;
	/**
	 * Additional headers to merge with default headers.
	 */
	headers?: Record<string, string>;
}

/**
 * Fetch JSON data from an API endpoint with standard headers and error handling.
 *
 * @param path - API path (will be prefixed with base path)
 * @param options - Fetch options with additional authOnly flag
 * @returns Parsed JSON response
 * @throws Error with formatted message on failure
 *
 * @example
 * ```typescript
 * // GET request
 * const models = await apiFetch<ApiModelListResponse>('/v1/models');
 *
 * // POST request
 * const result = await apiFetch<ApiResponse>('/models/load', {
 *   method: 'POST',
 *   body: JSON.stringify({ model: 'gpt-4' })
 * });
 * ```
 */
export async function apiFetch<T>(path: string, options: ApiFetchOptions = {}): Promise<T> {
	const { authOnly = false, headers: customHeaders, ...fetchOptions } = options;
	const baseHeaders = authOnly ? getAuthHeaders() : getJsonHeaders();
	const headers = { ...baseHeaders, ...customHeaders };
	// absolute URLs with an allowed protocol pass through untouched; relative paths get the base prefix
	const url = API_ABSOLUTE_URL_PROTOCOLS.some((p) => path.startsWith(p)) ? path : `${base}${path}`;

	let response;

	try {
		response = await fetch(url, {
			...fetchOptions,
			headers
		});
	} catch (e) {
		throw new Error(beautifyNetworkError(e));
	}

	if (!response.ok) {
		const errorMessage = await parseErrorMessage(response);

		throw new ApiError(errorMessage, response.status);
	}

	return response.json() as Promise<T>;
}

/**
 * Fetch with URL constructed from base URL and query parameters.
 *
 * @param basePath - Base API path
 * @param params - Query parameters to append
 * @param options - Fetch options
 * @returns Parsed JSON response
 *
 * @example
 * ```typescript
 * const props = await apiFetchWithParams<ApiProps>('./props', {
 *   model: 'gpt-4',
 *   autoload: 'false'
 * });
 * ```
 */
export async function apiFetchWithParams<T>(
	basePath: string,
	params: Record<string, string>,
	options: ApiFetchOptions = {}
): Promise<T> {
	const url = new URL(basePath, window.location.href);

	for (const [key, value] of Object.entries(params)) {
		if (value !== undefined && value !== null) {
			url.searchParams.set(key, value);
		}
	}

	return apiFetch<T>(url.toString(), options);
}

/**
 * POST JSON data to an API endpoint.
 *
 * @param path - API path
 * @param body - Request body (will be JSON stringified)
 * @param options - Additional fetch options
 * @returns Parsed JSON response
 */
export async function apiPost<T, B = unknown>(
	path: string,
	body: B,
	options: ApiFetchOptions = {}
): Promise<T> {
	return apiFetch<T>(path, {
		body: JSON.stringify(body),
		method: 'POST',
		...options
	});
}

/**
 * Parse error message from a failed response.
 * Tries to extract error message from JSON body, falls back to status text.
 */
async function parseErrorMessage(response: Response): Promise<string> {
	try {
		const errorData = await response.json();

		if (errorData?.error?.message) {
			return errorData.error.message;
		}

		if (errorData?.error && typeof errorData.error === 'string') {
			return errorData.error;
		}

		if (errorData?.message) {
			return errorData.message;
		}
	} catch {
		// JSON parsing failed, use status text
	}

	const httpErrorStr = HTTP_CODE_TO_STRING[response.status];

	if (httpErrorStr) {
		return httpErrorStr;
	}

	return `${ERROR_MESSAGES.HTTP.GENERIC}: ${response.status} ${response.statusText}`;
}

/**
 * Converts a network issue into a human-readable message.
 * @param throwable - The throwable raised during fetch operation
 * @returns Error in an human-readable format
 */
function beautifyNetworkError(throwable: unknown): string {
	let message;

	if (throwable instanceof Error) {
		message = throwable.message;

		if (throwable.name === 'TypeError' && message.includes('fetch')) {
			return ERROR_MESSAGES.NETWORK.UNREACHABLE;
		}
	} else {
		message = String(throwable);
	}

	if (message.includes('ECONNREFUSED')) {
		return ERROR_MESSAGES.NETWORK.REFUSED;
	} else if (message.includes('ENOTFOUND')) {
		return ERROR_MESSAGES.NETWORK.NXDOMAIN;
	} else if (message.includes('ETIMEDOUT')) {
		return ERROR_MESSAGES.NETWORK.TIMEOUT;
	}

	return `${ERROR_MESSAGES.NETWORK.GENERIC} (${message})`;
}
