//! Window/tab use cases (ARCHITECTURE §6, §10; L10).
//!
//! [`MoveTabToNewWindow`] detaches a tab into a new OS window. The topology
//! change is the pure [`Workspace::move_tab_to_new_window`] domain operation; the
//! use case only loads/persists the workspace and mints the new window id.

use std::sync::Arc;

use domain::ids::{TabId, WindowId};
use std::collections::HashSet;

use domain::layout::{PersistedWindowKind, PersistedWindowState, WindowStateSnapshot, Workspace};
use domain::ports::{IdGenerator, ProjectStore, WindowStateStore};

use crate::error::AppError;

/// Input for [`MoveTabToNewWindow::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MoveTabToNewWindowInput {
    /// The tab to detach into its own new window.
    pub tab_id: TabId,
}

/// Output of [`MoveTabToNewWindow::execute`].
#[derive(Debug, Clone, PartialEq)]
pub struct MoveTabToNewWindowOutput {
    /// The id minted for the new window.
    pub new_window_id: WindowId,
    /// The resulting workspace (already persisted).
    pub workspace: Workspace,
}

/// Detaches a tab into a freshly-created window and persists the workspace.
pub struct MoveTabToNewWindow {
    store: Arc<dyn ProjectStore>,
    ids: Arc<dyn IdGenerator>,
}

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

    /// Executes the detach.
    ///
    /// # Errors
    /// - [`AppError::NotFound`] if the tab is not in the workspace,
    /// - [`AppError::Invalid`] if the resulting window is invalid,
    /// - [`AppError::Store`] on persistence failure.
    pub async fn execute(
        &self,
        input: MoveTabToNewWindowInput,
    ) -> Result<MoveTabToNewWindowOutput, AppError> {
        let workspace = self.store.load_workspace().await?;
        let new_window_id = WindowId::from_uuid(self.ids.new_uuid());

        let workspace = workspace
            .move_tab_to_new_window(input.tab_id, new_window_id)
            .map_err(|e| match e {
                domain::layout::LayoutError::TabNotFound(t) => {
                    AppError::NotFound(format!("tab {t}"))
                }
                other => AppError::Invalid(other.to_string()),
            })?;

        self.store.save_workspace(&workspace).await?;

        Ok(MoveTabToNewWindowOutput {
            new_window_id,
            workspace,
        })
    }
}

/// Input for [`SnapshotOpenWindows::execute`].
#[derive(Debug, Clone, PartialEq)]
pub struct SnapshotOpenWindowsInput {
    /// Open windows captured by the presentation adapter.
    pub windows: Vec<PersistedWindowState>,
}

/// Persists the latest open OS window snapshot.
pub struct SnapshotOpenWindows {
    store: Arc<dyn WindowStateStore>,
}

impl SnapshotOpenWindows {
    /// Builds the use case from its store port.
    #[must_use]
    pub fn new(store: Arc<dyn WindowStateStore>) -> Self {
        Self { store }
    }

    /// Saves a versioned window snapshot.
    ///
    /// # Errors
    /// [`AppError::Store`] on persistence failure.
    pub async fn execute(&self, input: SnapshotOpenWindowsInput) -> Result<(), AppError> {
        self.store
            .save_window_state(&WindowStateSnapshot::new(input.windows))
            .await?;
        Ok(())
    }
}

/// Output of [`RestoreOpenWindows::execute`].
#[derive(Debug, Clone, PartialEq)]
pub struct RestoreOpenWindowsOutput {
    /// Restorable windows, deduplicated by stable label.
    pub windows: Vec<PersistedWindowState>,
}

/// Loads and filters the latest persisted OS window snapshot.
pub struct RestoreOpenWindows {
    windows: Arc<dyn WindowStateStore>,
    _projects: Arc<dyn ProjectStore>,
}

impl RestoreOpenWindows {
    /// Builds the use case from its ports.
    #[must_use]
    pub fn new(windows: Arc<dyn WindowStateStore>, projects: Arc<dyn ProjectStore>) -> Self {
        Self {
            windows,
            _projects: projects,
        }
    }

    /// Loads restorable windows.
    ///
    /// Views are panel-only: their persisted project id, including legacy
    /// `view-<panel>-<project>` identities, is ignored. Duplicate labels are ignored
    /// after the first occurrence.
    ///
    /// # Errors
    /// [`AppError::Store`] on snapshot read failure.
    pub async fn execute(&self) -> Result<RestoreOpenWindowsOutput, AppError> {
        let snapshot = self.windows.load_window_state().await?;
        let mut seen = HashSet::new();
        let mut windows = Vec::new();

        for window in snapshot.windows {
            if window.label.is_empty() || !seen.insert(window.label.clone()) {
                continue;
            }

            match window.kind {
                PersistedWindowKind::Main => windows.push(window),
                PersistedWindowKind::View => {
                    if window.panel.is_none() {
                        continue;
                    }
                    windows.push(window);
                }
            }
        }

        Ok(RestoreOpenWindowsOutput { windows })
    }
}
