import express from 'express';
import { readFileSync } from 'fs';
import { resolve } from 'path';
import Handlebars from 'handlebars';
import { OidcConfigService } from '../config/oidcConfigService';
import Logger from '../utils/logger';
import { OidcAdapterService } from './oidcAdapterService';
import { ClientProvider } from '#providers/ClientProvider';
import { User } from '#models/User';
import { rotateSigningKey, retireOldKeys, getActiveKey } from '../jobs/secretRotation';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnyOidcProvider = any;

export class OidcService {
	private static instance: AnyOidcProvider | undefined;
	private static readonly configService = new OidcConfigService();

	private constructor() {}

	static async initialize(): Promise<AnyOidcProvider> {
		if (this.instance) return this.instance;

		const oidcModule = await import('oidc-provider');
		const OidcProvider = oidcModule.default ?? oidcModule;

		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		const sequelize = ((await import('#services/database/sequelize/sequelizeService.js')).default as any) as import('sequelize').Sequelize;
		const adapterService = new OidcAdapterService(sequelize);

		const clients = await this.loadClients();

		// loadExistingGrant is called by oidc-provider AFTER the provider is fully initialized,
		// so we look it up via the singleton rather than a closure over a let variable.
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		const loadExistingGrant = async (ctx: any) => {
			// eslint-disable-next-line no-console
			console.log('[loadExistingGrant] called for account:', ctx.oidc.account?.accountId, 'client:', ctx.oidc.client?.clientId);

			// First: try to load an existing grant from a prior consent interaction.
			const grantId = ctx.oidc.result?.consent?.grantId
				|| ctx.oidc.session?.grantIdFor(ctx.oidc.client.clientId);
			if (grantId) {
				const provider = OidcService.getInstance() as any;
				const found = await provider.Grant.find(grantId);
				// eslint-disable-next-line no-console
				if (found) { console.log('[loadExistingGrant] found existing grant:', grantId); return found; }
			}

			// Second: for first-party clients, pre-build a Grant with all requested scopes accepted.
			// The grantId is stored in the session, so subsequent requests find it via grantIdFor().
			// eslint-disable-next-line no-console
			console.log('[loadExistingGrant] no existing grant — building new for client:', ctx.oidc.client?.clientId);
			const { User } = await import('#models/User.js');
			const user = await User.findByPk(ctx.oidc.account.accountId);
			if (!user) return undefined;

			const requestedScopes = String(ctx.oidc.params?.scope ?? 'openid');
			const hasOpenId = ctx.oidc.client?.scopes?.has('openid') ?? false;
			const requestedClaims: string[] = hasOpenId
				? (ctx.oidc.client?.claims?.filter((c: string) => c !== 'sub') ?? [])
				: [];

			const provider = OidcService.getInstance() as any;
			const grant = new (provider.Grant as any)({
				accountId: ctx.oidc.account.accountId,
				clientId: ctx.oidc.client.clientId,
			});
			grant.addOIDCScope(requestedScopes);

			const standardClaims = ['name', 'email', 'email_verified', 'preferred_username', 'picture', 'profile'];
			const knownClaims = requestedClaims.filter((c: string) => standardClaims.includes(c));
			if (knownClaims.length > 0) {
				grant.addOIDCClaims(knownClaims);
			}

			await grant.save();
			// eslint-disable-next-line no-console
			console.log('[loadExistingGrant] saved new grant with jti:', grant.jti);
			// Store grantId in session so the next authorization request finds it immediately.
			ctx.oidc.session?.grantIdFor(ctx.oidc.client.clientId, grant.jti);

			return grant;
		};

		// ExternalSigningKey bypasses private-field validation so we can store full JWKs on disk.
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		const { ExternalSigningKey } = (await import('oidc-provider/lib/helpers/keystore.js')) as any;

		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		class RsaExternalSigningKey extends (ExternalSigningKey as any) {
			constructor(private jwk: unknown) {
				super();
				// eslint-disable-next-line @typescript-eslint/no-explicit-any
				const j = jwk as any;
				this.kid = j.kid;
				this.alg = j.alg ?? 'RS256';
			}

			// eslint-disable-next-line @typescript-eslint/no-explicit-any
			keyObject(): any {
				// eslint-disable-next-line @typescript-eslint/no-explicit-any
				return (require('crypto') as any).createPrivateKey({ key: this.jwk, format: 'jwk' });
			}

			async sign(data: ArrayBuffer): Promise<Buffer> {
				// eslint-disable-next-line @typescript-eslint/no-explicit-any
				return (require('crypto') as any).sign(null, Buffer.from(data), this.keyObject());
			}
		}

		await retireOldKeys();
		const activeKey = getActiveKey();
		let signingKeys: unknown[];
		if (!activeKey) {
			await rotateSigningKey();
			const fresh = getActiveKey();
			if (!fresh) throw new Error('[OIDC] Failed to generate signing key');
			signingKeys = [new RsaExternalSigningKey(fresh)];
			console.log(`[OIDC] Generated first signing key: kid=${fresh.kid}`);
		} else {
			signingKeys = [new RsaExternalSigningKey(activeKey)];
			console.log(`[OIDC] Loaded signing key from disk: kid=${activeKey.kid}`);
		}

		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		const provider = new (OidcProvider as any)(this.configService.issuer, {
			clients,
			adapter: (name: string) => adapterService.createAdapter(name),
			jwks: { keys: signingKeys },
			cookies: {
				keys: this.configService.cookieKeys,
				long: { httpOnly: true, sameSite: 'lax' },
				short: { httpOnly: true, sameSite: 'lax' },
			},
			claims: {
				openid: ['sub'],
				profile: ['name', 'preferred_username'],
				email: ['email', 'email_verified'],
			},
			ttl: {
				AccessToken: this.configService.accessTokenTtl,
				RefreshToken: this.configService.refreshTokenTtl,
			},
			features: {
				devInteractions: { enabled: false },
				introspection: { enabled: true },
				revocation: { enabled: true },
				rpInitiatedLogout: { enabled: true },
				backchannelLogout: { enabled: true },
				externalSigningSupport: { enabled: true, ack: 'experimental-01' },
			},
			routes: {
				authorization: '/oauth/authorize',
				token: '/oauth/token',
				userinfo: '/oauth/userinfo',
				jwks: '/oauth/jwks',
				introspection: '/oauth/introspect',
				revocation: '/oauth/revoke',
				end_session: '/oauth/logout',
			},
			interactions: {
				url: (_ctx: unknown, interaction: { uid: string }) => `/oidc/interaction/${interaction.uid}`,
			},
			loadExistingGrant,
			findAccount: async (_ctx: unknown, sub: string) => {
				// eslint-disable-next-line no-console
				console.log('[OIDC findAccount] called with sub:', sub);

				const { User } = await import('#models/User.js');
				const user = await User.findByPk(sub);

				return {
					accountId: sub,
					claims: () => {
						// eslint-disable-next-line no-console
						console.log('[OIDC findAccount.claims] called for sub:', sub, '| user:', user?.email);
						const fullName = [user?.first_name, user?.last_name].filter(Boolean).join(' ') || undefined;
						return {
							sub,
							...(user?.email ? { email: user.email } : {}),
							...(user?.email_verified_at != null ? { email_verified: true } : { email_verified: false }),
							...(fullName ? { name: fullName } : {}),
							...(user?.username ? { preferred_username: user.username } : {}),
						};
					},
				};
			},
		});

		this.instance = provider;
		// eslint-disable-next-line no-console
		console.log(`[OIDC] Provider initialized with ${clients.length} client(s): ${(clients as { client_id: string }[]).map((c) => c.client_id).join(', ') || '(none)'}`);

		// ── Custom logout views using Handlebars ─────────────────────────────
		// Full form HTML with XSRF token and both action buttons embedded.
		const logoutFormHtml = (action: string, xsrf: string, postLogoutRedirectUri?: string, clientId?: string) =>
			`<form id="op.logoutForm" method="post" action="${action}">` +
			`<input type="hidden" name="xsrf" value="${xsrf}"/>` +
			`<input type="hidden" name="client_id" value="${clientId ?? ''}"/>` +
			(postLogoutRedirectUri ? `<input type="hidden" name="post_logout_redirect_uri" value="${postLogoutRedirectUri}"/>` : '') +
			`<button type="submit" name="logout" value="yes" class="btn-signout">Yes, sign me out</button>` +
			`<button type="submit" name="logout" value="no" class="btn-stay">No, stay signed in</button>` +
			`</form>`;

		// Load weak_cache inside the function to avoid top-level import issues.
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
		// @ts-ignore -- internal module, no types available
		const weakCache = await import('oidc-provider/lib/helpers/weak_cache.js');
		const oidcInstanceFn = weakCache.get ?? weakCache.default;
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		const internal = oidcInstanceFn(provider) as any;
		if (!internal?.configuration?.features?.rpInitiatedLogout) {
			// eslint-disable-next-line no-console
			console.error('[OIDC] Failed to access rpInitiatedLogout config:', Object.keys(internal ?? {}));
			throw new Error('[OIDC] Cannot configure logout views — internal config not accessible');
		}
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		const rpLogout = internal.configuration.features.rpInitiatedLogout as any;

		rpLogout.logoutSource = async (ctx: any, _form: unknown) => {
			try {
				const action = ctx.oidc.urlFor('end_session_confirm');
				const xsrf = ctx.oidc.session?.state?.secret ?? '';
				const clientName = ctx.oidc.client?.clientName ?? ctx.oidc.client?.clientId ?? 'SSO';
				const clientId = ctx.oidc.client?.clientId ?? '';
				const postLogoutRedirectUri = ctx.oidc.session?.state?.postLogoutRedirectUri ?? '';
				const formHtml = logoutFormHtml(action, xsrf, postLogoutRedirectUri, clientId);
				const logoutViewPath = resolve(__dirname, './views/logout.hbs');
				const tpl = readFileSync(logoutViewPath, 'utf8');
				ctx.body = Handlebars.compile(tpl)({ formHtml, client: clientName });
				ctx.type = 'html';
			} catch (err) {
				// eslint-disable-next-line no-console
				console.error('[OIDC logoutSource] failed to render logout form:', err);
				ctx.status = 500;
				ctx.body = 'Unable to render logout page';
			}
		};

		rpLogout.postLogoutSuccessSource = async (ctx: any) => {
			try {
				// After logout is confirmed, redirect to post_logout_redirect_uri if available.
				// Use params (query string from original /logout request) as primary source
				// because session may have been destroyed by the time this handler runs.
				// Fall back to session state if params are not available.
				const postLogoutRedirectUri =
					(ctx.oidc.params as any)?.post_logout_redirect_uri
					|| ctx.oidc.session?.state?.postLogoutRedirectUri
					|| '';

				if (postLogoutRedirectUri) {
					// Validate the URI against registered URIs for the client to prevent open-redirect.
					const clientId = ctx.oidc.client?.clientId;
					const allowed = Array.isArray(ctx.oidc.client?.postLogoutRedirectUris)
						? ctx.oidc.client.postLogoutRedirectUris
						: [];
					if (allowed.includes(postLogoutRedirectUri)) {
						ctx.redirect(postLogoutRedirectUri);
						return;
					}
					// If URI not registered for this client, fall through to success page.
				}

				const logoutSuccessViewPath = resolve(__dirname, './views/logout-success.hbs');
				const tpl = readFileSync(logoutSuccessViewPath, 'utf8');
				ctx.body = Handlebars.compile(tpl)({});
				ctx.type = 'html';
			} catch (err) {
				// eslint-disable-next-line no-console
				console.error('[OIDC postLogoutSuccessSource] failed to render:', err);
				ctx.status = 500;
				ctx.body = 'Unable to render logout page';
			}
		};

		// ── Custom error rendering ────────────────────────────────────────────
		// Replaces the oidc-provider default error page (which is unstyled HTML)
		// with a branded page that matches the SSO login/consent UI.
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		(internal as any).renderer = async (ctx: any, out: { error?: string; error_description?: string; status?: number }) => {
			try {
				const errorViewPath = resolve(__dirname, './views/error.hbs');
				const tpl = readFileSync(errorViewPath, 'utf8');
				ctx.body = Handlebars.compile(tpl)({
					error: out.error ?? 'server_error',
					error_description: out.error_description ?? 'An unexpected error occurred. Please try again.',
				});
				ctx.type = 'html';
				if (out.status) ctx.status = out.status;
			} catch (renderErr) {
				// Fallback to plain text if template fails
				console.error('[OIDC renderError] failed to render error template:', renderErr);
				ctx.status = out.status ?? 500;
				ctx.body = `${out.error ?? 'server_error'}: ${out.error_description ?? 'An unexpected error occurred.'}`;
			}
		};

		return provider;
	}

