"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.hasPermission = void 0;
const lodash_1 = __importDefault(require("lodash"));
const cloud_1 = require("../../globalConfig/cloud");
const callControlPlaneApi_1 = require("../../util/callContolPlane/callControlPlaneApi");
const verifyApiKey_1 = require("../../util/verifyApiKey");
const node_cache_1 = require("../../util/node-cache");
const redisClient_1 = require("../../util/redisClient");
const crypto_1 = __importDefault(require("crypto"));
// Permission cache configuration
const PERMISSION_CACHE_TTL = 60; // Cache for 60 seconds
const permissionCache = node_cache_1.NodeCache.getInstance(); // Fallback when Redis is unavailable
const inFlightRequests = new Map();
/**
 * Generate a cache key for permission checks
 */
const getPermissionCacheKey = (token, requestedPermission, extraParams) => {
    const keyParts = [
        token || 'no-token',
        requestedPermission,
        extraParams.orgId || '',
        extraParams.projectId || '',
        //JSON.stringify(_.omit(extraParams, ['orgId', 'projectId'])),
    ];
    const keyString = keyParts.join('|');
    return `permission:${crypto_1.default.createHash('sha256').update(keyString).digest('hex')}`;
};
/**
 * Get cached permission result (prefers Redis, falls back to NodeCache)
 */
const getPermissionCache = async (cacheKey) => {
    // Try Redis first
    if ((0, redisClient_1.isRedisAvailable)()) {
        try {
            const client = (0, redisClient_1.getRedisClient)();
            const cached = await client.get(cacheKey);
            if (cached && cached !== '') {
                try {
                    return JSON.parse(cached);
                }
                catch {
                    // If JSON.parse fails, ignore and fall through to local cache
                }
            }
        }
        catch {
            // Redis unavailable or error – fall back to NodeCache
        }
    }
    else {
        // Fall back to NodeCache
        const local = permissionCache.get(cacheKey);
        return local && local !== '' ? local : null;
    }
};
/**
 * Set cached permission result (writes to Redis when available, always writes to NodeCache)
 */
const setPermissionCache = async (cacheKey, value) => {
    // Write to Redis if available
    if ((0, redisClient_1.isRedisAvailable)()) {
        try {
            const client = (0, redisClient_1.getRedisClient)();
            await client.set(cacheKey, JSON.stringify(value), 'EX', PERMISSION_CACHE_TTL);
        }
        catch {
            // Ignore Redis errors and still set local cache
        }
    }
    else {
        permissionCache.set(cacheKey, value, PERMISSION_CACHE_TTL);
    }
};
/**
 * Check permission with caching and request deduplication
 */
