/**
 * Minimal frontend-facing API client for the Lieferscheine backend.
 *
 * Goals:
 * - Centralize fetch defaults (credentials + no-store) to match backend caching rules.
 * - Provide a single, predictable error shape via ApiClientError.
 * - Offer thin domain helpers (login/logout/me/branches/...).
 *
 * Notes:
 * - JavaScript only (no TypeScript). We use JSDoc for "typed-by-convention".
 * - This client is intentionally small. It is not an SDK.
 */

/**
 * @typedef {Object} ApiErrorPayload
 * @property {{ message: string, code: string, details?: any }} error
 */

export class ApiClientError extends Error {
	/**
	 * @param {{
	 *   status: number,
	 *   code: string,
	 *   message: string,
	 *   details?: any,
	 *   url?: string,
	 *   method?: string,
	 *   cause?: any
	 * }} input
	 */
	constructor({ status, code, message, details, url, method, cause }) {
		super(message, cause ? { cause } : undefined);

		this.name = "ApiClientError";
		this.status = status;
		this.code = code;

		if (details !== undefined) this.details = details;
		if (url) this.url = url;
		if (method) this.method = method;
	}
}

const DEFAULT_HEADERS = { Accept: "application/json" };

function resolveUrl(path, baseUrl) {
	if (/^https?:\/\//i.test(path)) return path;

	const base = (baseUrl || "").trim();
	if (!base) return path;

	return new URL(path, base.endsWith("/") ? base : `${base}/`).toString();
}

function isJsonResponse(response) {
	const ct = response.headers.get("content-type") || "";
	return ct.toLowerCase().includes("application/json");
}

export async function apiFetch(path, options = {}) {
	const {
		method = "GET",
		headers = {},
		body,
		baseUrl,
		fetchImpl = fetch,
	} = options;

	const url = resolveUrl(path, baseUrl);

	const init = {
		method,
		credentials: "include",
		cache: "no-store",
		headers: { ...DEFAULT_HEADERS, ...headers },
	};

	if (body !== undefined) {
		if (typeof body === "string") {
			init.body = body;
		} else {
			init.body = JSON.stringify(body);
			if (!init.headers["Content-Type"]) {
				init.headers["Content-Type"] = "application/json";
			}
		}
	}

	let response;
	try {
		response = await fetchImpl(url, init);
	} catch (err) {
		throw new ApiClientError({
			status: 0,
			code: "CLIENT_NETWORK_ERROR",
			message: "Network error",
			url,
			method,
			cause: err,
		});
	}

	if (response.status === 204) return null;

	if (isJsonResponse(response)) {
		let payload;

		try {
			payload = await response.json();
		} catch (err) {
			throw new ApiClientError({
				status: response.status,
				code: "CLIENT_INVALID_JSON",
				message: "Invalid JSON response",
				url,
				method,
				cause: err,
			});
		}

		if (response.ok) return payload;

		const maybeError = payload;

		if (maybeError?.error?.code && maybeError?.error?.message) {
			throw new ApiClientError({
				status: response.status,
				code: maybeError.error.code,
				message: maybeError.error.message,
				details: maybeError.error.details,
				url,
				method,
			});
		}

		throw new ApiClientError({
			status: response.status,
			code: "CLIENT_HTTP_ERROR",
			message: `Request failed with status ${response.status}`,
			details: payload,
			url,
			method,
		});
	}

	const text = await response.text().catch(() => "");
	if (response.ok) return text || null;

	throw new ApiClientError({
		status: response.status,
		code: "CLIENT_HTTP_ERROR",
		message: text || `Request failed with status ${response.status}`,
		url,
		method,
	});
}

/* -------------------------------------------------------------------------- */
/* Domain helpers                                                              */
/* -------------------------------------------------------------------------- */

/**
 * @typedef {Object} MeUser
 * @property {string} userId
 * @property {string} role
 * @property {string|null} branchId
 * @property {string|null} email
 * @property {boolean} mustChangePassword
 */

export function login(input, options) {
	return apiFetch("/api/auth/login", {
		method: "POST",
		body: input,
		...options,
	});
}

export function logout(options) {
	return apiFetch("/api/auth/logout", { method: "GET", ...options });
}

/**
 * @returns {Promise<{ user: MeUser|null }>}
 */
export function getMe(options) {
	return apiFetch("/api/auth/me", { method: "GET", ...options });
}

export function changePassword(input, options) {
	return apiFetch("/api/auth/change-password", {
		method: "POST",
		body: input,
		...options,
	});
}

export function getBranches(options) {
	return apiFetch("/api/branches", { method: "GET", ...options });
}

export function getYears(branch, options) {
	return apiFetch(`/api/branches/${encodeURIComponent(branch)}/years`, {
		method: "GET",
		...options,
	});
}

export function getMonths(branch, year, options) {
	return apiFetch(
		`/api/branches/${encodeURIComponent(branch)}/${encodeURIComponent(year)}/months`,
		{ method: "GET", ...options },
	);
}

export function getDays(branch, year, month, options) {
	return apiFetch(
		`/api/branches/${encodeURIComponent(branch)}/${encodeURIComponent(
			year,
		)}/${encodeURIComponent(month)}/days`,
		{ method: "GET", ...options },
	);
}

export function getFiles(branch, year, month, day, options) {
	const qs = new URLSearchParams({
		branch: String(branch),
		year: String(year),
		month: String(month),
		day: String(day),
	});

	return apiFetch(`/api/files?${qs.toString()}`, { method: "GET", ...options });
}

export function search(input, options) {
	const { q, branch, scope, branches, from, to, limit, cursor } = input || {};
	const params = new URLSearchParams();

	if (typeof q === "string" && q.trim()) params.set("q", q.trim());
	if (typeof scope === "string" && scope.trim())
		params.set("scope", scope.trim());
	if (typeof branch === "string" && branch.trim())
		params.set("branch", branch.trim());

	if (Array.isArray(branches) && branches.length > 0) {
		const cleaned = branches.map((b) => String(b).trim()).filter(Boolean);
		if (cleaned.length > 0) params.set("branches", cleaned.join(","));
	}

	if (typeof from === "string" && from.trim()) params.set("from", from.trim());
	if (typeof to === "string" && to.trim()) params.set("to", to.trim());

	if (limit !== undefined && limit !== null) {
		const raw = String(limit).trim();
		if (raw) params.set("limit", raw);
	}

	if (typeof cursor === "string" && cursor.trim())
		params.set("cursor", cursor.trim());

	const qs = params.toString();
	return apiFetch(qs ? `/api/search?${qs}` : "/api/search", {
		method: "GET",
		...options,
	});
}

export function adminListUsers(input, options) {
	const { q, role, branchId, sort, limit, cursor } = input || {};
	const params = new URLSearchParams();

	if (typeof q === "string" && q.trim()) params.set("q", q.trim());
	if (typeof role === "string" && role.trim()) params.set("role", role.trim());
	if (typeof branchId === "string" && branchId.trim())
		params.set("branchId", branchId.trim());
	if (typeof sort === "string" && sort.trim()) params.set("sort", sort.trim());

	if (limit !== undefined && limit !== null) {
		const raw = String(limit).trim();
		if (raw) params.set("limit", raw);
	}

	if (typeof cursor === "string" && cursor.trim())
		params.set("cursor", cursor.trim());

	const qs = params.toString();
	return apiFetch(qs ? `/api/admin/users?${qs}` : "/api/admin/users", {
		method: "GET",
		...options,
	});
}

export function adminCreateUser(input, options) {
	return apiFetch("/api/admin/users", {
		method: "POST",
		body: input,
		...options,
	});
}

export function adminUpdateUser(userId, patch, options) {
	if (typeof userId !== "string" || !userId.trim()) {
		throw new Error("adminUpdateUser requires a userId string");
	}

	return apiFetch(`/api/admin/users/${encodeURIComponent(userId.trim())}`, {
		method: "PATCH",
		body: patch || {},
		...options,
	});
}

export function adminDeleteUser(userId, options) {
	if (typeof userId !== "string" || !userId.trim()) {
		throw new Error("adminDeleteUser requires a userId string");
	}

	return apiFetch(`/api/admin/users/${encodeURIComponent(userId.trim())}`, {
		method: "DELETE",
		...options,
	});
}

export function adminResetUserPassword(userId, options) {
	if (typeof userId !== "string" || !userId.trim()) {
		throw new Error("adminResetUserPassword requires a userId string");
	}

	return apiFetch(`/api/admin/users/${encodeURIComponent(userId.trim())}`, {
		method: "POST",
		...options,
	});
}
