/* eslint-disable @typescript-eslint/require-await */
/**
 * CSRF Token Manager
 * Generate, validate, and bind CSRF tokens to sessions
 */
import { Env } from '../config/env.js';
import { Logger } from '../config/logger.js';
import { createRedisConnection } from '../config/workers.js';
import { ZintrustLang } from '../lang/lang.js';
import { randomBytes } from '../node-singletons/crypto.js';
import { RedisKeys } from '../tools/redis/RedisKeyManager.js';
/**
 * Create a new CSRF token manager instance
 */
const normalizeStoreName = (name) => {
    const raw = String(name ?? '')
        .trim()
        .toLowerCase();
    if (raw === 'redis')
        return 'redis';
    return 'memory';
};
const isWorkersRuntime = () => {
    const globalAny = globalThis;
    if (globalAny.CF !== undefined)
        return true;
    if (typeof globalAny.WebSocketPair === 'function')
        return true;
    if (globalAny.caches !== undefined)
        return true;
    return false;
};
const resolveStoreName = (options) => {
    if (isWorkersRuntime())
        return 'memory';
    return normalizeStoreName(options?.store ?? Env.CSRF_STORE ?? Env.CSRF_DRIVER ?? Env.get('CSRF_STORE', 'memory'));
};
const toTokenData = (stored) => {
    return {
        token: stored.token,
        sessionId: stored.sessionId,
        createdAt: new Date(stored.createdAt),
        expiresAt: new Date(stored.expiresAt),
    };
};
const createMemoryManager = (tokenLength, tokenTtl) => {
    const tokens = new Map();
    return {
        async generateToken(sessionId) {
            tokens.delete(sessionId);
            const token = randomBytes(tokenLength).toString('hex');
            const now = new Date();
            const expiresAt = new Date(now.getTime() + tokenTtl);
            const tokenData = { token, sessionId, createdAt: now, expiresAt };
            tokens.set(sessionId, tokenData);
            return token;
        },
        async validateToken(sessionId, token) {
            const tokenData = tokens.get(sessionId);
            if (!tokenData)
                return false;
            const isValid = tokenData.token === token;
            const isExpired = new Date() > tokenData.expiresAt;
            if (isExpired) {
                tokens.delete(sessionId);
                return false;
            }
            return isValid;
        },
        async invalidateToken(sessionId) {
            tokens.delete(sessionId);
        },
        async getTokenData(sessionId) {
            return tokens.get(sessionId) ?? null;
        },
        async refreshToken(sessionId) {
            const tokenData = tokens.get(sessionId);
            if (!tokenData)
                return null;
            const isExpired = new Date() > tokenData.expiresAt;
            if (isExpired) {
                tokens.delete(sessionId);
                return null;
            }
            tokenData.expiresAt = new Date(Date.now() + tokenTtl);
            return tokenData.token;
        },
        async cleanup() {
            let removed = 0;
            const now = new Date();
            for (const [sessionId, tokenData] of tokens.entries()) {
                if (now > tokenData.expiresAt) {
                    tokens.delete(sessionId);
                    removed++;
                }
            }
            return removed;
        },
        async clear() {
            tokens.clear();
        },
        async getTokenCount() {
            return tokens.size;
        },
    };
};
// Helper functions for Redis CSRF manager
const createRedisClientFactory = (options) => {
    let redisClient = options?.redis ?? null;
    return () => {
        if (redisClient)
            return redisClient;
        const dbFromEnv = Env.CSRF_REDIS_DB;
        const database = dbFromEnv >= 0 ? dbFromEnv : Env.getInt('REDIS_QUEUE_DB', ZintrustLang.REDIS_DEFAULT_DB);
        redisClient = createRedisConnection({
            host: Env.get('REDIS_HOST', 'localhost'),
            port: Env.getInt('REDIS_PORT', ZintrustLang.REDIS_DEFAULT_PORT),
            password: Env.get('REDIS_PASSWORD'),
            db: database,
        });
        return redisClient;
    };
};
const createRedisPrefixVersioner = (keyPrefix, getRedisClient) => {
    const versionKey = `${keyPrefix}__v`;
    let cachedVersion = null;
    const getVersion = async () => {
        if (cachedVersion !== null)
            return cachedVersion;
        const client = getRedisClient();
        const raw = await client.get(versionKey);
        const v = (raw ?? '1').trim();
        cachedVersion = v === '' ? '1' : v;
        return cachedVersion;
    };
    const getEffectivePrefix = async () => {
        const v = await getVersion();
        return `${keyPrefix}${v}:`;
    };
    const bumpPrefixVersion = async () => {
        const client = getRedisClient();
        // INCR creates the key if it does not exist.
        const next = await client.incr(versionKey);
        cachedVersion = String(next);
    };
    return {
        getEffectivePrefix,
        bumpPrefixVersion,
    };
};
const scanRedisKeys = async (client, match) => {
    const keys = [];
    const stream = client.scanStream({ match, count: 200 });
    return new Promise((resolve, reject) => {
        stream.on('data', (resultKeys) => {
            if (Array.isArray(resultKeys) && resultKeys.length) {
                keys.push(...resultKeys);
            }
        });
        stream.on('end', () => resolve(keys));
        stream.on('error', (err) => reject(err));
    });
};
const createRedisTokenOperations = (keyPrefix, tokenTtl, getRedisClient) => {
    const { getEffectivePrefix, bumpPrefixVersion } = createRedisPrefixVersioner(keyPrefix, getRedisClient);
    const buildKey = (prefix, sessionId) => `${prefix}${sessionId}`;
    const fetchTokenData = async (sessionId) => {
        try {
            const client = getRedisClient();
            const prefix = await getEffectivePrefix();
            const payload = await client.get(buildKey(prefix, sessionId));
            if (payload === null || payload === '')
                return null;
            const parsed = JSON.parse(payload);
            return toTokenData(parsed);
        }
        catch (error) {
            Logger.error('CSRF Redis fetch failed', error);
            return null;
        }
    };
    const saveTokenData = async (data) => {
        try {
            const client = getRedisClient();
            const prefix = await getEffectivePrefix();
            const stored = {
                token: data.token,
                sessionId: data.sessionId,
                createdAt: data.createdAt.getTime(),
                expiresAt: data.expiresAt.getTime(),
            };
            await client.set(buildKey(prefix, data.sessionId), JSON.stringify(stored), 'PX', tokenTtl);
        }
        catch (error) {
            Logger.error('CSRF Redis save failed', error);
        }
    };
    const deleteToken = async (sessionId) => {
        try {
            const client = getRedisClient();
            const prefix = await getEffectivePrefix();
            await client.del(buildKey(prefix, sessionId));
        }
        catch (error) {
            Logger.error('CSRF Redis delete failed', error);
        }
    };
    const scanKeys = async (effectivePrefix) => {
        const client = getRedisClient();
        return scanRedisKeys(client, `${effectivePrefix}*`);
    };
    return {
        fetchTokenData,
        saveTokenData,
        deleteToken,
        scanKeys,
        getEffectivePrefix,
        bumpPrefixVersion,
    };
};
const createRedisManager = (tokenLength, tokenTtl, options) => {
    const keyPrefix = options?.keyPrefix ?? RedisKeys.getCsrfPrefix();
    const getRedisClient = createRedisClientFactory(options);
    const { fetchTokenData, saveTokenData, deleteToken, scanKeys, getEffectivePrefix, bumpPrefixVersion, } = createRedisTokenOperations(keyPrefix, tokenTtl, getRedisClient);
    return {
        async generateToken(sessionId) {
            const token = randomBytes(tokenLength).toString('hex');
            const now = new Date();
            const expiresAt = new Date(now.getTime() + tokenTtl);
            const tokenData = { token, sessionId, createdAt: now, expiresAt };
            await saveTokenData(tokenData);
            return token;
        },
        async validateToken(sessionId, token) {
            const tokenData = await fetchTokenData(sessionId);
            if (!tokenData)
                return false;
            const isValid = tokenData.token === token;
            const isExpired = new Date() > tokenData.expiresAt;
            if (isExpired) {
                await deleteToken(sessionId);
                return false;
            }
            return isValid;
        },
        async invalidateToken(sessionId) {
            await deleteToken(sessionId);
        },
        async getTokenData(sessionId) {
            return fetchTokenData(sessionId);
        },
        async refreshToken(sessionId) {
            const tokenData = await fetchTokenData(sessionId);
            if (!tokenData)
                return null;
            const isExpired = new Date() > tokenData.expiresAt;
            if (isExpired) {
                await deleteToken(sessionId);
                return null;
            }
            tokenData.expiresAt = new Date(Date.now() + tokenTtl);
            await saveTokenData(tokenData);
            return tokenData.token;
        },
        async cleanup() {
            // Redis handles expiry via TTL, so nothing to do here.
            return Promise.resolve(0); // NOSONAR
        },
        async clear() {
            try {
                // Logical clear: bump the version so future operations use a new prefix.
                await bumpPrefixVersion();
            }
            catch (error) {
                Logger.error('CSRF Redis clear failed', error);
            }
        },
        async getTokenCount() {
            try {
                const prefix = await getEffectivePrefix();
                const keys = await scanKeys(prefix);
                return keys.length;
            }
            catch (error) {
                Logger.error('CSRF Redis count failed', error);
                return 0;
            }
        },
    };
};
const create = (options) => {
    const tokenLength = options?.tokenLength ?? Env.TOKEN_LENGTH; // 256 bits
    const tokenTtl = options?.tokenTtlMs ?? Env.TOKEN_TTL; // 1 hour in milliseconds
    const store = resolveStoreName(options);
    if (store === 'redis') {
        return createRedisManager(tokenLength, tokenTtl, options);
    }
    return createMemoryManager(tokenLength, tokenTtl);
};
/**
 * CsrfTokenManager namespace - sealed for immutability
 */
export const CsrfTokenManager = Object.freeze({
    create,
});