	private static async loadClients(): Promise<unknown[]> {
		const rows = await ClientProvider.getInstance().listActive();
		return rows.map((r) => ClientProvider.getInstance().toOidcProviderConfig(r));
	}

	public static getInstance(): AnyOidcProvider {
		if (!this.instance) {
			throw new Error('[OIDC] Provider not initialized — call OidcService.initialize() first');
		}
		return this.instance;
	}

	static getOidcInstance(): AnyOidcProvider {
		return this.getInstance();
	}

	// eslint-disable-next-line @typescript-eslint/no-explicit-any
	private static _callbackMiddleware: any | undefined;

	static getCallbackMiddleware() {
		if (!this._callbackMiddleware) {
			// eslint-disable-next-line @typescript-eslint/no-explicit-any
			const provider = this.getInstance() as any;
			this._callbackMiddleware = async (req: express.Request, res: express.Response, next: express.NextFunction) => {
				// --- Isolated response state — completely decoupled from Express res ---
				// oidc-provider writes everything to this object (not the real Express res).
				// We replay it to the browser only AFTER the provider chain finishes.
				// eslint-disable-next-line @typescript-eslint/no-explicit-any
				const responseState: any = {
					statusCode: 200,
					statusMessage: 'OK',
					headers: {} as Record<string, string | string[]>,
					body: '',
					ended: false,
				};

				// eslint-disable-next-line @typescript-eslint/no-explicit-any
				const mockIncomingReq: any = {
					method: req.method,
					url: req.originalUrl,
					originalUrl: req.originalUrl,
					headers: { ...req.headers },
					body: req.body,  // Needed for token endpoint (client_secret_post auth)
					socket: { encrypted: req.protocol === 'https' },
					secure: req.protocol === 'https',
				};

				// Mock ServerResponse — all methods write to responseState, NEVER to
				// the real Express res. This prevents Express from auto-ending the
				// response when the middleware chain finishes.
				// eslint-disable-next-line @typescript-eslint/no-explicit-any
				const mockServerRes: any = {
					statusCode: 200,
					statusMessage: 'OK',
					getHeader(name: string) { return responseState.headers[name.toLowerCase()]; },
					setHeader(name: string, val: string | string[]) { responseState.headers[name.toLowerCase()] = val; },
					removeHeader(name: string) { delete responseState.headers[name.toLowerCase()]; },
					getHeaders: () => ({ ...responseState.headers }),
					hasHeader(name: string) { return name.toLowerCase() in responseState.headers; },
					end(...args: unknown[]) {
						if (args.length > 0 && typeof args[0] !== 'function') {
							responseState.body += String(args[0]);
						}
						responseState.ended = true;
					},
					on: () => mockServerRes,
					once: () => mockServerRes,
					writeHead(status: number, headers?: Record<string, string | string[]>) {
						responseState.statusCode = status;
						if (headers) {
							for (const [k, v] of Object.entries(headers)) {
								responseState.headers[k.toLowerCase()] = v;
							}
						}
					},
				};

				// Intercept res.redirect() on the REAL Express res BEFORE provider.callback()
				// so we can capture and replay it after the provider chain finishes.
				let pendingRedirect: { status: number; location: string } | undefined;
				const originalRedirect = res.redirect.bind(res);
				// eslint-disable-next-line @typescript-eslint/no-explicit-any
				(res as any).redirect = function (statusOrUrl: number | string, location?: string) {
					if (typeof statusOrUrl === 'string') {
						pendingRedirect = { status: 302, location: statusOrUrl };
					} else {
						pendingRedirect = { status: statusOrUrl ?? 302, location: location ?? '' };
					}
				};

				// Build the Koa context using the isolated mock response
				// eslint-disable-next-line @typescript-eslint/no-explicit-any
				const ctx = (OidcService as any).buildKoaContext(req, res, mockServerRes, mockIncomingReq) as any;

				try {
					let cbErr: unknown;
					try {
						await provider.callback()(ctx.req, ctx.res);
					} catch (err) {
						cbErr = err;
						// eslint-disable-next-line no-console
						console.error('[OIDC callback] provider.callback() threw:', err);
					}

					// eslint-disable-next-line no-console
					console.log('[OIDC callback] finished — status:', responseState.statusCode, 'headers:', JSON.stringify(responseState.headers), 'body len:', responseState.body.length, 'redirect:', pendingRedirect);

					// Re-throw if the callback itself threw (don't continue processing an errored state)
					if (cbErr) throw cbErr;

					// oidc-provider uses ctx.redirect() which sets Location header + 303 body
					// WITHOUT calling res.redirect(), so we intercept the location header here
					// and replay it as a proper 303 Express redirect.
					const location = responseState.headers['location'] as string | undefined;
					if (location && !pendingRedirect) {
						// eslint-disable-next-line no-console
						console.log('[OIDC callback] detected location header redirect:', location, 'status:', responseState.statusCode);
						// Forward set-cookie headers FIRST so browser stores them before following the redirect.
						const setCookies = responseState.headers['set-cookie'] as string[] | string | undefined;
						if (setCookies) {
							const cookies = Array.isArray(setCookies) ? setCookies : [setCookies];
							for (const cookie of cookies) {
								res.append('Set-Cookie', cookie);
							}
						}
						// Set the redirect status and Location header directly, then send the body.
						// Do NOT call res.redirect() — it internally writes a body AND tries to set
						// Content-Length after headers are already committed, causing silent failures.
						res.status(303);
						res.setHeader('Location', location);
						const body = `Redirecting to ${location}.`;
						res.setHeader('Content-Type', 'text/html; charset=utf-8');
						res.setHeader('Content-Length', String(Buffer.byteLength(body)));
						res.send(body);
						return;
					}

					// Propagate the Koa mock response state to the real Express res
					if (responseState.statusCode !== 200) {
						res.status(responseState.statusCode);
					}
				for (const entry of Object.entries(responseState.headers)) {
					const value = entry[1];
					if (value !== undefined) {
						res.setHeader(entry[0], value as string | readonly string[]);
					}
				}

					// Replay captured redirect through Express as a proper 302/303
					if (pendingRedirect) {
						// eslint-disable-next-line no-console
						console.log('[OIDC callback] replaying redirect:', pendingRedirect.status, pendingRedirect.location);
						originalRedirect(pendingRedirect.status, pendingRedirect.location);
						return;
					}

					// Send the body — Express delivers the full response in one shot
					if (responseState.body) {
						// eslint-disable-next-line no-console
						console.log('[OIDC callback] sending body, first 200 chars:', responseState.body.slice(0, 200));
						res.send(responseState.body);
					}
				} finally {
					// Always restore original redirect on the real Express res
					// eslint-disable-next-line @typescript-eslint/no-explicit-any
					(res as any).redirect = originalRedirect;
				}
			};
		}
		return this._callbackMiddleware;
	}

