//! Best-effort conversation inspection use case (CONTEXT §T7, Part A).
//!
//! [`InspectConversation`] enriches a resume popup with the *last topic* and a
//! *token indicator* read from a CLI's on-disk transcript. It is **best-effort
//! by construction**: it routes the agent's [`AgentProfile`] to the first
//! injected [`SessionInspector`] that [`supports`](SessionInspector::supports)
//! it, and *any* miss — no inspector at all, an unsupported profile,
//! [`InspectError::NotFound`], or [`InspectError::Read`] — degrades to **empty
//! details** (`last_topic: None, token_count: None`) instead of an error. The
//! resume must never be blocked by inspection.
//!
//! Extensibility (Open/Closed): adding a new inspectable CLI is *pushing one
//! more adapter into the `Vec`* at the composition root — no change here.
//!
//! Like [`super::lifecycle::LaunchAgent`], it resolves the agent from the
//! project manifest and its profile from the [`ProfileStore`], and inspects
//! against the agent's **isolated run directory** (the very `cwd` the CLI was
//! launched with) so the inspector points at the right transcript folder.

use std::sync::Arc;

use domain::ports::{
    AgentContextStore, ConversationDetails, InspectError, ProfileStore, SessionInspector,
};
use domain::{AgentId, Project};

use super::lifecycle::agent_run_dir;
use crate::error::AppError;

/// Input for [`InspectConversation::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InspectConversationInput {
    /// The project owning the agent.
    pub project: Project,
    /// The agent whose conversation is being inspected.
    pub agent_id: AgentId,
    /// The persistent CLI conversation id recorded on the hosting cell.
    pub conversation_id: String,
}

/// Output of [`InspectConversation::execute`]: the (possibly empty) best-effort
/// details. Never an inspection error — a miss surfaces as empty fields.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InspectConversationOutput {
    /// Enriched, best-effort details (every field optional).
    pub details: ConversationDetails,
}

/// Reads best-effort [`ConversationDetails`] for an agent's conversation.
///
/// Holds a (possibly empty) `Vec<Arc<dyn SessionInspector>>`: the agent's
/// profile is routed to the first inspector that supports it. The use case still
/// needs the context store (resolve the agent) and the profile store (resolve
/// the profile), exactly like [`super::lifecycle::LaunchAgent`].
pub struct InspectConversation {
    contexts: Arc<dyn AgentContextStore>,
    profiles: Arc<dyn ProfileStore>,
    inspectors: Vec<Arc<dyn SessionInspector>>,
}

impl InspectConversation {
    /// Builds the use case from its injected ports and the inspector list (which
    /// may be empty: that path simply yields empty details).
    #[must_use]
    pub fn new(
        contexts: Arc<dyn AgentContextStore>,
        profiles: Arc<dyn ProfileStore>,
        inspectors: Vec<Arc<dyn SessionInspector>>,
    ) -> Self {
        Self {
            contexts,
            profiles,
            inspectors,
        }
    }

    /// Resolves the agent + profile + run dir, then asks the first supporting
    /// inspector for details. Returns **empty** details when no inspector
    /// matches or inspection misses (`NotFound`/`Read`); only genuine store
    /// failures (loading the manifest / profiles) surface as an error.
    ///
    /// # Errors
    /// - [`AppError::NotFound`] if the agent or its profile is unknown,
    /// - [`AppError::Invalid`] if a persisted manifest entry / run dir is invalid,
    /// - [`AppError::Store`] on a manifest / profile store failure.
    pub async fn execute(
        &self,
        input: InspectConversationInput,
    ) -> Result<InspectConversationOutput, AppError> {
        // Resolve the agent from the manifest (for its profile + run dir).
        let manifest = self.contexts.load_manifest(&input.project).await?;
        let entry = manifest
            .entries
            .iter()
            .find(|e| e.agent_id == input.agent_id)
            .ok_or_else(|| AppError::NotFound(format!("agent {}", input.agent_id)))?;
        let agent = entry
            .to_agent()
            .map_err(|e| AppError::Invalid(e.to_string()))?;

        let profile = self
            .profiles
            .list()
            .await?
            .into_iter()
            .find(|p| p.id == agent.profile_id)
            .ok_or_else(|| AppError::NotFound(format!("profile {} for agent", agent.profile_id)))?;

        // The CLI runs with its isolated run dir as cwd; the inspector keys its
        // transcript lookup off that same cwd (same value LaunchAgent uses).
        let run_dir = agent_run_dir(&input.project.root, &agent.id)
            .map_err(|e| AppError::Invalid(e.to_string()))?;

        // Route to the first inspector that supports this profile. No match ⇒
        // empty details (best-effort).
        let Some(inspector) = self.inspectors.iter().find(|i| i.supports(&profile)) else {
            return Ok(InspectConversationOutput {
                details: empty_details(),
            });
        };

        // Any inspection miss (NotFound / Read) degrades to empty details — it
        // must never block a resume.
        let details = match inspector
            .details(&profile, &input.conversation_id, &run_dir)
            .await
        {
            Ok(details) => details,
            Err(InspectError::NotFound | InspectError::Read(_)) => empty_details(),
        };

        Ok(InspectConversationOutput { details })
    }
}

/// The empty, fully-degraded [`ConversationDetails`] (no topic, no tokens).
fn empty_details() -> ConversationDetails {
    ConversationDetails {
        last_topic: None,
        token_count: None,
    }
}
