//! Named-layout management use cases (#4): list, create, rename, delete and set
//! the active layout. Each loads the project's layouts store (see
//! [`super::store`]), applies the change and persists it.

use std::sync::Arc;

use domain::ports::{EventBus, FileSystem, IdGenerator, ProjectStore};
use domain::{DomainEvent, LayoutId, ProjectId};

use crate::error::AppError;

use super::store::{default_tree, persist_doc, resolve_doc, LayoutKind, NamedLayout};

/// Lightweight descriptor of a named layout (no tree), for the layouts tab bar.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LayoutInfo {
    /// Stable identifier.
    pub id: LayoutId,
    /// Display name.
    pub name: String,
    /// Kind of this layout.
    pub kind: LayoutKind,
}

// ---------------------------------------------------------------------------
// ListLayouts
// ---------------------------------------------------------------------------

/// Input for [`ListLayouts::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListLayoutsInput {
    /// Project whose layouts to list.
    pub project_id: ProjectId,
}

/// Output of [`ListLayouts::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListLayoutsOutput {
    /// All named layouts (id + name), in order.
    pub layouts: Vec<LayoutInfo>,
    /// The active layout.
    pub active_id: LayoutId,
}

/// Lists a project's named layouts and the active one.
pub struct ListLayouts {
    store: Arc<dyn ProjectStore>,
    fs: Arc<dyn FileSystem>,
}

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

    /// Lists the layouts.
    ///
    /// # Errors
    /// [`AppError::NotFound`] for an unknown project, [`AppError::FileSystem`] /
    /// [`AppError::Store`] on I/O failure.
    pub async fn execute(&self, input: ListLayoutsInput) -> Result<ListLayoutsOutput, AppError> {
        let project = self.store.load_project(input.project_id).await?;
        let doc = resolve_doc(self.fs.as_ref(), &project).await?;
        Ok(ListLayoutsOutput {
            layouts: doc
                .layouts
                .iter()
                .map(|l| LayoutInfo {
                    id: l.id,
                    name: l.name.clone(),
                    kind: l.kind,
                })
                .collect(),
            active_id: doc.active_id,
        })
    }
}

// ---------------------------------------------------------------------------
// CreateLayout
// ---------------------------------------------------------------------------

/// Input for [`CreateLayout::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateLayoutInput {
    /// Owning project.
    pub project_id: ProjectId,
    /// Display name for the new layout.
    pub name: String,
    /// Kind of the new layout (defaults to Terminal).
    pub kind: LayoutKind,
}

/// Output of [`CreateLayout::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateLayoutOutput {
    /// The id minted for the new (now active) layout.
    pub layout_id: LayoutId,
}

/// Creates a new empty named layout and makes it active.
pub struct CreateLayout {
    store: Arc<dyn ProjectStore>,
    fs: Arc<dyn FileSystem>,
    ids: Arc<dyn IdGenerator>,
    events: Arc<dyn EventBus>,
}

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

    /// Creates the layout.
    ///
    /// # Errors
    /// [`AppError::Invalid`] for an empty name, [`AppError::NotFound`] for an
    /// unknown project, I/O errors otherwise.
    pub async fn execute(&self, input: CreateLayoutInput) -> Result<CreateLayoutOutput, AppError> {
        let name = input.name.trim();
        if name.is_empty() {
            return Err(AppError::Invalid("layout name is empty".to_owned()));
        }
        let project = self.store.load_project(input.project_id).await?;
        let mut doc = resolve_doc(self.fs.as_ref(), &project).await?;

        let id = LayoutId::from_uuid(self.ids.new_uuid());
        doc.layouts.push(NamedLayout {
            id,
            name: name.to_owned(),
            kind: input.kind,
            tree: default_tree(),
        });
        doc.active_id = id; // a freshly-created layout becomes active.

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

// ---------------------------------------------------------------------------
// RenameLayout
// ---------------------------------------------------------------------------

/// Input for [`RenameLayout::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenameLayoutInput {
    /// Owning project.
    pub project_id: ProjectId,
    /// Layout to rename.
    pub layout_id: LayoutId,
    /// New display name.
    pub name: String,
}

