// #region ApiModule [C:5] [TYPE Module] [SEMANTICS api, client, fetch, auth, error-handling]
// @BRIEF Core API communication layer — typed fetch wrappers with auth injection, error normalization, toast feedback, and endpoint registry.
// @LAYER Infrastructure
// @RELATION DEPENDS_ON -> [ToastsModule]
// @RELATION CALLED_BY -> [ReportsApi]
// @RELATION CALLED_BY -> [AssistantApi]
// @RELATION CALLED_BY -> [TranslateRunsApi]
// @RELATION CALLED_BY -> [DatasetReviewApi]
// @RELATION CALLED_BY -> [MaintenanceApi]
// @RELATION CALLED_BY -> [ValidationRunDetailPageLoad]
// @PRE Auth token is available in localStorage under 'auth_token' after login.
// @POST Every API call returns typed JSON response or throws typed ApiError with status/detail/error_code.
// @SIDE_EFFECT Reads localStorage for auth token on every request. Dispatches error toasts on non-suppressed failures.
// @INVARIANT Every fetch MUST go through fetchApi/requestApi/postApi/deleteApi — never native fetch().
// @INVARIANT Every response.json() is wrapped in try/catch that converts HTTP errors to ApiError.
// @DATA_CONTRACT Input: (endpoint, body?, options?) → Output: Promise<T> | Promise<Blob> | Error(ApiError)
// @RATIONALE fetch wrappers exist to enforce auth token injection, centralized error handling,
//            toast feedback, and trace_id propagation — without wrapping every component in try/catch boilerplate.
// @REJECTED Native fetch() rejected — would bypass auth header injection, error normalization,
//           and toast feedback. Every component would need its own error handling.
// @REJECTED Axios rejected — unnecessary dependency for a single-domain API client; native fetch + wrappers
//           is simpler, tree-shakeable, and has zero bundle cost.

import { log } from '$lib/cot-logger';
import { addToast } from './toasts.svelte.js';
import type { FetchOptions, DashboardListParams } from '../types/api';

const API_BASE_URL = '/api';

// #region ApiTypes [C:1] [TYPE Block] [SEMANTICS api, types, interfaces]
// @BRIEF Internal type definitions for error handling and API configuration.
// @DATA_CONTRACT ApiError: { message, status, detail, error_code }

interface ApiError extends Error {
	status?: number;
	detail?: unknown;
	error_code?: string;
}

interface BuildApiErrorResponse {
	detail?: string | { message?: string; error_code?: string; [k: string]: unknown };
	error_code?: string;
	[k: string]: unknown;
}
// #endregion ApiTypes

// #region buildApiError [C:2] [TYPE Function] [SEMANTICS api, error, parsing]
// @BRIEF Parse an HTTP Response into a structured ApiError with status, detail, and error_code.
// @PRE response is a failed HTTP Response object (ok === false).
// @POST Returns ApiError with message, status, detail, and optional error_code extracted from response body.
// @SIDE_EFFECT Reads response body via .json() (consumes the response stream).
// @RELATION CALLED_BY -> [fetchApi]
// @RELATION CALLED_BY -> [postApi]
// @RELATION CALLED_BY -> [deleteApi]
// @RELATION CALLED_BY -> [requestApi]
// @RATIONALE JSON body parsing is wrapped in .catch() because some error responses (e.g. 502 proxy errors)
//            return non-JSON bodies. Without this, the error itself would throw and mask the original status code.
async function buildApiError(response: Response): Promise<ApiError> {
	const errorData: BuildApiErrorResponse = await response.json().catch(() => ({}));
	const detail = errorData?.detail;
	const message = detail
		? (typeof detail === 'string' ? detail : (typeof detail?.message === 'string' ? detail.message : JSON.stringify(detail)))
		: `API request failed with status ${response.status}`;
	const error: ApiError = new Error(message) as ApiError;
	error.status = response.status;
	error.detail = detail;
	if (detail && typeof detail === 'object' && (detail as Record<string, unknown>).error_code) {
		error.error_code = String((detail as Record<string, unknown>).error_code);
	}
	return error;
}
// #endregion buildApiError

// #region notifyApiError [C:2] [TYPE Function] [SEMANTICS api, error, toast, feedback]
// @BRIEF Dispatch an error toast with severity-based messaging.
// @PRE error is a structured ApiError (may have status).
// @POST Toast is dispatched with appropriate message and error severity.
// @SIDE_EFFECT Calls addToast() which mutates the global toast store.
// @RELATION DEPENDS_ON -> [ToastsModule.addToast]
// @RELATION CALLED_BY -> [fetchApi]
// @RELATION CALLED_BY -> [postApi]
// @RELATION CALLED_BY -> [deleteApi]
// @RELATION CALLED_BY -> [requestApi]
// @UX_FEEDBACK 401 → "401 Unauthorized" toast. 500+ → "Server error (N)" toast. Others → error.message toast.
function notifyApiError(error: ApiError): void {
	if (error?.status === 401) { addToast(`401 Unauthorized: ${error.message}`, 'error'); return; }
	if (error?.status >= 500) { addToast(`Server error (${error.status}): ${error.message}`, 'error'); return; }
	addToast(error.message, 'error');
}
// #endregion notifyApiError

