import { generateSecureJobId } from '../common/utility.js';
const DEFAULT_OPTIONS = {
    cookieName: 'ZIN_SESSION_ID',
    headerName: 'x-session-id',
    ttlMs: 7 * 24 * 60 * 60 * 1000,
    maxSessions: 10000,
    cleanupIntervalMs: 60000,
};
function isUnrefableTimer(value) {
    return (typeof value === 'object' &&
        value instanceof Object &&
        'unref' in value &&
        typeof value.unref === 'function');
}
function cleanupExpiredSessions(sessions) {
    const now = Date.now();
    let removed = 0;
    for (const [id, stored] of sessions.entries()) {
        if (stored.expiresAt <= now) {
            sessions.delete(id);
            removed++;
        }
    }
    return removed;
}
function enforceSessionCapacity(sessions, maxSessions, preserveId) {
    if (sessions.size < maxSessions)
        return;
    cleanupExpiredSessions(sessions);
    if (sessions.size < maxSessions)
        return;
    for (const id of sessions.keys()) {
        if (id === preserveId)
            continue;
        sessions.delete(id);
        break;
    }
}
function parseCookies(cookieHeader) {
    const list = {};
    if (cookieHeader.length === 0)
        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 appendSetCookie(res, cookie) {
    const existing = res.getHeader('Set-Cookie');
    if (existing === undefined) {
        res.setHeader('Set-Cookie', cookie);
        return;
    }
    if (Array.isArray(existing)) {
        const existingCookies = existing.map(String);
        res.setHeader('Set-Cookie', [...existingCookies, cookie]);
        return;
    }
    if (typeof existing === 'string') {
        res.setHeader('Set-Cookie', [existing, cookie]);
        return;
    }
    res.setHeader('Set-Cookie', cookie);
}
function buildSessionCookie(cookieName, sessionId) {
    // Keep this minimal; callers can override behavior later.
    // HttpOnly prevents JS access; SameSite=Lax is a reasonable default for app sessions.
    return `${cookieName}=${encodeURIComponent(sessionId)}; Path=/; HttpOnly; SameSite=Lax`;
}
function createSessionApi(sessions, sessionId, ttlMs, maxSessions) {
    const withoutKey = (data, key) => {
        if (!Object.prototype.hasOwnProperty.call(data, key))
            return data;
        const record = data;
        const { [key]: _removed, ...rest } = record;
        return rest;
    };
    const ensureStored = () => {
        const existing = sessions.get(sessionId);
        const now = Date.now();
        if (existing !== undefined && existing.expiresAt > now) {
            return existing;
        }
        enforceSessionCapacity(sessions, maxSessions, sessionId);
        const created = { data: {}, expiresAt: now + ttlMs };
        sessions.set(sessionId, created);
        return created;
    };
    return {
        id: sessionId,
        get(key) {
            return ensureStored().data[key];
        },
        set(key, value) {
            const stored = ensureStored();
            stored.data[key] = value;
            stored.expiresAt = Date.now() + ttlMs;
        },
        has(key) {
            return Object.prototype.hasOwnProperty.call(ensureStored().data, key);
        },
        forget(key) {
            const stored = ensureStored();
            stored.data = withoutKey(stored.data, key);
            stored.expiresAt = Date.now() + ttlMs;
        },
        all() {
            return { ...ensureStored().data };
        },
        clear() {
            const stored = ensureStored();
            stored.data = {};
            stored.expiresAt = Date.now() + ttlMs;
        },
    };
}
export const SessionManager = Object.freeze({
    create(options = {}) {
        const config = { ...DEFAULT_OPTIONS, ...options };
        const sessions = new Map();
        const cleanupInterval = config.cleanupIntervalMs > 0
            ? globalThis.setInterval(() => {
                cleanupExpiredSessions(sessions);
            }, config.cleanupIntervalMs)
            : undefined;
        if (cleanupInterval && isUnrefableTimer(cleanupInterval)) {
            cleanupInterval.unref();
        }
        return {
            getIdFromCookieHeader(cookieHeader) {
                if (cookieHeader === undefined || cookieHeader.length === 0)
                    return undefined;
                const cookies = parseCookies(cookieHeader);
                return cookies[config.cookieName];
            },
            getIdFromRequest(req) {
                const cookieHeader = req.getHeader('cookie');
                if (typeof cookieHeader === 'string') {
                    const fromCookie = this.getIdFromCookieHeader(cookieHeader);
                    if (fromCookie !== undefined)
                        return fromCookie;
                }
                const fromHeader = req.getHeader(config.headerName);
                if (typeof fromHeader === 'string' && fromHeader.length > 0)
                    return fromHeader;
                if (typeof req.sessionId === 'string' && req.sessionId.length > 0)
                    return req.sessionId;
                const fromContext = req.context?.['sessionId'];
                if (typeof fromContext === 'string' && fromContext.length > 0)
                    return fromContext;
                return undefined;
            },
            async ensureSessionId(req, res) {
                const existing = this.getIdFromRequest(req);
                const sessionId = existing ??
                    (await Promise.resolve(generateSecureJobId('SessionManager: secure crypto API not available to generate a session id')));
                req.context['sessionId'] = sessionId;
                // If the cookie is missing, set it.
                const cookieHeader = req.getHeader('cookie');
                const fromCookie = typeof cookieHeader === 'string' ? this.getIdFromCookieHeader(cookieHeader) : undefined;
                if (fromCookie === undefined) {
                    appendSetCookie(res, buildSessionCookie(config.cookieName, sessionId));
                }
                return sessionId;
            },
            get(sessionId) {
                return createSessionApi(sessions, sessionId, config.ttlMs, config.maxSessions);
            },
            destroy(sessionId) {
                sessions.delete(sessionId);
            },
            cleanup() {
                return cleanupExpiredSessions(sessions);
            },
            dispose() {
                if (cleanupInterval !== undefined) {
                    clearInterval(cleanupInterval);
                }
                sessions.clear();
            },
        };
    },
});
export default SessionManager;
