//! [`ListResumableAgents`] — inventaire, en lecture seule, des cellules d'agent
//! reprenables à la réouverture d'un projet (ARCHITECTURE §15.2, chantier B,
//! lot B1).
//!
//! À la réouverture d'un projet, [`crate::OpenProject`] a déjà rechargé le
//! manifeste et les layouts persistés : chaque [`domain::LeafCell`] retrouve
//! donc son `conversation_id` et son `agent_was_running` gelés à la fermeture.
//! Ce use case **calcule l'inventaire** des cellules d'agent reprenables pour
//! que la couche supérieure (commande Tauri + `ResumeProjectPanel`, lot B2)
//! puisse proposer un panneau de reprise groupé.
//!
//! Contraintes (§15.2) :
//! - **Lecture seule** : aucun PTY, aucun spawn, aucune persistance. On
//!   compose `resolve_doc` (lecture des layouts), le manifeste (nom +
//!   `profile_id`) et la liste des profils (`resume_supported`).
//! - **Best-effort, jamais d'erreur** : projet / mémoire / agent absent ⇒
//!   **liste vide**, jamais de panique ni d'`AppError`. C'est un inventaire
//!   indicatif à l'ouverture, pas une opération critique.
//! - **Filtre** : on ne retient qu'une leaf d'agent dont `agent_was_running`
//!   est vrai **ou** qui porte un `conversation_id`. Une cellule d'agent jamais
//!   lancée (ni id, ni flag) n'apparaît pas : elle se lancera normalement au
//!   clic, sans popup.

use std::sync::Arc;

use domain::ports::{AgentContextStore, FileSystem, ProfileStore, ProjectStore};
use domain::{AgentId, NodeId, Project};

use crate::error::AppError;
use crate::layout::resolve_doc;

/// Input de [`ListResumableAgents::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListResumableAgentsInput {
    /// Le projet dont on inventorie les cellules reprenables.
    pub project: Project,
}

/// Une cellule d'agent reprenable, telle qu'exposée à la couche supérieure.
///
/// Champs alignés sur la spec §15.2 : l'identité de l'agent et de sa cellule
/// hôte, l'id de conversation à reprendre (`None` ⇒ relance à neuf), l'état
/// « tournait » gelé à la fermeture, et si son profil sait reprendre une
/// conversation CLI.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResumableAgent {
    /// Identifiant de l'agent.
    pub agent_id: AgentId,
    /// Nom d'affichage de l'agent (résolu via le manifeste).
    pub name: String,
    /// Cellule hôte où relancer/reprendre l'agent.
    pub node_id: NodeId,
    /// Id de conversation CLI persistant porté par la cellule. `None` ⇒ relance
    /// à neuf (pas d'historique à reprendre).
    pub conversation_id: Option<String>,
    /// Valeur de `agent_was_running` gelée à la fermeture de la cellule.
    pub was_running: bool,
    /// `true` si le profil de l'agent possède une [`domain::SessionStrategy`]
    /// exploitable (présence d'un `resume_flag`).
    pub resume_supported: bool,
}

/// Output de [`ListResumableAgents::execute`].
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ListResumableAgentsOutput {
    /// Les cellules d'agent reprenables, dans l'ordre de parcours des layouts.
    pub resumable: Vec<ResumableAgent>,
}

/// Inventorie, en lecture seule, les cellules d'agent reprenables d'un projet.
///
/// Compose trois ports déjà injectés au composition root, **sans** en ajouter
/// de nouveau :
/// - [`ProjectStore`] + [`FileSystem`] : charger les layouts persistés
///   (`resolve_doc`),
/// - [`AgentContextStore`] : le manifeste, pour le nom et le `profile_id` de
///   chaque agent,
/// - [`ProfileStore`] : pour déterminer `resume_supported`.
pub struct ListResumableAgents {
    #[allow(dead_code)]
    store: Arc<dyn ProjectStore>,
    fs: Arc<dyn FileSystem>,
    contexts: Arc<dyn AgentContextStore>,
    profiles: Arc<dyn ProfileStore>,
}

