/**
 * CSRF Middleware
 * Protects against Cross-Site Request Forgery attacks
 * Uses CsrfTokenManager for token generation and validation
 */
import { Logger } from '../config/logger.js';
import { respondWithMiddlewareFailure, } from './MiddlewareFailureResponder.js';
import { CsrfTokenManager } from '../security/CsrfTokenManager.js';
import { SessionManager } from '../session/SessionManager.js';
const DEFAULT_OPTIONS = {
    cookieName: 'XSRF-TOKEN',
    headerName: 'X-CSRF-Token',
    bodyKey: '_csrf',
    ignoreMethods: ['GET', 'HEAD', 'OPTIONS'],
};
// Global cleanup registry to avoid leaking intervals per middleware instance
// We use WeakRef so the manager (and middleware) can be garbage collected
// when no longer in use, even if this interval keeps running.
const canUseWeakRef = typeof WeakRef === 'function';
const managerRegistry = new Set();
let globalCleanupTimer = null;
let sharedSessionManager = null;
const getSharedSessionManager = () => {
    if (sharedSessionManager !== null)
        return sharedSessionManager;
    sharedSessionManager = SessionManager.create();
    return sharedSessionManager;
};
const ensureCleanupTimer = () => {
    if (globalCleanupTimer !== null)
        return;
    if (typeof setInterval !== 'function')
        return;
    if (globalThis.CF !== undefined)
        return;
    if (!canUseWeakRef)
        return;
    globalCleanupTimer = setInterval(() => {
        if (managerRegistry.size === 0)
            return;
        for (const ref of managerRegistry) {
            const mgr = ref.deref();
            if (mgr) {
                void mgr.cleanup().catch(() => undefined);
            }
            else {
                managerRegistry.delete(ref);
            }
        }
    }, 3600000);
    // Use helper to handle runtime differences (Node vs others)
    if (globalCleanupTimer !== null && isUnrefableTimer(globalCleanupTimer)) {
        globalCleanupTimer.unref();
    }
};
export const CsrfMiddleware = Object.freeze({
    /**
     * Create CSRF protection middleware
     */
    create(options = {}) {
        const config = { ...DEFAULT_OPTIONS, ...options };
        const manager = CsrfTokenManager.create();
        const sessions = getSharedSessionManager();
        ensureCleanupTimer();
        // Register for global cleanup instead of creating a local timer
        if (canUseWeakRef) {
            managerRegistry.add(new WeakRef(manager));
        }
        return async (req, res, next) => {
            if (shouldSkipCsrfForRequest(req, config)) {
                await next();
                return;
            }
            const cookieHeader = req.getHeader('cookie');
            const cookies = parseCookies(typeof cookieHeader === 'string' ? cookieHeader : '');
            // Guarantee a session id exists and a session cookie is set if missing.
            // This allows CSRF tokens to be bound to a stable session identifier.
            const sessionId = await sessions.ensureSessionId(req, res);
            const method = req.getMethod();
            // 1. Token Generation (for safe methods)
            if (config.ignoreMethods?.includes(method) ?? false) {
                const token = await manager.generateToken(sessionId);
                // Set cookie for Double Submit Cookie pattern (readable by frontend)
                appendSetCookie(res, `${config.cookieName}=${token}; Path=/; SameSite=Strict`);
                // Also expose in locals for server-side rendering
                res.locals['csrfToken'] = token;
                await next();
                return;
            }
            // 2. Token Validation (for unsafe methods)
            const tokenFromHeader = req.getHeader(config.headerName ?? 'X-CSRF-Token');
            const tokenFromBody = req.getBody()?.[config.bodyKey ?? '_csrf'];
            const tokenFromCookie = cookies[config.cookieName ?? 'XSRF-TOKEN'];
            const token = tokenFromHeader || tokenFromBody || tokenFromCookie;
            if (!token || !(await manager.validateToken(sessionId, token))) {
                Logger.warn(`CSRF validation failed for session ${sessionId}`);
                await respondWithMiddlewareFailure(req, res, config.onFailure, {
                    middleware: 'csrf',
                    reason: 'invalid_csrf_token',
                    statusCode: 403,
                    message: 'Invalid CSRF token',
                    body: {
                        error: 'Forbidden',
                        message: 'Invalid CSRF token',
                    },
                });
                return;
            }
            await next();
        };
    },
});
function shouldSkipCsrfForRequest(req, config) {
    const patterns = config.skipPaths;
    if (patterns === undefined || patterns.length === 0)
        return false;
    const path = req.getPath();
    for (const pattern of patterns) {
        const trimmed = pattern.trim();
        if (trimmed === '')
            continue;
        if (pathMatchesPattern(path, trimmed))
            return true;
    }
    return false;
}
function pathMatchesPattern(path, pattern) {
    if (pattern === '*')
        return true;
    if (pattern === path)
        return true;
    // Fast path: treat trailing "/*" as a prefix match.
    if (pattern.endsWith('/*')) {
        const prefix = pattern.slice(0, -1); // keep the trailing '/'
        return path.startsWith(prefix);
    }
    // Generic glob-to-regex conversion where '*' matches any characters.
    const escaped = pattern.replaceAll(/[.+?^${}()|[\]\\]/g, String.raw `\$&`);
    const regex = new RegExp(`^${escaped.replaceAll('*', '.*')}$`);
    return regex.test(path);
}
function appendSetCookie(res, cookie) {
    const existing = res.getHeader('Set-Cookie');
    if (existing === undefined) {
        res.setHeader('Set-Cookie', cookie);
        return;
    }
    if (Array.isArray(existing)) {
        res.setHeader('Set-Cookie', [...existing, cookie]);
        return;
    }
    res.setHeader('Set-Cookie', [existing, cookie]);
}
/**
 * Helper to parse cookies
 */
function parseCookies(cookieHeader) {
    const list = {};
    if (!cookieHeader)
        return list;
    cookieHeader.split(';').forEach((cookie) => {
        const parts = cookie.split('=');
        const name = parts.shift()?.trim();
        const value = parts.join('=');
        if (name !== null && name !== undefined)
            list[name] = decodeURIComponent(value);
    });
    return list;
}
function isUnrefableTimer(value) {
    if (typeof value !== 'object' || value === null)
        return false;
    return 'unref' in value && typeof value.unref === 'function';
}
