//! [`LoadLayout`] and [`MutateLayout`] (ARCHITECTURE §6, §7; L4 + #4).
//!
//! A project owns **several named layouts** (see [`super::store`]). These two use
//! cases operate on **one** layout — the one identified by `layout_id`, or the
//! active layout when `layout_id` is `None`. They stay thin orchestrators: the
//! mutating operations are the domain's pure `LayoutTree` functions.

use std::sync::Arc;

use domain::ports::{EventBus, FileSystem, ProjectStore};
use domain::{
    AgentId, Direction, DomainEvent, LayoutError, LayoutId, LayoutTree, LeafCell, NodeId,
    ProjectId, SessionId,
};

use crate::error::AppError;

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

/// Maps a [`LayoutError`] to the application error type.
fn map_layout_err(e: LayoutError) -> AppError {
    match e {
        LayoutError::NodeNotFound(id) => AppError::NotFound(format!("layout node {id}")),
        other => AppError::Invalid(other.to_string()),
    }
}

/// A layout mutation expressed in terms of the pure domain operations.
///
/// Each variant maps 1:1 to a pure `LayoutTree` method
/// (`split`/`merge`/`resize`/`move`/`set_session`). Decoupling the *operation*
/// from the *tree* keeps the use case a thin orchestrator and lets the
/// presentation layer (and undo/redo) speak in intentions.
#[derive(Debug, Clone, PartialEq)]
pub enum LayoutOperation {
    /// Split a leaf into a two-child split (original + a new empty leaf).
    Split {
        /// The leaf to split.
        target: NodeId,
        /// Row (columns) or Column (rows).
        direction: Direction,
        /// Id for the new sibling leaf.
        new_leaf: NodeId,
        /// Id for the wrapping split container.
        container: NodeId,
    },
    /// Collapse a split container back to one of its children.
    Merge {
        /// The split container to collapse.
        container: NodeId,
        /// Index of the child to keep.
        keep_index: usize,
    },
    /// Reassign the relative weights of a split's children.
    Resize {
        /// The split container to resize.
        container: NodeId,
        /// New weights (one per child, all `> 0`).
        weights: Vec<f32>,
    },
    /// Move the session hosted by one leaf to another (empty) leaf.
    Move {
        /// Source leaf (left empty).
        from: NodeId,
        /// Target leaf (must be empty).
        to: NodeId,
    },
    /// Attach or detach a session to/from a leaf (cell ↔ terminal binding).
    SetSession {
        /// The hosting leaf.
        target: NodeId,
        /// Session to host, or `None` to clear.
        session: Option<SessionId>,
    },
    /// Attach an existing live/background session to a leaf, clearing any
    /// previous visible host of the same session.
    AttachSession {
        /// The hosting leaf.
        target: NodeId,
        /// Session to make visible in this leaf.
        session: SessionId,
    },
    /// Attach or detach an agent to/from a leaf (per-cell agent, feature #3).
    SetCellAgent {
        /// The hosting leaf.
        target: NodeId,
        /// Agent to associate, or `None` to clear.
        agent: Option<AgentId>,
    },
    /// Record (or clear) the persistent CLI conversation id on a leaf (T4b).
    ///
    /// Persisting the id minted at first launch is what makes session resume
    /// effective: on the next open, the leaf carries the id and the launch
    /// resumes the conversation instead of assigning a new one.
    SetCellConversation {
        /// The hosting leaf.
        target: NodeId,
        /// Conversation id to record, or `None` to clear.
        conversation_id: Option<String>,
    },
    /// Persist opaque state for a plugin-provided custom layout leaf.
    SetPluginLayoutState {
        /// The custom plugin layout node.
        target: NodeId,
        /// Opaque plugin-owned state.
        state: serde_json::Value,
    },
}

impl LayoutOperation {
    /// Applies this operation to `tree`, returning the new validated tree.
    fn apply(&self, tree: &LayoutTree) -> Result<LayoutTree, AppError> {
        let result = match self {
            Self::Split {
                target,
                direction,
                new_leaf,
                container,
            } => tree.split(*target, *direction, LeafCell::new(*new_leaf), *container),
            Self::Merge {
                container,
                keep_index,
            } => tree.merge(*container, *keep_index),
            Self::Resize { container, weights } => tree.resize(*container, weights),
            Self::Move { from, to } => tree.move_session(*from, *to),
            Self::SetSession { target, session } => tree.set_session(*target, *session),
            Self::AttachSession { target, session } => tree.attach_session(*target, *session),
            Self::SetCellAgent { target, agent } => tree.set_cell_agent(*target, *agent),
            Self::SetCellConversation {
                target,
                conversation_id,
            } => tree.set_cell_conversation(*target, conversation_id.clone()),
            Self::SetPluginLayoutState { target, state } => {
                tree.set_plugin_layout_state(*target, state.clone())
            }
        };
        result.map_err(map_layout_err)
    }
}

