//! Agent-level controlled actions over live sessions (Lot D).
//!
//! Companion to the read-only work-state model: these use cases let the UI drive
//! an **already-running** agent's live session by **agent id**, never by spawning a
//! new process. [`AttachLiveAgent`] rebinds the hosting cell ("a cell is a view");
//! [`StopLiveAgent`] tears the live session down polymorphically (PTY kill vs
//! structured shutdown). Neither touches the agent entity, its tickets, the
//! conversation summaries, the handoff or the provider sessions.

use std::sync::Arc;

use domain::input::InputMediator;
use domain::{AgentId, NodeId, Project, SessionId};

use crate::error::AppError;
use crate::orchestrator::OrchestratorService;
use crate::terminal::{CloseTerminal, CloseTerminalInput, LiveSessionKind, LiveSessions};

/// Input for [`AttachLiveAgent::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AttachLiveAgentInput {
    /// The owning project (boundary/symmetry; the registries are global by agent).
    pub project: Project,
    /// The already-running agent whose live session is rebound.
    pub agent_id: AgentId,
    /// The layout leaf that should host the live session view.
    pub node_id: NodeId,
}

/// Output of [`AttachLiveAgent::execute`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AttachLiveAgentOutput {
    /// The rebound agent.
    pub agent_id: AgentId,
    /// The node now hosting the session view.
    pub node_id: NodeId,
    /// The unchanged live session id.
    pub session_id: SessionId,
    /// Runtime family that owns the session.
    pub kind: LiveSessionKind,
}

/// Rebinds an already-running agent session to a visible layout cell, never
/// spawning a process ("a cell is a view").
///
/// Resolves the live session by agent in the PTY registry first, then the
/// structured one, and updates only the hosting `node_id` (the PTY handle / agent
/// session, its id and scrollback stay intact). Idempotent when the agent is
/// already attached to the same node (the rebind is a no-op that returns the same
/// session). No log/handoff/provider-session is touched.
pub struct AttachLiveAgent {
    live: Arc<LiveSessions>,
}

impl AttachLiveAgent {
    /// Builds the use case from the aggregated live-session registry.
    #[must_use]
    pub fn new(live: Arc<LiveSessions>) -> Self {
        Self { live }
    }

    /// Rebinds the agent's live session onto `node_id`.
    ///
    /// # Errors
    /// [`AppError::NotFound`] when the agent has no live session in either registry.
    pub fn execute(&self, input: AttachLiveAgentInput) -> Result<AttachLiveAgentOutput, AppError> {
        // PTY first, then structured (one-live-session-per-agent ⇒ at most one match).
        if let Some(session) = self
            .live
            .pty
            .rebind_agent_node(&input.agent_id, input.node_id)
        {
            return Ok(AttachLiveAgentOutput {
                agent_id: input.agent_id,
                node_id: session.node_id,
                session_id: session.id,
                kind: LiveSessionKind::Pty,
            });
        }
        if let Some(session) = self
            .live
            .structured
            .rebind_agent_node(&input.agent_id, input.node_id)
        {
            return Ok(AttachLiveAgentOutput {
                agent_id: input.agent_id,
                node_id: input.node_id,
                session_id: session.id(),
                kind: LiveSessionKind::Structured,
            });
        }
        Err(AppError::NotFound(format!(
            "running session for agent {}",
            input.agent_id
        )))
    }
}

/// Input for [`StopLiveAgent::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StopLiveAgentInput {
    /// The owning project (boundary/symmetry; the registries are global by agent).
    pub project: Project,
    /// The agent whose live session is stopped.
    pub agent_id: AgentId,
}

/// Output of [`StopLiveAgent::execute`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StopLiveAgentOutput {
    /// The stopped agent.
    pub agent_id: AgentId,
    /// The id of the session that was torn down.
    pub session_id: SessionId,
    /// Runtime family that owned the session.
    pub kind: LiveSessionKind,
}

/// Stops an already-running agent's live session, polymorphically.
///
/// Resolves the session by agent (PTY first, then structured). A PTY session is
/// torn down through the existing [`CloseTerminal`] primitive (registry removal +
/// process kill); a structured session is removed then `shutdown()`. The agent
/// entity, its tickets, conversation summaries and handoff are **not** removed. Any
/// exit event is left to the existing infrastructure (the PTY pump / session
/// backend) — no bus is invented here.
pub struct StopLiveAgent {
    live: Arc<LiveSessions>,
    close: Arc<CloseTerminal>,
    input: Option<Arc<dyn InputMediator>>,
    waits: Option<Arc<OrchestratorService>>,
}

impl StopLiveAgent {
    /// Builds the use case from the live-session registry and the close primitive.
    #[must_use]
    pub fn new(live: Arc<LiveSessions>, close: Arc<CloseTerminal>) -> Self {
        Self {
            live,
            close,
            input: None,
            waits: None,
        }
    }

    /// Wires cancellation helpers used for user-driven stop cascades.
    ///
    /// Stopping A while A waits on B should stop B too. The orchestrator service is
    /// only used as a read-only provider of active wait edges; the input mediator is
    /// used to preempt the running turn before the session is torn down.
    #[must_use]
    pub fn with_cascade(
        mut self,
        input: Arc<dyn InputMediator>,
        waits: Arc<OrchestratorService>,
    ) -> Self {
        self.input = Some(input);
        self.waits = Some(waits);
        self
    }

    /// Tears down the agent's live session.
    ///
    /// # Errors
    /// - [`AppError::NotFound`] when the agent has no live session,
    /// - [`AppError::Process`] when the PTY kill or structured shutdown fails.
    pub async fn execute(
        &self,
        input: StopLiveAgentInput,
    ) -> Result<StopLiveAgentOutput, AppError> {
        self.stop_dependencies(input.agent_id).await;
        if let Some(mediator) = &self.input {
            mediator.preempt(input.agent_id);
        }
        self.stop_one(input.agent_id).await
    }

    async fn stop_dependencies(&self, agent_id: AgentId) {
        let Some(waits) = &self.waits else {
            return;
        };
        for dep in waits.active_wait_dependencies(agent_id) {
            if let Some(mediator) = &self.input {
                mediator.preempt(dep);
            }
            let _ = self.stop_one(dep).await;
        }
    }

    async fn stop_one(&self, agent_id: AgentId) -> Result<StopLiveAgentOutput, AppError> {
        // PTY first: delegate to the existing close primitive (removes + kills).
        if let Some(session_id) = self.live.pty.session_for_agent(&agent_id) {
            self.close
                .execute(CloseTerminalInput { session_id })
                .await?;
            return Ok(StopLiveAgentOutput {
                agent_id,
                session_id,
                kind: LiveSessionKind::Pty,
            });
        }
        // Structured: remove from the registry first (so the uniqueness guard no
        // longer sees a live session), then shut the session down out of the lock.
        if let Some(session_id) = self.live.structured.session_id_for_agent(&agent_id) {
            if let Some(session) = self.live.structured.remove(&session_id) {
                session
                    .shutdown()
                    .await
                    .map_err(|e| AppError::Process(e.to_string()))?;
            }
            return Ok(StopLiveAgentOutput {
                agent_id,
                session_id,
                kind: LiveSessionKind::Structured,
            });
        }
        Err(AppError::NotFound(format!(
            "running session for agent {agent_id}"
        )))
    }
}
