//! [`ReconcileLayouts`] — réconcilie, à l'**ouverture** d'un projet, les
//! `layouts.json` qui contiennent des feuilles en **doublon** sur un même agent
//! (lot R0c du cadrage orchestration v5, §3.4 « Trou C »).
//!
//! C'est la **jumelle** de [`super::snapshot::SnapshotRunningAgents`] : là où le
//! snapshot **gèle** `agent_was_running` à la *fermeture*, cette réconciliation
//! **dé-doublonne** à l'*ouverture*. Un `layouts.json` persisté peut déjà porter
//! deux feuilles sur le **même** `agent` id ; à la réouverture il ne faut **pas**
//! relancer la 2ᵉ. On garde **une** feuille « hôte » (potentiellement vivante /
//! reprenable) et on transforme les autres en **vues mortes** : leur
//! `agent_was_running` repasse à `false` et leur `conversation_id` est retiré.
//! C'est précisément ce qui éliminait le symptôme « une cellule reset au retour
//! d'onglet ».
//!
//! Le use case est un mince orchestrateur au-dessus de :
//! - le store des layouts persistés ([`super::store`]),
//! - l'opération pure du domaine
//!   [`domain::LayoutTree::reconcile_duplicate_agents`] (qui porte la **règle
//!   déterministe de choix de l'hôte** et la garantie d'idempotence).
//!
//! **Idempotence / no-op** : un layout sans doublon (ou déjà réconcilié) ressort
//! **identique** de l'opération pure ; on ne persiste alors **rien** (aucune
//! écriture). Une 2ᵉ ouverture du même projet réconcilié est donc un no-op.
//!
//! **Ordre d'insertion** : à appeler à l'ouverture du projet, **après** le
//! chargement/résolution des layouts et **avant** toute reprise
//! ([`crate::ListResumableAgents`]) — qui relit la version persistée — de sorte
//! qu'on ne propose / ne relance qu'**une** session par agent.

use std::sync::Arc;

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

use crate::error::AppError;

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

/// Input de [`ReconcileLayouts::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReconcileLayoutsInput {
    /// Le projet dont les layouts doivent être dé-doublonnés.
    pub project_id: ProjectId,
}

/// Output de [`ReconcileLayouts::execute`].
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ReconcileLayoutsOutput {
    /// `true` si au moins un layout a été modifié (et donc le doc persisté).
    /// `false` ⇒ aucun doublon : no-op, aucune écriture (idempotence).
    pub changed: bool,
}

/// Réconcilie les feuilles d'agent en doublon de tous les layouts d'un projet,
/// puis persiste le doc **uniquement** s'il a changé.
pub struct ReconcileLayouts {
    store: Arc<dyn ProjectStore>,
    fs: Arc<dyn FileSystem>,
}

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

    /// Exécute la réconciliation pour un projet.
    ///
    /// Pour chaque layout, applique l'opération pure
    /// [`domain::LayoutTree::reconcile_duplicate_agents`]. Si **aucun** arbre n'a
    /// changé, ne persiste **rien** (no-op idempotent). Sinon, persiste tout le
    /// doc une fois.
    ///
    /// # Errors
    /// - [`AppError::NotFound`] si le projet est inconnu,
    /// - [`AppError::Store`] sur défaillance du registre,
    /// - [`AppError::FileSystem`] sur défaillance de persistance.
    pub async fn execute(
        &self,
        input: ReconcileLayoutsInput,
    ) -> Result<ReconcileLayoutsOutput, AppError> {
        let project = self.store.load_project(input.project_id).await?;
        let mut doc = resolve_doc(self.fs.as_ref(), &project).await?;

        let mut changed = false;
        for named in &mut doc.layouts {
            let reconciled = named.tree.reconcile_duplicate_agents();
            if reconciled != named.tree {
                named.tree = reconciled;
                changed = true;
            }
        }

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

        Ok(ReconcileLayoutsOutput { changed })
    }
}
