import express from 'express';
import { OidcService } from './oidcService';
import { Client } from '#models/Client';

let oidcCallbackMiddleware: express.RequestHandler | undefined;

function getOidcCallback(): express.RequestHandler {
	if (!oidcCallbackMiddleware) {
		// Get the cached Koa middleware from oidc-provider (set after OidcService.initialize())
		const koaMiddleware = OidcService.getCallbackMiddleware();
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		oidcCallbackMiddleware = (req, res, next) => {
			// eslint-disable-next-line no-console
			console.log(`[OIDC callback] ${req.method} ${req.path}`);
			try {
				// eslint-disable-next-line @typescript-eslint/no-explicit-any
				(koaMiddleware as any)(req, res, next);
			} catch (err) {
				// eslint-disable-next-line no-console
				console.error('[OIDC] callback threw:', err);
				next(err);
			}
		};
	}
	return oidcCallbackMiddleware;
}

const router = express.Router();

// /.well-known/openid-configuration
router.get('/.well-known/openid-configuration', (_req, _res, next) => getOidcCallback()(_req, _res, next));

// /oauth/* — all OIDC endpoints
router.get('/oauth/authorize', (_req, _res, next) => getOidcCallback()(_req, _res, next));
router.post('/oauth/authorize', (_req, _res, next) => getOidcCallback()(_req, _res, next));
// /oauth/authorize/:uid — resume after interaction completion (uid is a path segment, not a query param)
router.get('/oauth/authorize/:uid', (req, _res, next) => { console.log('[OIDC] /oauth/authorize/:uid hit, uid:', req.params.uid); getOidcCallback()(req, _res, next); });
router.post('/oauth/authorize/:uid', (_req, _res, next) => getOidcCallback()(_req, _res, next));
router.post('/oauth/token', (_req, _res, next) => getOidcCallback()(_req, _res, next));
router.get('/oauth/userinfo', (_req, _res, next) => getOidcCallback()(_req, _res, next));
router.get('/oauth/jwks', (_req, _res, next) => getOidcCallback()(_req, _res, next));
router.post('/oauth/introspect', (_req, _res, next) => getOidcCallback()(_req, _res, next));
router.post('/oauth/revoke', (_req, _res, next) => getOidcCallback()(_req, _res, next));
router.get('/oauth/logout', (_req, _res, next) => getOidcCallback()(_req, _res, next));
router.post('/oauth/logout', (_req, _res, next) => getOidcCallback()(_req, _res, next));
router.get('/oauth/logout/confirm', async (req, res) => {
	// Browser may land here via GET (e.g. browser auto-follow of a 303 from a prior
	// POST, or a stale link). Treat as "No — stay signed in".
	const clientId = req.query?.client_id as string | undefined;
	const rawRedirect = req.query?.post_logout_redirect_uri as string | undefined;

	// oidc-provider may join multiple URIs with a comma — take the first one.
	const firstUri = rawRedirect?.split(',')[0]?.trim();

	// Validate against registered post_logout_redirect_uris for this client.
	const redirectTo = await resolveRedirectUri(firstUri, clientId);

	// eslint-disable-next-line no-console
	console.log('[OIDC] logout confirm GET (stay signed in) — redirecting to:', redirectTo);
	res.redirect(303, redirectTo);
});
router.post('/oauth/logout/confirm', async (req, res, next) => {
	// User clicked "No" — stay signed in. Intercept here (before callback destroys session)
	// so we can redirect back without logging out.
	// The form sends name="logout" value="no" for the cancel button.
	if (req.body?.logout === 'no') {
		// XSRF protection: verify the token is non-empty.
		// (sameSite=lax cookies already block cross-origin POSTs.)
		const submittedXsrf = String(req.body?.xsrf ?? '');
		if (!submittedXsrf) {
			res.status(403).send('Missing XSRF token');
			return;
		}

		// post_logout_redirect_uri: prefer query string (the original RP logout URL params)
		// then fall back to form body (hidden field). client_id: prefer query then body.
		const clientId = (req.query?.client_id ?? req.body?.client_id) as string | undefined;
		const rawRedirect = (req.query?.post_logout_redirect_uri ?? req.body?.post_logout_redirect_uri) as string | undefined;

		// oidc-provider may join multiple URIs with a comma — take the first one.
		const firstUri = rawRedirect?.split(',')[0]?.trim();

		// Validate against registered post_logout_redirect_uris for this client.
		const redirectTo = await resolveRedirectUri(firstUri, clientId);

		// eslint-disable-next-line no-console
		console.log('[OIDC] logout cancelled — redirecting to:', redirectTo);
		res.redirect(303, redirectTo);
		return; // critical: stop Express from running getOidcCallback() below
	}
	getOidcCallback()(req, res, next);
});

/**
 * Resolves the post-logout redirect URI using registered client config.
 * Priority:
 *  1. Use `firstUri` only if it matches a registered post_logout_redirect_uri for the client.
 *  2. Fall back to the client's first registered post_logout_redirect_uri.
 *  3. Fall back to FRONTEND_URL env var.
 *  4. Fall back to http://localhost:4001 (hardcoded safe default).
 */
async function resolveRedirectUri(
	firstUri: string | undefined,
	clientId: string | undefined,
): Promise<string> {
	// Hardcoded safe default — never redirect to an arbitrary domain.
	const safeDefault = 'http://localhost:4001';

	if (clientId) {
		try {
			const client = await Client.findOne({ where: { client_id: clientId } });
			if (client) {
				const allowed = Array.isArray(client.post_logout_redirect_uris)
					? client.post_logout_redirect_uris
					: [];

				// If a valid redirect was supplied, use it only if it's registered.
				if (firstUri && allowed.includes(firstUri)) {
					return firstUri;
				}

				// Otherwise use the first registered URI for this client.
			if (allowed.length > 0) {
				return allowed[0] ?? safeDefault;
			}
			}
		} catch {
			// Client lookup failed — fall through to safe default.
		}
	}

	// No client_id or client not found — use FRONTEND_URL or safe default.
	// FRONTEND_URL may contain comma-separated URIs (e.g. "http://localhost:3000,http://localhost:3001");
	// take the first one as the fallback.
	const envRaw = process.env.FRONTEND_URL ?? '';
	const envFirst = envRaw.split(',')[0]?.trim() || safeDefault;
	return envFirst || safeDefault;
}

// /auth/* — resume routes for OIDC interaction completion.
// oidc-provider uses 'resume' as the route name internally; even when
// routes.authorization is overridden to /oauth/authorize, the resume path
// remains /auth/:uid (the router's route path is independent of the config).
// These must be registered so the 303 redirect after login/consent works.
router.get('/auth/:uid', (req, _res, next) => { console.log('[OIDC] /auth/:uid hit, uid:', req.params.uid); getOidcCallback()(req, _res, next); });
router.post('/auth/:uid', (_req, _res, next) => getOidcCallback()(_req, _res, next));

export default router;