/// Input for [`LoadLayout::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoadLayoutInput {
    /// Project whose layout to load.
    pub project_id: ProjectId,
    /// Which named layout to load; `None` loads the active one.
    pub layout_id: Option<LayoutId>,
}

/// Output of [`LoadLayout::execute`].
#[derive(Debug, Clone, PartialEq)]
pub struct LoadLayoutOutput {
    /// The id of the layout that was loaded (resolved from active when omitted).
    pub layout_id: LayoutId,
    /// The loaded layout tree.
    pub layout: LayoutTree,
}

/// Loads one named layout (the active one by default), self-healing / migrating
/// the layouts store as needed (see [`super::store::resolve_doc`]).
pub struct LoadLayout {
    store: Arc<dyn ProjectStore>,
    fs: Arc<dyn FileSystem>,
}

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

    /// Executes the load.
    ///
    /// # Errors
    /// - [`AppError::NotFound`] if the project or the requested layout is unknown,
    /// - [`AppError::FileSystem`] if seeding the default layouts fails to persist,
    /// - [`AppError::Store`] on registry I/O failure.
    pub async fn execute(&self, input: LoadLayoutInput) -> Result<LoadLayoutOutput, AppError> {
        let project = self.store.load_project(input.project_id).await?;
        let doc = resolve_doc(self.fs.as_ref(), &project).await?;
        // Self-healing (I3): a stale requested id degrades to the valid active
        // id instead of hard-erroring. `find` is now infallible in practice;
        // the `ok_or` is kept as a purely defensive guard.
        let id = doc.resolve_existing_id(input.layout_id);
        let named = doc
            .find(id)
            .ok_or_else(|| AppError::NotFound(format!("layout {id}")))?;
        Ok(LoadLayoutOutput {
            layout_id: id,
            layout: named.tree.clone(),
        })
    }
}

/// Input for [`MutateLayout::execute`].
#[derive(Debug, Clone, PartialEq)]
pub struct MutateLayoutInput {
    /// Project whose layout to mutate.
    pub project_id: ProjectId,
    /// Which named layout to mutate; `None` mutates the active one.
    pub layout_id: Option<LayoutId>,
    /// The operation to apply.
    pub operation: LayoutOperation,
}

/// Output of [`MutateLayout::execute`].
#[derive(Debug, Clone, PartialEq)]
pub struct MutateLayoutOutput {
    /// The id of the mutated layout.
    pub layout_id: LayoutId,
    /// The new, persisted layout tree.
    pub layout: LayoutTree,
}

/// Applies a pure layout operation to one named layout, persists the whole
/// layouts store and publishes [`DomainEvent::LayoutChanged`].
pub struct MutateLayout {
    store: Arc<dyn ProjectStore>,
    fs: Arc<dyn FileSystem>,
    events: Arc<dyn EventBus>,
}

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

    /// Executes the mutation.
    ///
    /// # Errors
    /// - [`AppError::NotFound`] if the project, layout or a referenced node is unknown,
    /// - [`AppError::Invalid`] if the operation violates a layout invariant,
    /// - [`AppError::FileSystem`] on persistence failure,
    /// - [`AppError::Store`] on registry I/O failure.
    pub async fn execute(&self, input: MutateLayoutInput) -> Result<MutateLayoutOutput, AppError> {
        let project = self.store.load_project(input.project_id).await?;
        let mut doc = resolve_doc(self.fs.as_ref(), &project).await?;
        // Self-healing (I3): a stale requested id degrades to the valid active
        // id instead of hard-erroring. `find_mut` is now infallible in
        // practice; the `ok_or` is kept as a purely defensive guard.
        let id = doc.resolve_existing_id(input.layout_id);

        let named = doc
            .find_mut(id)
            .ok_or_else(|| AppError::NotFound(format!("layout {id}")))?;
        let next = input.operation.apply(&named.tree)?;
        named.tree = next.clone();

        persist_doc(self.fs.as_ref(), &project, &doc).await?;
        self.events.publish(DomainEvent::LayoutChanged {
            project_id: input.project_id,
        });

        Ok(MutateLayoutOutput {
            layout_id: id,
            layout: next,
        })
    }
}
