/**
 * Runtime permissions evaluator.
 *
 * Evaluates tool calls through a layered priority stack. First match wins.
 */
import { runSafetyChecks } from './safety-checks.js';
import { DecisionLog } from './decision-log.js';
import { evaluatePrefixRule } from './rules/prefix.js';
import { evaluateArgShapeRule } from './rules/arg-shape.js';
import { evaluatePathScopeRule } from './rules/path-scope.js';
import { evaluateNetworkScopeRule } from './rules/network-scope.js';
import { evaluateModeConstraintRule } from './rules/mode-constraint.js';
// ── Tool classification ──────────────────────────────────────────────────────────
const READ_TOOLS = new Set([
    'read', 'find', 'fetch', 'analyze', 'inspect', 'state', 'registry',
]);
const WRITE_TOOLS = new Set([
    'write', 'edit',
]);
const NETWORK_TOOLS = new Set([
    'fetch', 'http', 'request', 'curl',
]);
const ESCALATION_TOOLS = new Set([
    'agent', 'delegate', 'workflow', 'mcp',
]);
/**
 * classifyTool — Returns the semantic classification of a tool call.
 * For exec/execute, we classify based on arg content (destructive check
 * is done in safety layer; here we return 'write' as the baseline).
 */
function classifyTool(toolName) {
    if (NETWORK_TOOLS.has(toolName))
        return 'network';
    if (ESCALATION_TOOLS.has(toolName))
        return 'escalation';
    if (WRITE_TOOLS.has(toolName))
        return 'write';
    if (READ_TOOLS.has(toolName))
        return 'read';
    // exec-class tools are treated as 'write' (potentially destructive) by default
    return 'write';
}
/**
 * evaluateModeLayer — Applies mode-level constraints.
 *
 * Returns a deny decision for certain tool/classification combinations
 * depending on the active mode, or allow for `allow-all` mode.
 */
function evaluateModeLayer(mode, toolName, classification) {
    switch (mode) {
        case 'allow-all':
            return {
                deny: false,
                allow: true,
                reason: 'MODE_ALLOW_ALL',
                step: {
                    layer: 'mode',
                    check: 'mode-allow-all',
                    matched: true,
                    detail: 'allow-all mode: all tools auto-approved',
                },
            };
        case 'plan':
            // Plan mode blocks write, network, and destructive
            if (classification === 'write' || classification === 'network' || classification === 'destructive') {
                return {
                    deny: true,
                    allow: false,
                    reason: 'MODE_DENY_PLAN',
                    step: {
                        layer: 'mode',
                        check: 'mode-plan-constraint',
                        matched: true,
                        detail: `plan mode denies ${classification} tool "${toolName}"`,
                    },
                };
            }
            break;
        case 'background-restricted':
            // Background mode blocks escalation (agent/delegate) tools
            if (classification === 'escalation') {
                return {
                    deny: true,
                    allow: false,
                    reason: 'MODE_DENY_BACKGROUND',
                    step: {
                        layer: 'mode',
                        check: 'mode-background-constraint',
                        matched: true,
                        detail: `background-restricted mode denies escalation tool "${toolName}"`,
                    },
                };
            }
            break;
        case 'remote-restricted':
            // Remote-restricted mode blocks network tools
            if (classification === 'network') {
                return {
                    deny: true,
                    allow: false,
                    reason: 'MODE_DENY_REMOTE_RESTRICTED',
                    step: {
                        layer: 'mode',
                        check: 'mode-remote-restricted-constraint',
                        matched: true,
                        detail: `remote-restricted mode denies network tool "${toolName}"`,
                    },
                };
            }
            break;
        default:
            // 'default' and 'custom' modes: no mode-level constraints
            break;
    }
    return {
        deny: false,
        allow: false,
        step: {
            layer: 'mode',
            check: `mode-${mode}-constraint`,
            matched: false,
            detail: `mode "${mode}" imposes no constraint on "${toolName}" (${classification})`,
        },
    };
}
/**
 * dispatchPolicyRule — Routes a PolicyRule to its typed evaluator.
 */
