/**
 * RBAC Policy Evaluation Engine
 *
 * Evaluates access control policies based on:
 * - Role-based permissions
 * - Resource-based access control
 * - Trust tier requirements
 * - Time-based and conditional policies
 * - Tenant context isolation
 *
 * Integrates with TenantContext for multi-tenant authorization and
 * Trust-engine for trust-based access decisions.
 *
 * @packageDocumentation
 */
import { z } from 'zod';
import { createLogger } from '../../common/logger.js';
import { withSpan } from '../../common/trace.js';
import { Counter, Histogram } from 'prom-client';
import { vorionRegistry } from '../../common/metrics-registry.js';
import { getEffectivePermissions, getBuiltinRoles, } from './roles.js';
import { hasPermission, hasAllPermissions, getBuiltinPermissions, } from './permissions.js';
import { getSecurityAuditLogger, } from '../../audit/security-logger.js';
const logger = createLogger({ component: 'rbac-policy-engine' });
// =============================================================================
// METRICS
// =============================================================================
const policyEvaluations = new Counter({
    name: 'vorion_rbac_policy_evaluations_total',
    help: 'Total RBAC policy evaluations',
    labelNames: ['result', 'policy_type'],
    registers: [vorionRegistry],
});
const policyEvaluationDuration = new Histogram({
    name: 'vorion_rbac_policy_evaluation_duration_seconds',
    help: 'Duration of RBAC policy evaluation',
    labelNames: ['policy_type'],
    buckets: [0.001, 0.005, 0.01, 0.025, 0.05, 0.1],
    registers: [vorionRegistry],
});
const permissionChecks = new Counter({
    name: 'vorion_rbac_permission_checks_total',
    help: 'Total RBAC permission checks',
    labelNames: ['result', 'resource_type'],
    registers: [vorionRegistry],
});
// =============================================================================
// TYPES
// =============================================================================
/**
 * Access control policy types
 */
export const PolicyTypes = {
    /** Role-based permission check */
    ROLE_BASED: 'role_based',
    /** Resource-based permission check */
    RESOURCE_BASED: 'resource_based',
    /** Trust-tier requirement */
    TRUST_BASED: 'trust_based',
    /** Time-based restriction */
    TIME_BASED: 'time_based',
    /** Conditional policy */
    CONDITIONAL: 'conditional',
    /** Composite (multiple policies) */
    COMPOSITE: 'composite',
};
/**
 * Policy evaluation effect
 */
export const PolicyEffects = {
    ALLOW: 'allow',
    DENY: 'deny',
    INDETERMINATE: 'indeterminate',
    NOT_APPLICABLE: 'not_applicable',
};
/**
 * Policy combining algorithm
 */
export const CombiningAlgorithms = {
    /** First deny wins */
    DENY_OVERRIDES: 'deny_overrides',
    /** First allow wins */
    PERMIT_OVERRIDES: 'permit_overrides',
    /** All must allow */
    UNANIMOUS: 'unanimous',
    /** First applicable wins */
    FIRST_APPLICABLE: 'first_applicable',
    /** Priority-based (highest priority wins) */
    PRIORITY_BASED: 'priority_based',
};
// =============================================================================
// ZOD SCHEMAS
// =============================================================================
export const policyTargetSchema = z.object({
    subjectIds: z.array(z.string()).optional(),
    subjectTypes: z.array(z.enum(['user', 'agent', 'service'])).optional(),
    roleIds: z.array(z.string()).optional(),
    tenantIds: z.array(z.string()).optional(),
    excludeSubjectIds: z.array(z.string()).optional(),
});
export const policyConditionsSchema = z.object({
    ipAddresses: z.array(z.string()).optional(),
    timeWindow: z.object({
        startTime: z.string(),
        endTime: z.string(),
        daysOfWeek: z.array(z.number()),
        timezone: z.string(),
    }).optional(),
    attributes: z.record(z.unknown()).optional(),
    expression: z.string().optional(),
});
export const accessPolicySchema = z.lazy(() => z.object({
    id: z.string(),
    name: z.string(),
    description: z.string().optional(),
    type: z.nativeEnum(PolicyTypes),
    priority: z.number().int().min(0),
    effect: z.nativeEnum(PolicyEffects),
    target: policyTargetSchema.optional(),
    conditions: policyConditionsSchema.optional(),
    requiredPermissions: z.array(z.string()).optional(),
    requiredRoles: z.array(z.string()).optional(),
    requiredTrustTier: z.number().int().min(0).max(7).optional(),
    resourcePatterns: z.array(z.string()).optional(),
    timeRestrictions: z.array(z.object({
        startTime: z.string(),
        endTime: z.string(),
        daysOfWeek: z.array(z.number()),
        timezone: z.string(),
    })).optional(),
    childPolicies: z.array(z.lazy(() => accessPolicySchema)).optional(),
    combiningAlgorithm: z.nativeEnum(CombiningAlgorithms).optional(),
    enabled: z.boolean(),
    tenantId: z.string().optional(),
    metadata: z.record(z.unknown()).optional(),
    createdAt: z.string().datetime(),
    updatedAt: z.string().datetime(),
}));
/**
 * RBAC Policy Evaluation Engine
 *
 * Provides comprehensive policy evaluation for access control decisions.
 * Supports multiple policy types and combining algorithms.
 */
