/**
 * Approval Queue Service
 *
 * Manages pending tool approval requests for the Crewly Agent runtime.
 * When a tool requires approval (based on SecurityPolicy.requireApproval),
 * execution is blocked and a PendingApproval entry is created. External
 * callers (API, auditor) can then approve or reject the request.
 *
 * @module services/agent/crewly-agent/approval-queue.service
 */
/** Maximum number of pending approvals before oldest are auto-rejected */
const MAX_PENDING_APPROVALS = 100;
/** Default TTL for pending approvals in milliseconds (10 minutes) */
const DEFAULT_APPROVAL_TTL_MS = 10 * 60 * 1000;
/**
 * In-memory approval queue for tool execution requests.
 *
 * Stores pending approvals and provides approve/reject operations.
 * Approvals that exceed the TTL are automatically marked as expired
 * when queried.
 *
 * @example
 * ```typescript
 * const queue = new ApprovalQueueService();
 * const approval = queue.enqueue('session-1', 'edit_file', 'destructive', { path: '/foo' });
 * // Later, approve it:
 * const result = queue.approve(approval.id, 'api');
 * ```
 */
export class ApprovalQueueService {
    static instance = null;
    approvals = new Map();
    idCounter = 0;
    ttlMs;
    /**
     * Get the shared singleton instance.
     * All agent-runners and API controllers share the same queue.
     *
     * @returns The shared ApprovalQueueService instance
     */
    static getInstance() {
        if (!ApprovalQueueService.instance) {
            ApprovalQueueService.instance = new ApprovalQueueService();
        }
        return ApprovalQueueService.instance;
    }
    /**
     * Reset the singleton instance (for testing only).
     */
    static resetInstance() {
        ApprovalQueueService.instance = null;
    }
    /**
     * Create a new ApprovalQueueService.
     *
     * @param ttlMs - Time-to-live for pending approvals in milliseconds
     */
    constructor(ttlMs = DEFAULT_APPROVAL_TTL_MS) {
        this.ttlMs = ttlMs;
    }
    /**
     * Add a new approval request to the queue.
     *
     * @param sessionName - Agent session requesting approval
     * @param toolName - Name of the tool
     * @param sensitivity - Sensitivity classification
     * @param args - Sanitized tool arguments
     * @returns The created PendingApproval entry
     */
    enqueue(sessionName, toolName, sensitivity, args) {
        this.expireStale();
        // Enforce max pending limit — reject oldest if full
        if (this.getPendingCount() >= MAX_PENDING_APPROVALS) {
            const oldest = this.getOldestPending();
            if (oldest) {
                oldest.status = 'rejected';
                oldest.resolvedAt = new Date().toISOString();
                oldest.resolvedBy = 'auto-overflow';
                oldest.reason = 'Approval queue overflow — auto-rejected oldest entry';
            }
        }
        this.idCounter++;
        const id = `approval-${this.idCounter}-${Date.now()}`;
        const approval = {
            id,
            sessionName,
            toolName,
            sensitivity,
            args,
            status: 'pending',
            requestedAt: new Date().toISOString(),
        };
        this.approvals.set(id, approval);
        return approval;
    }
    /**
     * Approve a pending approval request.
     *
     * @param id - Approval ID to approve
     * @param resolvedBy - Who is approving (e.g. 'api', 'auditor')
     * @returns Resolution result
     */
    approve(id, resolvedBy = 'api') {
        return this.resolve(id, 'approved', resolvedBy);
    }
    /**
     * Reject a pending approval request.
     *
     * @param id - Approval ID to reject
     * @param resolvedBy - Who is rejecting
     * @param reason - Reason for rejection
     * @returns Resolution result
     */
    reject(id, resolvedBy = 'api', reason) {
        return this.resolve(id, 'rejected', resolvedBy, reason);
    }
    /**
     * Get all pending approval requests.
     *
     * @param sessionName - Optional filter by session name
     * @returns Array of pending approvals
     */
    getPending(sessionName) {
        this.expireStale();
        const pending = [];
        for (const approval of this.approvals.values()) {
            if (approval.status !== 'pending')
                continue;
            if (sessionName && approval.sessionName !== sessionName)
                continue;
            pending.push({ ...approval });
        }
        return pending;
    }
    /**
     * Get a specific approval by ID.
     *
     * @param id - Approval ID
     * @returns The approval entry or undefined
     */
    getById(id) {
        const approval = this.approvals.get(id);
        if (!approval)
            return undefined;
        return { ...approval };
    }
    /**
     * Get the count of currently pending approvals.
     *
     * @returns Number of pending approvals
     */
    getPendingCount() {
        let count = 0;
        for (const approval of this.approvals.values()) {
            if (approval.status === 'pending')
                count++;
        }
        return count;
    }
    /**
     * Clear all approvals (for testing or reset).
     */
    clear() {
        this.approvals.clear();
        this.idCounter = 0;
    }
    /**
     * Resolve a pending approval with the given status.
     *
     * @param id - Approval ID
     * @param status - New status
     * @param resolvedBy - Who resolved it
     * @param reason - Optional reason (for rejections)
     * @returns Resolution result
     */
    resolve(id, status, resolvedBy, reason) {
        const approval = this.approvals.get(id);
        if (!approval) {
            return { success: false, error: `Approval '${id}' not found` };
        }
        if (approval.status !== 'pending') {
            return {
                success: false,
                error: `Approval '${id}' is already ${approval.status}`,
                approval: { ...approval },
            };
        }
        approval.status = status;
        approval.resolvedAt = new Date().toISOString();
        approval.resolvedBy = resolvedBy;
        if (reason)
            approval.reason = reason;
        return { success: true, approval: { ...approval } };
    }
    /**
     * Expire stale pending approvals that have exceeded the TTL.
     */
    expireStale() {
        const now = Date.now();
        const resolvedRetentionMs = this.ttlMs * 3; // Keep resolved entries 3x TTL then prune
        for (const [id, approval] of this.approvals.entries()) {
            if (approval.status === 'pending') {
                const requestedAt = new Date(approval.requestedAt).getTime();
                if (now - requestedAt > this.ttlMs) {
                    approval.status = 'expired';
                    approval.resolvedAt = new Date().toISOString();
                    approval.resolvedBy = 'auto-expire';
                    approval.reason = 'Approval request expired';
                }
            }
            else if (approval.resolvedAt) {
                // Prune resolved entries that are older than retention window
                const resolvedAt = new Date(approval.resolvedAt).getTime();
                if (now - resolvedAt > resolvedRetentionMs) {
                    this.approvals.delete(id);
                }
            }
        }
    }
    /**
     * Get the oldest pending approval.
     *
     * @returns Oldest pending approval or undefined
     */
    getOldestPending() {
        let oldest;
        for (const approval of this.approvals.values()) {
            if (approval.status !== 'pending')
                continue;
            if (!oldest || approval.requestedAt < oldest.requestedAt) {
                oldest = approval;
            }
        }
        return oldest;
    }
}
//# sourceMappingURL=approval-queue.service.js.map