/**
 * Docker Sandbox Manager
 *
 * Manages per-session Docker container isolation.
 * Provides secure execution environment for tools and commands.
 */
import { EventEmitter } from 'events';
import * as crypto from 'crypto';
import { DEFAULT_SANDBOX_CONFIG, DEFAULT_NETWORK_POLICY, } from './types.js';
// ============================================================================
// Mock Docker Client (for testing/development)
// ============================================================================
/**
 * Mock Docker client for testing when Docker is not available
 */
export class MockDockerClient {
    containers = new Map();
    async isAvailable() {
        return true;
    }
    async createContainer(config) {
        const id = crypto.randomUUID().slice(0, 12);
        this.containers.set(id, { status: 'creating', config });
        return id;
    }
    async startContainer(containerId) {
        const container = this.containers.get(containerId);
        if (container) {
            container.status = 'running';
        }
    }
    async stopContainer(containerId, _timeout) {
        const container = this.containers.get(containerId);
        if (container) {
            container.status = 'stopped';
        }
    }
    async removeContainer(containerId, _force) {
        this.containers.delete(containerId);
    }
    async exec(_containerId, command, _options) {
        // Mock execution
        return {
            exitCode: 0,
            stdout: `Mock execution of: ${command.join(' ')}`,
            stderr: '',
        };
    }
    async getStats(_containerId) {
        return {
            cpuPercent: Math.random() * 10,
            memoryUsage: Math.floor(Math.random() * 100000000),
            memoryLimit: 536870912, // 512MB
            networkRx: Math.floor(Math.random() * 10000),
            networkTx: Math.floor(Math.random() * 10000),
            blockRead: Math.floor(Math.random() * 100000),
            blockWrite: Math.floor(Math.random() * 100000),
        };
    }
    async getStatus(containerId) {
        const container = this.containers.get(containerId);
        return container?.status || 'dead';
    }
    async pullImage(_image) {
        // Mock pull - instant
    }
    async imageExists(_image) {
        return true;
    }
}
// ============================================================================
// Docker Sandbox Manager
// ============================================================================
/**
 * Docker Sandbox Manager
 *
 * Manages Docker containers for isolated code execution per session.
 */
