/**
 * AgentMonitor - Heartbeat tracking and timeout enforcement
 *
 * Implements DD §7.6.
 * INV-1 exemption: heartbeat timestamps are the ONLY direct writes allowed.
 *
 * @module packages/runtime/src/scheduler/AgentMonitor
 */

export interface AgentHeartbeat {
  agent_id: string
  task_id: string
  last_heartbeat: string
  pid?: number
}

export type AgentState = 'running' | 'stalled' | 'lost' | 'timed_out'

export interface AgentStatus {
  agent_id: string
  state: AgentState
  last_heartbeat: string
  missed_count: number
  timeout_at?: string
}

export class AgentMonitor {
  private heartbeats: Map<string, AgentHeartbeat> = new Map()
  private missed_counts: Map<string, number> = new Map()
  private coalesce_window_ms: number = 5000  // 5s coalescing
  private soft_timeout_ms: number = 300000    // 5 min
  private hard_timeout_ms: number = 600000    // 10 min

  /**
   * Record a heartbeat (coalesced — only updates every 5s per agent).
   */
  record_heartbeat(agent_id: string, task_id: string, pid?: number): void {
    const existing = this.heartbeats.get(agent_id)
    const now = Date.now()

    if (existing) {
      const last_ms = new Date(existing.last_heartbeat).getTime()
      if (now - last_ms < this.coalesce_window_ms) {
        return // Coalesced — skip
      }
    }

    this.heartbeats.set(agent_id, {
      agent_id,
      task_id,
      last_heartbeat: new Date().toISOString(),
      pid
    })

    // Reset missed count on successful heartbeat
    this.missed_counts.set(agent_id, 0)
  }

  /**
   * Detect lost agents — missed heartbeat threshold exceeded.
   */
  detect_lost_agents(): AgentStatus[] {
    const lost: AgentStatus[] = []
    const now = Date.now()

    for (const [agent_id, hb] of this.heartbeats) {
      const last_ms = new Date(hb.last_heartbeat).getTime()
      const elapsed = now - last_ms
      const missed = this.missed_counts.get(agent_id) || 0

      if (elapsed > this.hard_timeout_ms) {
        lost.push({ agent_id, state: 'lost', last_heartbeat: hb.last_heartbeat, missed_count: missed + 1 })
        this.missed_counts.set(agent_id, missed + 1)
      } else if (elapsed > this.soft_timeout_ms) {
        lost.push({ agent_id, state: 'stalled', last_heartbeat: hb.last_heartbeat, missed_count: missed + 1, timeout_at: new Date(now + this.hard_timeout_ms - elapsed).toISOString() })
        this.missed_counts.set(agent_id, missed + 1)
      }
    }

    return lost
  }

  /**
   * Enforce timeouts — return agents that need cancellation.
   */
  enforce_timeouts(): Array<{ agent_id: string; action: 'ping' | 'soft_cancel' | 'hard_cancel' }> {
    const actions: Array<{ agent_id: string; action: 'ping' | 'soft_cancel' | 'hard_cancel' }> = []
    const now = Date.now()

    for (const [agent_id, hb] of this.heartbeats) {
      const elapsed = now - new Date(hb.last_heartbeat).getTime()

      if (elapsed > this.hard_timeout_ms * 1.5) {
        actions.push({ agent_id, action: 'hard_cancel' })
      } else if (elapsed > this.hard_timeout_ms) {
        actions.push({ agent_id, action: 'soft_cancel' })
      } else if (elapsed > this.soft_timeout_ms) {
        actions.push({ agent_id, action: 'ping' })
      }
    }

    return actions
  }

  /**
   * Remove an agent from monitoring.
   */
  remove(agent_id: string): void {
    this.heartbeats.delete(agent_id)
    this.missed_counts.delete(agent_id)
  }

  /**
   * Get agent heartbeat info.
   */
  get(agent_id: string): AgentHeartbeat | undefined {
    return this.heartbeats.get(agent_id)
  }
}
