import * as fs from 'fs/promises';
import * as path from 'path';
import * as os from 'os';
import { existsSync, mkdirSync } from 'fs';
import { LoggerService } from '../core/logger.service.js';
import { CREWLY_CONSTANTS, AGENT_IDENTITY_CONSTANTS, TIMING_CONSTANTS } from '../../constants.js';
/**
 * Agent Status Batcher - handles efficient bulk updates with timeout-based flushing
 *
 * Batches multiple agent status updates together to reduce file I/O overhead.
 * Automatically flushes batches after 2 seconds or when batch size reaches 50 items.
 */
export class AgentStatusBatcher {
    logger;
    pendingUpdates = new Map();
    flushTimer = null;
    maxBatchSize = 50;
    flushTimeoutMs = TIMING_CONSTANTS.TIMEOUTS.TASK_MONITOR_POLL; // 2 seconds
    heartbeatService;
    constructor(heartbeatService) {
        this.logger = LoggerService.getInstance().createComponentLogger('AgentStatusBatcher');
        this.heartbeatService = heartbeatService;
    }
    /**
     * Add an agent status update to the batch queue
     *
     * @param agentId - Agent identifier
     * @param sessionName - PTY session name
     * @param agentStatus - New agent status
     * @param teamMemberId - Team member ID (optional, only for team members)
     */
    addUpdate(agentId, sessionName, agentStatus, teamMemberId) {
        const updateKey = teamMemberId || agentId;
        const update = {
            agentId,
            sessionName,
            teamMemberId,
            agentStatus,
            timestamp: new Date().toISOString()
        };
        this.pendingUpdates.set(updateKey, update);
        this.logger.debug('Added update to batch', {
            agentId,
            sessionName,
            teamMemberId,
            agentStatus,
            batchSize: this.pendingUpdates.size
        });
        // Schedule flush if not already scheduled
        this.scheduleFlush();
        // Immediate flush if batch is full
        if (this.pendingUpdates.size >= this.maxBatchSize) {
            this.logger.debug('Batch size limit reached, flushing immediately', {
                batchSize: this.pendingUpdates.size
            });
            this.flushImmediately();
        }
    }
    /**
     * Schedule a batch flush after the timeout period
     */
    scheduleFlush() {
        if (this.flushTimer) {
            return; // Timer already scheduled
        }
        this.flushTimer = setTimeout(() => {
            this.flushImmediately();
        }, this.flushTimeoutMs);
    }
    /**
     * Immediately flush all pending updates
     */
    async flushImmediately() {
        if (this.flushTimer) {
            clearTimeout(this.flushTimer);
            this.flushTimer = null;
        }
        if (this.pendingUpdates.size === 0) {
            return;
        }
        const updates = Array.from(this.pendingUpdates.values());
        this.pendingUpdates.clear();
        this.logger.debug('Flushing batched updates', {
            updateCount: updates.length
        });
        try {
            await this.heartbeatService.processBatchedUpdates(updates);
            this.logger.debug('Successfully flushed batched updates', {
                updateCount: updates.length
            });
        }
        catch (error) {
            this.logger.error('Failed to flush batched updates', {
                updateCount: updates.length,
                error: error instanceof Error ? error.message : String(error),
                stack: error instanceof Error ? error.stack : undefined
            });
        }
    }
    /**
     * Get current batch size (for testing/monitoring)
     */
    getBatchSize() {
        return this.pendingUpdates.size;
    }
    /**
     * Clear all pending updates (for testing/cleanup)
     */
    clear() {
        if (this.flushTimer) {
            clearTimeout(this.flushTimer);
            this.flushTimer = null;
        }
        this.pendingUpdates.clear();
    }
}
/**
 * Agent Heartbeat Service - manages agent proof-of-life tracking
 *
 * This service transforms every API call into an agent heartbeat,
 * maintaining agent status and last active time in teamAgentStatus.json.
 *
 * Key responsibilities:
 * - Update agent status and lastActiveTime on every API call
 * - Batch updates for efficiency (2-second timeout, 50-item max)
 * - Detect stale agents (30-minute threshold)
 * - Manage separate teamAgentStatus.json file (owned by Agent Registration)
 */