	static async interactionDetails(req: unknown, res: unknown) {
		return this.getInstance().interactionDetails(req, res);
	}

	static async interaction_result(req: unknown, res: unknown, result: unknown, options?: unknown): Promise<string> {
		return this.getInstance().interactionResult(req, res, result, options);
	}

	static async interaction_finished(req: unknown, res: unknown, result: unknown, options?: unknown): Promise<void> {
		// eslint-disable-next-line no-console
		console.log('[OIDC interaction_finished] result:', JSON.stringify(result), 'options:', JSON.stringify(options));

		const expressReq = req as express.Request;
		const expressRes = res as express.Response;

		const uid = (expressReq.params as { uid?: string })?.uid ?? '';
		if (!uid) {
			// eslint-disable-next-line no-console
			console.error('[OIDC interaction_finished] ERROR: no uid found in req.params');
			expressRes.status(500).send('Internal Server Error: missing interaction uid');
			return;
		}

		let resumeUrl: string;
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		let ctx: any;
		try {
			// eslint-disable-next-line @typescript-eslint/no-explicit-any
			const instance = this.getInstance() as any;
			console.log('[OIDC interaction_finished] building Koa context for uid:', uid);
			ctx = this.buildKoaContext(expressReq, expressRes);
			console.log('[OIDC interaction_finished] calling interactionResult on instance');
			resumeUrl = await instance.interactionResult(ctx, ctx, result, options);
			// eslint-disable-next-line no-console
			console.log('[OIDC interaction_finished] interactionResult returned:', resumeUrl);

			// Forward any Set-Cookie headers that interactionResult wrote to ctx.response
			const setCookie = ctx.response.get('set-cookie');
			if (setCookie && Array.isArray(setCookie) && setCookie.length > 0) {
				for (const cookie of setCookie) {
					if (cookie) {
						expressRes.append('Set-Cookie', cookie);
					}
				}
			}
		} catch (err) {
			// eslint-disable-next-line no-console
			console.error('[OIDC interaction_finished] interactionResult threw:', err);
			expressRes.status(500).send(`Internal Server Error: failed to save interaction result — ${(err as Error).message}`);
			return;
		}

		// Ensure absolute URL for redirect
		const redirectUrl = resumeUrl.startsWith('http') ? resumeUrl : `${this.configService.issuer}${resumeUrl}`;
		// eslint-disable-next-line no-console
		console.log('[OIDC interaction_finished] redirecting to:', redirectUrl);
		expressRes.redirect(303, redirectUrl);
	}

