Source: utils/userUtils.js

/**
 * User utilities for handling user selection, validation, and assignment
 * 
 * With Bearer-only auth (no sessions), user data is cached in Redis to avoid
 * database queries on every API call.
 */
import { query, UUID2hex, HEX2uuid } from '@commtool/sql-query';
import { getRedisClient } from '@commtool/shared-auth';
import { isAdmin } from '../utils/authChecks.js';

/**
 * Invalidate user cache for a specific user (call when user data changes)
 * @param {string} userUID - User UUID to invalidate, or '*' for all users
 * @param {string} [orgaUID] - Optional: specific org to invalidate, otherwise all orgs
 */
export const invalidateUserCache = async (userUID, orgaUID = null) => {
    try {
        const redis = getRedisClient();
        
        if (userUID === '*' && orgaUID) {
            // Invalidate ALL users for a specific organization
            const keys = await redis.keys(`user-data-*-${orgaUID}`);
            const keys2 = await redis.keys(`user-org-*-${orgaUID}`);
            const allKeys = [...keys, ...keys2];
            if (allKeys.length > 0) {
                await redis.del(allKeys);
            }
            console.log(`[invalidateUserCache] Invalidated ${allKeys.length} cache entries for org ${orgaUID}`);
        } else if (orgaUID) {
            // Invalidate specific user-org combo
            await redis.del(`user-data-${userUID}-${orgaUID}`);
            await redis.del(`user-org-${userUID}-${orgaUID}`);
            console.log(`[invalidateUserCache] Invalidated cache for user ${userUID} in org ${orgaUID}`);
        } else {
            // Invalidate all cached data for this user across all orgs
            const keys = await redis.keys(`user-data-${userUID}-*`);
            const keys2 = await redis.keys(`user-org-${userUID}-*`);
            const allKeys = [...keys, ...keys2];
            if (allKeys.length > 0) {
                await redis.del(allKeys);
            }
            console.log(`[invalidateUserCache] Invalidated ${allKeys.length} cache entries for user ${userUID}`);
        }
    } catch (error) {
        console.error(`[invalidateUserCache] Error: ${error.message}`);
    }
};

/**
 * Validate user access to organization with caching
 * @param {string} userUID - User UUID
 * @param {string} orgaUID - Organization UUID
 * @param {string} tokenId - Optional token ID for caching (deprecated - auth library handles this)
 * @returns {Promise<Object>} - {valid, isAdmin, userData, userUID}
 */
export const validateUserForOrganization = async (userUID, orgaUID, tokenId = null) => {
    try {
        // Simplified cache key - auth library handles token caching, we just need org-specific validation
        const cacheKey = `user-org-${userUID}-${orgaUID}`;
        const cached = await getRedisClient().get(cacheKey);
        if (cached) {
            console.log(`[validateUserForOrganization] Cache hit for user ${userUID} in org ${orgaUID}`);
            return JSON.parse(cached);
        }

        // Validate user exists and has access to organization
        const userQuery = await query(`
            SELECT
                MUser.UID,
                MUser.Data,
                MUser.Display,
                MLink.Type,
                CASE WHEN MLink.Type IN ('memberA', 'memberSys') THEN 1 ELSE 0 END as isAdmin
            FROM Member AS MUser
            INNER JOIN Links AS MLink ON (MUser.UID = MLink.UID)
            INNER JOIN ObjectBase ON (MUser.UID = ObjectBase.UID)
            WHERE MUser.UID = ?
            AND MLink.UIDTarget = ?
            AND MLink.Type IN ('member', 'memberA', 'memberSys')
            AND ObjectBase.Type = 'extern'
        `, [UUID2hex(userUID), UUID2hex(orgaUID)], { cast: ['json', 'UUID'] });

        if (userQuery.length > 0) {
            const userData = userQuery[0];
            const result = {
                valid: true,
                isAdmin: userData.isAdmin === 1,
                userData: userData,
                userUID: userUID
            };

            // Cache for 15 minutes (reduced since auth library caches user identity)
            await getRedisClient().setEx(cacheKey, 900, JSON.stringify(result));
            console.log(`[validateUserForOrganization] User ${userUID} validated for org ${orgaUID}`);

            return result;
        } else {
            console.warn(`[validateUserForOrganization] User ${userUID} not found or no access to org ${orgaUID}`);
            return { valid: false, isAdmin: false };
        }
    } catch (error) {
        console.error(`[validateUserForOrganization] Error validating user: ${error.message}`);
        return { valid: false, isAdmin: false };
    }
};


/**
 * Get user object for the logged-in user for their organizations
 * Uses Redis caching to avoid database queries on every API call with Bearer auth
 * @param {Object} req - Express request object
 * @returns {Promise<Object>} - User object or false if not found
 */