function dispatchPolicyRule(rule, toolName, args, activeMode, classification, projectRoot) {
    let matched = false;
    let step;
    switch (rule.type) {
        case 'prefix': {
            const result = evaluatePrefixRule(rule, toolName, args);
            matched = result.matched;
            step = result.step;
            break;
        }
        case 'arg-shape': {
            const result = evaluateArgShapeRule(rule, toolName, args);
            matched = result.matched;
            step = result.step;
            break;
        }
        case 'path-scope': {
            const result = evaluatePathScopeRule(rule, toolName, args, projectRoot);
            matched = result.matched;
            step = result.step;
            break;
        }
        case 'network-scope': {
            const result = evaluateNetworkScopeRule(rule, toolName, args);
            matched = result.matched;
            step = result.step;
            break;
        }
        case 'mode-constraint': {
            const result = evaluateModeConstraintRule(rule, toolName, activeMode, classification);
            matched = result.matched;
            step = result.step;
            break;
        }
    }
    const reason = rule.effect === 'allow'
        ? rule.origin === 'user'
            ? 'RULE_ALLOW_USER'
            : 'RULE_ALLOW_MANAGED'
        : rule.origin === 'user'
            ? 'RULE_DENY_USER'
            : 'RULE_DENY_MANAGED';
    return { matched, effect: rule.effect, reason, step };
}
// ── LayeredPolicyEvaluator ───────────────────────────────────────────────────────
/**
 * LayeredPolicyEvaluator — Core runtime permissions evaluator.
 *
 * Evaluates tool calls through five layers in priority order.
 * Maintains a session approval cache and a structured audit log.
 *
 * Usage:
 * ```ts
 * const evaluator = new LayeredPolicyEvaluator({ mode: 'default', rules: [] });
 * const decision = evaluator.evaluate('write', { path: '/tmp/out.txt' });
 * ```
 */