// #region shouldSuppressApiErrorToast [C:2] [TYPE Function] [SEMANTICS api, error, suppression, heuristics]
// @BRIEF Determine whether an API error should be silently suppressed (no toast) based on endpoint + error heuristics.
// @PRE endpoint is a string path. error is a structured ApiError.
// @POST Returns boolean — true if the error is an expected "non-error" for the given endpoint.
// @RATIONALE Several endpoints legitimately return 4xx for "empty" states (no git repo, no clarification session).
//            Suppressing these toasts prevents noise pollution while still throwing for programmatic handling.
// @REJECTED A global "ignore 4xx" flag rejected — too broad. Each heuristic is endpoint-specific.
// @RELATION CALLED_BY -> [requestApi]
function shouldSuppressApiErrorToast(endpoint: string, error: ApiError): boolean {
	const isGitStatusEndpoint = typeof endpoint === 'string' && endpoint.startsWith('/git/repositories/') && endpoint.endsWith('/status');
	const isNoRepoError = (error?.status === 400 || error?.status === 404) && /Repository for dashboard .* not found/i.test(String(error?.message || ''));
	const isGitPullEndpoint = typeof endpoint === 'string' && endpoint.startsWith('/git/repositories/') && endpoint.endsWith('/pull');
	const isUnfinishedMergeError = error?.status === 409 && (String(error?.error_code || '') === 'GIT_UNFINISHED_MERGE' || String((error?.detail as Record<string, unknown>)?.error_code || '') === 'GIT_UNFINISHED_MERGE');
	const isDatasetClarificationEndpoint = typeof endpoint === 'string' && /\/dataset-orchestration\/sessions\/[^/]+\/clarification$/.test(endpoint);
	const isMissingClarificationSession = error?.status === 404 && /Clarification session not found/i.test(String(error?.message || ''));
	const isGitRepoEndpoint = typeof endpoint === 'string' && endpoint.startsWith('/git/repositories/');
	const isMissingEnvIdError = error?.status === 400 && /env_id is required/i.test(String(error?.message || ''));
	const isGitConfigReposEndpoint = typeof endpoint === 'string' && /^\/git\/config\/[^/]+\/repositories$/.test(endpoint);
	const isRepoAlreadyExistsError = error?.status === 409 && /already exists/i.test(String(error?.message || ''));
	return (isGitStatusEndpoint && isNoRepoError) || (isGitPullEndpoint && isUnfinishedMergeError) || (isDatasetClarificationEndpoint && isMissingClarificationSession) || (isGitRepoEndpoint && isMissingEnvIdError) || (isGitConfigReposEndpoint && isRepoAlreadyExistsError);
}
// #endregion shouldSuppressApiErrorToast

// #region wsUrlHelpers [C:2] [TYPE Block] [SEMANTICS websocket, url, task-logs, maintenance, translate]
// @BRIEF WebSocket URL builders — each constructs an authenticated WS endpoint for a specific channel.
// @PRE taskId / runId are non-empty strings where applicable.
// @POST Returns fully-qualified ws:// or wss:// URL with auth token as query parameter.
// @SIDE_EFFECT Reads localStorage for auth_token on each call.
// @RATIONALE WebSocket API does not support custom headers in browser, so auth token is appended as query param.

/**
 * Build a WebSocket URL for a task's log stream, including auth token.
 */
export const getWsUrl = (taskId: string): string => {
	const protocol = typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'wss:' : 'ws:';
	const host = typeof window !== 'undefined' ? window.location.host : 'localhost:8000';
	let url = `${protocol}//${host}/ws/logs/${taskId}`;
	if (typeof window !== 'undefined') {
		const token = localStorage.getItem('auth_token');
		if (token) {
			url += `?token=${encodeURIComponent(token)}`;
		}
	}
	return url;
};

/**
 * Build a WebSocket URL for global task events, including auth token.
 */
export const getTaskEventsWsUrl = (): string => {
	const protocol = typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'wss:' : 'ws:';
	const host = typeof window !== 'undefined' ? window.location.host : 'localhost:8000';
	let url = `${protocol}//${host}/ws/task-events`;
	if (typeof window !== 'undefined') {
		const token = localStorage.getItem('auth_token');
		if (token) {
			url += `?token=${encodeURIComponent(token)}`;
		}
	}
	return url;
};

