// SPDX-License-Identifier: Apache-2.0
// Copyright 2024-2026 Vorion LLC
/**
 * Rule Evaluator
 *
 * Evaluates policy rules and determines enforcement actions.
 * Supports all rule types: MFA, approval, block, rate-limit, encryption, audit, etc.
 *
 * @packageDocumentation
 */
import { createLogger } from '../../common/logger.js';
const logger = createLogger({ component: 'rule-evaluator' });
/**
 * RuleEvaluator class
 */
export class RuleEvaluator {
    rateLimiter;
    geoLocationProvider;
    customHandlers;
    defaultMfaTimeout;
    defaultApprovalTimeout;
    defaultSessionIdleTimeout;
    constructor(options = {}) {
        this.rateLimiter = options.rateLimiter;
        this.geoLocationProvider = options.geoLocationProvider;
        this.customHandlers = options.customHandlers ?? new Map();
        this.defaultMfaTimeout = options.defaultMfaTimeout ?? 300;
        this.defaultApprovalTimeout = options.defaultApprovalTimeout ?? 3600;
        this.defaultSessionIdleTimeout = options.defaultSessionIdleTimeout ?? 1800;
    }
    /**
     * Evaluate a rule against the context
     */
    async evaluate(rule, context) {
        try {
            if (!rule.enforced) {
                return {
                    ruleType: rule.type,
                    enforced: false,
                    passed: true,
                    reason: 'Rule is not enforced',
                };
            }
            switch (rule.type) {
                case 'require_mfa':
                    return this.evaluateMFA(rule, context);
                case 'require_approval':
                    return this.evaluateApproval(rule, context);
                case 'block_access':
                    return this.evaluateBlockAccess(rule, context);
                case 'rate_limit':
                    return this.evaluateRateLimit(rule, context);
                case 'require_encryption':
                    return this.evaluateEncryption(rule, context);
                case 'audit_log':
                    return this.evaluateAuditLog(rule, context);
                case 'step_up_auth':
                    return this.evaluateStepUpAuth(rule, context);
                case 'data_masking':
                    return this.evaluateDataMasking(rule, context);
                case 'session_timeout':
                    return this.evaluateSessionTimeout(rule, context);
                case 'geo_restriction':
                    return this.evaluateGeoRestriction(rule, context);
                case 'custom':
                    return this.evaluateCustom(rule, context);
                default:
                    return {
                        ruleType: rule.type,
                        enforced: true,
                        passed: false,
                        reason: `Unknown rule type: ${rule.type}`,
                    };
            }
        }
        catch (error) {
            const message = error instanceof Error ? error.message : 'Unknown error';
            logger.warn({ rule, error: message }, 'Rule evaluation failed');
            return {
                ruleType: rule.type,
                enforced: true,
                passed: false,
                reason: `Evaluation error: ${message}`,
            };
        }
    }
    /**
     * Evaluate all rules and return results
     */
    async evaluateAll(rules, context) {
        const results = [];
        for (const rule of rules) {
            const result = await this.evaluate(rule, context);
            results.push(result);
        }
        return results;
    }
    /**
     * Evaluate MFA requirement
     */
    evaluateMFA(rule, context) {
        const user = context.user;
        // Check if MFA has been verified
        if (!user?.mfaVerified) {
            return {
                ruleType: 'require_mfa',
                enforced: true,
                passed: false,
                reason: 'MFA verification required',
                metadata: {
                    methods: rule.methods,
                    timeout: rule.timeout ?? this.defaultMfaTimeout,
                },
            };
        }
        // Check if MFA is still valid (within timeout)
        if (rule.timeout && user.lastMfaAt) {
            const lastMfaTime = new Date(user.lastMfaAt).getTime();
            const now = Date.now();
            const timeoutMs = (rule.timeout ?? this.defaultMfaTimeout) * 1000;
            if (now - lastMfaTime > timeoutMs) {
                return {
                    ruleType: 'require_mfa',
                    enforced: true,
                    passed: false,
                    reason: 'MFA verification expired',
                    metadata: {
                        expiredAt: new Date(lastMfaTime + timeoutMs).toISOString(),
                    },
                };
            }
        }
        return {
            ruleType: 'require_mfa',
            enforced: true,
            passed: true,
            reason: 'MFA verification passed',
        };
    }
    /**
     * Evaluate approval requirement
     */
    evaluateApproval(rule, context) {
        // Check if approval has been granted (would be in custom context)
        const approved = context.custom?.approvalGranted === true;
        const approvalId = context.custom?.approvalId;
        if (approved && approvalId) {
            return {
                ruleType: 'require_approval',
                enforced: true,
                passed: true,
                reason: 'Approval granted',
                metadata: { approvalId },
            };
        }
        return {
            ruleType: 'require_approval',
            enforced: true,
            passed: false,
            reason: 'Approval required',
            metadata: {
                approvers: rule.approvers,
                approverRoles: rule.approverRoles,
                timeout: rule.approvalTimeout ?? this.defaultApprovalTimeout,
                minApprovers: rule.minApprovers ?? 1,
                requireJustification: rule.requireJustification ?? false,
            },
        };
    }
    /**
     * Evaluate block access rule
     */
    evaluateBlockAccess(rule, context) {
        // Block access rules always fail when enforced
        return {
            ruleType: 'block_access',
            enforced: true,
            passed: false,
            reason: rule.reason ?? 'Access blocked by policy',
            metadata: {
                errorCode: rule.errorCode,
                redirectUrl: rule.redirectUrl,
            },
        };
    }
    /**
     * Evaluate rate limit rule
     */
    async evaluateRateLimit(rule, context) {
        if (!this.rateLimiter) {
            logger.warn('Rate limiter not configured, skipping rate limit rule');
            return {
                ruleType: 'rate_limit',
                enforced: true,
                passed: true,
                reason: 'Rate limiter not configured',
            };
        }
        // Build rate limit key
        const keyParts = ['policy-rl'];
        const keyBy = rule.keyBy ?? ['user'];
        for (const keyType of keyBy) {
            switch (keyType) {
                case 'user':
                    if (context.user?.id)
                        keyParts.push(`u:${context.user.id}`);
                    break;
                case 'ip':
                    keyParts.push(`ip:${context.request.ip}`);
                    break;
                case 'tenant':
                    if (context.user?.tenant)
                        keyParts.push(`t:${context.user.tenant}`);
                    break;
                case 'api_key':
                    const apiKey = context.request.headers?.['x-api-key'];
                    if (typeof apiKey === 'string')
                        keyParts.push(`ak:${apiKey.slice(0, 8)}`);
                    break;
                case 'custom':
                    if (rule.customKey) {
                        const customValue = this.getNestedValue(context, rule.customKey);
                        if (customValue)
                            keyParts.push(`c:${String(customValue)}`);
                    }
                    break;
            }
        }
        const key = keyParts.join(':');
        // Calculate window in seconds
        const windowMultiplier = {
            second: 1,
            minute: 60,
            hour: 3600,
            day: 86400,
        };
        const windowSeconds = rule.window * (windowMultiplier[rule.windowUnit ?? 'second'] ?? 1);
        try {
            const result = await this.rateLimiter.check(key, rule.limit, windowSeconds);
            if (!result.allowed) {
                return {
                    ruleType: 'rate_limit',
                    enforced: true,
                    passed: false,
                    reason: 'Rate limit exceeded',
                    metadata: {
                        limit: rule.limit,
                        remaining: result.remaining,
                        resetAt: result.resetAt,
                        retryAfter: rule.retryAfter ?? Math.ceil((result.resetAt - Date.now()) / 1000),
                    },
                };
            }
            return {
                ruleType: 'rate_limit',
                enforced: true,
                passed: true,
                reason: 'Within rate limit',
                metadata: {
                    limit: rule.limit,
                    remaining: result.remaining,
                },
            };
        }
        catch (error) {
            const message = error instanceof Error ? error.message : 'Unknown error';
            logger.error({ error: message, key }, 'Rate limit check failed');
            // Fail open or closed based on configuration
            return {
                ruleType: 'rate_limit',
                enforced: true,
                passed: true,
                reason: `Rate limit check failed: ${message}`,
            };
        }
    }
    /**
     * Evaluate encryption requirement
     */
    evaluateEncryption(rule, context) {
        // Check if connection is encrypted (HTTPS)
        const isSecure = context.request.url.startsWith('https://') ||
            context.request.headers?.['x-forwarded-proto'] === 'https';
        if (!isSecure) {
            return {
                ruleType: 'require_encryption',
                enforced: true,
                passed: false,
                reason: 'Encrypted connection required',
                metadata: {
                    fields: rule.fields,
                    algorithm: rule.algorithm,
                },
            };
        }
        // If specific fields need encryption, that would be handled by the application
        return {
            ruleType: 'require_encryption',
            enforced: true,
            passed: true,
            reason: 'Encryption requirements met',
            metadata: {
                fields: rule.fields,
                algorithm: rule.algorithm,
            },
        };
    }
    /**
     * Evaluate audit log rule
     */
    evaluateAuditLog(rule, context) {
        // Audit rules always pass - they're informational
        return {
            ruleType: 'audit_log',
            enforced: true,
            passed: true,
            reason: 'Audit logging enabled',
            metadata: {
                level: rule.level ?? 'basic',
                includeRequest: rule.includeRequest ?? false,
                includeResponse: rule.includeResponse ?? false,
                destination: rule.destination,
            },
        };
    }
    /**
     * Evaluate step-up authentication rule
     */
    evaluateStepUpAuth(rule, context) {
        // Check current authentication level from context
        const currentLevel = context.custom?.authLevel ?? 0;
        if (currentLevel < rule.requiredLevel) {
            return {
                ruleType: 'step_up_auth',
                enforced: true,
                passed: false,
                reason: 'Step-up authentication required',
                metadata: {
                    currentLevel,
                    requiredLevel: rule.requiredLevel,
                    method: rule.method,
                    timeout: rule.timeout,
                },
            };
        }
        return {
            ruleType: 'step_up_auth',
            enforced: true,
            passed: true,
            reason: 'Authentication level sufficient',
            metadata: {
                currentLevel,
                requiredLevel: rule.requiredLevel,
            },
        };
    }
    /**
     * Evaluate data masking rule
     */
    evaluateDataMasking(rule, context) {
        // Data masking is applied on response - this just indicates it should be applied
        return {
            ruleType: 'data_masking',
            enforced: true,
            passed: true,
            reason: 'Data masking will be applied',
            metadata: {
                fields: rule.fields,
                maskType: rule.maskType ?? 'partial',
                partialMaskPattern: rule.partialMaskPattern,
            },
        };
    }
    /**
     * Evaluate session timeout rule
     */
    evaluateSessionTimeout(rule, context) {
        const user = context.user;
        if (!user?.sessionStartedAt) {
            return {
                ruleType: 'session_timeout',
                enforced: true,
                passed: true,
                reason: 'No session information available',
            };
        }
        const sessionStartTime = new Date(user.sessionStartedAt).getTime();
        const now = Date.now();
        // Check max session duration
        if (rule.maxDuration) {
            const maxDurationMs = rule.maxDuration * 1000;
            if (now - sessionStartTime > maxDurationMs) {
                return {
                    ruleType: 'session_timeout',
                    enforced: true,
                    passed: false,
                    reason: 'Session duration exceeded',
                    metadata: {
                        maxDuration: rule.maxDuration,
                        sessionAge: Math.floor((now - sessionStartTime) / 1000),
                        requireReauth: rule.requireReauth ?? true,
                    },
                };
            }
        }
        // Idle timeout would need last activity tracking in context
        const lastActivity = context.custom?.lastActivityAt ?? user.sessionStartedAt;
        const lastActivityTime = new Date(lastActivity).getTime();
        const idleTimeout = rule.idleTimeout ?? this.defaultSessionIdleTimeout;
        const idleTimeoutMs = idleTimeout * 1000;
        if (now - lastActivityTime > idleTimeoutMs) {
            return {
                ruleType: 'session_timeout',
                enforced: true,
                passed: false,
                reason: 'Session idle timeout exceeded',
                metadata: {
                    idleTimeout,
                    idleDuration: Math.floor((now - lastActivityTime) / 1000),
                    requireReauth: rule.requireReauth ?? true,
                },
            };
        }
        return {
            ruleType: 'session_timeout',
            enforced: true,
            passed: true,
            reason: 'Session is valid',
            metadata: {
                sessionAge: Math.floor((now - sessionStartTime) / 1000),
                idleDuration: Math.floor((now - lastActivityTime) / 1000),
            },
        };
    }
    /**
     * Evaluate geo restriction rule
     */
    async evaluateGeoRestriction(rule, context) {
        let geoInfo = context.environment?.geoLocation;
        // Try to look up geo info if not provided
        if (!geoInfo && this.geoLocationProvider) {
            try {
                geoInfo = await this.geoLocationProvider.lookup(context.request.ip) ?? undefined;
            }
            catch (error) {
                const message = error instanceof Error ? error.message : 'Unknown error';
                logger.warn({ error: message, ip: context.request.ip }, 'Geo lookup failed');
            }
        }
        if (!geoInfo?.country) {
            // If we can't determine location, fail closed
            return {
                ruleType: 'geo_restriction',
                enforced: true,
                passed: false,
                reason: 'Unable to determine geo location',
            };
        }
        const country = geoInfo.country.toUpperCase();
        const region = geoInfo.region?.toUpperCase();
        // Check blocked countries
        if (rule.blockedCountries?.length) {
            const blockedCountries = rule.blockedCountries.map(c => c.toUpperCase());
            if (blockedCountries.includes(country)) {
                return {
                    ruleType: 'geo_restriction',
                    enforced: true,
                    passed: false,
                    reason: `Access blocked from country: ${country}`,
                    metadata: { country, region },
                };
            }
        }
        // Check allowed countries
        if (rule.allowedCountries?.length) {
            const allowedCountries = rule.allowedCountries.map(c => c.toUpperCase());
            if (!allowedCountries.includes(country)) {
                return {
                    ruleType: 'geo_restriction',
                    enforced: true,
                    passed: false,
                    reason: `Access not allowed from country: ${country}`,
                    metadata: { country, region },
                };
            }
        }
        // Check blocked regions
        if (rule.blockedRegions?.length && region) {
            const blockedRegions = rule.blockedRegions.map(r => r.toUpperCase());
            if (blockedRegions.includes(region) || blockedRegions.includes(`${country}-${region}`)) {
                return {
                    ruleType: 'geo_restriction',
                    enforced: true,
                    passed: false,
                    reason: `Access blocked from region: ${region}`,
                    metadata: { country, region },
                };
            }
        }
        // Check allowed regions
        if (rule.allowedRegions?.length && region) {
            const allowedRegions = rule.allowedRegions.map(r => r.toUpperCase());
            if (!allowedRegions.includes(region) && !allowedRegions.includes(`${country}-${region}`)) {
                return {
                    ruleType: 'geo_restriction',
                    enforced: true,
                    passed: false,
                    reason: `Access not allowed from region: ${region}`,
                    metadata: { country, region },
                };
            }
        }
        return {
            ruleType: 'geo_restriction',
            enforced: true,
            passed: true,
            reason: 'Geo restriction passed',
            metadata: { country, region },
        };
    }
    /**
     * Evaluate custom rule
     */
    async evaluateCustom(rule, context) {
        const handler = this.customHandlers.get(rule.handler);
        if (!handler) {
            return {
                ruleType: 'custom',
                enforced: true,
                passed: false,
                reason: `No handler registered for: ${rule.handler}`,
            };
        }
        try {
            return await handler(rule, context);
        }
        catch (error) {
            const message = error instanceof Error ? error.message : 'Unknown error';
            return {
                ruleType: 'custom',
                enforced: true,
                passed: false,
                reason: `Custom handler error: ${message}`,
            };
        }
    }
    /**
     * Get nested value from object using dot notation
     */
    getNestedValue(obj, path) {
        if (obj === null || obj === undefined)
            return undefined;
        const parts = path.split('.');
        let current = obj;
        for (const part of parts) {
            if (current === null || current === undefined)
                return undefined;
            if (typeof current !== 'object')
                return undefined;
            current = current[part];
        }
        return current;
    }
    /**
     * Register a custom rule handler
     */
    registerCustomHandler(name, handler) {
        this.customHandlers.set(name, handler);
    }
    /**
     * Unregister a custom rule handler
     */
    unregisterCustomHandler(name) {
        return this.customHandlers.delete(name);
    }
    /**
     * Set rate limiter
     */
    setRateLimiter(rateLimiter) {
        this.rateLimiter = rateLimiter;
    }
    /**
     * Set geo location provider
     */
    setGeoLocationProvider(provider) {
        this.geoLocationProvider = provider;
    }
}
/**
 * Create a rule evaluator instance
 */
export function createRuleEvaluator(options) {
    return new RuleEvaluator(options);
}
//# sourceMappingURL=rule-evaluator.js.map