/**
 * AuthMiddleware - Authentication and authorization middleware
 *
 * Provides middleware factories for:
 * - Token extraction and validation
 * - User context propagation
 * - RBAC enforcement
 * - Public route handling
 */
import { createErrorFactory } from "../../core/infrastructure/baseError.js";
import { withTimeout } from "../../utils/async/withTimeout.js";
import { logger } from "../../utils/logger.js";
import { AuthProviderFactory } from "../AuthProviderFactory.js";
// =============================================================================
// ERROR FACTORY
// =============================================================================
/**
 * Auth middleware error codes
 */
export const AuthMiddlewareErrorCodes = {
    MISSING_TOKEN: "AUTH_MIDDLEWARE-001",
    INVALID_TOKEN: "AUTH_MIDDLEWARE-002",
    UNAUTHORIZED: "AUTH_MIDDLEWARE-003",
    FORBIDDEN: "AUTH_MIDDLEWARE-004",
    PROVIDER_ERROR: "AUTH_MIDDLEWARE-005",
    CONFIGURATION_ERROR: "AUTH_MIDDLEWARE-006",
};
/**
 * Auth middleware error factory
 */
export const AuthMiddlewareError = createErrorFactory("AuthMiddleware", AuthMiddlewareErrorCodes);
// =============================================================================
// HELPERS
// =============================================================================
/**
 * Create an AuthErrorInfo object for the onError callback.
 *
 * Avoids `as any` by constructing a proper Error with the required `code` field.
 */
function createAuthErrorInfo(message, code) {
    const err = new Error(message);
    err.code = code;
    return err;
}
// =============================================================================
// TOKEN EXTRACTION
// =============================================================================
/**
 * Extract token from request context based on configuration
 */
export async function extractToken(context, config) {
    // Default: extract from Authorization header
    const headerConfig = config?.fromHeader ?? {
        name: "authorization",
        prefix: "Bearer",
    };
    // Try header extraction (case-insensitive header lookup)
    const headerName = headerConfig.name?.toLowerCase() ?? "authorization";
    // Find header value with case-insensitive lookup
    let headerValue;
    for (const [key, value] of Object.entries(context.headers)) {
        if (key.toLowerCase() === headerName) {
            headerValue = value;
            break;
        }
    }
    if (headerValue) {
        const value = Array.isArray(headerValue) ? headerValue[0] : headerValue;
        if (value) {
            const prefix = headerConfig.prefix ?? "Bearer";
            if (!prefix) {
                // If no prefix required, return as-is
                return value;
            }
            // Compare scheme case-insensitively per RFC 7235
            const prefixWithSpace = `${prefix} `;
            if (value.length > prefixWithSpace.length &&
                value.slice(0, prefixWithSpace.length).toLowerCase() ===
                    prefixWithSpace.toLowerCase()) {
                return value.slice(prefixWithSpace.length);
            }
        }
    }
    // Try cookie extraction
    if (config?.fromCookie?.name && context.cookies) {
        const cookieToken = context.cookies[config.fromCookie.name];
        if (cookieToken) {
            return cookieToken;
        }
    }
    // Try query parameter extraction
    if (config?.fromQuery?.name && context.query) {
        const queryToken = context.query[config.fromQuery.name];
        if (queryToken) {
            return Array.isArray(queryToken) ? queryToken[0] : queryToken;
        }
    }
    // Try custom extraction
    if (config?.custom) {
        const customToken = await config.custom(context);
        if (customToken) {
            return customToken;
        }
    }
    return null;
}
// =============================================================================
// AUTH MIDDLEWARE FACTORY
// =============================================================================
/**
 * Create authentication middleware
 *
 * Validates tokens and attaches user context to requests.
 *
 * @example
 * ```typescript
 * const authMiddleware = await createAuthMiddleware({
 *   provider: 'auth0',
 *   providerConfig: {
 *     type: 'auth0',
 *     domain: 'your-tenant.auth0.com',
 *     clientId: 'your-client-id',
 *   },
 *   publicRoutes: ['/health', '/public/*'],
 * });
 *
 * // Use in request handler
 * const result = await authMiddleware(requestContext);
 * if (result.proceed) {
 *   // Access authenticated context
 *   console.log('User:', result.context?.user);
 * } else {
 *   // Return error response
 *   res.status(result.error.statusCode).json({ error: result.error.message });
 * }
 * ```
 */