/**
 * Build a WebSocket URL for maintenance events, including auth token.
 */
export const getMaintenanceEventsWsUrl = (): string => {
	const protocol = typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'wss:' : 'ws:';
	const host = typeof window !== 'undefined' ? window.location.host : 'localhost:8000';
	let url = `${protocol}//${host}/ws/maintenance/events`;
	if (typeof window !== 'undefined') {
		const token = localStorage.getItem('auth_token');
		if (token) {
			url += `?token=${encodeURIComponent(token)}`;
		}
	}
	return url;
};

/**
 * Build a WebSocket URL for translation run progress streaming.
 */
export const getTranslateRunWsUrl = (runId: string): string => {
	const protocol = typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'wss:' : 'ws:';
	const host = typeof window !== 'undefined' ? window.location.host : 'localhost:8000';
	let url = `${protocol}//${host}/ws/translate/run/${runId}`;
	if (typeof window !== 'undefined') {
		const token = localStorage.getItem('auth_token');
		if (token) {
			url += `?token=${encodeURIComponent(token)}`;
		}
	}
	return url;
};
// #endregion wsUrlHelpers

// #region getAuthHeaders [C:2] [TYPE Function] [SEMANTICS auth, headers, token, localStorage]
// @BRIEF Build request headers with Content-Type and optional Bearer token from localStorage.
// @PRE extraHeaders is an optional map of header key → value.
// @POST Returns headers object with Content-Type: application/json and Authorization: Bearer <token> if logged in.
// @SIDE_EFFECT Reads localStorage for auth_token on every call.
// @RELATION CALLED_BY -> [fetchApi]
// @RELATION CALLED_BY -> [fetchApiBlob]
// @RELATION CALLED_BY -> [postApi]
// @RELATION CALLED_BY -> [deleteApi]
// @RELATION CALLED_BY -> [requestApi]
function getAuthHeaders(extraHeaders: Record<string, string> = {}): Record<string, string> {
	const headers: Record<string, string> = { 'Content-Type': 'application/json', ...extraHeaders };
	if (typeof window !== 'undefined') {
		const token = localStorage.getItem('auth_token');
		if (token) headers['Authorization'] = `Bearer ${token}`;
	}
	return headers;
}
// #endregion getAuthHeaders

// #region fetchApi [C:4] [TYPE Function] [SEMANTICS api, fetch, get, json]
// @BRIEF Perform an authenticated GET request and return typed JSON response.
// @PRE endpoint is a non-empty string path (without /api prefix).
// @PRE options.headers may override default Content-Type.
// @POST Returns Promise<T> with parsed JSON. Returns null for 204 No Content.
//      Throws ApiError on non-2xx response.
// @SIDE_EFFECT Sends HTTP GET request. On failure, dispatches error toast (unless options.suppressToast).
//      Writes CoT log line via log() on entry and failure.
// @RELATION DEPENDS_ON -> [buildApiError]
// @RELATION DEPENDS_ON -> [notifyApiError]
// @RELATION DEPENDS_ON -> [getAuthHeaders]
async function fetchApi<T = unknown>(endpoint: string, options: FetchOptions = {}): Promise<T> {
	try {
		log('api', 'REASON', 'fetchApi', { endpoint });
		const response = await fetch(`${API_BASE_URL}${endpoint}`, { headers: getAuthHeaders(options.headers || {}) });
		if (!response.ok) throw await buildApiError(response);
		if (response.status === 204) return null as T;
		return await response.json() as T;
	} catch (error) {
		const apiError = error as ApiError;
		log('api', 'EXPLORE', 'fetchApi failed', { endpoint }, apiError?.message || 'unknown');
		if (!options.suppressToast) notifyApiError(apiError);
		throw error;
	}
}
// #endregion fetchApi

// #region fetchApiBlob [C:4] [TYPE Function] [SEMANTICS api, fetch, blob, thumbnail, file]
// @BRIEF Perform an authenticated GET request and return a Blob (for thumbnails, file downloads).
// @PRE endpoint is a non-empty string path.
// @POST Returns Promise<Blob> with binary data. Throws ApiError on failure or 202 "in progress".
// @SIDE_EFFECT Sends HTTP GET request. On failure (unless notifyError=false), dispatches error toast.
// @RELATION DEPENDS_ON -> [buildApiError]
// @RELATION DEPENDS_ON -> [notifyApiError]
// @RELATION DEPENDS_ON -> [getAuthHeaders]
// @RATIONALE 202 status is handled as a special case — the thumbnail generation may still be in progress.
//            The caller can retry after a delay. This is NOT treated as a server error.
async function fetchApiBlob(endpoint: string, options: FetchOptions = {}): Promise<Blob> {
	const notifyError = options.notifyError !== false;
	try {
		const response = await fetch(`${API_BASE_URL}${endpoint}`, { headers: getAuthHeaders(options.headers || {}) });
		if (response.status === 202) {
			const payload: Record<string, unknown> = await response.json().catch(() => ({ message: "Resource is being prepared" }));
			const error: ApiError = new Error((payload?.message as string) || "Resource is being prepared") as ApiError;
			error.status = 202;
			throw error;
		}
		if (!response.ok) throw await buildApiError(response);
		return await response.blob();
	} catch (error) {
		const apiError = error as ApiError;
		if (notifyError) notifyApiError(apiError);
		throw error;
	}
}
// #endregion fetchApiBlob

