import { randomBytes } from 'crypto';
import { Request, Response, NextFunction } from 'express';
import { GenericError } from '#interfaces/error/generic';
import Config from '#config';

/**
 * CSRF Protection Middleware
 *
 * Implements double-submit cookie pattern for CSRF protection on web cookie auth.
 *
 * SECURITY ARCHITECTURE:
 * This middleware provides multiple layers of CSRF defense:
 *
 * 1. SameSite=strict cookie setting (PRIMARY DEFENSE)
 *    - Prevents CSRF by blocking cross-origin cookie transmission
 *    - Most effective defense when using cookie-based auth
 *    - Works automatically for modern browsers
 *
 * 2. Origin validation (SECONDARY DEFENSE)
 *    - Validates Origin/Referer headers against allowed origins
 *    - Provides fallback for browsers with incomplete SameSite support
 *    - Configurable via CSRF_ORIGIN_VALIDATION env var
 *
 * 3. Double-submit cookie pattern (TERTIARY DEFENSE)
 *    - Requires CSRF token in both cookie and request (header/body)
 *    - Validates that tokens match before allowing state-changing requests
 *    - Configurable via CSRF_DOUBLE_SUBMIT env var
 *
 * SECURITY TRADE-OFFS & CONFIGURATION:
 *
 * CSRF_HTTPONLY Cookie Setting:
 * - Default: true (recommended for most applications)
 * - When true: Cookie is HttpOnly, not accessible to JavaScript
 *   - Pro: Prevents XSS from stealing CSRF token
 *   - Con: Requires server-side token injection (no AJAX support)
 * - When false: Cookie is accessible to JavaScript
 *   - Pro: Enables SPA/AJAX workflows with double-submit pattern
 *   - Con: Increases XSS attack surface - if XSS exists, attacker can steal token
 *   - Only use false when your application requires AJAX and you have strong XSS mitigation
 *
 * RECOMMENDED CONFIGURATIONS:
 *
 * For Traditional Server-Side Rendered Apps:
 * - CSRF_HTTPONLY=true (default)
 * - CSRF_ORIGIN_VALIDATION=true
 * - CSRF_DOUBLE_SUBMIT=false (not needed with HttpOnly)
 *
 * For Single-Page Applications (SPA):
 * - CSRF_HTTPONLY=false (to enable AJAX)
 * - CSRF_ORIGIN_VALIDATION=true (essential for defense)
 * - CSRF_DOUBLE_SUBMIT=true (provides additional defense)
 * - Ensure strong XSS mitigation: Content Security Policy, input validation, output encoding
 *
 * For Mobile Apps (using token auth):
 * - CSRF protection not needed (no cookies used)
 * - Use JWT in Authorization header instead
 */

export interface CsrfConfig {
	allowedOrigins: string[];
	enableOriginValidation: boolean;
	enableDoubleSubmit: boolean;
	httpOnly?: boolean; // SEC-01: Default to true for better security, set to false only when double-submit is needed
}

const defaultConfig: CsrfConfig = {
	allowedOrigins: Config.csrf.allowedOrigins,
	enableOriginValidation: Config.csrf.originValidation,
	enableDoubleSubmit: Config.csrf.doubleSubmit,
	httpOnly: Config.csrf.httpOnly,
};

/**
 * Generate a random CSRF token
 */
function generateCsrfToken(): string {
	return randomBytes(32).toString('hex');
}

/**
 * Validate Origin header against allowed origins
 * Uses exact domain matching to prevent subdomain bypass attacks
 */
function validateOrigin(origin: string | undefined, allowedOrigins: string[]): boolean {
	if (!origin) return false;
	try {
		const originUrl = new URL(origin);
		const originHostname = originUrl.hostname;
		return allowedOrigins.some((allowed) => {
			try {
				const allowedUrl = new URL(allowed.startsWith('http') ? allowed : `https://${allowed}`);
				return originHostname === allowedUrl.hostname;
			} catch {
				return false;
			}
		});
	} catch {
		return false;
	}
}

/**
 * CSRF middleware - generates token and validates on state-changing requests
 */
export function csrfProtection(config: Partial<CsrfConfig> = {}) {
	const finalConfig = { ...defaultConfig, ...config };

	return (req: Request, res: Response, next: NextFunction) => {
		// Skip CSRF for API requests in development if flag is set
		// eslint-disable-next-line no-restricted-syntax
		if (process.env.NODE_ENV === 'development' && req.path.startsWith('/api/')) {
			return next();
		}

		// Skip CSRF for Swagger and API docs
		if (req.path.startsWith('/swagger') || req.path.startsWith('/api-docs')) {
			return next();
		}

		// Skip CSRF for GET, HEAD, OPTIONS requests (safe methods)
		if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
			// Generate and set CSRF token for safe methods
			const csrfToken = generateCsrfToken();
			// SEC-01: Use configured httpOnly setting (default true for security)
			const httpOnly = finalConfig.httpOnly === undefined ? true : finalConfig.httpOnly;
			// eslint-disable-next-line no-restricted-syntax
			res.cookie('csrf_token', csrfToken, {
				httpOnly,
				secure: process.env.NODE_ENV === 'production',
				sameSite: 'strict',
				path: '/',
				maxAge: 3600 * 1000, // 1 hour
			});
			res.locals.csrfToken = csrfToken; // Make available to templates
			return next();
		}

		// For state-changing methods (POST, PUT, DELETE, PATCH)
		if (finalConfig.enableOriginValidation) {
			const origin = req.header('Origin') || req.header('Referer');
			if (!validateOrigin(origin, finalConfig.allowedOrigins)) {
				throw new GenericError('FORBIDDEN');
			}
		}

		if (finalConfig.enableDoubleSubmit) {
			const cookieToken = req.cookies.csrf_token;
			const headerToken = req.header('X-CSRF-Token') || (req.body as any)?.csrf_token;

			if (!cookieToken || !headerToken || cookieToken !== headerToken) {
				throw new GenericError('FORBIDDEN');
			}
		}

		// Generate new CSRF token for next request
		const newCsrfToken = generateCsrfToken();
		const httpOnly = finalConfig.httpOnly === undefined ? true : finalConfig.httpOnly;
		// eslint-disable-next-line no-restricted-syntax
		res.cookie('csrf_token', newCsrfToken, {
			httpOnly,
			secure: process.env.NODE_ENV === 'production',
			sameSite: 'strict',
			path: '/',
			maxAge: 3600 * 1000,
		});
		res.locals.csrfToken = newCsrfToken;

		next();
	};
}

/**
 * Simple origin validation middleware (for use without double-submit)
 */
export function validateOriginMiddleware(allowedOrigins: string[] = defaultConfig.allowedOrigins) {
	return (req: Request, res: Response, next: NextFunction) => {
		// Skip validation for GET, HEAD, OPTIONS
		if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
			return next();
		}

		const origin = req.header('Origin') || req.header('Referer');
		if (!validateOrigin(origin, allowedOrigins)) {
			throw new GenericError('FORBIDDEN');
		}

		next();
	};
}