const checkPermissionWithCache = async (token, requestedPermission, extraParams) => {
    const cacheKey = getPermissionCacheKey(token, requestedPermission, extraParams);
    // Check cache first
    const cached = await getPermissionCache(cacheKey);
    if (cached && cached !== '') {
        return cached;
    }
    // Check if there's already an in-flight request with the same parameters
    if (inFlightRequests.has(cacheKey)) {
        try {
            const result = await inFlightRequests.get(cacheKey);
            return result;
        }
        catch (error) {
            // If the in-flight request failed, remove it and continue to make a new request
            inFlightRequests.delete(cacheKey);
            throw error;
        }
    }
    // Create a new request promise
    const requestPromise = (0, callControlPlaneApi_1.callControlPlane)(`/permission`, {
        method: 'GET',
        params: {
            requestedPermission,
            ...extraParams,
        },
    }, token)
        .then((response) => {
        // Cache successful responses
        if (response && response.status === 1) {
            void setPermissionCache(cacheKey, response);
        }
        inFlightRequests.delete(cacheKey);
        return response;
    })
        .catch((error) => {
        inFlightRequests.delete(cacheKey);
        throw error;
    });
    // Store the in-flight request
    inFlightRequests.set(cacheKey, requestPromise);
    return requestPromise;
};
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const MAX_PERMISSION_RETRIES = 4;
const INITIAL_BACKOFF_MS = 500;
const MAX_BACKOFF_MS = 10000;
/** Retry callControlPlane on 429 (rate limit) with exponential backoff. Uses RetryAfterMs from error when present. */
async function callControlPlaneWithRetry(endpoint, options, token) {
    let lastResponse;
    for (let attempt = 0; attempt <= MAX_PERMISSION_RETRIES; attempt++) {
        lastResponse = await (0, callControlPlaneApi_1.callControlPlane)(endpoint, options, token);
        const status = lastResponse?.status;
        const is429 = status === 429;
        if (!is429) {
            return lastResponse;
        }
        if (attempt === MAX_PERMISSION_RETRIES) {
            return lastResponse;
        }
        let retryAfterMs;
        if (typeof lastResponse?.RetryAfterMs === 'number') {
            retryAfterMs = Math.min(lastResponse.RetryAfterMs, MAX_BACKOFF_MS);
        }
        else {
            const fromMessage = typeof lastResponse?.error === 'string' &&
                /RetryAfterMs=(\d+)/i.exec(lastResponse.error);
            retryAfterMs = fromMessage
                ? Math.min(Number(fromMessage[1]), MAX_BACKOFF_MS)
                : Math.min(INITIAL_BACKOFF_MS * Math.pow(2, attempt), MAX_BACKOFF_MS);
        }
        await sleep(retryAfterMs);
    }
    return lastResponse;
}
const hasPermission = (requestedPermission) => async (req, res, next) => {
    const token = req.headers['x-access-token'] || req.headers.authorization;
    const apiKey = req.headers['x-api-key'];
    const publicAssessKey = req.headers['x-public-access-key'];
    try {
        if (publicAssessKey) {
            req.query.accessKey = publicAssessKey;
            return next();
        }
        // If API key is present, validate it
        if (apiKey) {
            const callControlPlane = cloud_1.cloudConfig.getCpApiHost();
            const keyValidationRes = await (0, verifyApiKey_1.verifyApiKey)(apiKey, callControlPlane);
            const result = keyValidationRes?.result;
            // Optional: attach project/org info to request if returned
            if (result?.status) {
                const contentType = req.headers['content-type'] || '';
                const isFormData = contentType.includes('multipart/form-data');
                if (result?.apiKeyData?.project_id) {
                    if (isFormData) {
                        // For FormData requests, Express parses them into regular objects
                        req.body.projectId = result?.apiKeyData.project_id;
                        req.body.apiKey = apiKey;
                    }
                    else if (lodash_1.default.isEmpty(req.body) === false) {
                        req.body.projectId = result?.apiKeyData.project_id;
                        req.body.apiKey = apiKey;
                    }
                    else {
                        req.query.projectId = result?.apiKeyData.project_id;
                        req.query.apiKey = apiKey;
                    }
                }
                if (result?.apiKeyData?.org_id) {
                    if (isFormData) {
                        // For FormData requests, Express parses them into regular objects
                        req.body.orgId = result?.apiKeyData.org_id;
                        req.body.apiKey = apiKey;
                    }
                    else if (lodash_1.default.isEmpty(req.body) === false) {
                        req.body.orgId = result?.apiKeyData.org_id;
                        req.body.apiKey = apiKey;
                    }
                    else {
                        req.query.orgId = result?.apiKeyData.org_id;
                        req.query.apiKey = apiKey;
                    }
                }
                return next(); // API key verified, continue
            }
            else {
                res.status(401).send({ message: 'Error while verifying api key' });
                return;
            }
        }
        // Only take orgId and projectId from req.body if req.body is present
        const extraParams = {
            ...req.query,
            ...req.params,
        };
        if (req.body) {
            // Check if this is a multipart/form-data request by content-type header
            const contentType = req.headers['content-type'] || '';
            const isFormData = contentType.includes('multipart/form-data');
            if (isFormData) {
                // For FormData requests, Express parses them into regular objects
                // but we need to check for the fields directly
                if (req.body.orgId) {
                    extraParams.orgId = req.body.orgId;
                }
                if (req.body.projectId) {
                    extraParams.projectId = req.body.projectId;
                }
            }
            else if (typeof req.body === 'object' && req.body !== null) {
                // Handle regular JSON requests
                if (req.body.orgId) {
                    extraParams.orgId = req.body.orgId;
                }
                if (req.body.projectId) {
                    extraParams.projectId = req.body.projectId;
                }
            }
        }
        const response = await checkPermissionWithCache(token, requestedPermission, extraParams);
        if (response.status === 1) {
            req.auth = { payload: response?.response?.signedInUser };
            next();
            return;
        }
        else {
            res.status(401).send({ message: 'Unauthorized Access' });
            return;
        }
    }
    catch (error) {
        console.error('Error while authorizing user:', error);
        res.status(403).send({
            status: 0,
            message: error.message,
        });
        return;
    }
};
exports.hasPermission = hasPermission;
//# sourceMappingURL=permission.js.map