// #region postApi [C:4] [TYPE Function] [SEMANTICS api, post, create, json]
// @BRIEF Perform an authenticated POST request with JSON body and return typed response.
// @PRE endpoint is a non-empty string path.
// @PRE body is JSON-serializable (will be passed through JSON.stringify).
// @POST Returns Promise<T> with parsed JSON. Returns null for 204 No Content.
//      Throws ApiError on non-2xx response.
// @SIDE_EFFECT Sends HTTP POST request. On failure, dispatches error toast (unless options.suppressToast).
//      Writes CoT log line on entry and failure.
// @RELATION DEPENDS_ON -> [buildApiError]
// @RELATION DEPENDS_ON -> [notifyApiError]
// @RELATION DEPENDS_ON -> [getAuthHeaders]
async function postApi<T = unknown>(endpoint: string, body: unknown, options: FetchOptions = {}): Promise<T> {
	try {
		const response = await fetch(`${API_BASE_URL}${endpoint}`, {
			method: 'POST', headers: getAuthHeaders(options.headers || {}), body: JSON.stringify(body),
		});
		if (!response.ok) throw await buildApiError(response);
		if (response.status === 204) return null as T;
		return await response.json() as T;
	} catch (error) {
		const apiError = error as ApiError;
		log('api', 'EXPLORE', 'postApi failed', { endpoint }, apiError?.message || 'unknown');
		if (!options.suppressToast) notifyApiError(apiError);
		throw error;
	}
}
// #endregion postApi

// #region deleteApi [C:4] [TYPE Function] [SEMANTICS api, delete, remove]
// @BRIEF Perform an authenticated DELETE request and return typed response.
// @PRE endpoint is a non-empty string path.
// @POST Returns Promise<T> with parsed JSON. Returns null for 204 No Content.
//      Throws ApiError on non-2xx response. Always dispatches error toast on failure.
// @SIDE_EFFECT Sends HTTP DELETE request. On failure, dispatches error toast.
//      Writes CoT log line on entry and failure.
// @RELATION DEPENDS_ON -> [buildApiError]
// @RELATION DEPENDS_ON -> [notifyApiError]
// @RELATION DEPENDS_ON -> [getAuthHeaders]
// @RATIONALE deleteApi always notifies on error (no suppressToast option) because deletions
//            are destructive operations — the user must know if one failed.
async function deleteApi<T = unknown>(endpoint: string, options: FetchOptions = {}): Promise<T> {
	try {
		const response = await fetch(`${API_BASE_URL}${endpoint}`, { method: 'DELETE', headers: getAuthHeaders(options.headers || {}) });
		if (!response.ok) throw await buildApiError(response);
		if (response.status === 204) return null as T;
		return await response.json() as T;
	} catch (error) {
		const apiError = error as ApiError;
		log('api', 'EXPLORE', 'deleteApi failed', { endpoint }, apiError?.message || 'unknown');
		notifyApiError(apiError);
		throw error;
	}
}
// #endregion deleteApi

// #region requestApi [C:4] [TYPE Function] [SEMANTICS api, request, generic, patch, put]
// @BRIEF Generic authenticated HTTP request — supports any method, optional JSON body, and contextual toast suppression.
// @PRE endpoint is a non-empty string path.
// @PRE body is JSON-serializable if provided (null = no body).
// @POST Returns Promise<T> with parsed JSON. Returns null for 204 No Content.
//      Throws ApiError on non-2xx response.
//      Error toast suppressed if shouldSuppressApiErrorToast returns true for the endpoint+error combo.
// @SIDE_EFFECT Sends HTTP request. On failure, conditionally dispatches error toast (subject to suppression heuristics).
//      Writes CoT log line on entry and failure.
// @RELATION DEPENDS_ON -> [buildApiError]
// @RELATION DEPENDS_ON -> [notifyApiError]
// @RELATION DEPENDS_ON -> [shouldSuppressApiErrorToast]
// @RELATION DEPENDS_ON -> [getAuthHeaders]
async function requestApi<T = unknown>(endpoint: string, method: string = 'GET', body: unknown = null, requestOptions: FetchOptions = {}): Promise<T> {
	try {
		const fetchOptions: RequestInit = { method, headers: getAuthHeaders(requestOptions.headers || {}) };
		if (body) fetchOptions.body = JSON.stringify(body);
		const response = await fetch(`${API_BASE_URL}${endpoint}`, fetchOptions);
		if (!response.ok) throw await buildApiError(response);
		if (response.status === 204) return null as T;
		return await response.json() as T;
	} catch (error) {
		const apiError = error as ApiError;
		log('api', 'EXPLORE', 'requestApi failed', { method, endpoint }, apiError?.message || 'unknown');
		if (!requestOptions.suppressToast && !shouldSuppressApiErrorToast(endpoint, apiError)) {
			notifyApiError(apiError);
		}
		throw error;
	}
}
// #endregion requestApi

