/**
 * Execution Storage
 *
 * Database operations for agent_work table.
 */
import { execSync } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import { PMO_TABLES } from '../pmo/schema.js';
const T = PMO_TABLES;
// =============================================================================
// Type Conversion
// =============================================================================
function rowToAgentWork(row) {
    return {
        id: row.id,
        ticketId: row.ticket_id,
        agentName: row.agent_name,
        executor: row.executor,
        environment: (row.environment || 'host'),
        displayMode: (row.display_mode || 'terminal'),
        permissionMode: (row.permission_mode || 'safe'),
        status: row.status,
        branch: row.branch || undefined,
        pid: row.pid || undefined,
        containerId: row.container_id || undefined,
        sessionId: row.session_id || undefined,
        host: row.host || undefined,
        logPath: row.log_path || undefined,
        externalSource: row.external_source || undefined,
        externalKey: row.external_key || undefined,
        externalId: row.external_id || undefined,
        externalUrl: row.external_url || undefined,
        startedAt: new Date(row.started_at),
        completedAt: row.completed_at ? new Date(row.completed_at) : undefined,
        exitCode: row.exit_code ?? undefined,
        errorMessage: row.error_message || undefined,
    };
}
// =============================================================================
// Execution Storage Class
// =============================================================================
export class ExecutionStorage {
    db;
    constructor(db) {
        this.db = db;
    }
    /**
     * Create a new execution record.
     * Uses UUID-based IDs to guarantee uniqueness without race conditions.
     */
    createExecution(params) {
        const now = Date.now();
        // Generate a unique ID using UUID (first 8 chars, uppercase)
        // Format: WORK-A1B2C3D4 - guaranteed unique, no race conditions
        const id = `WORK-${randomUUID().substring(0, 8).toUpperCase()}`;
        this.db.prepare(`
      INSERT INTO ${T.agent_work} (
        id, ticket_id, agent_name, executor, environment, display_mode, permission_mode,
        status, branch, pid, container_id, session_id, host, log_path,
        external_source, external_key, external_id, external_url, started_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?, 'starting', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).run(id, params.ticketId, params.agentName, params.executor, params.environment, params.displayMode, params.permissionMode, params.branch || null, params.pid || null, params.containerId || null, params.sessionId || null, params.host || null, params.logPath || null, params.externalSource || null, params.externalKey || null, params.externalId || null, params.externalUrl || null, now);
        return this.getExecution(id);
    }
    /**
     * Get execution by ID
     */
    getExecution(id) {
        const row = this.db
            .prepare(`SELECT * FROM ${T.agent_work} WHERE id = ?`)
            .get(id);
        return row ? rowToAgentWork(row) : null;
    }
    /**
     * Update execution status
     */
    updateStatus(id, status, exitCode, errorMessage) {
        const completedAt = ['completed', 'failed', 'stopped'].includes(status) ? Date.now() : null;
        if (exitCode !== undefined && errorMessage) {
            this.db.prepare(`
        UPDATE ${T.agent_work}
        SET status = ?, completed_at = ?, exit_code = ?, error_message = ?
        WHERE id = ?
      `).run(status, completedAt, exitCode, errorMessage, id);
        }
        else if (exitCode !== undefined) {
            this.db.prepare(`
        UPDATE ${T.agent_work}
        SET status = ?, completed_at = ?, exit_code = ?
        WHERE id = ?
      `).run(status, completedAt, exitCode, id);
        }
        else if (errorMessage) {
            this.db.prepare(`
        UPDATE ${T.agent_work}
        SET status = ?, completed_at = ?, error_message = ?
        WHERE id = ?
      `).run(status, completedAt, errorMessage, id);
        }
        else {
            this.db.prepare(`
        UPDATE ${T.agent_work}
        SET status = ?, completed_at = ?
        WHERE id = ?
      `).run(status, completedAt, id);
        }
    }
    /**
     * Update execution with process info
     */
    updateProcessInfo(id, info) {
        const updates = [];
        const params = [];
        if (info.pid !== undefined) {
            updates.push('pid = ?');
            params.push(info.pid);
        }
        if (info.containerId !== undefined) {
            updates.push('container_id = ?');
            params.push(info.containerId);
        }
        if (info.sessionId !== undefined) {
            updates.push('session_id = ?');
            params.push(info.sessionId);
        }
        if (info.host !== undefined) {
            updates.push('host = ?');
            params.push(info.host);
        }
        if (info.logPath !== undefined) {
            updates.push('log_path = ?');
            params.push(info.logPath);
        }
        if (updates.length > 0) {
            params.push(id);
            this.db.prepare(`
        UPDATE ${T.agent_work}
        SET ${updates.join(', ')}
        WHERE id = ?
      `).run(...params);
        }
    }
    /**
     * List executions with optional filters
     */
    listExecutions(filter) {
        let query = `SELECT * FROM ${T.agent_work} WHERE 1=1`;
        const params = [];
        if (filter?.status) {
            query += ` AND status = ?`;
            params.push(filter.status);
        }
        if (filter?.agentName) {
            query += ` AND agent_name = ?`;
            params.push(filter.agentName);
        }
        if (filter?.ticketId) {
            query += ` AND ticket_id = ?`;
            params.push(filter.ticketId);
        }
        query += ` ORDER BY started_at DESC`;
        if (filter?.limit) {
            query += ` LIMIT ?`;
            params.push(filter.limit);
        }
        const rows = this.db.prepare(query).all(...params);
        return rows.map(rowToAgentWork);
    }
    /**
     * Get running execution for a ticket (if any)
     */
    getRunningExecution(ticketId) {
        const row = this.db
            .prepare(`
        SELECT * FROM ${T.agent_work}
        WHERE ticket_id = ? AND status IN ('starting', 'running')
        ORDER BY started_at DESC
        LIMIT 1
      `)
            .get(ticketId);
        return row ? rowToAgentWork(row) : null;
    }
    /**
     * Get all running executions for an agent
     */
    getAgentRunningExecutions(agentName) {
        const rows = this.db
            .prepare(`
        SELECT * FROM ${T.agent_work}
        WHERE agent_name = ? AND status IN ('starting', 'running')
        ORDER BY started_at DESC
      `)
            .all(agentName);
        return rows.map(rowToAgentWork);
    }
    /**
     * Check if agent is available (not running anything)
     */
    isAgentAvailable(agentName) {
        const count = this.db
            .prepare(`
        SELECT COUNT(*) as count FROM ${T.agent_work}
        WHERE agent_name = ? AND status IN ('starting', 'running')
      `)
            .get(agentName);
        return count.count === 0;
    }
    /**
     * Clean up stale executions where the tmux session no longer exists.
     * This fixes the bug where agents appear "busy" after sessions terminate unexpectedly.
     * Returns the number of stale executions cleaned up.
     */
    cleanupStaleExecutions() {
        // Get all "running" or "starting" executions
        const activeExecutions = this.listExecutions({ status: 'running' })
            .concat(this.listExecutions({ status: 'starting' }));
        if (activeExecutions.length === 0) {
            return 0;
        }
        // Get list of actual tmux sessions on host
        const hostTmuxSessions = this.getHostTmuxSessionNames();
        // Get map of container -> tmux sessions
        const containerTmuxSessions = this.getContainerTmuxSessionMap();
        let cleanedCount = 0;
        for (const exec of activeExecutions) {
            if (!exec.sessionId) {
                // Executions without sessionId might be stale from early termination
                // Check if they're older than 5 minutes and mark as stopped
                const ageMs = Date.now() - exec.startedAt.getTime();
                if (ageMs > 5 * 60 * 1000) {
                    this.updateStatus(exec.id, 'stopped');
                    cleanedCount++;
                }
                continue;
            }
            let sessionExists = false;
            if (exec.environment === 'devcontainer' && exec.containerId) {
                // Check if session exists in container (use prefix matching for ID format differences)
                const containerSessions = this.findContainerSessionsByPrefix(containerTmuxSessions, exec.containerId);
                sessionExists = containerSessions.includes(exec.sessionId);
            }
            else {
                // Check if session exists on host
                sessionExists = hostTmuxSessions.includes(exec.sessionId);
            }
            if (!sessionExists) {
                // Session doesn't exist, mark execution as stopped
                this.updateStatus(exec.id, 'stopped');
                cleanedCount++;
            }
        }
        return cleanedCount;
    }
    /**
     * Find container sessions using prefix matching.
     * Handles cases where the stored containerId format differs from docker ps output.
     */
    findContainerSessionsByPrefix(containerTmuxSessions, containerId) {
        const exact = containerTmuxSessions.get(containerId);
        if (exact)
            return exact;
        for (const [key, sessions] of containerTmuxSessions) {
            if (key.startsWith(containerId) || containerId.startsWith(key)) {
                return sessions;
            }
        }
        return [];
    }
    /**
     * Get list of host tmux session names
     */
    getHostTmuxSessionNames() {
        try {
            execSync('which tmux', { stdio: 'pipe' });
            const output = execSync('tmux list-sessions -F "#{session_name}"', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
            if (!output)
                return [];
            return output.split('\n');
        }
        catch {
            return [];
        }
    }
    /**
     * Get map of containerId -> tmux session names
     */
    getContainerTmuxSessionMap() {
        const sessionMap = new Map();
        try {
            const containersOutput = execSync('docker ps --filter "label=devcontainer.local_folder" --format "{{.ID}}"', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
            if (!containersOutput)
                return sessionMap;
            for (const containerId of containersOutput.split('\n')) {
                try {
                    const tmuxOutput = execSync(`docker exec ${containerId} tmux list-sessions -F "#{session_name}" 2>/dev/null`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
                    if (tmuxOutput) {
                        sessionMap.set(containerId, tmuxOutput.split('\n'));
                    }
                }
                catch {
                    // Container has no tmux sessions
                }
            }
        }
        catch {
            // Docker not available
        }
        return sessionMap;
    }
    /**
     * Get total execution count for an agent (historical)
     * Used by least-busy agent selection strategy.
     */
    getAgentExecutionCount(agentName) {
        const result = this.db
            .prepare(`
        SELECT COUNT(*) as count FROM ${T.agent_work}
        WHERE agent_name = ?
      `)
            .get(agentName);
        return result?.count || 0;
    }
    /**
     * Delete execution record
     */
    deleteExecution(id) {
        this.db.prepare(`DELETE FROM ${T.agent_work} WHERE id = ?`).run(id);
    }
}
function rowToContainer(row) {
    return {
        id: row.id,
        agentName: row.agent_name,
        dockerId: row.docker_id,
        dockerName: row.docker_name,
        image: row.image,
        status: row.status,
        currentExecutionId: row.current_execution_id,
        createdAt: new Date(row.created_at),
        lastSeenAt: new Date(row.last_seen_at),
    };
}
// =============================================================================
// Container Storage Class
// =============================================================================
export class ContainerStorage {
    db;
    constructor(db) {
        this.db = db;
        this.ensureTable();
    }
    /**
     * Ensure containers table exists (for existing databases)
     */
    ensureTable() {
        this.db.exec(`
      CREATE TABLE IF NOT EXISTS ${T.containers} (
        id TEXT PRIMARY KEY,
        agent_name TEXT NOT NULL,
        docker_id TEXT NOT NULL,
        docker_name TEXT,
        image TEXT,
        status TEXT NOT NULL DEFAULT 'unknown',
        current_execution_id TEXT,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        last_seen_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (agent_name) REFERENCES agents(name) ON DELETE CASCADE,
        FOREIGN KEY (current_execution_id) REFERENCES ${T.agent_work}(id) ON DELETE SET NULL
      );
      CREATE INDEX IF NOT EXISTS idx_containers_agent ON ${T.containers}(agent_name);
      CREATE INDEX IF NOT EXISTS idx_containers_docker_id ON ${T.containers}(docker_id);
      CREATE INDEX IF NOT EXISTS idx_containers_status ON ${T.containers}(status);
    `);
    }
    /**
     * Upsert a container record (create or update by docker_id)
     */
    upsertContainer(params) {
        const now = Date.now();
        // Check if container exists by docker_id
        const existing = this.getContainerByDockerId(params.dockerId);
        if (existing) {
            // Update existing container
            this.db.prepare(`
        UPDATE ${T.containers}
        SET agent_name = ?, docker_name = ?, image = ?, status = ?,
            current_execution_id = ?, last_seen_at = ?
        WHERE docker_id = ?
      `).run(params.agentName, params.dockerName || existing.dockerName, params.image || existing.image, params.status || existing.status, params.currentExecutionId ?? existing.currentExecutionId, now, params.dockerId);
            return this.getContainerByDockerId(params.dockerId);
        }
        else {
            // Create new container
            const id = `CNT-${params.dockerId.substring(0, 12)}`;
            this.db.prepare(`
        INSERT INTO ${T.containers} (
          id, agent_name, docker_id, docker_name, image, status,
          current_execution_id, created_at, last_seen_at
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
      `).run(id, params.agentName, params.dockerId, params.dockerName || null, params.image || null, params.status || 'unknown', params.currentExecutionId || null, now, now);
            return this.getContainer(id);
        }
    }
    /**
     * Get container by ID
     */
    getContainer(id) {
        const row = this.db
            .prepare(`SELECT * FROM ${T.containers} WHERE id = ?`)
            .get(id);
        return row ? rowToContainer(row) : null;
    }
    /**
     * Get container by Docker ID (full or short)
     */
    getContainerByDockerId(dockerId) {
        // Match by prefix (supports short IDs like 226eda1721d3)
        const row = this.db
            .prepare(`SELECT * FROM ${T.containers} WHERE docker_id LIKE ? || '%'`)
            .get(dockerId);
        return row ? rowToContainer(row) : null;
    }
    /**
     * Get container for an agent (most recent)
     */
    getContainerForAgent(agentName) {
        const row = this.db
            .prepare(`
        SELECT * FROM ${T.containers}
        WHERE agent_name = ?
        ORDER BY last_seen_at DESC
        LIMIT 1
      `)
            .get(agentName);
        return row ? rowToContainer(row) : null;
    }
    /**
     * List all containers
     */
    listContainers(filter) {
        let query = `SELECT * FROM ${T.containers} WHERE 1=1`;
        const params = [];
        if (filter?.agentName) {
            query += ` AND agent_name = ?`;
            params.push(filter.agentName);
        }
        if (filter?.status) {
            query += ` AND status = ?`;
            params.push(filter.status);
        }
        query += ` ORDER BY last_seen_at DESC`;
        if (filter?.limit) {
            query += ` LIMIT ?`;
            params.push(filter.limit);
        }
        const rows = this.db.prepare(query).all(...params);
        return rows.map(rowToContainer);
    }
    /**
     * Update container status
     */
    updateStatus(dockerId, status) {
        const now = Date.now();
        this.db.prepare(`
      UPDATE ${T.containers}
      SET status = ?, last_seen_at = ?
      WHERE docker_id LIKE ? || '%'
    `).run(status, now, dockerId);
    }
    /**
     * Update container's current execution
     */
    setCurrentExecution(dockerId, executionId) {
        const now = Date.now();
        this.db.prepare(`
      UPDATE ${T.containers}
      SET current_execution_id = ?, last_seen_at = ?
      WHERE docker_id LIKE ? || '%'
    `).run(executionId, now, dockerId);
    }
    /**
     * Mark container as removed
     */
    markRemoved(dockerId) {
        this.updateStatus(dockerId, 'removed');
    }
    /**
     * Delete container record
     */
    deleteContainer(id) {
        this.db.prepare(`DELETE FROM ${T.containers} WHERE id = ?`).run(id);
    }
    /**
     * Sync container status from Docker
     * Updates status for all known containers based on Docker state
     * Wrapped in a transaction for consistency
     */
    syncFromDocker(dockerContainers) {
        const now = Date.now();
        let added = 0;
        let updated = 0;
        let removed = 0;
        // Create a set of docker IDs currently running
        const activeDockerIds = new Set(dockerContainers.map(c => c.id.substring(0, 12)));
        // Wrap all operations in a transaction for atomicity
        const syncTransaction = this.db.transaction(() => {
            // Update or add containers from Docker
            for (const dc of dockerContainers) {
                const status = dc.status.startsWith('Up') ? 'running' :
                    dc.status.includes('Paused') ? 'paused' : 'exited';
                const existing = this.getContainerByDockerId(dc.id);
                if (existing) {
                    this.db.prepare(`
            UPDATE ${T.containers}
            SET docker_name = ?, image = ?, status = ?, last_seen_at = ?
            WHERE docker_id LIKE ? || '%'
          `).run(dc.name, dc.image, status, now, dc.id);
                    updated++;
                }
                else {
                    this.upsertContainer({
                        agentName: dc.agentName,
                        dockerId: dc.id,
                        dockerName: dc.name,
                        image: dc.image,
                        status,
                    });
                    added++;
                }
            }
            // Mark containers not in Docker as removed
            const knownContainers = this.listContainers();
            for (const container of knownContainers) {
                if (!activeDockerIds.has(container.dockerId.substring(0, 12)) &&
                    container.status !== 'removed') {
                    this.markRemoved(container.dockerId);
                    removed++;
                }
            }
        });
        syncTransaction();
        return { added, updated, removed };
    }
}
//# sourceMappingURL=storage.js.map