/// Renames a named layout.
pub struct RenameLayout {
    store: Arc<dyn ProjectStore>,
    fs: Arc<dyn FileSystem>,
    events: Arc<dyn EventBus>,
}

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

    /// Renames the layout.
    ///
    /// # Errors
    /// [`AppError::Invalid`] for an empty name, [`AppError::NotFound`] if the
    /// project or layout is unknown.
    pub async fn execute(&self, input: RenameLayoutInput) -> Result<(), AppError> {
        let name = input.name.trim();
        if name.is_empty() {
            return Err(AppError::Invalid("layout name is empty".to_owned()));
        }
        let project = self.store.load_project(input.project_id).await?;
        let mut doc = resolve_doc(self.fs.as_ref(), &project).await?;
        let named = doc
            .find_mut(input.layout_id)
            .ok_or_else(|| AppError::NotFound(format!("layout {}", input.layout_id)))?;
        named.name = name.to_owned();

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

// ---------------------------------------------------------------------------
// DeleteLayout
// ---------------------------------------------------------------------------

/// Input for [`DeleteLayout::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeleteLayoutInput {
    /// Owning project.
    pub project_id: ProjectId,
    /// Layout to delete.
    pub layout_id: LayoutId,
}

/// Output of [`DeleteLayout::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeleteLayoutOutput {
    /// The active layout after the deletion.
    pub active_id: LayoutId,
}

/// Deletes a named layout. The last remaining layout cannot be deleted; if the
/// active layout is removed, the first remaining one becomes active.
pub struct DeleteLayout {
    store: Arc<dyn ProjectStore>,
    fs: Arc<dyn FileSystem>,
    events: Arc<dyn EventBus>,
}

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

    /// Deletes the layout.
    ///
    /// # Errors
    /// [`AppError::Invalid`] if it is the last layout, [`AppError::NotFound`] if
    /// the project or layout is unknown.
    pub async fn execute(&self, input: DeleteLayoutInput) -> Result<DeleteLayoutOutput, AppError> {
        let project = self.store.load_project(input.project_id).await?;
        let mut doc = resolve_doc(self.fs.as_ref(), &project).await?;

        if doc.layouts.len() <= 1 {
            return Err(AppError::Invalid(
                "cannot delete the last layout".to_owned(),
            ));
        }
        if doc.find(input.layout_id).is_none() {
            return Err(AppError::NotFound(format!("layout {}", input.layout_id)));
        }
        doc.layouts.retain(|l| l.id != input.layout_id);
        if doc.active_id == input.layout_id {
            doc.active_id = doc.layouts[0].id;
        }

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

// ---------------------------------------------------------------------------
// SetActiveLayout
// ---------------------------------------------------------------------------

/// Input for [`SetActiveLayout::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SetActiveLayoutInput {
    /// Owning project.
    pub project_id: ProjectId,
    /// Layout to make active.
    pub layout_id: LayoutId,
}

/// Output of [`SetActiveLayout::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SetActiveLayoutOutput {
    /// The id of the layout that was **actually** made active. Equals the
    /// requested id when it exists; otherwise the unchanged current active id
    /// (self-healing fallback). Authoritative for the frontend (invariant I4).
    pub active_id: LayoutId,
}

/// Switches the active layout of a project.
pub struct SetActiveLayout {
    store: Arc<dyn ProjectStore>,
    fs: Arc<dyn FileSystem>,
    events: Arc<dyn EventBus>,
}

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

    /// Sets the active layout.
    ///
    /// A stale requested id (e.g. an `activeId` left over after git overwrote
    /// `layouts.json`) must **not** freeze the workspace: instead of hard-
    /// erroring, it degrades silently to the current active layout (invariant
    /// I3). The returned [`SetActiveLayoutOutput::active_id`] is the id that was
    /// actually activated and is authoritative for the frontend (invariant I4).
    ///
    /// # Errors
    /// - [`AppError::FileSystem`] on persistence failure,
    /// - [`AppError::Store`] on registry I/O failure.
    pub async fn execute(
        &self,
        input: SetActiveLayoutInput,
    ) -> Result<SetActiveLayoutOutput, AppError> {
        let project = self.store.load_project(input.project_id).await?;
        let mut doc = resolve_doc(self.fs.as_ref(), &project).await?;
        // Self-heal: keep the requested id when valid, else fall back to the
        // (always-valid, I2) current active id rather than erroring.
        let active_id = doc.resolve_existing_id(Some(input.layout_id));
        doc.active_id = active_id;

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