export const getUser = async (req) => {
    try {
        const userUID = req.session.authUser.userUID;
        const orgaUID = req.session.root;
        
        // Cache key based on user + organization
        const cacheKey = `user-data-${userUID}-${orgaUID}`;
        
        // Try to get from cache first
        const cached = await getRedisClient().get(cacheKey);
        if (cached) {
            const user = JSON.parse(cached);
            // Re-evaluate isAdmin from current session (in case roles changed)
            user.isAdmin = await isAdmin(req.session);
            return user;
        }
        
        // Cache miss - query database
        let users = await query(
            `SELECT Links.UIDTarget AS UID, MUser.Data, MUser.Display, MLink.UIDTarget AS UIDorga, ObjectBase.Type FROM Links
            INNER JOIN Links AS MLink ON (Links.UIDTarget=MLink.UID AND MLink.Type IN ('member','memberA','memberSys'))
            INNER JOIN Member AS MUser ON (MLink.UID=MUser.UID)
            INNER JOIN ObjectBase ON (MUser.UID=ObjectBase.UID)
            WHERE Links.UID=? AND Links.Type='identifyer'`,
            [UUID2hex(userUID)],
            { cast: ['json', 'UUID'], log: false }
        );

        const user = users.find(u => u.UIDorga === orgaUID);

        if (user) {
            // Add the orgas the user has access to
            user.userOrgas = users.map(o => o.UIDorga);
            user.isAdmin = await isAdmin(req.session);
            
            // Cache for 15 minutes (user data doesn't change often)
            await getRedisClient().setEx(cacheKey, 900, JSON.stringify(user));
            
            return user;
        } else {
            if (await isAdmin(req.session)) {
                // Return system user for this group
                const sysUsers = await query(
                    `SELECT ObjectBase.UID, SysUser.Data, SysUser.Display, ObjectBase.Type, 'extra' AS module
                    FROM Member AS SysUser
                    INNER JOIN ObjectBase ON (SysUser.UID=ObjectBase.UIDBelongsTo AND ObjectBase.Type='extern')
                    INNER JOIN Links ON (ObjectBase.UID= Links.UID AND Links.UIDTarget=? AND Links.Type = 'memberSys')`,
                    [UUID2hex(orgaUID)],
                    { cast: ['json', 'UUID'] }
                );

                if (sysUsers.length > 0) {
                    const sysUser = sysUsers[0];
                    sysUser.userOrgas = [orgaUID];
                    sysUser.isAdmin = true;
                    
                    // Cache sysUser too
                    await getRedisClient().setEx(cacheKey, 900, JSON.stringify(sysUser));
                    
                    return sysUser;
                } else {
                    console.error('[getUser] No sysuser defined for org:', orgaUID);
                    return false;
                }
            } else {
                console.error('[getUser] No user found and user not admin');
                return false;
            }
        }
    } catch (error) {
        console.error('[getUser] Error retrieving user:', error);
        throw error;
    }
};

/**
 * Get fake user data for impersonation (admin only)
 * Uses Redis caching to avoid database queries on every API call
 * @param {string} fakeUserUID - The UUID of the user to impersonate
 * @param {string} orgaUID - The organization UUID
 * @returns {Promise<Object|null>} - Fake user object or null if not found
 */
export const getFakeUser = async (fakeUserUID, orgaUID) => {
    try {
        // Validate UUID format
        if (!fakeUserUID?.match(/^UUID-[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/)) {
            return null;
        }
        
        // Use cached fake user data (same cache as getUser)
        const cacheKey = `user-data-${fakeUserUID}-${orgaUID}`;
        const cached = await getRedisClient().get(cacheKey);
        
        if (cached) {
            return JSON.parse(cached);
        }
        
        // Cache miss - query database
        const fake = await query(
            `SELECT Links.UIDTarget AS UID, MUser.Data, MUser.Display, MLink.UIDTarget AS UIDorga FROM Links
            INNER JOIN Links AS MLink ON (Links.UIDTarget=MLink.UID AND MLink.Type IN ('member','memberA','memberSys'))
            INNER JOIN Member AS MUser ON (MLink.UID=MUser.UID)
            WHERE Links.UID=? AND Links.Type='identifyer'
            AND MLink.UIDTarget=?`,
            [UUID2hex(fakeUserUID), UUID2hex(orgaUID)],
            { cast: ['json', 'UUID'] }
        );

        if (fake.length > 0) {
            const fakeUser = fake[0];
            fakeUser.userOrgas = fake.map(o => o.UIDorga);
            // Cache for 15 minutes
            await getRedisClient().setEx(cacheKey, 900, JSON.stringify(fakeUser));
            return fakeUser;
        }
        
        return null;
    } catch (error) {
        console.error('[getFakeUser] Error:', error.message);
        return null;
    }
};