import { getToken, clearAuth } from './auth';

const BASE = '/api/v1';

export class ApiError extends Error {
	status: number;
	code: string;

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

const TIMEOUT_MS = 20_000;

/** Pages that ARE the recovery flow — redirecting from them would loop. */
const AUTH_ROUTES = ['/join', '/recover'];

/**
 * Send a guest whose session died back to the join screen.
 *
 * Deliberately uses `window.location` rather than SvelteKit's `goto`: `toast-store` already
 * imports `ApiError` from this module, so pulling a store or `$app/navigation` in here would
 * create an import cycle. A full document load is also the more correct behaviour after a
 * session loss — it resets every module-level store, which is exactly what we want, and the
 * queued upload blobs live in IndexedDB so they survive it.
 */
function redirectToJoin(): void {
	if (typeof window === 'undefined') return;
	const path = window.location.pathname;
	if (AUTH_ROUTES.some((r) => path === r || path.startsWith(`${r}/`))) return;
	window.location.assign('/join');
}

async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
	const headers: Record<string, string> = {};
	const token = getToken();
	if (token) {
		headers['Authorization'] = `Bearer ${token}`;
	}
	if (body !== undefined) {
		headers['Content-Type'] = 'application/json';
	}

	// Abort hung requests so a dead connection surfaces as a friendly error
	// instead of a spinner that never resolves.
	const controller = new AbortController();
	const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);

	let res: Response;
	try {
		res = await fetch(`${BASE}${path}`, {
			method,
			headers,
			body: body !== undefined ? JSON.stringify(body) : undefined,
			signal: controller.signal
		});
	} catch (e) {
		if (e instanceof DOMException && e.name === 'AbortError') {
			throw new ApiError(0, 'timeout', 'Zeitüberschreitung – bitte erneut versuchen.');
		}
		throw new ApiError(0, 'network', 'Netzwerkfehler – bitte Verbindung prüfen.');
	} finally {
		clearTimeout(timer);
	}

	if (res.status === 204) {
		return undefined as T;
	}

	// A 5xx behind a proxy (or a crash page) can return HTML, not JSON — parsing
	// it directly would throw an opaque SyntaxError. Read text, parse defensively.
	const raw = await res.text();
	let data: { error?: string; message?: string } | unknown = null;
	if (raw) {
		try {
			data = JSON.parse(raw);
		} catch {
			data = null;
		}
	}

	if (!res.ok) {
		// An expired/invalid token (401) clears the dead session. Banned users are
		// NOT logged out — they keep read access by design (USER_JOURNEYS §10) and
		// simply get a 403 "gesperrt" toast on writes.
		if (res.status === 401) {
			clearAuth();
			// Clearing auth alone leaves the guest stranded: the bottom nav and FAB are
			// gated on `isAuthenticated` so they simply vanish, route guards only run in
			// onMount (which does not re-run), and a standalone PWA has no URL bar — so
			// there is no way back to /join. Real triggers mid-event are a host PIN reset
			// (which revokes that guest's sessions) and a redeployed JWT_SECRET.
			// Queued upload blobs survive in IndexedDB and are picked up again after
			// re-joining — via the `onSetAuth` hook in upload-queue.ts, NOT the boot-time
			// call in +layout.svelte: this redirect lands on /join with no token, so the
			// layout's `if (getToken())` skips it, and the subsequent recover navigates
			// with `goto()`, which never re-runs `onMount`.
			redirectToJoin();
		}
		const d = (data ?? {}) as { error?: string; message?: string };
		throw new ApiError(
			res.status,
			d.error ?? 'unknown',
			d.message ?? `Serverfehler (${res.status}).`
		);
	}

	return data as T;
}

export const api = {
	get: <T>(path: string) => request<T>('GET', path),
	post: <T>(path: string, body?: unknown) => request<T>('POST', path, body),
	patch: <T>(path: string, body?: unknown) => request<T>('PATCH', path, body),
	delete: <T>(path: string) => request<T>('DELETE', path)
};
