import * as Sentry from '@sentry/sveltekit';
import { isRedirect, redirect, type Handle, type HandleFetch } from '@sveltejs/kit';
import { paraglideMiddleware } from '$lib/paraglide/server';
import { sequence } from '@sveltejs/kit/hooks';
import { env } from 'process';
import { cookieName, cookieMaxAge } from '$lib/paraglide/runtime';
import { detectLocale } from '$lib/shared/server/locale';

// VITE_SENTRY_DSN is a write-only ingest key — it can POST events to GlitchTip
// but cannot read them. Safe to include in the client bundle per Sentry security model.
Sentry.init({
	dsn: import.meta.env.VITE_SENTRY_DSN,
	environment: import.meta.env.MODE,
	tracesSampleRate: 0.1,
	sendDefaultPii: false,
	enabled: !!import.meta.env.VITE_SENTRY_DSN
});

const PUBLIC_PATHS = [
	'/login',
	'/logout',
	'/forgot-password',
	'/reset-password',
	'/register',
	'/hilfe/transkription' // prerendered help page — must be reachable without an auth cookie
];

const handleLocaleDetection: Handle = ({ event, resolve }) => {
	if (!event.cookies.get(cookieName)) {
		const locale = detectLocale(event.request.headers.get('accept-language') ?? '');
		if (locale) {
			event.cookies.set(cookieName, locale, {
				path: '/',
				sameSite: 'lax',
				maxAge: cookieMaxAge,
				httpOnly: false
			});
		}
	}
	return resolve(event);
};

const handleAuth: Handle = async ({ event, resolve }) => {
	const isPublic = PUBLIC_PATHS.some((p) => event.url.pathname.startsWith(p));
	if (!isPublic && !event.locals.user) {
		throw redirect(302, '/login');
	}
	return resolve(event);
};

const handleParaglide: Handle = ({ event, resolve }) =>
	paraglideMiddleware(event.request, ({ request, locale }) => {
		event.request = request;

		return resolve(event, {
			transformPageChunk: ({ html }) => html.replace('%paraglide.lang%', locale)
		});
	});

const userGroup: Handle = async ({ event, resolve }) => {
	// One-off cleanup of the legacy Basic-credentials cookie from before the Spring Session migration (#523).
	if (event.cookies.get('auth_token')) {
		event.cookies.delete('auth_token', { path: '/' });
	}

	const sessionId = event.cookies.get('fa_session');
	if (!sessionId) {
		return resolve(event);
	}

	try {
		const apiUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
		const response = await fetch(`${apiUrl}/api/users/me`, {
			headers: { Cookie: `fa_session=${sessionId}` }
		});

		if (response.ok) {
			event.locals.user = await response.json();
		} else if (response.status === 401) {
			// Backend rejected the session (expired or invalidated). Drop the stale
			// cookie and surface the reason on the login page. PUBLIC_PATHS check
			// avoids a redirect loop if the user is already on /login.
			event.cookies.delete('fa_session', { path: '/' });
			const isPublic = PUBLIC_PATHS.some((p) => event.url.pathname.startsWith(p));
			if (!isPublic) {
				throw redirect(302, '/login?reason=expired');
			}
		}
	} catch (error) {
		// Re-throw SvelteKit redirects (e.g. the /login?reason=expired throw above)
		// using the official guard rather than duck-typing on the error shape.
		if (isRedirect(error)) throw error;
		console.error('Error fetching user in hook:', error);
	}

	return resolve(event);
};

const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);

// Auth endpoints that establish/check their own credentials — skip fa_session injection
// but still need CSRF tokens on mutating requests.
const PUBLIC_API_PATHS = [
	'/api/auth/login',
	'/api/auth/logout',
	'/api/auth/forgot-password',
	'/api/auth/reset-password',
	'/api/auth/invite/',
	'/api/auth/register'
];

export const handleFetch: HandleFetch = async ({ event, request, fetch }) => {
	const apiUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
	const isApi = request.url.startsWith(apiUrl) || new URL(request.url).pathname.startsWith('/api/');

	if (!isApi) return fetch(request);

	const isMutating = MUTATING_METHODS.has(request.method);
	const isPublicAuthApi = PUBLIC_API_PATHS.some((p) => request.url.includes(p));

	const sessionId = !isPublicAuthApi ? event.cookies.get('fa_session') : null;
	if (!isPublicAuthApi && !sessionId) {
		return new Response('Unauthorized', { status: 401 });
	}

	// Read the browser's XSRF-TOKEN cookie; fall back to a fresh UUID for the
	// double-submit cookie pattern (both cookie and header must match — no server secret).
	const xsrfToken = isMutating ? (event.cookies.get('XSRF-TOKEN') ?? crypto.randomUUID()) : null;

	const cookieParts: string[] = [];
	if (sessionId) cookieParts.push(`fa_session=${sessionId}`);
	if (xsrfToken) cookieParts.push(`XSRF-TOKEN=${xsrfToken}`);

	if (cookieParts.length === 0) {
		return fetch(request);
	}

	// Clone first so the body stream is preserved on the new Request.
	const cloned = request.clone();
	const extraHeaders: Record<string, string> = { Cookie: cookieParts.join('; ') };
	if (xsrfToken) extraHeaders['X-XSRF-TOKEN'] = xsrfToken;

	const modified = new Request(cloned, {
		headers: {
			...Object.fromEntries(cloned.headers),
			...extraHeaders
		}
	});
	return fetch(modified);
};

export const handle = sequence(userGroup, handleAuth, handleLocaleDetection, handleParaglide);

export const handleError = Sentry.handleErrorWithSentry(() => {
	const errorId = Sentry.lastEventId() ?? crypto.randomUUID();
	return { message: 'An unexpected error occurred', errorId };
});
