//! Reap: removes stale `Failed` pool entries left behind by orphan detection,
//! watchdog kills, or internal spawn failures, and respawns the story's agent
//! (or blocks the story once its retry budget is exhausted).
//!
//! `check_orphaned_agents` only scans `Running`/`Pending` entries, so once an
//! entry is marked `Failed` it becomes invisible to every later watchdog
//! pass. Without this step a `Failed` entry with no live process sits in the
//! pool forever: `list_agents` keeps showing it, and the story never gets a
//! new agent unless some unrelated CRDT transition happens to trigger a
//! system-wide auto-assign scan.

use std::collections::HashSet;
use std::path::Path;

use crate::agents::AgentStatus;
use crate::agents::pool::AgentPool;
use crate::agents::pool::pipeline::should_block_story;
use crate::config::ProjectConfig;
use crate::io::watcher::WatcherEvent;
use crate::{slog, slog_warn};

/// Reap every `Failed` pool entry with no live process, except keys in
/// `exclude_keys` (already handled by the caller's own retry/block logic in
/// this same pass — e.g. limit-exceeded kills).
///
/// For each reaped entry: removes it from the pool (so `list_agents` stops
/// showing it), increments the story's retry count via [`should_block_story`]
/// and, unless that blocks the story, respawns the agent by name.
/// `start_agent`'s own session-store lookup resumes the prior session
/// automatically whenever one was recorded — no explicit session plumbing
/// needed here.
pub(super) async fn reap_failed_agents(
    pool: &AgentPool,
    project_root: &Path,
    config: &ProjectConfig,
    exclude_keys: &HashSet<String>,
) -> usize {
    let dead: Vec<(String, String, String)> = {
        let mut agents = pool.agents.lock().await;
        let keys: Vec<String> = agents
            .iter()
            .filter(|(key, agent)| {
                agent.status == AgentStatus::Failed
                    && !exclude_keys.contains(*key)
                    && agent
                        .task_handle
                        .as_ref()
                        .map(|h| h.is_finished())
                        .unwrap_or(true)
            })
            .map(|(key, _)| key.clone())
            .collect();
        keys.into_iter()
            .filter_map(|key| {
                agents.remove(&key).map(|agent| {
                    let story_id = key
                        .rsplit_once(':')
                        .map(|(s, _)| s.to_string())
                        .unwrap_or_else(|| key.clone());
                    (key, story_id, agent.agent_name)
                })
            })
            .collect()
    };

    let count = dead.len();
    for (key, story_id, agent_name) in dead {
        if let Some(block_reason) = should_block_story(&story_id, config.max_retries, "watchdog") {
            let _ = pool.watcher_tx.send(WatcherEvent::StoryBlocked {
                story_id: story_id.clone(),
                reason: block_reason,
            });
            slog!(
                "[watchdog] Story '{story_id}' blocked after exceeding retry limit \
                 (reaped dead pool entry '{key}')."
            );
            continue;
        }

        slog!(
            "[watchdog] Reaping dead pool entry '{key}'; respawning '{agent_name}' \
             for '{story_id}'."
        );
        if let Err(e) = pool
            .start_agent(project_root, &story_id, Some(&agent_name), None, None)
            .await
        {
            slog_warn!(
                "[watchdog] Failed to respawn '{agent_name}' for '{story_id}' after reap: {e}"
            );
        }
    }

    if count > 0 {
        AgentPool::notify_agent_state_changed(&pool.watcher_tx);
    }

    count
}
