/**
 * Agent Registry Management
 *
 * Simple file-based registry for agent identities and tasks.
 * Designed to be robust against corruption and failures.
 */
import { join } from 'path';
import { homedir } from 'os';
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmdirSync, renameSync, statSync, unlinkSync } from 'fs';
import { log } from '../utils/logger.js';
const REGISTRY_DIR = join(homedir(), '.clawx', 'agents');
const REGISTRY_FILE = join(REGISTRY_DIR, 'registry.json');
const LOCK_DIR = join(REGISTRY_DIR, 'registry.lock');
const LOCK_STALE_MS = 10_000; // Break stale locks older than 10s
const DEFAULT_REGISTRY = {
    version: '1.0.0',
    agents: [],
    tasks: [],
    updated: Date.now(),
};
export class AgentRegistryManager {
    registry;
    dirty = false;
    constructor() {
        this.registry = this.loadRegistry();
    }
    /** Load registry from disk, creating default if missing or invalid */
    loadRegistry() {
        try {
            if (!existsSync(REGISTRY_FILE)) {
                this.ensureRegistryDir();
                return { ...DEFAULT_REGISTRY };
            }
            const content = readFileSync(REGISTRY_FILE, 'utf8');
            const parsed = JSON.parse(content);
            // Validate basic structure
            if (!parsed.version || !Array.isArray(parsed.agents) || !Array.isArray(parsed.tasks)) {
                log.warn('Agent registry invalid, creating fresh registry');
                return { ...DEFAULT_REGISTRY };
            }
            return {
                version: parsed.version,
                agents: parsed.agents || [],
                tasks: parsed.tasks || [],
                updated: parsed.updated || Date.now(),
            };
        }
        catch (error) {
            log.error('Failed to load agent registry:', error);
            return { ...DEFAULT_REGISTRY };
        }
    }
    /** Acquire cross-process lock using atomic mkdir */
    acquireLock(retries = 5, delay = 100) {
        for (let i = 0; i < retries; i++) {
            try {
                mkdirSync(LOCK_DIR);
                return true;
            }
            catch (err) {
                if (err.code === 'EEXIST') {
                    // Check if lock is stale (process crashed while holding it)
                    try {
                        const stat = statSync(LOCK_DIR);
                        if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
                            log.warn('Breaking stale registry lock');
                            try {
                                rmdirSync(LOCK_DIR);
                            }
                            catch { }
                            continue; // Retry immediately after breaking stale lock
                        }
                    }
                    catch { }
                    // Lock held by another process — wait and retry
                    if (i < retries - 1) {
                        const waitMs = delay * (i + 1);
                        const start = Date.now();
                        while (Date.now() - start < waitMs) { /* busy wait for sync context */ }
                    }
                }
                else {
                    throw err;
                }
            }
        }
        log.debug('Failed to acquire registry lock after retries');
        return false;
    }
    /** Release cross-process lock */
    releaseLock() {
        try {
            rmdirSync(LOCK_DIR);
        }
        catch { }
    }
    /** Save registry to disk if dirty (with file locking and atomic write) */
    save() {
        if (!this.dirty)
            return;
        const locked = this.acquireLock();
        try {
            this.ensureRegistryDir();
            this.registry.updated = Date.now();
            // Atomic write: write to temp file, then rename
            const tmpFile = REGISTRY_FILE + '.tmp';
            writeFileSync(tmpFile, JSON.stringify(this.registry, null, 2), 'utf8');
            // Retry rename — Windows can transiently lock the target file (antivirus, indexer, concurrent reads)
            let renamed = false;
            for (let attempt = 0; attempt < 5; attempt++) {
                try {
                    renameSync(tmpFile, REGISTRY_FILE);
                    renamed = true;
                    break;
                }
                catch (renameErr) {
                    if ((renameErr.code === 'EPERM' || renameErr.code === 'EACCES' || renameErr.code === 'ENOENT') && attempt < 4) {
                        const waitMs = 50 * (attempt + 1);
                        const start = Date.now();
                        while (Date.now() - start < waitMs) { /* busy wait — sync context */ }
                        continue;
                    }
                    throw renameErr;
                }
            }
            if (renamed) {
                this.dirty = false;
                log.debug('Agent registry saved');
            }
        }
        catch (error) {
            log.debug('Failed to save agent registry:', error);
            // Clean up temp file on failure
            try {
                unlinkSync(REGISTRY_FILE + '.tmp');
            }
            catch { }
        }
        finally {
            if (locked)
                this.releaseLock();
        }
    }
    /** Ensure registry directory exists */
    ensureRegistryDir() {
        if (!existsSync(REGISTRY_DIR)) {
            mkdirSync(REGISTRY_DIR, { recursive: true });
        }
    }
    /** Get all agents */
    getAgents() {
        return [...this.registry.agents];
    }
    /** Get agent by ID */
    getAgent(id) {
        return this.registry.agents.find(agent => agent.id === id);
    }
    /** Get agent by name */
    getAgentByName(name) {
        return this.registry.agents.find(agent => agent.name === name);
    }
    /** Add or update agent */
    upsertAgent(agent) {
        const index = this.registry.agents.findIndex(a => a.id === agent.id);
        if (index >= 0) {
            this.registry.agents[index] = agent;
        }
        else {
            this.registry.agents.push(agent);
        }
        this.dirty = true;
    }
    /** Remove agent by ID */
    removeAgent(id) {
        const initialLength = this.registry.agents.length;
        this.registry.agents = this.registry.agents.filter(agent => agent.id !== id);
        if (this.registry.agents.length !== initialLength) {
            this.dirty = true;
            return true;
        }
        return false;
    }
    /** Update agent status */
    updateAgentStatus(id, status) {
        const agent = this.getAgent(id);
        if (!agent)
            return false;
        agent.status = status;
        agent.lastHeartbeat = Date.now();
        this.dirty = true;
        return true;
    }
    /** Get all tasks */
    getTasks() {
        return [...this.registry.tasks];
    }
    /** Get task by ID */
    getTask(id) {
        return this.registry.tasks.find(task => task.id === id);
    }
    /** Get tasks for agent */
    getAgentTasks(agentId) {
        return this.registry.tasks.filter(task => task.agentId === agentId);
    }
    /** Add or replace task by ID */
    addTask(task) {
        const index = this.registry.tasks.findIndex(t => t.id === task.id);
        if (index >= 0) {
            this.registry.tasks[index] = task;
        }
        else {
            this.registry.tasks.push(task);
        }
        this.dirty = true;
    }
    /** Update task */
    updateTask(id, updates) {
        const task = this.getTask(id);
        if (!task)
            return false;
        Object.assign(task, updates);
        this.dirty = true;
        return true;
    }
    /** Remove task */
    removeTask(id) {
        const initialLength = this.registry.tasks.length;
        this.registry.tasks = this.registry.tasks.filter(task => task.id !== id);
        if (this.registry.tasks.length !== initialLength) {
            this.dirty = true;
            return true;
        }
        return false;
    }
    /** Clean up old tasks, including stale pending/running tasks whose agents are gone/offline */
    cleanupOldTasks(maxAge = 24 * 60 * 60 * 1000, staleRunningAge = 30 * 60 * 1000) {
        const cutoff = Date.now() - maxAge;
        const runningCutoff = Date.now() - staleRunningAge;
        const initialLength = this.registry.tasks.length;
        this.registry.tasks = this.registry.tasks.filter(task => {
            if (task.status === 'pending' || task.status === 'running') {
                const age = task.started || task.created;
                const agent = this.getAgent(task.agentId);
                const agentMissing = !agent;
                const agentOffline = agent?.status === 'offline';
                const staleRunning = age < runningCutoff;
                if ((agentMissing || agentOffline) && staleRunning) {
                    return false;
                }
                return true;
            }
            // Remove old completed/failed/cancelled tasks
            return !!(task.completed && task.completed > cutoff);
        });
        const removed = initialLength - this.registry.tasks.length;
        if (removed > 0) {
            this.dirty = true;
        }
        return removed;
    }
    /** Mark offline agents that haven't heartbeated recently */
    markOfflineAgents(timeout = 30 * 1000) {
        const cutoff = Date.now() - timeout;
        let marked = 0;
        for (const agent of this.registry.agents) {
            if (agent.status !== 'offline' && agent.lastHeartbeat && agent.lastHeartbeat < cutoff) {
                agent.status = 'offline';
                marked++;
                this.dirty = true;
            }
        }
        return marked;
    }
    /** Get registry path for debugging */
    getRegistryPath() {
        return REGISTRY_FILE;
    }
    /** Get agent workspace directory */
    getAgentWorkspace(agentId) {
        return join(REGISTRY_DIR, agentId);
    }
    /** Ensure agent workspace exists */
    ensureAgentWorkspace(agentId) {
        const workspace = this.getAgentWorkspace(agentId);
        if (!existsSync(workspace)) {
            mkdirSync(workspace, { recursive: true });
        }
        return workspace;
    }
}
//# sourceMappingURL=agent-registry.js.map