export class AgentHeartbeatService {
    logger;
    crewlyHome;
    teamAgentStatusFile;
    fileLocks = new Map();
    batcher;
    static instance = null;
    constructor(crewlyHome) {
        this.logger = LoggerService.getInstance().createComponentLogger('AgentHeartbeatService');
        this.crewlyHome = crewlyHome || path.join(os.homedir(), CREWLY_CONSTANTS.PATHS.CREWLY_HOME);
        this.teamAgentStatusFile = path.join(this.crewlyHome, 'teamAgentStatus.json');
        this.batcher = new AgentStatusBatcher(this);
        this.ensureDirectories();
    }
    /**
     * Get singleton instance to prevent multiple instances from interfering
     */
    static getInstance(crewlyHome) {
        if (!AgentHeartbeatService.instance) {
            AgentHeartbeatService.instance = new AgentHeartbeatService(crewlyHome);
        }
        return AgentHeartbeatService.instance;
    }
    /**
     * Clear singleton instance (useful for testing)
     */
    static clearInstance() {
        if (AgentHeartbeatService.instance) {
            AgentHeartbeatService.instance.batcher.clear();
        }
        AgentHeartbeatService.instance = null;
    }
    /**
     * Ensure required directories exist
     */
    ensureDirectories() {
        if (!existsSync(this.crewlyHome)) {
            mkdirSync(this.crewlyHome, { recursive: true });
        }
    }
    /**
     * Update agent heartbeat - called by every API
     *
     * This is the main entry point that transforms API calls into agent heartbeats.
     * Uses batching for efficiency and error resilience.
     *
     * @param sessionName - PTY session name from API call
     * @param teamMemberId - Team member ID from API call (optional)
     * @param agentStatus - Current agent status (defaults to 'active')
     *
     * @example
     * ```typescript
     * // Called by every API handler
     * await updateAgentHeartbeat('dev-session-1', 'member_123', 'active');
     * ```
     */
    async updateAgentHeartbeat(sessionName, teamMemberId, agentStatus = CREWLY_CONSTANTS.AGENT_STATUSES.ACTIVE) {
        try {
            // Determine agent ID based on session name
            const agentId = this.determineAgentId(sessionName, teamMemberId);
            this.logger.debug('Updating agent heartbeat', {
                sessionName,
                teamMemberId,
                agentId,
                agentStatus
            });
            // Add to batch for efficient processing
            this.batcher.addUpdate(agentId, sessionName, agentStatus, teamMemberId);
        }
        catch (error) {
            // Log error but don't throw - heartbeat failures shouldn't break APIs
            this.logger.error('Failed to update agent heartbeat', {
                sessionName,
                teamMemberId,
                agentStatus,
                error: error instanceof Error ? error.message : String(error),
                stack: error instanceof Error ? error.stack : undefined
            });
        }
    }
    /**
     * Determine agent ID from session name and team member ID
     */
    determineAgentId(sessionName, teamMemberId) {
        // Orchestrator has special handling
        if (sessionName === AGENT_IDENTITY_CONSTANTS.ORCHESTRATOR.SESSION_NAME) {
            return AGENT_IDENTITY_CONSTANTS.ORCHESTRATOR.ID;
        }
        // For team members, use teamMemberId if provided, otherwise session name
        return teamMemberId ? teamMemberId : sessionName;
    }
    /**
     * Process batched updates - called by AgentStatusBatcher
     *
     * @param updates - Array of batched status updates to process
     */
    async processBatchedUpdates(updates) {
        if (updates.length === 0) {
            return;
        }
        this.logger.debug('Processing batched agent status updates', {
            updateCount: updates.length
        });
        try {
            // Load current status file
            const statusData = await this.loadTeamAgentStatusFile();
            // Apply all updates
            for (const update of updates) {
                this.applyHeartbeatUpdate(statusData, update);
            }
            // Update metadata
            statusData.metadata.lastUpdated = new Date().toISOString();
            // Save updated file
            await this.saveTeamAgentStatusFile(statusData);
            this.logger.debug('Successfully processed batched updates', {
                updateCount: updates.length
            });
        }
        catch (error) {
            this.logger.error('Failed to process batched updates', {
                updateCount: updates.length,
                error: error instanceof Error ? error.message : String(error),
                stack: error instanceof Error ? error.stack : undefined
            });
            throw error;
        }
    }
    /**
     * Apply a single heartbeat update to the status data
     */
    applyHeartbeatUpdate(statusData, update) {
        const now = new Date().toISOString();
        if (update.agentId === AGENT_IDENTITY_CONSTANTS.ORCHESTRATOR.ID) {
            // Update orchestrator
            if (!statusData.orchestrator) {
                statusData.orchestrator = this.createDefaultOrchestratorHeartbeat();
            }
            statusData.orchestrator.agentStatus = update.agentStatus;
            statusData.orchestrator.lastActiveTime = update.timestamp;
            statusData.orchestrator.updatedAt = now;
            statusData.orchestrator.sessionName = update.sessionName;
        }
        else {
            // Update team member
            const memberId = update.teamMemberId || update.agentId;
            if (!statusData.teamMembers[memberId]) {
                statusData.teamMembers[memberId] = this.createDefaultTeamMemberHeartbeat(update.agentId, update.sessionName, memberId);
            }
            const member = statusData.teamMembers[memberId];
            member.agentStatus = update.agentStatus;
            member.lastActiveTime = update.timestamp;
            member.updatedAt = now;
            member.sessionName = update.sessionName;
        }
    }
    /**
     * Load teamAgentStatus.json file with proper initialization
     */
    async loadTeamAgentStatusFile() {
        await this.ensureTeamAgentStatusFile();
        try {
            const content = await fs.readFile(this.teamAgentStatusFile, 'utf-8');
            const data = JSON.parse(content);
            // Validate structure
            if (!data.orchestrator || !data.teamMembers || !data.metadata) {
                this.logger.warn('Invalid teamAgentStatus.json structure, reinitializing');
                return this.createDefaultTeamAgentStatusFile();
            }
            return data;
        }
        catch (error) {
            this.logger.error('Failed to load teamAgentStatus.json, creating new file', {
                error: error instanceof Error ? error.message : String(error)
            });
            return this.createDefaultTeamAgentStatusFile();
        }
    }
    /**
     * Save teamAgentStatus.json file with atomic write
     */
    async saveTeamAgentStatusFile(data) {
        const content = JSON.stringify(data, null, 2);
        await this.atomicWriteFile(this.teamAgentStatusFile, content);
    }
    /**
     * Ensure teamAgentStatus.json file exists with proper structure
     */
    async ensureTeamAgentStatusFile() {
        if (!existsSync(this.teamAgentStatusFile)) {
            const defaultData = this.createDefaultTeamAgentStatusFile();
            await this.saveTeamAgentStatusFile(defaultData);
            this.logger.info('Created new teamAgentStatus.json file');
        }
    }
    /**
     * Create default teamAgentStatus.json structure
     */
    createDefaultTeamAgentStatusFile() {
        return {
            orchestrator: this.createDefaultOrchestratorHeartbeat(),
            teamMembers: {},
            metadata: {
                lastUpdated: new Date().toISOString(),
                version: '1.0.0'
            }
        };
    }
    /**
     * Create default orchestrator heartbeat record
     */
    createDefaultOrchestratorHeartbeat() {
        const now = new Date().toISOString();
        return {
            agentId: AGENT_IDENTITY_CONSTANTS.ORCHESTRATOR.ID,
            sessionName: AGENT_IDENTITY_CONSTANTS.ORCHESTRATOR.SESSION_NAME,
            agentStatus: CREWLY_CONSTANTS.AGENT_STATUSES.INACTIVE,
            lastActiveTime: now,
            createdAt: now,
            updatedAt: now
        };
    }
    /**
     * Create default team member heartbeat record
     */
    createDefaultTeamMemberHeartbeat(agentId, sessionName, teamMemberId) {
        const now = new Date().toISOString();
        return {
            agentId,
            sessionName,
            teamMemberId,
            agentStatus: CREWLY_CONSTANTS.AGENT_STATUSES.INACTIVE,
            lastActiveTime: now,
            createdAt: now,
            updatedAt: now
        };
    }
    /**
     * Detect stale agents based on lastActiveTime threshold
     *
     * @param thresholdMinutes - Minutes of inactivity before considering agent stale (default: 30)
     * @returns Array of stale agent IDs that should be marked as 'potentialInactive'
     */
    async detectStaleAgents(thresholdMinutes = 30) {
        try {
            const statusData = await this.loadTeamAgentStatusFile();
            const staleAgents = [];
            const thresholdTime = new Date(Date.now() - thresholdMinutes * 60 * 1000);
            // Check orchestrator
            const orchLastActive = new Date(statusData.orchestrator.lastActiveTime);
            if (orchLastActive < thresholdTime &&
                statusData.orchestrator.agentStatus === CREWLY_CONSTANTS.AGENT_STATUSES.ACTIVE) {
                staleAgents.push(statusData.orchestrator.agentId);
            }
            // Check team members
            for (const [memberId, member] of Object.entries(statusData.teamMembers)) {
                const memberLastActive = new Date(member.lastActiveTime);
                if (memberLastActive < thresholdTime &&
                    member.agentStatus === CREWLY_CONSTANTS.AGENT_STATUSES.ACTIVE) {
                    staleAgents.push(memberId);
                }
            }
            if (staleAgents.length > 0) {
                this.logger.debug('Detected stale agents', {
                    staleAgents,
                    thresholdMinutes
                });
            }
            return staleAgents;
        }
        catch (error) {
            this.logger.error('Failed to detect stale agents', {
                thresholdMinutes,
                error: error instanceof Error ? error.message : String(error)
            });
            return [];
        }
    }
    /**
     * Get current agent status and heartbeat information
     *
     * @param agentId - Agent identifier or team member ID
     * @returns Agent heartbeat information or null if not found
     */
    async getAgentHeartbeat(agentId) {
        try {
            const statusData = await this.loadTeamAgentStatusFile();
            // Check if it's the orchestrator
            if (agentId === AGENT_IDENTITY_CONSTANTS.ORCHESTRATOR.ID) {
                return statusData.orchestrator;
            }
            // Check team members
            return statusData.teamMembers[agentId] || null;
        }
        catch (error) {
            this.logger.error('Failed to get agent heartbeat', {
                agentId,
                error: error instanceof Error ? error.message : String(error)
            });
            return null;
        }
    }
    /**
     * Get all agent heartbeats
     *
     * @returns Complete team agent status data
     */
    async getAllAgentHeartbeats() {
        return await this.loadTeamAgentStatusFile();
    }
    /**
     * Atomic write operation with file locking to prevent race conditions
     */
    async atomicWriteFile(filePath, content) {
        const lockKey = filePath;
        // Wait for any existing write operation on this file to complete
        if (this.fileLocks.has(lockKey)) {
            await this.fileLocks.get(lockKey);
        }
        // Create a new lock for this write operation
        const writeOperation = this.performAtomicWrite(filePath, content);
        this.fileLocks.set(lockKey, writeOperation);
        try {
            await writeOperation;
        }
        finally {
            // Clean up the lock after operation completes
            this.fileLocks.delete(lockKey);
        }
    }
    /**
     * Performs the actual atomic write using a temporary file and rename
     */
    async performAtomicWrite(filePath, content) {
        const tempPath = `${filePath}.tmp.${Date.now()}.${Math.random().toString(36).substring(2)}`;
        try {
            // Write to temporary file first
            await fs.writeFile(tempPath, content, 'utf8');
            // Ensure data is written to disk before rename
            const fileHandle = await fs.open(tempPath, 'r+');
            await fileHandle.sync();
            await fileHandle.close();
            // Atomically move temp file to target (this is atomic on most filesystems)
            await fs.rename(tempPath, filePath);
        }
        catch (error) {
            // Clean up temp file if something went wrong
            try {
                await fs.unlink(tempPath);
            }
            catch (unlinkError) {
                // Ignore cleanup errors
            }
            throw error;
        }
    }
    /**
     * Flush any pending batched updates immediately
     * Useful for testing or ensuring all updates are persisted before shutdown
     */
    async flushPendingUpdates() {
        await this.batcher.flushImmediately();
    }
}
/**
 * Convenience function to update agent heartbeat
 * Creates a singleton instance and calls updateAgentHeartbeat
 *
 * This is the main function that all API handlers should call.
 *
 * @param sessionName - PTY session name from API call
 * @param teamMemberId - Team member ID from API call (optional)
 * @param agentStatus - Current agent status (defaults to 'active')
 *
 * @example
 * ```typescript
 * // In API handler:
 * import { updateAgentHeartbeat } from '../services/agent/agent-heartbeat.service.js';
 *
 * export async function handleSendMessage(params: any) {
 *   try {
 *     // Update heartbeat first
 *     await updateAgentHeartbeat(params.sessionName, params.teamMemberId);
 *
 *     // Handle the actual API logic
 *     // ...
 *   } catch (error) {
 *     // Handle errors
 *   }
 * }
 * ```
 */
export async function updateAgentHeartbeat(sessionName, teamMemberId, agentStatus = CREWLY_CONSTANTS.AGENT_STATUSES.ACTIVE) {
    const service = AgentHeartbeatService.getInstance();
    await service.updateAgentHeartbeat(sessionName, teamMemberId, agentStatus);
}
//# sourceMappingURL=agent-heartbeat.service.js.map