export async function createAuthMiddleware(config) {
    // Create provider instance
    const provider = await AuthProviderFactory.createProvider(config.provider, config.providerConfig);
    logger.debug(`[AuthMiddleware] Created middleware with ${config.provider} provider`);
    return async (context) => {
        try {
            // Check if route is public
            if (isPublicRoute(context.path ?? "", config.publicRoutes)) {
                logger.debug(`[AuthMiddleware] Public route: ${context.path}`);
                return { proceed: true };
            }
            // Extract token
            const token = await extractToken(context, config.tokenExtraction);
            if (!token) {
                // If auth is optional, proceed without user
                if (config.optional) {
                    return { proceed: true };
                }
                const error = {
                    statusCode: 401,
                    message: "Authentication required",
                    code: "AUTH-005",
                };
                if (config.onError) {
                    await config.onError(createAuthErrorInfo(error.message, error.code), context);
                }
                return { proceed: false, error };
            }
            // Validate token (with 5s timeout to prevent hanging on slow providers)
            const validationResult = await withTimeout(provider.authenticateToken(token), 5000, "Token authentication timed out after 5000ms");
            if (!validationResult.valid) {
                // If auth is optional, proceed without user
                if (config.optional) {
                    return { proceed: true };
                }
                const errorCode = validationResult.errorCode ?? "AUTH-001";
                const error = {
                    statusCode: 401,
                    message: validationResult.error ?? "Invalid token",
                    code: errorCode,
                };
                if (config.onError) {
                    await config.onError(createAuthErrorInfo(error.message, error.code), context);
                }
                return { proceed: false, error };
            }
            // Fail closed: valid token without a user object is treated as failure
            if (!validationResult.user) {
                const error = {
                    statusCode: 401,
                    message: "Token valid but no user identity resolved",
                    code: "AUTH-001",
                };
                if (config.onError) {
                    await config.onError(createAuthErrorInfo(error.message, error.code), context);
                }
                return { proceed: false, error };
            }
            // Create authenticated context
            // Providers populate `payload` (most) or `claims` (Cognito, Keycloak).
            // Prefer `payload`, fall back to `claims` for compatibility.
            const authenticatedContext = {
                ...context,
                user: validationResult.user,
                token,
                claims: validationResult.payload ??
                    validationResult.claims,
            };
            // Call success hook
            if (config.onAuthenticated) {
                await config.onAuthenticated(authenticatedContext);
            }
            logger.debug(`[AuthMiddleware] Authenticated user: ${validationResult.user?.id}`);
            return { proceed: true, context: authenticatedContext };
        }
        catch (error) {
            logger.error(`[AuthMiddleware] Error:`, error);
            const errorResult = {
                statusCode: 500,
                message: error instanceof Error ? error.message : "Authentication error",
                code: "AUTH-014",
            };
            if (config.onError) {
                await config.onError(createAuthErrorInfo(errorResult.message, errorResult.code), context);
            }
            return { proceed: false, error: errorResult };
        }
    };
}
// =============================================================================
// RBAC MIDDLEWARE FACTORY
// =============================================================================
/**
 * Create RBAC (Role-Based Access Control) middleware
 *
 * Checks if authenticated user has required roles/permissions.
 *
 * @example
 * ```typescript
 * const rbacMiddleware = createRBACMiddleware({
 *   roles: ['admin', 'moderator'],
 *   permissions: ['read:users'],
 * });
 *
 * // Use after auth middleware
 * const authResult = await authMiddleware(context);
 * if (authResult.proceed && authResult.context) {
 *   const rbacResult = await rbacMiddleware(authResult.context);
 *   if (!rbacResult.proceed) {
 *     res.status(403).json({ error: rbacResult.error.message });
 *   }
 * }
 * ```
 */