export class RBACPolicyEngine {
    policies = new Map();
    roleRegistry = new Map();
    permissionRegistry = new Map();
    roleAssignments = new Map();
    options;
    auditLogger;
    evaluationCache = new Map();
    constructor(options = {}) {
        this.options = {
            defaultCombiningAlgorithm: options.defaultCombiningAlgorithm ?? CombiningAlgorithms.DENY_OVERRIDES,
            defaultEffect: options.defaultEffect ?? PolicyEffects.DENY,
            enableAuditLogging: options.enableAuditLogging ?? true,
            enableTracing: options.enableTracing ?? true,
            enableCaching: options.enableCaching ?? true,
            cacheTtlMs: options.cacheTtlMs ?? 60000, // 1 minute
        };
        this.auditLogger = getSecurityAuditLogger();
        // Initialize with built-in roles and permissions
        this.initializeBuiltins();
        logger.info({
            defaultCombiningAlgorithm: this.options.defaultCombiningAlgorithm,
            defaultEffect: this.options.defaultEffect,
        }, 'RBAC policy engine initialized');
    }
    /**
     * Initialize built-in roles and permissions
     */
    initializeBuiltins() {
        // Register built-in roles
        for (const role of getBuiltinRoles()) {
            this.roleRegistry.set(role.id, role);
        }
        // Register built-in permissions
        for (const permission of getBuiltinPermissions()) {
            this.permissionRegistry.set(permission.id, permission);
        }
        logger.debug({
            roles: this.roleRegistry.size,
            permissions: this.permissionRegistry.size,
        }, 'Built-in roles and permissions initialized');
    }
    // ===========================================================================
    // POLICY EVALUATION
    // ===========================================================================
    /**
     * Evaluate access request against all applicable policies
     */
    async evaluate(context) {
        const startTime = performance.now();
        // Check cache
        if (this.options.enableCaching) {
            const cacheKey = this.buildCacheKey(context);
            const cached = this.evaluationCache.get(cacheKey);
            if (cached && cached.expiresAt > Date.now()) {
                policyEvaluations.inc({ result: cached.result.effect, policy_type: 'cached' });
                return cached.result;
            }
        }
        const evaluatedPolicies = [];
        const effects = [];
        // Get applicable policies sorted by priority
        const applicablePolicies = this.getApplicablePolicies(context);
        for (const policy of applicablePolicies) {
            const policyStartTime = performance.now();
            const evaluation = await this.evaluatePolicy(policy, context);
            const policyDuration = performance.now() - policyStartTime;
            const evaluatedPolicy = {
                policyId: policy.id,
                policyName: policy.name,
                policyType: policy.type,
                effect: evaluation.effect,
                matched: evaluation.matched,
                reason: evaluation.reason,
                durationMs: policyDuration,
            };
            evaluatedPolicies.push(evaluatedPolicy);
            if (evaluation.matched && evaluation.effect !== PolicyEffects.NOT_APPLICABLE) {
                effects.push({
                    effect: evaluation.effect,
                    policy: evaluatedPolicy,
                    priority: policy.priority,
                });
            }
            policyEvaluationDuration.observe({ policy_type: policy.type }, policyDuration / 1000);
        }
        // Combine effects using the configured algorithm
        const { effect, decidingPolicy } = this.combineEffects(effects, this.options.defaultCombiningAlgorithm);
        const finalEffect = effects.length === 0 ? this.options.defaultEffect : effect;
        const permitted = finalEffect === PolicyEffects.ALLOW;
        const durationMs = performance.now() - startTime;
        // Build missing requirements if denied
        const missingRequirements = !permitted
            ? this.getMissingRequirements(context, applicablePolicies)
            : undefined;
        const result = {
            effect: finalEffect,
            permitted,
            evaluatedPolicies,
            decidingPolicy,
            reason: this.buildDecisionReason(finalEffect, decidingPolicy, missingRequirements),
            missingRequirements,
            durationMs,
            evaluatedAt: new Date().toISOString(),
        };
        // Cache result
        if (this.options.enableCaching) {
            const cacheKey = this.buildCacheKey(context);
            this.evaluationCache.set(cacheKey, {
                result,
                expiresAt: Date.now() + this.options.cacheTtlMs,
            });
        }
        // Audit logging
        if (this.options.enableAuditLogging) {
            await this.logAccessDecision(context, result);
        }
        policyEvaluations.inc({ result: finalEffect, policy_type: 'evaluated' });
        logger.debug({
            subjectId: context.subject.id,
            resourceType: context.resource.type,
            action: context.resource.action,
            effect: finalEffect,
            permitted,
            policiesEvaluated: evaluatedPolicies.length,
            durationMs,
        }, 'Policy evaluation completed');
        return result;
    }
    /**
     * Evaluate with OpenTelemetry tracing
     */
    async evaluateWithTracing(context) {
        if (!this.options.enableTracing) {
            return this.evaluate(context);
        }
        return withSpan('rbac.evaluatePolicy', async (span) => {
            span.attributes['subject.id'] = context.subject.id;
            span.attributes['subject.type'] = context.subject.type;
            span.attributes['resource.type'] = context.resource.type;
            span.attributes['resource.action'] = context.resource.action;
            const result = await this.evaluate(context);
            span.attributes['result.effect'] = result.effect;
            span.attributes['result.permitted'] = result.permitted;
            span.attributes['result.policiesEvaluated'] = result.evaluatedPolicies.length;
            span.attributes['result.durationMs'] = result.durationMs;
            return result;
        }, { 'tenant.id': context.environment.tenantId });
    }
    /**
     * Evaluate a single policy
     */
    async evaluatePolicy(policy, context) {
        // Check if policy targets this subject
        if (!this.policyTargetsSubject(policy, context)) {
            return { effect: PolicyEffects.NOT_APPLICABLE, matched: false, reason: 'Target mismatch' };
        }
        // Check conditions
        if (policy.conditions && !this.checkConditions(policy.conditions, context)) {
            return { effect: PolicyEffects.NOT_APPLICABLE, matched: false, reason: 'Conditions not met' };
        }
        // Check time restrictions
        if (policy.timeRestrictions && !this.checkTimeRestrictions(policy.timeRestrictions, context.environment.timestamp)) {
            return { effect: PolicyEffects.NOT_APPLICABLE, matched: false, reason: 'Outside time window' };
        }
        // Evaluate based on policy type
        switch (policy.type) {
            case PolicyTypes.ROLE_BASED:
                return this.evaluateRoleBasedPolicy(policy, context);
            case PolicyTypes.RESOURCE_BASED:
                return this.evaluateResourceBasedPolicy(policy, context);
            case PolicyTypes.TRUST_BASED:
                return this.evaluateTrustBasedPolicy(policy, context);
            case PolicyTypes.TIME_BASED:
                return this.evaluateTimeBasedPolicy(policy, context);
            case PolicyTypes.CONDITIONAL:
                return this.evaluateConditionalPolicy(policy, context);
            case PolicyTypes.COMPOSITE:
                return this.evaluateCompositePolicy(policy, context);
            default:
                return { effect: PolicyEffects.INDETERMINATE, matched: false, reason: 'Unknown policy type' };
        }
    }
    /**
     * Evaluate role-based policy
     */
    evaluateRoleBasedPolicy(policy, context) {
        // Check required roles
        if (policy.requiredRoles && policy.requiredRoles.length > 0) {
            const hasRequiredRole = policy.requiredRoles.some(role => context.subject.roles.includes(role));
            if (!hasRequiredRole) {
                return {
                    effect: PolicyEffects.DENY,
                    matched: true,
                    reason: `Missing required role: ${policy.requiredRoles.join(' or ')}`,
                };
            }
        }
        // Check required permissions
        if (policy.requiredPermissions && policy.requiredPermissions.length > 0) {
            const hasAllRequired = hasAllPermissions(context.subject.permissions, policy.requiredPermissions);
            if (!hasAllRequired) {
                return {
                    effect: PolicyEffects.DENY,
                    matched: true,
                    reason: 'Missing required permissions',
                };
            }
        }
        return { effect: policy.effect, matched: true };
    }
    /**
     * Evaluate resource-based policy
     */
    evaluateResourceBasedPolicy(policy, context) {
        // Check resource patterns
        if (policy.resourcePatterns && policy.resourcePatterns.length > 0) {
            const resourceKey = `${context.resource.type}:${context.resource.action}`;
            const matches = policy.resourcePatterns.some(pattern => this.matchPattern(resourceKey, pattern));
            if (!matches) {
                return {
                    effect: PolicyEffects.NOT_APPLICABLE,
                    matched: false,
                    reason: 'Resource pattern mismatch',
                };
            }
        }
        // Check if subject has permission for this resource action
        const requiredPermission = `${context.resource.type}:${context.resource.action}`;
        if (!hasPermission(context.subject.permissions, requiredPermission)) {
            return {
                effect: PolicyEffects.DENY,
                matched: true,
                reason: `Missing permission: ${requiredPermission}`,
            };
        }
        return { effect: policy.effect, matched: true };
    }
    /**
     * Evaluate trust-based policy
     */
    evaluateTrustBasedPolicy(policy, context) {
        if (policy.requiredTrustTier !== undefined) {
            if (context.subject.trustTier < policy.requiredTrustTier) {
                return {
                    effect: PolicyEffects.DENY,
                    matched: true,
                    reason: `Insufficient trust tier: requires T${policy.requiredTrustTier}, has T${context.subject.trustTier}`,
                };
            }
        }
        return { effect: policy.effect, matched: true };
    }
    /**
     * Evaluate time-based policy
     */
    evaluateTimeBasedPolicy(policy, context) {
        if (policy.timeRestrictions && policy.timeRestrictions.length > 0) {
            const withinWindow = this.checkTimeRestrictions(policy.timeRestrictions, context.environment.timestamp);
            if (!withinWindow) {
                return {
                    effect: PolicyEffects.DENY,
                    matched: true,
                    reason: 'Access outside allowed time window',
                };
            }
        }
        return { effect: policy.effect, matched: true };
    }
    /**
     * Evaluate conditional policy
     */
    evaluateConditionalPolicy(policy, context) {
        if (!policy.conditions) {
            return { effect: policy.effect, matched: true };
        }
        const conditionsMet = this.checkConditions(policy.conditions, context);
        if (!conditionsMet) {
            return {
                effect: PolicyEffects.DENY,
                matched: true,
                reason: 'Policy conditions not met',
            };
        }
        return { effect: policy.effect, matched: true };
    }
    /**
     * Evaluate composite policy (with child policies)
     */
    async evaluateCompositePolicy(policy, context) {
        if (!policy.childPolicies || policy.childPolicies.length === 0) {
            return { effect: policy.effect, matched: true };
        }
        const childEffects = [];
        for (const childPolicy of policy.childPolicies) {
            if (!childPolicy.enabled)
                continue;
            const evaluation = await this.evaluatePolicy(childPolicy, context);
            if (evaluation.matched) {
                childEffects.push({
                    effect: evaluation.effect,
                    policy: childPolicy,
                    priority: childPolicy.priority,
                });
            }
        }
        const algorithm = policy.combiningAlgorithm ?? this.options.defaultCombiningAlgorithm;
        const { effect } = this.combineEffects(childEffects.map(e => ({
            effect: e.effect,
            policy: {
                policyId: e.policy.id,
                policyName: e.policy.name,
                policyType: e.policy.type,
                effect: e.effect,
                matched: true,
                durationMs: 0,
            },
            priority: e.priority,
        })), algorithm);
        return { effect, matched: true };
    }
    // ===========================================================================
    // PERMISSION CHECKING
    // ===========================================================================
    /**
     * Check if a subject has specific permissions
     */
    async checkPermissions(request) {
        const startTime = performance.now();
        const grantedPermissions = [];
        const deniedPermissions = [];
        // Get subject's effective permissions from roles
        const subjectPermissions = await this.getSubjectPermissions(request.subjectId, request.tenantId);
        for (const required of request.permissions) {
            if (hasPermission(subjectPermissions, required)) {
                grantedPermissions.push(required);
            }
            else {
                deniedPermissions.push(required);
            }
        }
        const granted = deniedPermissions.length === 0;
        const result = {
            granted,
            grantedPermissions,
            deniedPermissions,
            reason: granted
                ? undefined
                : `Missing permissions: ${deniedPermissions.join(', ')}`,
            grantSource: granted ? this.determineGrantSource(subjectPermissions, grantedPermissions) : undefined,
            evaluatedAt: new Date().toISOString(),
        };
        permissionChecks.inc({
            result: granted ? 'granted' : 'denied',
            resource_type: request.resourceType ?? 'unknown',
        });
        logger.debug({
            subjectId: request.subjectId,
            permissions: request.permissions,
            granted,
            durationMs: performance.now() - startTime,
        }, 'Permission check completed');
        return result;
    }
    /**
     * Get all effective permissions for a subject
     */
    async getSubjectPermissions(subjectId, tenantId) {
        const permissions = new Set();
        // Get role assignments for subject
        const assignments = this.roleAssignments.get(subjectId) ?? [];
        for (const assignment of assignments) {
            if (!assignment.active)
                continue;
            if (assignment.expiresAt && new Date(assignment.expiresAt) < new Date())
                continue;
            if (tenantId && assignment.tenantId && assignment.tenantId !== tenantId)
                continue;
            const role = this.roleRegistry.get(assignment.roleId);
            if (!role || !role.enabled)
                continue;
            // Get effective permissions for role (including inherited)
            const rolePermissions = getEffectivePermissions(role, this.roleRegistry);
            for (const perm of rolePermissions) {
                permissions.add(perm);
            }
        }
        return permissions;
    }
    /**
     * Get all roles assigned to a subject
     */
    async getSubjectRoles(subjectId, tenantId) {
        const roles = [];
        const assignments = this.roleAssignments.get(subjectId) ?? [];
        for (const assignment of assignments) {
            if (!assignment.active)
                continue;
            if (assignment.expiresAt && new Date(assignment.expiresAt) < new Date())
                continue;
            if (tenantId && assignment.tenantId && assignment.tenantId !== tenantId)
                continue;
            const role = this.roleRegistry.get(assignment.roleId);
            if (role && role.enabled) {
                roles.push(role);
            }
        }
        return roles;
    }
    // ===========================================================================
    // POLICY MANAGEMENT
    // ===========================================================================
    /**
     * Add an access policy
     */
    addPolicy(policy) {
        accessPolicySchema.parse(policy);
        this.policies.set(policy.id, policy);
        this.clearCache();
        logger.info({ policyId: policy.id, policyName: policy.name }, 'Policy added');
    }
    /**
     * Update an access policy
     */
    updatePolicy(policyId, updates) {
        const existing = this.policies.get(policyId);
        if (!existing) {
            throw new Error(`Policy not found: ${policyId}`);
        }
        const updated = {
            ...existing,
            ...updates,
            updatedAt: new Date().toISOString(),
        };
        accessPolicySchema.parse(updated);
        this.policies.set(policyId, updated);
        this.clearCache();
        logger.info({ policyId }, 'Policy updated');
    }
    /**
     * Remove an access policy
     */
    removePolicy(policyId) {
        const removed = this.policies.delete(policyId);
        if (removed) {
            this.clearCache();
            logger.info({ policyId }, 'Policy removed');
        }
        return removed;
    }
    /**
     * Get a policy by ID
     */
    getPolicy(policyId) {
        return this.policies.get(policyId);
    }
    /**
     * Get all policies
     */
    getAllPolicies() {
        return Array.from(this.policies.values());
    }
    /**
     * Get enabled policies
     */
    getEnabledPolicies() {
        return Array.from(this.policies.values()).filter(p => p.enabled);
    }
    // ===========================================================================
    // ROLE MANAGEMENT
    // ===========================================================================
    /**
     * Add a custom role
     */
    addRole(role) {
        if (role.isSystemRole) {
            throw new Error('Cannot add system role');
        }
        this.roleRegistry.set(role.id, role);
        this.clearCache();
        logger.info({ roleId: role.id, roleName: role.name }, 'Role added');
    }
    /**
     * Update a custom role
     */
    updateRole(roleId, updates) {
        const existing = this.roleRegistry.get(roleId);
        if (!existing) {
            throw new Error(`Role not found: ${roleId}`);
        }
        if (existing.isSystemRole) {
            throw new Error('Cannot modify system role');
        }
        const updated = {
            ...existing,
            ...updates,
            updatedAt: new Date().toISOString(),
        };
        this.roleRegistry.set(roleId, updated);
        this.clearCache();
        logger.info({ roleId }, 'Role updated');
    }
    /**
     * Remove a custom role
     */
    removeRole(roleId) {
        const existing = this.roleRegistry.get(roleId);
        if (existing?.isSystemRole) {
            throw new Error('Cannot remove system role');
        }
        const removed = this.roleRegistry.delete(roleId);
        if (removed) {
            this.clearCache();
            logger.info({ roleId }, 'Role removed');
        }
        return removed;
    }
    /**
     * Get a role by ID
     */
    getRole(roleId) {
        return this.roleRegistry.get(roleId);
    }
    /**
     * Get all roles
     */
    getAllRoles() {
        return Array.from(this.roleRegistry.values());
    }
    // ===========================================================================
    // ROLE ASSIGNMENT
    // ===========================================================================
    /**
     * Assign a role to a subject
     */
    assignRole(assignment) {
        const role = this.roleRegistry.get(assignment.roleId);
        if (!role) {
            throw new Error(`Role not found: ${assignment.roleId}`);
        }
        const assignments = this.roleAssignments.get(assignment.subjectId) ?? [];
        assignments.push(assignment);
        this.roleAssignments.set(assignment.subjectId, assignments);
        this.clearCache();
        logger.info({
            subjectId: assignment.subjectId,
            roleId: assignment.roleId,
        }, 'Role assigned');
    }
    /**
     * Revoke a role from a subject
     */
    revokeRole(subjectId, roleId, tenantId) {
        const assignments = this.roleAssignments.get(subjectId);
        if (!assignments)
            return false;
        const index = assignments.findIndex(a => a.roleId === roleId &&
            (!tenantId || a.tenantId === tenantId));
        if (index === -1)
            return false;
        assignments[index].active = false;
        this.clearCache();
        logger.info({ subjectId, roleId }, 'Role revoked');
        return true;
    }
    /**
     * Get role assignments for a subject
     */
    getRoleAssignments(subjectId) {
        return this.roleAssignments.get(subjectId) ?? [];
    }
    // ===========================================================================
    // HELPER METHODS
    // ===========================================================================
    /**
     * Get applicable policies for a context
     */
    getApplicablePolicies(context) {
        return Array.from(this.policies.values())
            .filter(policy => {
            if (!policy.enabled)
                return false;
            // Check tenant scope
            if (policy.tenantId && policy.tenantId !== context.environment.tenantId) {
                return false;
            }
            return true;
        })
            .sort((a, b) => b.priority - a.priority); // Higher priority first
    }
    /**
     * Check if a policy targets the subject
     */
    policyTargetsSubject(policy, context) {
        if (!policy.target)
            return true;
        const target = policy.target;
        // Check exclusions first
        if (target.excludeSubjectIds?.includes(context.subject.id)) {
            return false;
        }
        // Check subject ID
        if (target.subjectIds && !target.subjectIds.includes(context.subject.id)) {
            return false;
        }
        // Check subject type
        if (target.subjectTypes && !target.subjectTypes.includes(context.subject.type)) {
            return false;
        }
        // Check role
        if (target.roleIds) {
            const hasRole = target.roleIds.some(roleId => context.subject.roles.includes(roleId));
            if (!hasRole)
                return false;
        }
        // Check tenant
        if (target.tenantIds && context.environment.tenantId) {
            if (!target.tenantIds.includes(context.environment.tenantId)) {
                return false;
            }
        }
        return true;
    }
    /**
     * Check policy conditions
     */
    checkConditions(conditions, context) {
        // Check IP address
        if (conditions.ipAddresses && context.environment.ipAddress) {
            const matchesIp = conditions.ipAddresses.some(allowed => this.matchIpAddress(context.environment.ipAddress, allowed));
            if (!matchesIp)
                return false;
        }
        // Check time window
        if (conditions.timeWindow) {
            const withinWindow = this.checkTimeRestriction(conditions.timeWindow, context.environment.timestamp);
            if (!withinWindow)
                return false;
        }
        // Check custom attributes
        if (conditions.attributes) {
            for (const [key, value] of Object.entries(conditions.attributes)) {
                const subjectValue = context.subject.attributes?.[key];
                if (subjectValue !== value)
                    return false;
            }
        }
        return true;
    }
    /**
     * Check time restrictions
     */
    checkTimeRestrictions(restrictions, now) {
        return restrictions.some(restriction => this.checkTimeRestriction(restriction, now));
    }
    /**
     * Check a single time restriction
     */
    checkTimeRestriction(restriction, now) {
        const hour = now.getHours();
        const minute = now.getMinutes();
        const currentTime = hour * 60 + minute;
        const [startHour, startMinute] = restriction.startTime.split(':').map(Number);
        const [endHour, endMinute] = restriction.endTime.split(':').map(Number);
        const startTime = startHour * 60 + startMinute;
        const endTime = endHour * 60 + endMinute;
        // Check time range
        let withinTime;
        if (startTime <= endTime) {
            withinTime = currentTime >= startTime && currentTime <= endTime;
        }
        else {
            // Wrapping (e.g., 22:00 to 06:00)
            withinTime = currentTime >= startTime || currentTime <= endTime;
        }
        if (!withinTime)
            return false;
        // Check day of week
        if (restriction.daysOfWeek.length > 0) {
            const day = now.getDay();
            if (!restriction.daysOfWeek.includes(day))
                return false;
        }
        return true;
    }
    /**
     * Combine policy effects using the specified algorithm
     */
    combineEffects(effects, algorithm) {
        if (effects.length === 0) {
            return { effect: this.options.defaultEffect };
        }
        switch (algorithm) {
            case CombiningAlgorithms.DENY_OVERRIDES: {
                const deny = effects.find(e => e.effect === PolicyEffects.DENY);
                if (deny)
                    return { effect: PolicyEffects.DENY, decidingPolicy: deny.policy };
                const allow = effects.find(e => e.effect === PolicyEffects.ALLOW);
                if (allow)
                    return { effect: PolicyEffects.ALLOW, decidingPolicy: allow.policy };
                return { effect: PolicyEffects.INDETERMINATE };
            }
            case CombiningAlgorithms.PERMIT_OVERRIDES: {
                const allow = effects.find(e => e.effect === PolicyEffects.ALLOW);
                if (allow)
                    return { effect: PolicyEffects.ALLOW, decidingPolicy: allow.policy };
                const deny = effects.find(e => e.effect === PolicyEffects.DENY);
                if (deny)
                    return { effect: PolicyEffects.DENY, decidingPolicy: deny.policy };
                return { effect: PolicyEffects.INDETERMINATE };
            }
            case CombiningAlgorithms.UNANIMOUS: {
                const allAllow = effects.every(e => e.effect === PolicyEffects.ALLOW);
                if (allAllow)
                    return { effect: PolicyEffects.ALLOW, decidingPolicy: effects[0]?.policy };
                const anyDeny = effects.find(e => e.effect === PolicyEffects.DENY);
                return { effect: PolicyEffects.DENY, decidingPolicy: anyDeny?.policy };
            }
            case CombiningAlgorithms.FIRST_APPLICABLE: {
                const first = effects[0];
                if (first)
                    return { effect: first.effect, decidingPolicy: first.policy };
                return { effect: PolicyEffects.INDETERMINATE };
            }
            case CombiningAlgorithms.PRIORITY_BASED: {
                // Sort by priority (highest first) and take first
                const sorted = [...effects].sort((a, b) => b.priority - a.priority);
                const highest = sorted[0];
                if (highest)
                    return { effect: highest.effect, decidingPolicy: highest.policy };
                return { effect: PolicyEffects.INDETERMINATE };
            }
            default:
                return { effect: this.options.defaultEffect };
        }
    }
    /**
     * Match a pattern (supports wildcards)
     */
    matchPattern(value, pattern) {
        if (pattern === '*')
            return true;
        if (pattern.endsWith('*')) {
            return value.startsWith(pattern.slice(0, -1));
        }
        if (pattern.startsWith('*')) {
            return value.endsWith(pattern.slice(1));
        }
        return value === pattern;
    }
    /**
     * Match IP address against a pattern.
     *
     * Supports:
     * - Wildcard '*' (matches any IP)
     * - CIDR notation for IPv4 (e.g., "192.168.1.0/24")
     * - CIDR notation for IPv6 (e.g., "fe80::/10")
     * - Exact string match as fallback
     *
     * When a CIDR pattern omits the prefix length, it defaults to
     * /32 for IPv4 and /128 for IPv6 (exact host match).
     */
    matchIpAddress(ip, pattern) {
        if (pattern === '*')
            return true;
        // Attempt CIDR match if the pattern looks like an IP or CIDR
        try {
            return RBACPolicyEngine.ipMatchesCidr(ip, pattern);
        }
        catch {
            // If parsing fails, fall back to exact string comparison
            return ip === pattern;
        }
    }
    // ---------------------------------------------------------------------------
    // Static IP / CIDR helpers (pure TypeScript, no external dependencies)
    // ---------------------------------------------------------------------------
    /**
     * Determine whether `ip` falls within the network described by `cidr`.
     * `cidr` may be a bare IP (treated as /32 or /128) or "ip/prefix".
     */
    static ipMatchesCidr(ip, cidr) {
        const [networkStr, prefixStr] = cidr.split('/');
        const isIpV6 = RBACPolicyEngine.isIPv6(ip);
        const isPatternV6 = RBACPolicyEngine.isIPv6(networkStr);
        // IP family mismatch → no match
        if (isIpV6 !== isPatternV6)
            return false;
        if (isIpV6) {
            const defaultPrefix = 128;
            const prefixLen = prefixStr !== undefined ? parseInt(prefixStr, 10) : defaultPrefix;
            if (isNaN(prefixLen) || prefixLen < 0 || prefixLen > 128)
                return false;
            const ipBits = RBACPolicyEngine.ipv6ToBits(ip);
            const networkBits = RBACPolicyEngine.ipv6ToBits(networkStr);
            return RBACPolicyEngine.bitsMatchPrefix(ipBits, networkBits, prefixLen);
        }
        else {
            const defaultPrefix = 32;
            const prefixLen = prefixStr !== undefined ? parseInt(prefixStr, 10) : defaultPrefix;
            if (isNaN(prefixLen) || prefixLen < 0 || prefixLen > 32)
                return false;
            const ipNum = RBACPolicyEngine.ipv4ToNumber(ip);
            const networkNum = RBACPolicyEngine.ipv4ToNumber(networkStr);
            // Throw on unparseable addresses so the caller can fall back to
            // exact string comparison for non-IP values (e.g., hostnames).
            if (ipNum === null || networkNum === null) {
                throw new Error(`Invalid IPv4 address: ip=${ip}, network=${networkStr}`);
            }
            if (prefixLen === 0)
                return true; // /0 matches everything
            // Build a 32-bit mask: e.g. prefix 24 → 0xFFFFFF00
            const mask = (~0 << (32 - prefixLen)) >>> 0;
            return ((ipNum & mask) >>> 0) === ((networkNum & mask) >>> 0);
        }
    }
    /** Returns true when the address string contains a colon (→ IPv6). */
    static isIPv6(addr) {
        return addr.includes(':');
    }
    /**
     * Parse a dotted-quad IPv4 string into a 32-bit unsigned integer.
     * Returns null on invalid input.
     */
    static ipv4ToNumber(ip) {
        const parts = ip.split('.');
        if (parts.length !== 4)
            return null;
        let num = 0;
        for (const part of parts) {
            const octet = parseInt(part, 10);
            if (isNaN(octet) || octet < 0 || octet > 255)
                return null;
            num = ((num << 8) | octet) >>> 0;
        }
        return num;
    }
    /**
     * Expand an IPv6 address (handling :: shorthand) into a 128-element
     * bit array (array of 0 | 1).
     */
    static ipv6ToBits(addr) {
        // Handle IPv4-mapped IPv6 (e.g., ::ffff:192.168.1.1)
        const v4Suffix = addr.match(/::ffff:(\d+\.\d+\.\d+\.\d+)$/i);
        if (v4Suffix) {
            const ipv4Num = RBACPolicyEngine.ipv4ToNumber(v4Suffix[1]);
            if (ipv4Num === null)
                throw new Error(`Invalid IPv4-mapped IPv6: ${addr}`);
            // Construct the full 128-bit representation
            // First 80 bits are 0, next 16 bits are 0xFFFF, last 32 bits are the IPv4
            const bits = new Array(80).fill(0);
            for (let i = 15; i >= 0; i--) {
                bits.push((0xFFFF >> (15 - i)) & 1);
            }
            for (let i = 31; i >= 0; i--) {
                bits.push((ipv4Num >> i) & 1);
            }
            return bits;
        }
        // Expand :: into the correct number of zero groups
        let fullAddr = addr;
        if (fullAddr.includes('::')) {
            const halves = fullAddr.split('::');
            const left = halves[0] ? halves[0].split(':') : [];
            const right = halves[1] ? halves[1].split(':') : [];
            const missing = 8 - left.length - right.length;
            const middle = new Array(Math.max(missing, 0)).fill('0');
            fullAddr = [...left, ...middle, ...right].join(':');
        }
        const groups = fullAddr.split(':');
        if (groups.length !== 8)
            throw new Error(`Invalid IPv6 address: ${addr}`);
        const bits = [];
        for (const group of groups) {
            const val = parseInt(group || '0', 16);
            if (isNaN(val) || val < 0 || val > 0xFFFF) {
                throw new Error(`Invalid IPv6 group "${group}" in ${addr}`);
            }
            for (let i = 15; i >= 0; i--) {
                bits.push((val >> i) & 1);
            }
        }
        return bits;
    }
    /**
     * Compare the first `prefixLen` bits of two 128-element bit arrays.
     */
    static bitsMatchPrefix(a, b, prefixLen) {
        for (let i = 0; i < prefixLen; i++) {
            if (a[i] !== b[i])
                return false;
        }
        return true;
    }
    /**
     * Get missing requirements for a denied request
     */
    getMissingRequirements(context, policies) {
        const missingPermissions = [];
        const missingRoles = [];
        let requiredTrustTier;
        for (const policy of policies) {
            if (policy.requiredPermissions) {
                for (const perm of policy.requiredPermissions) {
                    if (!hasPermission(context.subject.permissions, perm)) {
                        missingPermissions.push(perm);
                    }
                }
            }
            if (policy.requiredRoles) {
                for (const role of policy.requiredRoles) {
                    if (!context.subject.roles.includes(role)) {
                        missingRoles.push(role);
                    }
                }
            }
            if (policy.requiredTrustTier !== undefined) {
                if (context.subject.trustTier < policy.requiredTrustTier) {
                    if (!requiredTrustTier || policy.requiredTrustTier > requiredTrustTier) {
                        requiredTrustTier = policy.requiredTrustTier;
                    }
                }
            }
        }
        return {
            permissions: missingPermissions.length > 0 ? [...new Set(missingPermissions)] : undefined,
            roles: missingRoles.length > 0 ? [...new Set(missingRoles)] : undefined,
            trustTier: requiredTrustTier,
        };
    }
    /**
     * Build decision reason string
     */
    buildDecisionReason(effect, decidingPolicy, missingRequirements) {
        if (effect === PolicyEffects.ALLOW) {
            return decidingPolicy
                ? `Access granted by policy: ${decidingPolicy.policyName}`
                : 'Access granted (default)';
        }
        if (effect === PolicyEffects.DENY) {
            const reasons = [];
            if (decidingPolicy?.reason) {
                reasons.push(decidingPolicy.reason);
            }
            else if (decidingPolicy) {
                reasons.push(`Denied by policy: ${decidingPolicy.policyName}`);
            }
            if (missingRequirements?.permissions?.length) {
                reasons.push(`Missing permissions: ${missingRequirements.permissions.join(', ')}`);
            }
            if (missingRequirements?.roles?.length) {
                reasons.push(`Missing roles: ${missingRequirements.roles.join(', ')}`);
            }
            if (missingRequirements?.trustTier !== undefined) {
                reasons.push(`Insufficient trust tier: requires T${missingRequirements.trustTier}`);
            }
            return reasons.length > 0 ? reasons.join('; ') : 'Access denied';
        }
        return 'Unable to determine access';
    }
    /**
     * Determine how permission was granted
     */
    determineGrantSource(permissions, grantedPermissions) {
        if (permissions.has('*')) {
            return 'wildcard';
        }
        // Check if any are direct matches vs wildcards
        for (const perm of grantedPermissions) {
            if (permissions.has(perm)) {
                return 'direct';
            }
        }
        return 'inherited';
    }
    /**
     * Build cache key for evaluation context
     */
    buildCacheKey(context) {
        return [
            context.subject.id,
            context.subject.type,
            context.resource.type,
            context.resource.action,
            context.resource.id ?? '',
            context.environment.tenantId ?? '',
        ].join(':');
    }
    /**
     * Clear evaluation cache
     */
    clearCache() {
        this.evaluationCache.clear();
    }
    /**
     * Log access decision for audit
     */
    async logAccessDecision(context, result) {
        const actor = {
            type: context.subject.type === 'user' ? 'user' : 'agent',
            id: context.subject.id,
            tenantId: context.environment.tenantId,
            ip: context.environment.ipAddress,
        };
        const resource = {
            type: context.resource.type,
            id: context.resource.id ?? context.resource.type,
            path: `${context.resource.type}:${context.resource.action}`,
        };
        if (result.permitted) {
            await this.auditLogger.logAccessGranted(actor, resource, {
                effect: result.effect,
                decidingPolicy: result.decidingPolicy?.policyId,
                durationMs: result.durationMs,
            });
        }
        else {
            await this.auditLogger.logAccessDenied(actor, resource, result.reason, {
                effect: result.effect,
                decidingPolicy: result.decidingPolicy?.policyId,
                missingRequirements: result.missingRequirements,
                durationMs: result.durationMs,
            });
        }
    }
    /**
     * Get engine statistics
     */
    getStats() {
        const roles = Array.from(this.roleRegistry.values());
        let totalAssignments = 0;
        for (const assignments of this.roleAssignments.values()) {
            totalAssignments += assignments.length;
        }
        return {
            totalPolicies: this.policies.size,
            enabledPolicies: Array.from(this.policies.values()).filter(p => p.enabled).length,
            totalRoles: roles.length,
            systemRoles: roles.filter(r => r.isSystemRole).length,
            customRoles: roles.filter(r => !r.isSystemRole).length,
            totalAssignments,
            cacheSize: this.evaluationCache.size,
        };
    }
    /**
     * Destroy the engine and cleanup resources
     */
    destroy() {
        this.clearCache();
        logger.info('RBAC policy engine destroyed');
    }
}
/**
 * Create a new RBAC policy engine
 */
export function createRBACPolicyEngine(options) {
    return new RBACPolicyEngine(options);
}
/**
 * Create a policy evaluation context
 */
export function createPolicyEvaluationContext(params) {
    return {
        subject: {
            id: params.subjectId,
            type: params.subjectType,
            trustTier: params.trustTier,
            roles: params.roles,
            permissions: new Set(params.permissions),
            attributes: params.subjectAttributes,
        },
        resource: {
            id: params.resourceId,
            type: params.resourceType,
            action: params.resourceAction,
            tenantId: params.tenantId,
            attributes: params.resourceAttributes,
        },
        environment: {
            timestamp: new Date(),
            ipAddress: params.ipAddress,
            userAgent: params.userAgent,
            tenantId: params.tenantId,
            requestId: params.requestId,
        },
    };
}
/**
 * Create an access policy
 */
export function createAccessPolicy(partial) {
    const now = new Date().toISOString();
    return {
        ...partial,
        enabled: partial.enabled ?? true,
        createdAt: now,
        updatedAt: now,
    };
}
//# sourceMappingURL=policy-engine.js.map