export class DockerSandboxManager extends EventEmitter {
    docker;
    defaultConfig;
    containers = new Map();
    sessionToContainer = new Map();
    healthCheckTimer = null;
    constructor(docker, config) {
        super();
        this.docker = docker || new MockDockerClient();
        this.defaultConfig = { ...DEFAULT_SANDBOX_CONFIG, ...config };
    }
    // ============================================================================
    // Initialization
    // ============================================================================
    /**
     * Initialize the sandbox manager
     */
    async initialize() {
        // Check Docker availability
        const available = await this.docker.isAvailable();
        if (!available) {
            throw new Error('Docker is not available');
        }
        // Ensure image exists
        const imageExists = await this.docker.imageExists(this.defaultConfig.image);
        if (!imageExists) {
            await this.docker.pullImage(this.defaultConfig.image);
        }
        // Start health check timer
        this.startHealthCheck();
    }
    /**
     * Shutdown the sandbox manager
     */
    async shutdown() {
        this.stopHealthCheck();
        // Destroy all containers
        const destroyPromises = Array.from(this.containers.keys()).map(id => this.destroyContainer(id).catch(() => { }));
        await Promise.all(destroyPromises);
    }
    // ============================================================================
    // Container Lifecycle
    // ============================================================================
    /**
     * Create a container for a session
     */
    async createContainer(sessionId, config) {
        // Check if session already has a container
        const existingId = this.sessionToContainer.get(sessionId);
        if (existingId) {
            const existing = this.containers.get(existingId);
            if (existing && existing.status !== 'dead') {
                return existing;
            }
        }
        const mergedConfig = { ...this.defaultConfig, ...config };
        const id = crypto.randomUUID();
        // Create Docker container
        const containerId = await this.docker.createContainer({
            image: mergedConfig.image,
            name: `codebuddy-${sessionId.slice(0, 8)}-${id.slice(0, 8)}`,
            env: mergedConfig.env,
            workDir: mergedConfig.workDir,
            user: mergedConfig.user,
            networkMode: mergedConfig.networkMode,
            memory: this.parseMemory(mergedConfig.memoryLimit),
            cpuShares: Math.floor(mergedConfig.cpuLimit * 1024),
            volumes: mergedConfig.volumes?.map(v => ({
                source: v.source,
                target: v.target,
                readOnly: v.readOnly,
            })),
            labels: {
                'codebuddy.session': sessionId,
                'codebuddy.sandbox': 'true',
            },
        });
        // Start the container
        await this.docker.startContainer(containerId);
        const container = {
            id,
            sessionId,
            containerId,
            status: 'running',
            image: mergedConfig.image,
            createdAt: new Date(),
            lastActivityAt: new Date(),
            toolAllowlist: [],
            toolDenylist: [],
            networkPolicy: DEFAULT_NETWORK_POLICY,
        };
        this.containers.set(id, container);
        this.sessionToContainer.set(sessionId, id);
        this.emit('container:created', container);
        this.emit('container:started', containerId);
        return container;
    }
    /**
     * Get container for a session
     */
    getContainerForSession(sessionId) {
        const id = this.sessionToContainer.get(sessionId);
        return id ? this.containers.get(id) : undefined;
    }
    /**
     * Get container by ID
     */
    getContainer(id) {
        return this.containers.get(id);
    }
    /**
     * Get all containers
     */
    getAllContainers() {
        return Array.from(this.containers.values());
    }
    /**
     * Destroy a container
     */
    async destroyContainer(id) {
        const container = this.containers.get(id);
        if (!container)
            return;
        try {
            // Stop container if running
            if (container.status === 'running') {
                await this.docker.stopContainer(container.containerId, 10);
                this.emit('container:stopped', container.containerId, 'destroyed');
            }
            // Remove container
            await this.docker.removeContainer(container.containerId, true);
        }
        catch (error) {
            // Container might already be gone
        }
        // Clean up mappings
        this.containers.delete(id);
        this.sessionToContainer.delete(container.sessionId);
        this.emit('container:destroyed', container.containerId);
    }
    /**
     * Destroy container for a session
     */
    async destroyContainerForSession(sessionId) {
        const id = this.sessionToContainer.get(sessionId);
        if (id) {
            await this.destroyContainer(id);
        }
    }
    // ============================================================================
    // Command Execution
    // ============================================================================
    /**
     * Execute a command in a container
     */
    async executeInContainer(id, command, options) {
        const container = this.containers.get(id);
        if (!container) {
            throw new Error(`Container ${id} not found`);
        }
        if (container.status !== 'running') {
            throw new Error(`Container ${id} is not running (status: ${container.status})`);
        }
        // Update activity
        container.lastActivityAt = new Date();
        const timeout = options?.timeout || this.defaultConfig.timeout;
        const startTime = Date.now();
        this.emit('execution:start', container.containerId, command);
        try {
            // Parse command into array
            const cmdParts = this.parseCommand(command);
            // Execute with timeout
            const result = await Promise.race([
                this.docker.exec(container.containerId, cmdParts, {
                    workDir: options?.workDir || this.defaultConfig.workDir,
                    env: options?.env,
                    user: options?.user,
                    stdin: options?.stdin,
                    timeout,
                }),
                new Promise((_, reject) => setTimeout(() => reject(new Error('Execution timed out')), timeout)),
            ]);
            const executionResult = {
                exitCode: result.exitCode,
                stdout: result.stdout,
                stderr: result.stderr,
                durationMs: Date.now() - startTime,
                timedOut: false,
                oomKilled: false,
            };
            this.emit('execution:complete', container.containerId, executionResult);
            return executionResult;
        }
        catch (error) {
            const isTimeout = error instanceof Error && error.message.includes('timed out');
            const executionResult = {
                exitCode: isTimeout ? 124 : 1,
                stdout: '',
                stderr: error instanceof Error ? error.message : 'Unknown error',
                durationMs: Date.now() - startTime,
                timedOut: isTimeout,
                oomKilled: false,
            };
            this.emit('execution:complete', container.containerId, executionResult);
            return executionResult;
        }
    }
    /**
     * Execute in session's container
     */
    async executeInSession(sessionId, command, options) {
        const container = this.getContainerForSession(sessionId);
        if (!container) {
            throw new Error(`No container for session ${sessionId}`);
        }
        return this.executeInContainer(container.id, command, options);
    }
    // ============================================================================
    // Tool Policies
    // ============================================================================
    /**
     * Set tool allowlist for a container
     */
    setToolAllowlist(id, tools) {
        const container = this.containers.get(id);
        if (container) {
            container.toolAllowlist = [...tools];
        }
    }
    /**
     * Set tool denylist for a container
     */
    setToolDenylist(id, tools) {
        const container = this.containers.get(id);
        if (container) {
            container.toolDenylist = [...tools];
        }
    }
    /**
     * Check if a tool is allowed
     */
    isToolAllowed(id, tool) {
        const container = this.containers.get(id);
        if (!container)
            return false;
        // Check denylist first
        if (container.toolDenylist.includes(tool)) {
            this.emit('policy:violation', container.containerId, `Tool ${tool} is denied`);
            return false;
        }
        // If allowlist is empty, allow all (except denied)
        if (container.toolAllowlist.length === 0) {
            return true;
        }
        // Check allowlist
        const allowed = container.toolAllowlist.includes(tool);
        if (!allowed) {
            this.emit('policy:violation', container.containerId, `Tool ${tool} is not in allowlist`);
        }
        return allowed;
    }
    /**
     * Set network policy for a container
     */
    setNetworkPolicy(id, policy) {
        const container = this.containers.get(id);
        if (container) {
            container.networkPolicy = { ...policy };
        }
    }
    // ============================================================================
    // Metrics & Monitoring
    // ============================================================================
    /**
     * Get container metrics
     */
    async getMetrics(id) {
        const container = this.containers.get(id);
        if (!container || container.status !== 'running') {
            return null;
        }
        try {
            const stats = await this.docker.getStats(container.containerId);
            const metrics = {
                cpuPercent: stats.cpuPercent,
                memoryUsage: stats.memoryUsage,
                memoryLimit: stats.memoryLimit,
                networkRxBytes: stats.networkRx,
                networkTxBytes: stats.networkTx,
                blockIoReadBytes: stats.blockRead,
                blockIoWriteBytes: stats.blockWrite,
            };
            container.metrics = metrics;
            return metrics;
        }
        catch {
            return null;
        }
    }
    /**
     * Get all container metrics
     */
    async getAllMetrics() {
        const metrics = new Map();
        for (const [id] of this.containers) {
            const m = await this.getMetrics(id);
            if (m) {
                metrics.set(id, m);
            }
        }
        return metrics;
    }
    // ============================================================================
    // Cleanup
    // ============================================================================
    /**
     * Clean up idle containers
     */
    async cleanupIdleContainers(maxIdleMs) {
        const now = Date.now();
        let cleaned = 0;
        for (const [id, container] of this.containers) {
            const idleTime = now - container.lastActivityAt.getTime();
            if (idleTime > maxIdleMs) {
                await this.destroyContainer(id);
                cleaned++;
            }
        }
        return cleaned;
    }
    /**
     * Get manager statistics
     */
    getStats() {
        const containers = Array.from(this.containers.values());
        return {
            totalContainers: containers.length,
            runningContainers: containers.filter(c => c.status === 'running').length,
            stoppedContainers: containers.filter(c => c.status === 'stopped').length,
            sessionsWithContainers: this.sessionToContainer.size,
        };
    }
    // ============================================================================
    // Private Helpers
    // ============================================================================
    parseMemory(limit) {
        const match = limit.match(/^(\d+)([kmg]?)$/i);
        if (!match)
            return 536870912; // Default 512MB
        const value = parseInt(match[1], 10);
        const unit = match[2].toLowerCase();
        switch (unit) {
            case 'k': return value * 1024;
            case 'm': return value * 1024 * 1024;
            case 'g': return value * 1024 * 1024 * 1024;
            default: return value;
        }
    }
    parseCommand(command) {
        // Simple shell-style parsing
        const parts = [];
        let current = '';
        let inQuote = false;
        let quoteChar = '';
        for (const char of command) {
            if (!inQuote && (char === '"' || char === "'")) {
                inQuote = true;
                quoteChar = char;
            }
            else if (inQuote && char === quoteChar) {
                inQuote = false;
                quoteChar = '';
            }
            else if (!inQuote && char === ' ') {
                if (current) {
                    parts.push(current);
                    current = '';
                }
            }
            else {
                current += char;
            }
        }
        if (current) {
            parts.push(current);
        }
        return parts.length > 0 ? parts : ['sh', '-c', command];
    }
    startHealthCheck() {
        if (this.healthCheckTimer)
            return;
        this.healthCheckTimer = setInterval(async () => {
            for (const [id, container] of this.containers) {
                try {
                    const status = await this.docker.getStatus(container.containerId);
                    if (status !== container.status) {
                        container.status = status;
                        if (status === 'dead') {
                            this.emit('container:stopped', container.containerId, 'died');
                        }
                    }
                }
                catch {
                    // Container might be gone
                    container.status = 'dead';
                }
            }
        }, 30000); // Check every 30 seconds
    }
    stopHealthCheck() {
        if (this.healthCheckTimer) {
            clearInterval(this.healthCheckTimer);
            this.healthCheckTimer = null;
        }
    }
}
// ============================================================================
// Singleton
// ============================================================================
let sandboxManagerInstance = null;
/**
 * Get Docker sandbox manager instance
 */
export function getDockerSandboxManager(docker, config) {
    if (!sandboxManagerInstance) {
        sandboxManagerInstance = new DockerSandboxManager(docker, config);
    }
    return sandboxManagerInstance;
}
/**
 * Reset sandbox manager instance
 */
export async function resetDockerSandboxManager() {
    if (sandboxManagerInstance) {
        await sandboxManagerInstance.shutdown();
        sandboxManagerInstance = null;
    }
}
export default DockerSandboxManager;
//# sourceMappingURL=manager.js.map