export function createRBACMiddleware(config) {
    return async (context) => {
        try {
            const user = context.user;
            if (!user) {
                return {
                    proceed: false,
                    error: {
                        statusCode: 401,
                        message: "User not authenticated",
                        code: "AUTH-005",
                    },
                };
            }
            // Super admin roles bypass all role/permission checks
            const superAdminRoles = config.superAdminRoles ?? [];
            if (superAdminRoles.length > 0 &&
                user.roles.some((r) => superAdminRoles.includes(r))) {
                logger.debug(`[RBACMiddleware] Super admin bypass for user: ${user.id}`);
                return { proceed: true, context };
            }
            // Build effective permissions from rolePermissions mapping and
            // role hierarchy so that checks below consider inherited grants.
            const effectivePermissions = new Set(user.permissions);
            const rolePermissions = config.rolePermissions ?? {};
            const roleHierarchy = config.roleHierarchy ?? {};
            const expandRoles = (roles) => {
                const expanded = new Set();
                const queue = [...roles];
                while (queue.length > 0) {
                    const role = queue.pop();
                    if (role === undefined || expanded.has(role)) {
                        continue;
                    }
                    expanded.add(role);
                    const children = roleHierarchy[role];
                    if (children) {
                        queue.push(...children);
                    }
                }
                return [...expanded];
            };
            const allRoles = expandRoles(user.roles);
            for (const role of allRoles) {
                const perms = rolePermissions[role];
                if (perms) {
                    for (const p of perms) {
                        effectivePermissions.add(p);
                    }
                }
            }
            // Check custom authorization first
            if (config.custom) {
                const customResult = await config.custom(user, context);
                if (!customResult) {
                    const result = {
                        authorized: false,
                        user,
                        reason: "Custom authorization denied",
                    };
                    if (config.onDenied) {
                        await config.onDenied(result, context);
                    }
                    return {
                        proceed: false,
                        error: {
                            statusCode: 403,
                            message: "Access denied",
                            code: "AUTH-013",
                        },
                    };
                }
            }
            // Check roles (includes inherited roles from hierarchy)
            if (config.roles && config.roles.length > 0) {
                const userRolesSet = new Set(allRoles);
                const hasRequiredRoles = config.requireAllRoles
                    ? config.roles.every((r) => userRolesSet.has(r))
                    : config.roles.some((r) => userRolesSet.has(r));
                if (!hasRequiredRoles) {
                    const missingRoles = config.roles.filter((r) => !userRolesSet.has(r));
                    const result = {
                        authorized: false,
                        user,
                        requiredRoles: config.roles,
                        missingRoles,
                        reason: `Missing roles: ${missingRoles.join(", ")}`,
                    };
                    if (config.onDenied) {
                        await config.onDenied(result, context);
                    }
                    return {
                        proceed: false,
                        error: {
                            statusCode: 403,
                            message: `Insufficient roles. Required: ${config.roles.join(", ")}`,
                            code: "AUTH-012",
                        },
                    };
                }
            }
            // Check permissions (all required; uses effective permissions from role mapping)
            if (config.permissions && config.permissions.length > 0) {
                const missingPermissions = config.permissions.filter((p) => !effectivePermissions.has(p));
                if (missingPermissions.length > 0) {
                    const result = {
                        authorized: false,
                        user,
                        requiredPermissions: config.permissions,
                        missingPermissions,
                        reason: `Missing permissions: ${missingPermissions.join(", ")}`,
                    };
                    if (config.onDenied) {
                        await config.onDenied(result, context);
                    }
                    return {
                        proceed: false,
                        error: {
                            statusCode: 403,
                            message: `Insufficient permissions. Required: ${config.permissions.join(", ")}`,
                            code: "AUTH-011",
                        },
                    };
                }
            }
            logger.debug(`[RBACMiddleware] Authorized user: ${user.id}`);
            return { proceed: true, context };
        }
        catch (error) {
            logger.error(`[RBACMiddleware] Error:`, error);
            return {
                proceed: false,
                error: {
                    statusCode: 500,
                    message: error instanceof Error ? error.message : "Authorization error",
                    code: "AUTH-014",
                },
            };
        }
    };
}
// =============================================================================
// COMBINED MIDDLEWARE
// =============================================================================
/**
 * Create combined auth + RBAC middleware
 *
 * Convenience function that combines authentication and authorization.
 *
 * @example
 * ```typescript
 * const protectedMiddleware = await createProtectedMiddleware({
 *   auth: {
 *     provider: 'auth0',
 *     providerConfig: { type: 'auth0', domain: '...', clientId: '...' },
 *   },
 *   rbac: {
 *     roles: ['admin'],
 *   },
 * });
 *
 * const result = await protectedMiddleware(context);
 * ```
 */