// ── Named type helpers for API methods ───────────────────────
// These are internal interfaces used by the api registry below.

interface TaskListQueryOptions {
	limit?: number;
	offset?: number;
	status?: string;
	task_type?: string;
	completed_only?: boolean;
	plugin_id?: string[];
}

interface TaskLogQueryOptions {
	level?: string;
	source?: string;
	search?: string;
	offset?: number;
	limit?: number;
}

interface DashboardThumbnailOptions {
	force?: boolean;
}

interface DatasetQueryOptions {
	search?: string;
	filter?: string;
	page?: string;
	page_size?: string;
}

interface SupersetAccountOptions {
	search?: string;
	page_index?: number;
	page_size?: number;
	sort_column?: string;
	sort_order?: string;
}

interface ValidationTaskQueryParams {
	page?: number;
	page_size?: number;
	is_active?: boolean;
	environment_id?: string;
	search?: string;
}

interface ValidationRunQueryParams {
	page?: number;
	page_size?: number;
}

// #region ApiRegistry [C:3] [TYPE Block] [SEMANTICS api, endpoints, registry]
// @BRIEF Named endpoint registry — maps backend API paths to typed frontend methods.
// @LAYER API
// @RELATION DEPENDS_ON -> [fetchApi]
// @RELATION DEPENDS_ON -> [postApi]
// @RELATION DEPENDS_ON -> [deleteApi]
// @RELATION DEPENDS_ON -> [requestApi]
// @RELATION DEPENDS_ON -> [fetchApiBlob]
// @INVARIANT Every method delegates to fetchApi/postApi/deleteApi/requestApi — never native fetch.
// @RATIONALE The registry pattern keeps endpoint paths in one place and eliminates path-string duplication across components.
export const api = {
	fetchApi: fetchApi as <T = unknown>(endpoint: string, options?: FetchOptions) => Promise<T>,
	postApi: postApi as <T = unknown>(endpoint: string, body: unknown, options?: FetchOptions) => Promise<T>,
	deleteApi: deleteApi as <T = unknown>(endpoint: string, options?: FetchOptions) => Promise<T>,
	requestApi: requestApi as <T = unknown>(endpoint: string, method?: string, body?: unknown, requestOptions?: FetchOptions) => Promise<T>,
	fetchApiBlob: fetchApiBlob as (endpoint: string, options?: FetchOptions) => Promise<Blob>,

	// #region taskEndpoints [C:2] [TYPE Block] [SEMANTICS tasks, api, crud]
	getPlugins: <T = unknown>() => fetchApi<T>('/plugins'),
	getTasks: <T = unknown>(options: TaskListQueryOptions = {}) => {
		const params = new URLSearchParams();
		if (options.limit != null) params.append('limit', String(options.limit));
		if (options.offset != null) params.append('offset', String(options.offset));
		if (options.status) params.append('status', options.status);
		if (options.task_type) params.append('task_type', options.task_type);
		if (options.completed_only != null) params.append('completed_only', String(Boolean(options.completed_only)));
		if (Array.isArray(options.plugin_id)) options.plugin_id.forEach((pid: string) => params.append('plugin_id', pid));
		const query = params.toString();
		return fetchApi<T>(`/tasks${query ? `?${query}` : ''}`);
	},
	getTask: <T = unknown>(taskId: string) => fetchApi<T>(`/tasks/${taskId}`),
	getTaskLogs: <T = unknown>(taskId: string, options: TaskLogQueryOptions = {}) => {
		const params = new URLSearchParams();
		if (options.level) params.append('level', options.level);
		if (options.source) params.append('source', options.source);
		if (options.search) params.append('search', options.search);
		if (options.offset != null) params.append('offset', String(options.offset));
		if (options.limit != null) params.append('limit', String(options.limit));
		return fetchApi<T>(`/tasks/${taskId}/logs${params.toString() ? `?${params.toString()}` : ''}`);
	},
	createTask: <T = unknown>(pluginId: string, params: unknown) => postApi<T>('/tasks', { plugin_id: pluginId, params }),
	// #endregion taskEndpoints

	// #region profileEndpoints [C:2] [TYPE Block] [SEMANTICS profile, preferences, api]
	getProfilePreferences: <T = unknown>() => fetchApi<T>('/profile/preferences'),
	updateProfilePreferences: <T = unknown>(payload: unknown) => requestApi<T>('/profile/preferences', 'PATCH', payload),
	lookupSupersetAccounts: <T = unknown>(environmentId: string, options: SupersetAccountOptions = {}) => {
		const eid = String(environmentId || '').trim();
		if (!eid) throw new Error('environmentId is required for Superset account lookup');
		const params = new URLSearchParams({ environment_id: eid });
		if (options.search) params.append('search', options.search);
		if (options.page_index != null) params.append('page_index', String(options.page_index));
		if (options.page_size != null) params.append('page_size', String(options.page_size));
		if (options.sort_column) params.append('sort_column', options.sort_column);
		if (options.sort_order) params.append('sort_order', options.sort_order);
		return fetchApi<T>(`/profile/superset-accounts?${params.toString()}`);
	},
	// #endregion profileEndpoints

	// #region settingsEndpoints [C:2] [TYPE Block] [SEMANTICS settings, environments, storage, api]
	getSettings: <T = unknown>() => fetchApi<T>('/settings'),
	updateGlobalSettings: <T = unknown>(s: unknown) => requestApi<T>('/settings/global', 'PATCH', s),
	getEnvironments: <T = unknown>() => fetchApi<T>('/settings/environments'),
	addEnvironment: <T = unknown>(env: unknown) => postApi<T>('/settings/environments', env),
	updateEnvironment: <T = unknown>(id: string, env: unknown) => requestApi<T>(`/settings/environments/${id}`, 'PUT', env),
	deleteEnvironment: <T = unknown>(id: string) => requestApi<T>(`/settings/environments/${id}`, 'DELETE'),
	testEnvironmentConnection: <T = unknown>(id: string) => postApi<T>(`/settings/environments/${id}/test`, {}),
	updateEnvironmentSchedule: <T = unknown>(id: string, s: unknown) => requestApi<T>(`/environments/${id}/schedule`, 'PUT', s),
	getStorageSettings: <T = unknown>() => fetchApi<T>('/settings/storage'),
	updateStorageSettings: <T = unknown>(s: unknown) => requestApi<T>('/settings/storage', 'PUT', s),
	getEnvironmentsList: <T = unknown>() => fetchApi<T>('/environments'),
	// #endregion settingsEndpoints

	// #region llmEndpoints [C:2] [TYPE Block] [SEMANTICS llm, models, providers, api]
	getLlmStatus: <T = unknown>() => fetchApi<T>('/llm/status'),
	fetchLlmModels: <T = unknown>(p: unknown) => postApi<T>('/llm/providers/fetch-models', p),
	getEnvironmentDatabases: <T = unknown>(id: string) => fetchApi<T>(`/environments/${id}/databases`),
	// #endregion llmEndpoints

	// #region storageEndpoints [C:2] [TYPE Block] [SEMANTICS storage, file, blob, api]
	getStorageFileBlob: (path: string) => fetchApiBlob(`/storage/file?path=${encodeURIComponent(path)}`),
	// #endregion storageEndpoints

	// #region dashboardEndpoints [C:2] [TYPE Block] [SEMANTICS dashboards, api, crud]
	getDashboards: <T = unknown>(envId: string, options: DashboardListParams = {}) => {
		const params = new URLSearchParams({ env_id: envId });
		if (options.search) params.append('search', options.search);
		if (options.page) params.append('page', options.page);
		if (options.page_size) params.append('page_size', options.page_size);
		if (options.page_context) params.append('page_context', options.page_context);
		if (options.apply_profile_default != null) params.append('apply_profile_default', String(Boolean(options.apply_profile_default)));
		if (options.override_show_all != null) params.append('override_show_all', String(Boolean(options.override_show_all)));
		if (options.filters?.title) for (const v of options.filters.title) params.append('filter_title', v);
		if (options.filters?.git_status) for (const v of options.filters.git_status) params.append('filter_git_status', v);
		if (options.filters?.llm_status) for (const v of options.filters.llm_status) params.append('filter_llm_status', v);
		if (options.filters?.changed_on) for (const v of options.filters.changed_on) params.append('filter_changed_on', v);
		if (options.filters?.actor) for (const v of options.filters.actor) params.append('filter_actor', v);
		return fetchApi<T>(`/dashboards?${params.toString()}`);
	},
	getDashboardDetail: <T = unknown>(envId: string, ref: string) => fetchApi<T>(`/dashboards/${encodeURIComponent(String(ref))}?env_id=${envId}`),
	getDashboardTaskHistory: <T = unknown>(envId: string, ref: string, opts: { limit?: number } = {}) => {
		const params = new URLSearchParams();
		if (envId) params.append('env_id', envId);
		if (opts.limit) params.append('limit', opts.limit);
		return fetchApi<T>(`/dashboards/${encodeURIComponent(String(ref))}/tasks?${params.toString()}`);
	},
	getDashboardThumbnail: (envId: string, ref: string, opts: DashboardThumbnailOptions = {}) => {
		const params = new URLSearchParams({ env_id: envId });
		if (opts.force != null) params.append('force', String(Boolean(opts.force)));
		return fetchApiBlob(`/dashboards/${encodeURIComponent(String(ref))}/thumbnail?${params.toString()}`, { notifyError: false });
	},
	getDatabaseMappings: <T = unknown>(src: string, tgt: string) => fetchApi<T>(`/dashboards/db-mappings?source_env_id=${src}&target_env_id=${tgt}`),
	calculateMigrationDryRun: <T = unknown>(p: unknown) => postApi<T>('/migration/dry-run', p),
	// #endregion dashboardEndpoints

	// #region datasetEndpoints [C:2] [TYPE Block] [SEMANTICS datasets, api, crud]
	getDatasets: <T = unknown>(envId: string, opts: DatasetQueryOptions = {}) => {
		const params = new URLSearchParams({ env_id: envId });
		if (opts.search) params.append('search', opts.search);
		if (opts.filter) params.append('filter', opts.filter);
		if (opts.page) params.append('page', opts.page);
		if (opts.page_size) params.append('page_size', opts.page_size);
		return fetchApi<T>(`/datasets?${params.toString()}`);
	},
	getDatasetIds: <T = unknown>(envId: string, opts: { search?: string } = {}) => {
		const params = new URLSearchParams({ env_id: envId });
		if (opts.search) params.append('search', opts.search);
		return fetchApi<T>(`/datasets/ids?${params.toString()}`);
	},
	getDatasetDetail: <T = unknown>(envId: string, datasetId: string) => fetchApi<T>(`/datasets/${datasetId}?env_id=${envId}`),
	// #endregion datasetEndpoints

	// #region consolidatedSettingsEndpoints [C:2] [TYPE Block] [SEMANTICS settings, consolidated, api]
	getConsolidatedSettings: <T = unknown>() => fetchApi<T>('/settings/consolidated'),
	updateConsolidatedSettings: <T = unknown>(s: unknown) => requestApi<T>('/settings/consolidated', 'PATCH', s),
	// #endregion consolidatedSettingsEndpoints

	// #region automationEndpoints [C:2] [TYPE Block] [SEMANTICS automation, policies, schedules, api]
	getValidationPolicies: <T = unknown>() => fetchApi<T>('/settings/automation/policies'),
	createValidationPolicy: <T = unknown>(p: unknown) => postApi<T>('/settings/automation/policies', p),
	updateValidationPolicy: <T = unknown>(id: string, p: unknown) => requestApi<T>(`/settings/automation/policies/${id}`, 'PATCH', p),
	deleteValidationPolicy: <T = unknown>(id: string) => requestApi<T>(`/settings/automation/policies/${id}`, 'DELETE'),
	getTranslationSchedules: <T = unknown>() => fetchApi<T>('/settings/automation/translation-schedules'),
	// #endregion automationEndpoints

	// #region healthEndpoints [C:2] [TYPE Block] [SEMANTICS health, summary, api]
	getHealthSummary: <T = unknown>(environmentId?: string) => {
		const query = environmentId ? `?env_id=${encodeURIComponent(environmentId)}` : '';
		return fetchApi<T>(`/health/summary${query}`, { suppressToast: true });
	},
	// #endregion healthEndpoints

	// #region llmProviderEndpoints [C:2] [TYPE Block] [SEMANTICS llm, providers, api]
	getLlmProviders: <T = unknown>() => fetchApi<T>('/llm/providers'),
	// #endregion llmProviderEndpoints

	// #region validationTaskEndpoints [C:2] [TYPE Block] [SEMANTICS validation, tasks, api, crud]
	parseValidationUrl: <T = unknown>(url: string, envId: string) => postApi<T>('/validation-tasks/parse-url', { url, environment_id: envId }),
	getValidationTasks: <T = unknown>(params: ValidationTaskQueryParams = {}) => {
		const qs = new URLSearchParams();
		if (params.page != null) qs.append('page', String(params.page));
		if (params.page_size != null) qs.append('page_size', String(params.page_size));
		if (params.is_active != null) qs.append('is_active', String(Boolean(params.is_active)));
		if (params.environment_id) qs.append('environment_id', params.environment_id);
		if (params.search) qs.append('search', params.search);
		const query = qs.toString();
		return fetchApi<T>(`/validation-tasks${query ? `?${query}` : ''}`);
	},
	createValidationTask: <T = unknown>(data: unknown) => postApi<T>('/validation-tasks', data),
	getValidationTask: <T = unknown>(id: string) => fetchApi<T>(`/validation-tasks/${id}`),
	updateValidationTask: <T = unknown>(id: string, data: unknown) => requestApi<T>(`/validation-tasks/${id}`, 'PUT', data),
	deleteValidationTask: <T = unknown>(id: string, deleteRuns: boolean = true) => {
		const qs = deleteRuns ? '?delete_runs=true' : '';
		return deleteApi<T>(`/validation-tasks/${id}${qs}`);
	},
	triggerValidationRun: <T = unknown>(id: string) => postApi<T>(`/validation-tasks/${id}/run`, {}),
	toggleValidationTaskStatus: <T = unknown>(id: string, isActive: boolean) => requestApi<T>(`/validation-tasks/${id}/status`, 'PATCH', { is_active: isActive }),
	getValidationRuns: <T = unknown>(taskId: string, params: ValidationRunQueryParams = {}) => {
		const qs = new URLSearchParams();
		if (params.page != null) qs.append('page', String(params.page));
		if (params.page_size != null) qs.append('page_size', String(params.page_size));
		const query = qs.toString();
		return fetchApi<T>(`/validation-tasks/${taskId}/runs${query ? `?${query}` : ''}`);
	},
	getValidationRunDetail: <T = unknown>(taskId: string, runId: string) => fetchApi<T>(`/validation-tasks/${taskId}/runs/${runId}`),
	getValidationStatusBatch: async <T = unknown>(): Promise<T> => ({ results: [] } as T),
	// #endregion validationTaskEndpoints

	// #region apiKeyEndpoints [C:2] [TYPE Block] [SEMANTICS api-keys, admin, api]
	listApiKeys: <T = unknown>() => fetchApi<T>('/admin/api-keys/'),
	createApiKey: <T = unknown>(payload: unknown) => postApi<T>('/admin/api-keys/', payload, { suppressToast: true }),
	revokeApiKey: <T = unknown>(keyId: string) => requestApi<T>(`/admin/api-keys/${keyId}`, 'DELETE'),
	// #endregion apiKeyEndpoints
};
// #endregion ApiRegistry
// #endregion ApiModule