	// eslint-disable-next-line @typescript-eslint/no-explicit-any
	static buildKoaContext(req: express.Request, res: express.Response, mockServerRes?: any, mockIncomingReq?: any): any {
		// Lazily-collected set-cookie values written by ctx.cookies.set()
		const setCookieCapture: string[] = [];

		// Provide a minimal Express-like headers map so ctx.cookies.get() works
		const cookieHeader: Record<string, string> = {};
		const rawReq = req as unknown as Record<string, unknown>;
		for (const [name, value] of Object.entries(rawReq.cookies ?? {})) {
			if (typeof value === 'string') cookieHeader[name] = value;
		}

		// --- Raw Node.js IncomingMessage mock ---
		// Required so ctx.request.socket / .protocol / .secure work
		if (!mockIncomingReq) {
			// eslint-disable-next-line @typescript-eslint/no-explicit-any
			mockIncomingReq = {
				method: req.method,
				url: req.originalUrl,
				originalUrl: req.originalUrl,
				headers: { ...req.headers },
				socket: { encrypted: req.protocol === 'https' },
				secure: req.protocol === 'https',
			};
		}

		// --- Raw Node.js ServerResponse mock ---
		// Required so ctx.response.status / .getHeader / .setHeader work
		if (!mockServerRes) {
			// eslint-disable-next-line @typescript-eslint/no-explicit-any
			mockServerRes = {
				statusCode: res.statusCode,
				statusMessage: res.statusMessage,
				getHeader(name: string) { return res.getHeader(name); },
				setHeader(name: string, val: string | string[]) { res.setHeader(name, val); },
				removeHeader(name: string) { res.removeHeader(name); },
				getHeaders: () => ({}),
				end: (...args: unknown[]) => (res.end as unknown as (...a: unknown[]) => void)(...args),
				on: () => mockServerRes,
				once: () => mockServerRes,
				writeHead: (...args: unknown[]) => (res.writeHead as unknown as (...a: unknown[]) => void)(...args),
			};
		}

		// --- Use Koa's internal plain-object prototypes ---
		// Koa 3 exports request/response/context as plain objects (no class constructors).
		// require() returns the module.exports object directly.
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		const KoaRequestProto: any = require('koa/lib/request');
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		const KoaResponseProto: any = require('koa/lib/response');
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		const KoaContextProto: any = require('koa/lib/context');

		// Instantiate Koa Request — its prototype chain handles:
		// ctx.request.protocol  → this.req.socket.encrypted
		// ctx.request.secure    → this.protocol === 'https'
		// ctx.request.host      → parseurl(this)
		// ctx.request.get()     → this.req.headers
		// Note: Object.create(plainObject) creates object with plainObject as prototype.
		// plainObject has no .prototype property (it's a plain object itself), so
		// we must use Object.create(plainObject), NOT Object.create(plainObject.prototype).
		const mockKoaReq = Object.create(KoaRequestProto);
		mockKoaReq.req = mockIncomingReq;
		mockKoaReq.url = req.originalUrl;
		mockKoaReq.originalUrl = req.originalUrl;
		mockKoaReq.query = req.query;
		mockKoaReq.body = req.body;  // Required for token endpoint client_secret_post auth

		// Instantiate Koa Response
		const mockKoaRes = Object.create(KoaResponseProto);
		mockKoaRes.res = mockServerRes;
		mockKoaRes._body = undefined;

		// Instantiate the Koa context from its prototype
		const ctx = Object.create(KoaContextProto);

		// Wire ctx ↔ request ↔ response (circular refs Koa expects)
		ctx.request = mockKoaReq;
		ctx.response = mockKoaRes;
		ctx.req = mockIncomingReq;    // alias
		ctx.res = mockServerRes;      // alias
		mockKoaReq.ctx = mockKoaRes.ctx = ctx;
		mockKoaReq.app = mockKoaRes.app = { keys: [], proxy: false };

		// Custom cookies object — oidc-provider calls ctx.cookies.get(name) and .set(name, val)
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		const cookiesObj: any = {
			get(name: string) {
				return cookieHeader[name] ?? '';
			},
			set(name: string, val: string) {
				res.cookie(name, val); // Forward to Express so browser stores it
				setCookieCapture.push(`${name}=${val}`); // Capture for set-cookie forwarding
			},
		};
		ctx.cookies = cookiesObj;
		mockKoaReq.cookies = cookiesObj;

		// oidc-provider reads ctx.response.get('set-cookie') after writing interaction result cookies.
		// Wrap KoaResponse's built-in get() method to inject the captured values.
		const parentGet = mockKoaRes.get.bind(mockKoaRes);
		Object.defineProperty(mockKoaRes, 'get', {
			value(name: string) {
				if (name === 'set-cookie') {
					return setCookieCapture.length > 0 ? setCookieCapture : undefined;
				}
				return parentGet(name);
			},
		});

		// OIDCContext is lazily accessed via ctx.oidc — set a plain object placeholder
		// so it can be populated by oidc-provider internals
		ctx.oidc = {};

		// Override ctx.redirect so that relative URL redirects (e.g. /oidc/interaction/...)
		// always use status 303 instead of Koa's default 302. 303 is the correct OIDC
		// behavior: after a login/consent interaction the browser must do a GET to the
		// redirect target, not re-POST the original request body.
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		(ctx as any).redirect = (url: string, alt?: string) => {
			// eslint-disable-next-line @typescript-eslint/no-explicit-any
			const status = url.startsWith('http') || url.startsWith('//')
				? 302
				: 303;
			// eslint-disable-next-line @typescript-eslint/no-explicit-any
			(ctx as any).status = status;
			// eslint-disable-next-line @typescript-eslint/no-explicit-any
			(ctx as any).set('Location', url);
		};

		return ctx;
	}

	static async close(): Promise<void> {
		this.instance = undefined;
	}
}