export class LayeredPolicyEvaluator {
    mode;
    rules;
    defaultEffect;
    projectRoot;
    sessionCache = new Map();
    sessionCacheInsertOrder = [];
    log;
    /** GC-PERM-011: Provenance from the loaded policy bundle, if any. */
    provenance;
    static MAX_SESSION_CACHE_SIZE = 500;
    constructor(config, provenance) {
        this.mode = config.mode ?? 'default';
        this.rules = config.rules ?? [];
        this.defaultEffect = config.defaultEffect ?? 'deny';
        this.projectRoot = config.projectRoot;
        this.log = new DecisionLog();
        this.provenance = provenance;
    }
    /**
     * evaluate — Evaluates a tool call and returns a PermissionDecision.
     *
     * Runs all five layers in priority order. Populates a full evaluation trace.
     * Appends the decision to the audit log if `auditLog` is enabled (default: true).
     *
     * @param toolName — The tool name being called.
     * @param args     — The arguments passed to the tool.
     */
    evaluate(toolName, args) {
        const trace = [];
        const classification = classifyTool(toolName);
        // ── Layer 1: Safety checks (bypass-immune) ──────────────────────────
        const safety = runSafetyChecks(toolName, args);
        trace.push(...safety.steps);
        if (safety.blocked) {
            return this.finalize({
                allowed: false,
                reason: safety.reason ?? 'SAFETY_DENY_GUARDRAIL',
                sourceLayer: 'safety',
                toolName,
                args,
                classification: safety.classification ?? classification,
                trace,
            });
        }
        // ── Layer 2: Mode constraints ───────────────────────────────────
        const modeResult = evaluateModeLayer(this.mode, toolName, classification);
        trace.push(modeResult.step);
        if (modeResult.deny) {
            return this.finalize({
                allowed: false,
                reason: modeResult.reason ?? 'MODE_DENY_PLAN',
                sourceLayer: 'mode',
                toolName,
                args,
                classification,
                trace,
            });
        }
        if (modeResult.allow) {
            return this.finalize({
                allowed: true,
                reason: modeResult.reason ?? 'MODE_ALLOW_ALL',
                sourceLayer: 'mode',
                toolName,
                args,
                classification,
                trace,
            });
        }
        // ── Layer 3: Session overrides ─────────────────────────────────
        const sessionKey = this.getSessionKey(toolName, args);
        const cached = this.sessionCache.get(sessionKey);
        if (cached !== undefined) {
            const cacheReason = cached
                ? 'SESSION_CACHED_ALLOW'
                : 'SESSION_CACHED_DENY';
            trace.push({
                layer: 'session',
                check: 'session-cache',
                matched: true,
                detail: `cache hit for key "${sessionKey}" → ${cached ? 'allow' : 'deny'}`,
            });
            return this.finalize({
                allowed: cached,
                reason: cacheReason,
                sourceLayer: 'session',
                toolName,
                args,
                classification,
                trace,
            });
        }
        trace.push({
            layer: 'session',
            check: 'session-cache',
            matched: false,
            detail: `no session cache entry for key "${sessionKey}"`,
        });
        // ── Layer 4: Policy rules (user before managed, first match wins) ─────
        const userRules = this.rules.filter((r) => r.origin === 'user');
        const managedRules = this.rules.filter((r) => r.origin === 'managed');
        const orderedRules = [...userRules, ...managedRules];
        for (const rule of orderedRules) {
            const result = dispatchPolicyRule(rule, toolName, args, this.mode, classification, this.projectRoot);
            trace.push(result.step);
            if (result.matched) {
                return this.finalize({
                    allowed: result.effect === 'allow',
                    reason: result.reason,
                    sourceLayer: 'policy',
                    toolName,
                    args,
                    classification,
                    trace,
                });
            }
        }
        // ── Layer 5: Default policy ─────────────────────────────────────
        // Default: auto-allow reads, apply configured defaultEffect for everything else
        const defaultAllow = this.defaultEffect === 'allow' || classification === 'read';
        const defaultReason = defaultAllow ? 'DEFAULT_ALLOW' : 'DEFAULT_DENY';
        trace.push({
            layer: 'default',
            check: 'default-policy',
            matched: true,
            detail: `no rule matched; default=${defaultAllow ? 'allow' : 'deny'} (classification=${classification})`,
        });
        return this.finalize({
            allowed: defaultAllow,
            reason: defaultReason,
            sourceLayer: 'default',
            toolName,
            args,
            classification,
            trace,
        });
    }
    /**
     * recordSessionOverride — Records a user-provided session approval/denial
     * in the cache (used after a user prompt resolves).
     *
     * @param toolName  — Tool name.
     * @param args      — Tool arguments.
     * @param approved  — Whether the user approved or denied.
     * @param remember  — Whether to persist for the session (default: false = once only).
     */
    recordSessionOverride(toolName, args, approved, remember = false) {
        if (remember) {
            const key = this.getSessionKey(toolName, args);
            if (!this.sessionCache.has(key)) {
                // Evict oldest entry if at capacity
                if (this.sessionCacheInsertOrder.length >= LayeredPolicyEvaluator.MAX_SESSION_CACHE_SIZE) {
                    const oldest = this.sessionCacheInsertOrder.shift();
                    this.sessionCache.delete(oldest);
                }
                this.sessionCacheInsertOrder.push(key);
            }
            this.sessionCache.set(key, approved);
        }
    }
    /**
     * getMode — Returns the active PermissionMode.
     */
    getMode() {
        return this.mode;
    }
    // ── Private helpers ────────────────────────────────────────────────────────
    finalize(params) {
        const decision = {
            allowed: params.allowed,
            reason: params.reason,
            sourceLayer: params.sourceLayer,
            toolName: params.toolName,
            args: params.args,
            classification: params.classification,
            timestamp: Date.now(),
            evaluationTrace: params.trace,
            // GC-PERM-011: Attach provenance from the loaded policy bundle
            ...(this.provenance !== undefined && {
                policyBundleId: this.provenance.policyBundleId,
                signatureStatus: this.provenance.signatureStatus,
                provenanceSource: this.provenance.provenanceSource,
            }),
        };
        this.log.append(decision);
        return decision;
    }
    getSessionKey(toolName, args) {
        if (typeof args['path'] === 'string')
            return `${toolName}:path:${args['path']}`;
        if (typeof args['command'] === 'string')
            return `${toolName}:cmd:${args['command']}`;
        if (typeof args['url'] === 'string')
            return `${toolName}:url:${args['url']}`;
        const argKeysHash = JSON.stringify(Object.keys(args).sort());
        return `${toolName}:${argKeysHash}`;
    }
}