export { fetchApi, postApi, deleteApi, requestApi };
export const getPlugins = api.getPlugins;
export const getTasks = api.getTasks;
export const getTask = api.getTask;
export const createTask = api.createTask;
export const getProfilePreferences = api.getProfilePreferences;
export const updateProfilePreferences = api.updateProfilePreferences;
export const lookupSupersetAccounts = api.lookupSupersetAccounts;
export const getSettings = api.getSettings;
export const updateGlobalSettings = api.updateGlobalSettings;
export const getEnvironments = api.getEnvironments;
export const addEnvironment = api.addEnvironment;
export const updateEnvironment = api.updateEnvironment;
export const deleteEnvironment = api.deleteEnvironment;
export const testEnvironmentConnection = api.testEnvironmentConnection;
export const updateEnvironmentSchedule = api.updateEnvironmentSchedule;
export const getEnvironmentsList = api.getEnvironmentsList;
export const getStorageSettings = api.getStorageSettings;
export const updateStorageSettings = api.updateStorageSettings;
export const getDashboards = api.getDashboards;
export const getDatasets = api.getDatasets;
export const getConsolidatedSettings = api.getConsolidatedSettings;
export const updateConsolidatedSettings = api.updateConsolidatedSettings;
export const getValidationPolicies = api.getValidationPolicies;
export const createValidationPolicy = api.createValidationPolicy;
export const updateValidationPolicy = api.updateValidationPolicy;
export const deleteValidationPolicy = api.deleteValidationPolicy;
export const getTranslationSchedules = api.getTranslationSchedules;
export const getHealthSummary = api.getHealthSummary;
export const getValidationTasks = api.getValidationTasks;
export const getValidationTask = api.getValidationTask;
export const createValidationTask = api.createValidationTask;
export const updateValidationTask = api.updateValidationTask;
export const deleteValidationTask = api.deleteValidationTask;
export const triggerValidationRun = api.triggerValidationRun;
export const toggleValidationTaskStatus = api.toggleValidationTaskStatus;
export const getValidationRuns = api.getValidationRuns;
export const getValidationRunDetail = api.getValidationRunDetail;
