//! [`SnapshotRunningAgents`] — freeze, at close time, which agent cells still
//! held a live PTY (feature: "conversation resume", task T5).
//!
//! When the IDE (or a single project) closes, every live PTY is about to be
//! killed. *Before* that happens, we record on each agent-bearing leaf whether
//! its agent process was still running (`agent_was_running = true`) or had
//! already exited / never launched (`false`). On reopen, that flag is what tells
//! the resume popup "this conversation was still in progress" vs "it was closed".
//!
//! The decision is **universal**: it is derived purely from the process
//! lifecycle (the live-session registry), never from parsing CLI output. The use
//! case itself is a thin orchestrator over:
//! - the persisted layouts store ([`super::store`]),
//! - the pure domain operation [`domain::LayoutTree::set_agent_running`],
//! - a [`LiveAgentRegistry`] liveness query.
//!
//! **Ordering contract:** this snapshot reads the registry *as it is at call
//! time*. The composition root (app-tauri) is responsible for calling it
//! **before** the global PTY kill; if the kill ran first, every agent would look
//! "closed". See `app-tauri`'s `CloseRequested` hook and `close.rs` callers.

use std::sync::Arc;

use domain::ports::{FileSystem, ProjectStore};
use domain::ProjectId;

use crate::error::AppError;
use crate::terminal::LiveAgentRegistry;

use super::store::{persist_doc, resolve_doc};

/// Input for [`SnapshotRunningAgents::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SnapshotRunningAgentsInput {
    /// The project whose layouts must be frozen.
    pub project_id: ProjectId,
}

/// Output of [`SnapshotRunningAgents::execute`].
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SnapshotRunningAgentsOutput {
    /// Number of agent-bearing leaves that were found running at snapshot time.
    pub running: usize,
    /// Number of agent-bearing leaves that were found stopped at snapshot time.
    pub stopped: usize,
}

/// Freezes the `agent_was_running` flag on every agent leaf of a project's
/// layouts, from the live-session registry, then persists the layouts.
pub struct SnapshotRunningAgents {
    store: Arc<dyn ProjectStore>,
    fs: Arc<dyn FileSystem>,
    live: Arc<dyn LiveAgentRegistry>,
}

impl SnapshotRunningAgents {
    /// Builds the use case from its injected ports.
    #[must_use]
    pub fn new(
        store: Arc<dyn ProjectStore>,
        fs: Arc<dyn FileSystem>,
        live: Arc<dyn LiveAgentRegistry>,
    ) -> Self {
        Self { store, fs, live }
    }

    /// Executes the snapshot for one project.
    ///
    /// Walks every named layout of the project; for each agent-bearing leaf it
    /// applies [`domain::LayoutTree::set_agent_running`] with the agent's *current*
    /// liveness, then persists the whole layouts store. A project with no
    /// agent leaf is a no-op (the doc is still resolved/healed, never written if
    /// unchanged).
    ///
    /// # Errors
    /// - [`AppError::NotFound`] if the project is unknown,
    /// - [`AppError::FileSystem`] on persistence failure,
    /// - [`AppError::Store`] on registry I/O failure.
    pub async fn execute(
        &self,
        input: SnapshotRunningAgentsInput,
    ) -> Result<SnapshotRunningAgentsOutput, AppError> {
        let project = self.store.load_project(input.project_id).await?;
        let mut doc = resolve_doc(self.fs.as_ref(), &project).await?;

        let mut out = SnapshotRunningAgentsOutput::default();
        let mut changed = false;

        for named in &mut doc.layouts {
            for (leaf_id, _agent_id) in named.tree.agent_leaves() {
                // Liveness is keyed on the hosting *node*, not the agent: with the
                // one-live-session-per-agent invariant, an agent pinned on several
                // leaves is live in at most one cell, so only that cell must be
                // marked running (a duplicate leaf for the same agent stays false).
                let running = self.live.is_node_live(&leaf_id);
                if running {
                    out.running += 1;
                } else {
                    out.stopped += 1;
                }
                // Pure op: only NodeNotFound is possible, which cannot happen
                // since `leaf_id` came from this very tree.
                named.tree = named
                    .tree
                    .set_agent_running(leaf_id, running)
                    .map_err(|e| AppError::Invalid(e.to_string()))?;
                changed = true;
            }
        }

        if changed {
            persist_doc(self.fs.as_ref(), &project, &doc).await?;
        }

        Ok(out)
    }
}