export async function createProtectedMiddleware(config) {
    const authMiddleware = await createAuthMiddleware(config.auth);
    const rbacMiddleware = config.rbac ? createRBACMiddleware(config.rbac) : null;
    return async (context) => {
        // Run auth middleware
        const authResult = await authMiddleware(context);
        if (!authResult.proceed) {
            return authResult;
        }
        // If no RBAC configured, return auth result as-is
        if (!rbacMiddleware) {
            return authResult;
        }
        // Build the context for RBAC. When auth is optional and no token was
        // provided, authResult.context is undefined. We still need to run RBAC
        // so that role/permission checks are not silently bypassed. Pass a
        // context without a user — the RBAC middleware already handles the
        // missing-user case and returns a 401.
        const rbacContext = authResult.context ?? context;
        // Run RBAC middleware
        return rbacMiddleware(rbacContext);
    };
}
// =============================================================================
// UTILITY FUNCTIONS
// =============================================================================
/**
 * Check if a path matches public routes
 */
function isPublicRoute(path, publicRoutes) {
    if (!publicRoutes || publicRoutes.length === 0) {
        return false;
    }
    // Strip query string before matching
    const pathWithoutQuery = path.split("?")[0];
    const normalizedPath = pathWithoutQuery.replace(/\/$/, "") || "/";
    for (const route of publicRoutes) {
        // Exact match
        if (route === normalizedPath) {
            return true;
        }
        // Wildcard match (e.g., '/public/*')
        if (route.endsWith("*")) {
            const prefix = route.slice(0, -1);
            if (normalizedPath.startsWith(prefix)) {
                return true;
            }
        }
        // Pattern match with path segments
        if (route.includes(":")) {
            const routeParts = route.split("/");
            const pathParts = normalizedPath.split("/");
            if (routeParts.length === pathParts.length) {
                const matches = routeParts.every((part, i) => {
                    return part.startsWith(":") || part === pathParts[i];
                });
                if (matches) {
                    return true;
                }
            }
        }
    }
    return false;
}
/**
 * Create request context from standard request object
 */
export function createRequestContext(req) {
    return {
        method: req.method ?? "GET",
        path: req.path ?? req.url ?? "/",
        headers: req.headers ?? {},
        cookies: req.cookies,
        query: req.query,
        body: req.body,
        ip: req.ip,
        ipAddress: req.ip,
        userAgent: req.headers?.["user-agent"],
    };
}
/**
 * Create Express-compatible middleware
 */
export async function createExpressAuthMiddleware(config) {
    const middleware = await createAuthMiddleware(config);
    return async (req, res, next) => {
        const context = createRequestContext(req);
        const result = await middleware(context);
        if (result.proceed) {
            // Attach user to request
            if (result.context) {
                req.user = result.context.user;
                req.authContext = result.context;
            }
            next();
        }
        else {
            res.status(result.error?.statusCode ?? 401).json({
                error: result.error?.message ?? "Unauthorized",
                code: result.error?.code,
            });
        }
    };
}
//# sourceMappingURL=AuthMiddleware.js.map