//! 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::{
    PersistedPluginLayoutWindow, PersistedWindowKind, PersistedWindowState, WindowStateSnapshot,
    Workspace,
};
use domain::ports::{
    IdGenerator, PluginManifestValidator, PluginPackageStore, PluginRegistryStore, ProjectStore,
    WindowStateStore,
};
use domain::{PluginId, PluginLayoutType};

use crate::error::AppError;
use crate::plugin::{ensure_runtime_plugin_layout_contribution, PluginRuntimeLayoutContribution};

/// 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>,
    packages: Arc<dyn PluginPackageStore>,
    registry: Arc<dyn PluginRegistryStore>,
    validator: Arc<dyn PluginManifestValidator>,
}

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

    /// 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);
                }
                PersistedWindowKind::PluginLayout => {
                    let Some(surface) = &window.plugin_layout else {
                        continue;
                    };
                    if self.validate_plugin_layout_surface(surface).await.is_ok() {
                        windows.push(window);
                    }
                }
            }
        }

        Ok(RestoreOpenWindowsOutput { windows })
    }

    async fn validate_plugin_layout_surface(
        &self,
        surface: &PersistedPluginLayoutWindow,
    ) -> Result<(), AppError> {
        ensure_runtime_plugin_layout_contribution(
            self.packages.as_ref(),
            self.registry.as_ref(),
            self.validator.as_ref(),
            &surface.plugin_id,
            &surface.layout_type,
        )
        .await
        .map(|_| ())
    }
}

/// Input for [`OpenPluginLayoutWindow::execute`].
#[derive(Debug, Clone, PartialEq)]
pub struct OpenPluginLayoutWindowInput {
    /// Provider plugin id.
    pub plugin_id: PluginId,
    /// Layout type declared by the provider plugin.
    pub layout_type: PluginLayoutType,
    /// Opaque plugin-owned initial/window state.
    pub state: serde_json::Value,
}

/// Output of [`OpenPluginLayoutWindow::execute`].
#[derive(Debug, Clone, PartialEq)]
pub struct OpenPluginLayoutWindowOutput {
    /// Runtime contribution that was validated.
    pub contribution: PluginRuntimeLayoutContribution,
    /// Persistable plugin layout surface.
    pub surface: PersistedPluginLayoutWindow,
}

/// Validates a plugin layout contribution before a detached OS window hosts it.
pub struct OpenPluginLayoutWindow {
    packages: Arc<dyn PluginPackageStore>,
    registry: Arc<dyn PluginRegistryStore>,
    validator: Arc<dyn PluginManifestValidator>,
}

impl OpenPluginLayoutWindow {
    /// Builds the use case from plugin runtime ports.
    #[must_use]
    pub fn new(
        packages: Arc<dyn PluginPackageStore>,
        registry: Arc<dyn PluginRegistryStore>,
        validator: Arc<dyn PluginManifestValidator>,
    ) -> Self {
        Self {
            packages,
            registry,
            validator,
        }
    }

    /// Executes the validation.
    ///
    /// # Errors
    /// [`AppError::Invalid`] when the plugin is inactive or does not declare the
    /// requested layout contribution; other errors bubble from plugin loading.
    pub async fn execute(
        &self,
        input: OpenPluginLayoutWindowInput,
    ) -> Result<OpenPluginLayoutWindowOutput, AppError> {
        let contribution = ensure_runtime_plugin_layout_contribution(
            self.packages.as_ref(),
            self.registry.as_ref(),
            self.validator.as_ref(),
            &input.plugin_id,
            &input.layout_type,
        )
        .await?;
        let surface = PersistedPluginLayoutWindow {
            plugin_id: input.plugin_id,
            layout_type: input.layout_type,
            state: input.state,
        };
        Ok(OpenPluginLayoutWindowOutput {
            contribution,
            surface,
        })
    }
}