impl ListResumableAgents {
    /// Construit le use case à partir de ses ports injectés.
    #[must_use]
    pub fn new(
        store: Arc<dyn ProjectStore>,
        fs: Arc<dyn FileSystem>,
        contexts: Arc<dyn AgentContextStore>,
        profiles: Arc<dyn ProfileStore>,
    ) -> Self {
        Self {
            store,
            fs,
            contexts,
            profiles,
        }
    }

    /// Calcule l'inventaire des cellules reprenables.
    ///
    /// Parcourt chaque layout du projet et, pour chaque leaf portant un agent,
    /// lit `conversation_id`/`agent_was_running` (via l'accessor pur
    /// [`domain::LayoutTree::leaf`]). Ne retient que les leaves passant le
    /// filtre `was_running || conversation_id.is_some()`, résout le nom via le
    /// manifeste et `resume_supported` via le profil.
    ///
    /// **Best-effort** : toute défaillance de lecture (projet/mémoire absent,
    /// layouts illisibles, manifeste/profils en erreur) dégrade vers une
    /// **liste vide** ; cette fonction ne renvoie donc jamais d'erreur, mais sa
    /// signature reste `Result` pour rester homogène avec les autres use cases.
    ///
    /// # Errors
    /// N'échoue jamais en pratique (best-effort) ; la signature `Result` est
    /// conservée par cohérence.
    pub async fn execute(
        &self,
        input: ListResumableAgentsInput,
    ) -> Result<ListResumableAgentsOutput, AppError> {
        // Layouts persistés. Toute erreur ⇒ inventaire vide (best-effort).
        let Ok(doc) = resolve_doc(self.fs.as_ref(), &input.project).await else {
            return Ok(ListResumableAgentsOutput::default());
        };

        // Manifeste (nom + profile_id). Absent/illisible ⇒ inventaire vide :
        // sans manifeste on ne peut résoudre ni nom ni profil.
        let Ok(manifest) = self.contexts.load_manifest(&input.project).await else {
            return Ok(ListResumableAgentsOutput::default());
        };

        // Profils disponibles, pour `resume_supported`. Indisponibles ⇒ on
        // considère qu'aucun profil ne sait reprendre (best-effort), sans
        // pour autant masquer les agents reprenables par `conversation_id`.
        let profiles = self.profiles.list().await.unwrap_or_default();

        let mut resumable = Vec::new();

        for named in &doc.layouts {
            for (node_id, agent_id) in named.tree.agent_leaves() {
                // Lecture pure de la cellule hôte pour ses champs de reprise.
                let Some(leaf) = named.tree.leaf(node_id) else {
                    continue;
                };

                let conversation_id = leaf.conversation_id.clone();
                let was_running = leaf.agent_was_running;

                // Filtre §15.2 : reprenable au sens strict uniquement.
                if !was_running && conversation_id.is_none() {
                    continue;
                }

                // Nom via le manifeste ; agent absent ⇒ on ignore la leaf
                // (best-effort : pas d'entrée orpheline dans l'inventaire).
                let Some(entry) = manifest.entries.iter().find(|e| e.agent_id == agent_id) else {
                    continue;
                };
                let name = entry.name.clone();

                // `resume_supported` : le profil de l'agent sait-il reprendre une
                // conversation ? Vrai si :
                // - il porte une `SessionStrategy` (TUI/PTY avec `resume_flag`,
                //   sémantique §15), **ou**
                // - il est **structuré** (`structured_adapter`, §17) : l'adapter
                //   Claude (`--resume`) / Codex (`exec resume`) passe le flag de
                //   reprise du moteur via `SessionPlan::Resume`, donc la reprise est
                //   intrinsèquement supportée.
                let resume_supported = entry
                    .to_agent()
                    .ok()
                    .and_then(|agent| profiles.iter().find(|p| p.id == agent.profile_id))
                    .is_some_and(|profile| {
                        profile.session.is_some() || profile.structured_adapter.is_some()
                    });

                resumable.push(ResumableAgent {
                    agent_id,
                    name,
                    node_id,
                    conversation_id,
                    was_running,
                    resume_supported,
                });
            }
        }

        Ok(ListResumableAgentsOutput { resumable })
    }
}
