/**
 * Session Manager - High-level session management
 *
 * Provides secure session lifecycle management including:
 * - Session creation with device tracking
 * - Session validation with security checks
 * - Revocation on security events
 * - Re-authentication for sensitive operations
 *
 * @packageDocumentation
 */
import { createLogger } from '../common/logger.js';
import { getSessionStore, } from './session-store.js';
import { getFingerprintService, } from './fingerprint-service.js';
const logger = createLogger({ component: 'session-manager' });
const DEFAULT_CONFIG = {
    requireReauthForSensitive: true,
    sensitiveOperationWindow: 300, // 5 minutes
    inactivityTimeout: 3600, // 1 hour
    revokeOnPasswordChange: true,
    revokeOnEmailChange: true,
    detectConcurrentSessions: true,
    fingerprintEnabled: true,
    fingerprintStrictness: 'warn',
};
/**
 * Session manager for secure session lifecycle
 */
export class SessionManager {
    store;
    config;
    fingerprintService;
    constructor(config = {}) {
        this.config = { ...DEFAULT_CONFIG, ...config };
        this.store = getSessionStore(config);
        this.fingerprintService = getFingerprintService();
    }
    /**
     * Create a new session for a user
     */
    async createSession(input) {
        // Check for suspicious concurrent sessions if enabled
        if (this.config.detectConcurrentSessions) {
            await this.checkConcurrentSessions(input);
        }
        return this.store.create(input);
    }
    /**
     * Validate a session and check security conditions
     */
    async validateSession(sessionId, options) {
        const result = await this.store.validate(sessionId);
        const warnings = [];
        if (!result.valid) {
            return {
                valid: false,
                session: result.session,
                reason: result.reason,
            };
        }
        const session = result.session;
        // Check inactivity timeout
        if (options?.checkInactivity !== false) {
            const inactiveSeconds = (Date.now() - session.lastActivityAt.getTime()) / 1000;
            if (inactiveSeconds > this.config.inactivityTimeout) {
                return {
                    valid: false,
                    session,
                    reason: 'Session inactive for too long',
                    requiresReauth: true,
                };
            }
        }
        // Check IP address change (warning only, not blocking)
        if (options?.ipAddress && options.ipAddress !== session.ipAddress) {
            warnings.push(`IP address changed from ${session.ipAddress} to ${options.ipAddress}`);
            logger.warn({
                sessionId,
                userId: session.userId,
                originalIp: session.ipAddress,
                currentIp: options.ipAddress,
            }, 'Session IP address changed');
        }
        // Validate fingerprint if enabled and session has stored fingerprint
        let fingerprintValidation;
        if (this.config.fingerprintEnabled &&
            session.deviceFingerprint &&
            options?.requestHeaders) {
            fingerprintValidation = this.fingerprintService.validateFingerprint(options.requestHeaders, session.deviceFingerprint, sessionId);
            if (!fingerprintValidation.valid) {
                if (fingerprintValidation.shouldBlock) {
                    return {
                        valid: false,
                        session,
                        reason: 'Fingerprint mismatch detected - session may be compromised',
                        fingerprintValidation,
                    };
                }
                else {
                    warnings.push('Fingerprint mismatch detected - client characteristics changed');
                }
            }
        }
        // Check if sensitive operation requires recent authentication
        if (options?.operation && this.config.requireReauthForSensitive) {
            const requiresReauth = this.operationRequiresReauth(session, options.operation);
            if (requiresReauth) {
                return {
                    valid: true,
                    session,
                    requiresReauth: true,
                    reason: `Operation "${options.operation}" requires recent authentication`,
                    securityWarnings: warnings.length > 0 ? warnings : undefined,
                    fingerprintValidation,
                };
            }
        }
        return {
            valid: true,
            session,
            securityWarnings: warnings.length > 0 ? warnings : undefined,
            fingerprintValidation,
        };
    }
    /**
     * Revoke a specific session
     */
    async revokeSession(sessionId, reason, revokedBy) {
        return this.store.revoke(sessionId, reason, revokedBy);
    }
    /**
     * Revoke all sessions for a user (e.g., on password change)
     */
    async revokeAllUserSessions(userId, reason, revokedBy, exceptCurrentSession) {
        return this.store.revokeAllForUser(userId, reason, revokedBy, exceptCurrentSession);
    }
    /**
     * Revoke other sessions (keep current)
     */
    async revokeOtherSessions(userId, currentSessionId, reason = 'User requested logout of other sessions') {
        return this.store.revokeAllForUser(userId, reason, userId, currentSessionId);
    }
    /**
     * Regenerate a session with a new ID while preserving session data
     *
     * This is a security best practice after privilege changes (e.g., login,
     * role change, password reset) to prevent session fixation attacks.
     *
     * Steps performed:
     * 1. Gets the old session data
     * 2. Creates a new session with the same data (optionally with updates)
     * 3. Revokes the old session
     * 4. Returns the new session
     *
     * @param oldSessionId - The current session ID to regenerate
     * @param options - Options for the new session
     * @returns The new session, or null if old session doesn't exist
     */
    async regenerateSession(oldSessionId, options = {}) {
        // Get the old session
        const oldSession = await this.store.get(oldSessionId);
        if (!oldSession || oldSession.revoked) {
            logger.warn({ oldSessionId, reason: options.reason }, 'Cannot regenerate session - old session not found or already revoked');
            return null;
        }
        // Create new session with data from old session
        const newSessionInput = {
            userId: oldSession.userId,
            tenantId: oldSession.tenantId,
            ipAddress: options.ipAddress ?? oldSession.ipAddress,
            userAgent: options.userAgent ?? oldSession.userAgent,
            deviceFingerprint: options.deviceFingerprint ?? oldSession.deviceFingerprint,
            metadata: {
                ...oldSession.metadata,
                ...options.metadata,
                regeneratedFrom: oldSessionId,
                regeneratedAt: new Date().toISOString(),
                regenerationReason: options.reason,
            },
        };
        // Create the new session
        const newSession = await this.store.create(newSessionInput);
        // Revoke the old session
        await this.store.revoke(oldSessionId, options.reason ?? 'Session regenerated', 'system');
        logger.info({
            oldSessionId,
            newSessionId: newSession.id,
            userId: oldSession.userId,
            reason: options.reason,
        }, 'Session regenerated successfully');
        return newSession;
    }
    /**
     * Compute and store fingerprint for a session
     *
     * Use this when creating sessions to enable fingerprint validation.
     *
     * @param sessionId - Session to update
     * @param headers - Request headers for fingerprint computation
     * @returns The computed fingerprint, or null if session not found
     */
    async setSessionFingerprint(sessionId, headers) {
        const session = await this.store.get(sessionId);
        if (!session || session.revoked) {
            return null;
        }
        const fingerprintResult = this.fingerprintService.computeFingerprint(headers);
        // Update session with fingerprint
        // Note: This requires getting and re-saving the session
        // In production, consider adding a dedicated update method to SessionStore
        session.deviceFingerprint = fingerprintResult.fingerprint;
        session.metadata = {
            ...session.metadata,
            fingerprintComponents: fingerprintResult.componentsUsed,
            fingerprintMissingComponents: fingerprintResult.missingComponents,
        };
        // Touch to persist changes
        await this.store.touch(sessionId);
        logger.debug({
            sessionId,
            fingerprintPrefix: fingerprintResult.fingerprint.substring(0, 8),
            componentsUsed: fingerprintResult.componentsUsed,
        }, 'Session fingerprint set');
        return fingerprintResult.fingerprint;
    }
    /**
     * Handle password change - revoke all sessions if configured
     */
    async onPasswordChange(userId, currentSessionId) {
        if (!this.config.revokeOnPasswordChange) {
            return { revokedCount: 0 };
        }
        const revokedCount = await this.revokeAllUserSessions(userId, 'Password changed', 'system', currentSessionId);
        logger.info({ userId, revokedCount, keptSession: currentSessionId }, 'Sessions revoked after password change');
        return { revokedCount };
    }
    /**
     * Handle email change - revoke all sessions if configured
     */
    async onEmailChange(userId, currentSessionId) {
        if (!this.config.revokeOnEmailChange) {
            return { revokedCount: 0 };
        }
        const revokedCount = await this.revokeAllUserSessions(userId, 'Email changed', 'system', currentSessionId);
        logger.info({ userId, revokedCount, keptSession: currentSessionId }, 'Sessions revoked after email change');
        return { revokedCount };
    }
    /**
     * Handle security incident - revoke all sessions immediately
     */
    async onSecurityIncident(userId, incidentType, details) {
        const reason = `Security incident: ${incidentType}${details ? ` - ${details}` : ''}`;
        const revokedCount = await this.revokeAllUserSessions(userId, reason, 'security-system');
        logger.warn({ userId, incidentType, revokedCount, details }, 'All sessions revoked due to security incident');
        return { revokedCount };
    }
    /**
     * Get all active sessions for a user
     */
    async getUserSessions(userId) {
        return this.store.getSessionsForUser(userId);
    }
    /**
     * Get session count for a user
     */
    async getUserSessionCount(userId) {
        const sessions = await this.store.getSessionsForUser(userId);
        return sessions.length;
    }
    /**
     * Record that user re-authenticated (for sensitive operations)
     */
    async recordReauthentication(sessionId) {
        const session = await this.store.get(sessionId);
        if (!session || session.revoked) {
            return false;
        }
        // Update metadata to record re-auth time
        session.metadata = {
            ...session.metadata,
            lastReauthAt: new Date().toISOString(),
        };
        // Touch the session to update it
        await this.store.touch(sessionId);
        logger.info({ sessionId, userId: session.userId }, 'Re-authentication recorded');
        return true;
    }
    /**
     * Check if an operation requires re-authentication
     */
    operationRequiresReauth(session, operation) {
        // Check when user last authenticated
        const lastReauthAt = session.metadata?.['lastReauthAt'];
        const lastAuthTime = lastReauthAt
            ? new Date(lastReauthAt).getTime()
            : session.createdAt.getTime();
        const secondsSinceAuth = (Date.now() - lastAuthTime) / 1000;
        // If within the sensitive operation window, no re-auth needed
        if (secondsSinceAuth < this.config.sensitiveOperationWindow) {
            return false;
        }
        // These operations always require recent auth
        const alwaysRequireReauth = [
            'password_change',
            'delete_account',
            'mfa_change',
        ];
        return alwaysRequireReauth.includes(operation);
    }
    /**
     * Check for suspicious concurrent sessions
     */
    async checkConcurrentSessions(input) {
        const existingSessions = await this.store.getSessionsForUser(input.userId);
        for (const session of existingSessions) {
            // Check if there's a session from a very different location
            // (This is a simple check - could be enhanced with IP geolocation)
            if (session.ipAddress !== input.ipAddress) {
                const timeSinceLastActivity = Date.now() - session.lastActivityAt.getTime();
                // If there was recent activity from a different IP, log a warning
                if (timeSinceLastActivity < 300000) { // 5 minutes
                    logger.warn({
                        userId: input.userId,
                        existingSessionIp: session.ipAddress,
                        newSessionIp: input.ipAddress,
                        existingSessionId: session.id,
                    }, 'Concurrent session from different IP detected');
                }
            }
        }
    }
}
/**
 * Singleton session manager instance
 */
let sessionManager = null;
/**
 * Get the session manager singleton
 */
export function getSessionManager(config) {
    if (!sessionManager) {
        sessionManager = new SessionManager(config);
    }
    return sessionManager;
}
/**
 * Create a new session manager instance (for testing)
 */
export function createSessionManager(config) {
    return new SessionManager(config);
}
export { extractFingerprintHeaders } from './fingerprint-service.js';
//# sourceMappingURL=session-manager.js.map