//! # IdeA — `app-tauri` (presentation / driving adapter + composition root)
//!
//! This crate is the **only** place that knows every other crate. It:
//! - builds the concrete adapters and injects them into use cases
//!   ([`state::AppState`], the composition root),
//! - exposes `#[tauri::command]` handlers ([`commands`]) mapping DTOs ↔ use cases,
//! - relays domain events to the frontend ([`events::TauriEventRelay`]),
//! - hosts the generic PTY↔Channel bridge ([`pty::PtyBridge`]) for L3 and its
//!   structured-chat twin ([`chat::ChatBridge`]) for §17.
//!
//! The wiring lives in the library (testable) and `main.rs` is a thin shim.

#![forbid(unsafe_code)]
#![warn(missing_docs)]

pub mod chat;
pub mod commands;
pub mod dto;
pub mod embedded_server;
pub mod events;
pub mod mcp_bridge;
pub mod mcp_endpoint;
pub mod openai_tools;
pub mod plugins;
pub mod pty;
pub mod server;
pub mod state;
pub mod stream;
pub mod templates;
pub mod tickets;

use std::process::ExitCode;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;

use application::{AppError, GetAppExitWorkGuardStateInput, SnapshotOpenWindowsInput};
use domain::{
    PersistedMonitorState, PersistedPluginLayoutWindow, PersistedWindowKind,
    PersistedWindowPosition, PersistedWindowSize, PersistedWindowState, ProjectId,
};
use tauri::{
    Emitter, Manager, PhysicalPosition, PhysicalSize, WebviewUrl, WebviewWindow,
    WebviewWindowBuilder,
};
use uuid::Uuid;

use state::AppState;

static EXIT_GUARD_CONFIRMED: AtomicBool = AtomicBool::new(false);
static PANIC_HOOK_INSTALLED: AtomicBool = AtomicBool::new(false);

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MainCloseAction {
    AllowShutdown,
    PreventAndNotify,
}

fn decide_main_close_action(
    has_work_in_progress: bool,
    already_confirmed: bool,
) -> MainCloseAction {
    if has_work_in_progress && !already_confirmed {
        MainCloseAction::PreventAndNotify
    } else {
        MainCloseAction::AllowShutdown
    }
}

fn should_install_exit_guard(window_label: &str) -> bool {
    window_label == "main"
}

fn install_panic_diagnostics_hook() {
    if PANIC_HOOK_INSTALLED.swap(true, Ordering::SeqCst) {
        return;
    }
    let previous = std::panic::take_hook();
    std::panic::set_hook(Box::new(move |info| {
        let message = info
            .payload()
            .downcast_ref::<&str>()
            .copied()
            .or_else(|| info.payload().downcast_ref::<String>().map(String::as_str))
            .unwrap_or("<non-string panic payload>");
        let location = info
            .location()
            .map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column()))
            .unwrap_or_else(|| "<unknown>".to_owned());
        let thread = std::thread::current();
        let thread_name = thread.name().unwrap_or("<unnamed>");
        application::diag!("[panic] thread={thread_name} location={location} message={message}");
        application::diag!(
            "[panic] backtrace:\n{}",
            std::backtrace::Backtrace::force_capture()
        );
        previous(info);
    }));
}

fn apply_main_close_decision(
    guard: application::AppExitWorkGuardState,
    already_confirmed: bool,
    mut prevent_close: impl FnMut(),
    mut emit_guard: impl FnMut(application::AppExitWorkGuardState),
    mut shutdown: impl FnMut(),
) -> MainCloseAction {
    let action = decide_main_close_action(guard.has_work_in_progress, already_confirmed);
    match action {
        MainCloseAction::PreventAndNotify => {
            prevent_close();
            emit_guard(guard);
        }
        MainCloseAction::AllowShutdown => shutdown(),
    }
    action
}

/// The `argv[1]` subcommand token that switches the binary into the headless
/// `mcp-server` **bridge** mode (cadrage v5 §1.3) instead of launching Tauri.
pub const MCP_SERVER_SUBCOMMAND: &str = "mcp-server";
/// The `argv[1]` subcommand token that starts the secure HTTP server adapter.
pub const SERVE_SUBCOMMAND: &str = "--serve";

/// Process entry point: routes `argv` **before** anything Tauri/WebKit is touched.
///
/// When invoked as `<exe> mcp-server …` (an MCP CLI spawned us from the injected
/// `.mcp.json` declaration), we run the **stdio↔loopback bridge** headless and
/// **never** initialise the webview — see [`mcp_bridge::run_mcp_bridge`]. When
/// invoked as `<exe> --serve …`, we run the secure HTTP adapter headless. Any
/// other invocation is the normal IDE launch: [`run`] (which blocks until the
/// window closes and then exits the process itself).
///
/// Returns the [`ExitCode`] for the bridge path; the normal path does not return.
#[must_use]
pub fn dispatch() -> ExitCode {
    let mut args = std::env::args_os().skip(1);
    if args.next().is_some_and(|a| a == *MCP_SERVER_SUBCOMMAND) {
        // Headless bridge: bypass Tauri entirely. Forward the remaining args
        // (`--endpoint`, `--project`, `--requester`) to the bridge parser.
        let rest: Vec<String> = args.map(|a| a.to_string_lossy().into_owned()).collect();
        return mcp_bridge::run_mcp_bridge(rest);
    }
    let mut args = std::env::args_os().skip(1);
    if args.next().is_some_and(|a| a == *SERVE_SUBCOMMAND) {
        let rest: Vec<String> = args.map(|a| a.to_string_lossy().into_owned()).collect();
        return server::run_from_args(rest);
    }

    run();
    ExitCode::SUCCESS
}

/// Builds and runs the Tauri application.
///
/// Sets up the composition root (resolving the app-data directory via the Tauri
/// path API), registers commands, spawns the event relay, and starts the main
/// window.
///
/// # Panics
/// Panics if the Tauri application fails to build or run (no window/webview), or
/// if the app-data directory cannot be resolved.
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_dialog::init())
        .register_uri_scheme_protocol("idea-plugin", |ctx, request| {
            plugins::plugin_asset_protocol(ctx.app_handle(), request)
        })
        .setup(|app| {
            // Resolve the machine-local IDE data directory (ARCHITECTURE §9.2)
            // and build the composition root once the app handle exists, so the
            // stores receive a concrete path without ever touching Tauri.
            let app_data_dir = app
                .path()
                .app_data_dir()
                .expect("failed to resolve the app data directory");
            let resource_dir = app.path().resource_dir().ok();
            // Point the orchestrator's best-effort diagnostics at a persistent file
            // (`<app-data>/logs/idea.log`) so inter-agent rendezvous beacons survive a
            // click-launched AppImage (whose stderr is otherwise discarded). Best-effort:
            // if the file can't be opened the beacons simply stay on stderr.
            application::diag::set_log_path(app_data_dir.join("logs").join("idea.log"));
            install_panic_diagnostics_hook();
            application::diag!("[startup] IdeA launched; diagnostics log armed");
            let app_state = AppState::build_with_resource_dir(app_data_dir, resource_dir);

            // Wire the domain event bus → Tauri events relay.
            events::spawn_relay(app.handle().clone(), &app_state.event_bus);

            let embedded_server = Arc::clone(&app_state.embedded_server);
            let plugin_mcp_reconcile = Arc::clone(&app_state.reconcile_plugin_mcp_servers);
            let core = app_state.core();
            app.manage(app_state);
            tauri::async_runtime::spawn(async move {
                if let Err(err) = embedded_server.auto_start_if_enabled(core).await {
                    application::diag!(
                        "[embedded-server] auto-start failed: {}: {}",
                        err.code,
                        err.message
                    );
                }
            });
            tauri::async_runtime::spawn(async move {
                if let Err(err) = plugin_mcp_reconcile.execute().await {
                    application::diag!("[plugins] MCP reconcile at boot failed: {err}");
                }
            });

            // Kill all live PTYs cleanly when the main window is closing. This is
            // independent of the per-view (navigation/layout) lifecycle — those
            // must NEVER kill a PTY — and only fires on a genuine app shutdown.
            // A brutal crash is best-effort and out of scope.
            if should_install_exit_guard("main") && app.get_webview_window("main").is_some() {
                let window = app
                    .get_webview_window("main")
                    .expect("main window existence checked above");
                let handle = app.handle().clone();
                window.on_window_event(move |event| {
                    if let tauri::WindowEvent::CloseRequested { api, .. } = event {
                        let already_confirmed = consume_exit_guard_confirmation();
                        let guard =
                            tauri::async_runtime::block_on(app_exit_work_guard_state(&handle));
                        if let Ok(guard) = guard {
                            let action = apply_main_close_decision(
                                guard,
                                already_confirmed,
                                || api.prevent_close(),
                                |guard| {
                                    let _ = handle.emit(
                                        "app-exit-work-guard",
                                        backend::dto::AppExitWorkGuardStateDto::from(guard),
                                    );
                                },
                                || shutdown_app_after_confirm(&handle),
                            );
                            if action == MainCloseAction::PreventAndNotify {
                                return;
                            }
                        } else {
                            shutdown_app_after_confirm(&handle);
                        }
                    }
                });
            }

            restore_open_webview_windows(app.handle());

            Ok(())
        })
        .invoke_handler(tauri::generate_handler![
            commands::health,
            commands::create_project,
            commands::open_project,
            commands::close_project,
            commands::list_projects,
            commands::read_project_context,
            commands::update_project_context,
            commands::get_project_permissions,
            commands::update_project_permissions,
            commands::update_agent_permissions,
            commands::update_agent_effort,
            commands::resolve_agent_permissions,
            commands::get_project_system_permissions,
            commands::update_project_system_permissions,
            commands::update_agent_system_permissions,
            commands::resolve_agent_system_permissions,
            commands::get_mcp_tool_permissions,
            commands::update_project_mcp_tool_permissions,
            commands::update_agent_mcp_tool_permissions,
            commands::open_terminal,
            commands::write_terminal,
            commands::resize_terminal,
            commands::close_terminal,
            commands::reattach_terminal,
            commands::load_layout,
            commands::mutate_layout,
            commands::list_layouts,
            commands::create_layout,
            commands::rename_layout,
            commands::delete_layout,
            commands::set_active_layout,
            commands::first_run_state,
            commands::reference_profiles,
            commands::detect_profiles,
            commands::list_profiles,
            commands::save_profile,
            commands::save_opencode_provider_profile,
            commands::list_opencode_providers,
            commands::list_claude_models,
            commands::list_codex_models,
            commands::clone_profile_from_seed,
            commands::clone_opencode_profile_from_seed,
            commands::delete_profile,
            commands::configure_profiles,
            commands::list_model_servers,
            commands::save_model_server,
            commands::preview_model_server_command,
            commands::delete_model_server,
            commands::delete_model_artifact,
            commands::list_embedder_profiles,
            commands::save_embedder_profile,
            commands::delete_embedder_profile,
            commands::describe_embedder_engines,
            commands::dismiss_embedder_suggestion,
            commands::create_agent,
            commands::list_agents,
            commands::get_app_exit_work_guard_state,
            commands::confirm_app_exit,
            tickets::ticket_create,
            tickets::ticket_read,
            tickets::ticket_delete,
            tickets::ticket_bulk_update_status,
            tickets::ticket_bulk_update_priority,
            tickets::ticket_bulk_delete,
            tickets::open_ticket_chat,
            tickets::close_ticket_chat,
            tickets::ticket_list,
            tickets::ticket_update,
            tickets::ticket_attachment_add,
            tickets::ticket_attachment_read,
            tickets::ticket_attachment_mark_summarized,
            tickets::ticket_read_carnet,
            tickets::ticket_update_carnet,
            tickets::ticket_link,
            tickets::ticket_unlink,
            tickets::ticket_assign,
            tickets::sprint_create,
            tickets::sprint_list,
            tickets::sprint_rename,
            tickets::sprint_reorder,
            tickets::sprint_delete,
            tickets::ticket_assign_sprint,
            tickets::ticket_unassign_sprint,
            commands::get_project_work_state,
            commands::read_conversation_page,
            commands::list_live_agents,
            commands::attach_live_agent,
            commands::stop_live_agent,
            commands::read_agent_context,
            commands::update_agent_context,
            commands::delete_agent,
            commands::launch_agent,
            commands::change_agent_profile,
            commands::agent_send,
            commands::cancel_resume,
            commands::set_resume_at,
            commands::interrupt_agent,
            commands::delegation_delivered,
            commands::set_front_attached,
            commands::reattach_agent_chat,
            commands::close_agent_session,
            commands::list_resumable_agents,
            commands::inspect_conversation,
            commands::create_template,
            commands::update_template,
            commands::list_templates,
            commands::delete_template,
            commands::create_agent_from_template,
            commands::detect_agent_drift,
            commands::sync_agent_with_template,
            commands::git_status,
            commands::git_stage,
            commands::git_unstage,
            commands::git_commit,
            commands::git_branches,
            commands::git_checkout,
            commands::git_log,
            commands::git_init,
            commands::git_graph,
            commands::create_skill,
            commands::update_skill,
            commands::list_skills,
            commands::delete_skill,
            commands::assign_skill_to_agent,
            commands::unassign_skill_from_agent,
            commands::create_memory,
            commands::update_memory,
            commands::list_memories,
            commands::get_memory,
            commands::delete_memory,
            commands::read_memory_index,
            commands::recall_memory,
            commands::resolve_memory_links,
            commands::set_focused_project,
            commands::get_focused_project,
            commands::list_open_view_windows,
            commands::open_plugin_layout_window,
            commands::open_view_window,
            commands::close_view_window,
            commands::move_tab_to_new_window,
            commands::spawn_background_command,
            commands::cancel_background_task,
            commands::retry_background_task,
            commands::attach_background_task,
            commands::list_background_tasks,
            plugins::plugin_list_plugins,
            plugins::plugin_review_package,
            plugins::plugin_install_from_archive,
            plugins::plugin_install_from_directory,
            plugins::plugin_set_enabled,
            plugins::plugin_uninstall,
            plugins::plugin_list_runtime_contributions,
            plugins::plugin_workspace_read_text,
            plugins::plugin_workspace_read_binary,
            plugins::plugin_workspace_write_text,
            plugins::plugin_workspace_write_binary,
            plugins::plugin_storage_get,
            plugins::plugin_storage_set,
            plugins::plugin_storage_delete,
            plugins::plugin_workspace_list_dir,
            plugins::plugin_workspace_stat,
            plugins::plugin_query_project_structure,
            plugins::plugin_config_read_document,
            plugins::plugin_config_update_document,
            plugins::plugin_task_run_command,
            plugins::plugin_task_get_status,
            plugins::plugin_toolchain_diagnose,
            plugins::plugin_events_subscribe,
            plugins::plugin_events_poll,
            plugins::plugin_events_unsubscribe,
            plugins::plugin_open_plugins_folder,
            commands::get_server_exposure_settings,
            commands::save_server_exposure_settings,
            commands::preview_server_exposure_settings,
            commands::embedded_server_status,
            commands::embedded_server_start,
            commands::embedded_server_stop,
            commands::embedded_server_generate_pairing_code,
            commands::list_devices,
            commands::create_pairing_code,
            commands::rename_device,
            commands::revoke_device,
            commands::revoke_all_devices,
        ])
        .run(tauri::generate_context!())
        .expect("error while running IdeA Tauri application");
}

#[cfg(test)]
fn plugin_workspace_invoke_handler<R: tauri::Runtime>(
) -> impl Fn(tauri::ipc::Invoke<R>) -> bool + Send + Sync + 'static {
    tauri::generate_handler![
        plugins::plugin_workspace_read_text,
        plugins::plugin_workspace_read_binary,
        plugins::plugin_workspace_write_text,
        plugins::plugin_workspace_write_binary,
        plugins::plugin_storage_get,
        plugins::plugin_storage_set,
        plugins::plugin_storage_delete,
        plugins::plugin_workspace_list_dir,
        plugins::plugin_workspace_stat,
        plugins::plugin_query_project_structure,
        plugins::plugin_config_read_document,
        plugins::plugin_config_update_document,
        plugins::plugin_task_run_command,
        plugins::plugin_task_get_status,
        plugins::plugin_toolchain_diagnose,
        plugins::plugin_events_subscribe,
        plugins::plugin_events_poll,
        plugins::plugin_events_unsubscribe,
    ]
}

async fn app_exit_work_guard_state(
    handle: &tauri::AppHandle,
) -> Result<application::AppExitWorkGuardState, AppError> {
    let Some(state) = handle.try_state::<AppState>() else {
        return Ok(application::AppExitWorkGuardState {
            has_work_in_progress: false,
            busy_agent_count: 0,
            active_background_task_count: 0,
            details: Vec::new(),
        });
    };

    let mut projects = Vec::new();
    for project_id in state.open_project_ids() {
        projects.push(state.project_store.load_project(project_id).await?);
    }
    state
        .get_app_exit_work_guard_state
        .execute(GetAppExitWorkGuardStateInput { projects })
        .await
}

/// Executes the global shutdown teardown after the close guard has allowed exit.
///
/// The order intentionally mirrors the historical inline `CloseRequested` hook:
/// snapshot windows, snapshot `agent_was_running`, kill PTYs, stop model servers,
/// stop the embedded server, then close secondary webview windows.
pub fn shutdown_app_after_confirm(handle: &tauri::AppHandle) {
    if let Some(state) = handle.try_state::<AppState>() {
        let open_windows = snapshot_open_webview_windows(handle);
        let window_snapshot = std::sync::Arc::clone(&state.snapshot_open_windows);
        let pty = std::sync::Arc::clone(&state.pty_port);
        let snapshot = std::sync::Arc::clone(&state.snapshot_running_agents);
        let model_servers = std::sync::Arc::clone(&state.ensure_local_model_server);
        let embedded_server = std::sync::Arc::clone(&state.embedded_server);
        let open_projects = state.open_project_ids();
        let handles = state.terminal_sessions.handles();
        tauri::async_runtime::block_on(async move {
            let _ = window_snapshot
                .execute(SnapshotOpenWindowsInput {
                    windows: open_windows,
                })
                .await;
            for project_id in open_projects {
                let _ = snapshot
                    .execute(application::SnapshotRunningAgentsInput { project_id })
                    .await;
            }
            for h in handles {
                let _ = pty.kill(&h).await;
            }
            let _ = model_servers.stop_on_app_exit().await;
            let _ = embedded_server.stop().await;
        });
    }

    close_non_main_webview_windows(handle);
}

pub(crate) fn confirm_next_main_window_close() {
    EXIT_GUARD_CONFIRMED.store(true, Ordering::SeqCst);
}

fn consume_exit_guard_confirmation() -> bool {
    EXIT_GUARD_CONFIRMED.swap(false, Ordering::SeqCst)
}

pub(crate) async fn read_app_exit_work_guard_state(
    handle: &tauri::AppHandle,
) -> Result<application::AppExitWorkGuardState, AppError> {
    app_exit_work_guard_state(handle).await
}

fn close_non_main_webview_windows(handle: &tauri::AppHandle) {
    for (label, window) in handle.webview_windows() {
        if !should_close_with_main_window(&label) {
            continue;
        }

        let _ = window.close();
    }
}

fn should_close_with_main_window(label: &str) -> bool {
    label != "main"
}

fn snapshot_open_webview_windows(handle: &tauri::AppHandle) -> Vec<PersistedWindowState> {
    handle
        .webview_windows()
        .into_iter()
        .filter_map(|(label, window)| snapshot_webview_window(handle, &label, &window))
        .collect()
}

fn snapshot_webview_window(
    handle: &tauri::AppHandle,
    label: &str,
    window: &WebviewWindow,
) -> Option<PersistedWindowState> {
    let (kind, panel, project_id, mut plugin_layout, url) =
        persisted_window_identity_from_label(label)?;
    if kind == PersistedWindowKind::PluginLayout {
        plugin_layout = handle
            .try_state::<AppState>()
            .and_then(|state| state.get_plugin_window_surface(label))
            .or(plugin_layout);
    }
    let outer_position = window
        .outer_position()
        .ok()
        .map(|p| PersistedWindowPosition { x: p.x, y: p.y });
    let outer_size = window.outer_size().ok().map(|s| PersistedWindowSize {
        width: s.width,
        height: s.height,
    });
    let monitor = window.current_monitor().ok().flatten().map(|m| {
        let position = m.position();
        let size = m.size();
        PersistedMonitorState {
            name: m.name().cloned(),
            scale_factor: Some(m.scale_factor()),
            position: Some(PersistedWindowPosition {
                x: position.x,
                y: position.y,
            }),
            size: Some(PersistedWindowSize {
                width: size.width,
                height: size.height,
            }),
        }
    });

    Some(PersistedWindowState {
        label: label.to_owned(),
        kind,
        panel,
        project_id,
        plugin_layout,
        url,
        visible: window.is_visible().unwrap_or(true),
        maximized: window.is_maximized().unwrap_or(false),
        fullscreen: window.is_fullscreen().unwrap_or(false),
        outer_position,
        outer_size,
        monitor,
        last_focused_at: None,
    })
}

fn persisted_window_identity_from_label(
    label: &str,
) -> Option<(
    PersistedWindowKind,
    Option<String>,
    Option<ProjectId>,
    Option<PersistedPluginLayoutWindow>,
    Option<String>,
)> {
    if label == "main" {
        return Some((PersistedWindowKind::Main, None, None, None, None));
    }

    if let Some(surface) = commands::plugin_layout_window_from_label(label) {
        let url = commands::plugin_layout_window_url(&surface);
        return Some((
            PersistedWindowKind::PluginLayout,
            None,
            None,
            Some(surface),
            Some(url),
        ));
    }

    let panel = persisted_view_identity_from_label(label)?;
    Some((
        PersistedWindowKind::View,
        Some(panel.as_str().to_owned()),
        None,
        None,
        Some(commands::view_window_url(panel)),
    ))
}

fn persisted_view_identity_from_label(label: &str) -> Option<commands::ViewPanel> {
    let rest = label.strip_prefix("view-")?;
    if let Ok(panel) = commands::ViewPanel::parse(rest) {
        return Some(panel);
    }

    let (panel, project_id) = rest.rsplit_once('-')?;
    if project_id.len() != 32 || Uuid::parse_str(project_id).is_err() {
        return None;
    }
    commands::ViewPanel::parse(panel).ok()
}

fn restore_open_webview_windows(handle: &tauri::AppHandle) {
    let Some(state) = handle.try_state::<AppState>() else {
        return;
    };
    let restore = std::sync::Arc::clone(&state.restore_open_windows);
    let Ok(output) = tauri::async_runtime::block_on(async move { restore.execute().await }) else {
        return;
    };

    for window_state in output.windows {
        match window_state.kind {
            PersistedWindowKind::Main => {
                if let Some(window) = handle.get_webview_window("main") {
                    apply_persisted_window_state(handle, &window, &window_state);
                }
            }
            PersistedWindowKind::View => {
                restore_view_window(handle, &window_state);
            }
            PersistedWindowKind::PluginLayout => {
                restore_plugin_layout_window(handle, &window_state);
            }
        }
    }
}

fn restore_view_window(handle: &tauri::AppHandle, state: &PersistedWindowState) {
    let Some(panel) = state
        .panel
        .as_deref()
        .and_then(|raw| commands::ViewPanel::parse(raw).ok())
        .or_else(|| persisted_view_identity_from_label(&state.label))
    else {
        return;
    };
    let label = commands::view_window_label(panel);
    if handle.get_webview_window(&label).is_some() {
        return;
    }
    let url = commands::view_window_url(panel);

    let Ok(window) = WebviewWindowBuilder::new(handle, &label, WebviewUrl::App(url.into()))
        .title(format!("IdeA - {}", panel.title()))
        .inner_size(1120.0, 760.0)
        .min_inner_size(720.0, 480.0)
        .resizable(true)
        .maximizable(true)
        .minimizable(true)
        .closable(true)
        .decorations(true)
        .visible(state.visible)
        .build()
    else {
        return;
    };

    let event_app = handle.clone();
    let event_label = label.clone();
    window.on_window_event(move |event| {
        if let tauri::WindowEvent::CloseRequested { .. } = event {
            commands::emit_view_window_lifecycle(&event_app, "closed", panel, &event_label);
        }
    });

    apply_persisted_window_state(handle, &window, state);
    commands::emit_view_window_lifecycle(handle, "opened", panel, &label);
}

fn restore_plugin_layout_window(handle: &tauri::AppHandle, state: &PersistedWindowState) {
    let Some(surface) = state
        .plugin_layout
        .clone()
        .or_else(|| commands::plugin_layout_window_from_label(&state.label))
    else {
        return;
    };
    let label = commands::plugin_layout_window_label(&surface.plugin_id, &surface.layout_type);
    if handle.get_webview_window(&label).is_some() {
        return;
    }
    let url = commands::plugin_layout_window_url(&surface);

    let Ok(window) = WebviewWindowBuilder::new(handle, &label, WebviewUrl::App(url.into()))
        .title(format!("IdeA - {}", surface.layout_type.as_str()))
        .inner_size(1120.0, 760.0)
        .min_inner_size(720.0, 480.0)
        .resizable(true)
        .maximizable(true)
        .minimizable(true)
        .closable(true)
        .decorations(true)
        .visible(state.visible)
        .build()
    else {
        return;
    };

    if let Some(app_state) = handle.try_state::<AppState>() {
        app_state.set_plugin_window_surface(label.clone(), surface);
    }
    let event_app = handle.clone();
    let event_label = label.clone();
    window.on_window_event(move |event| {
        if let tauri::WindowEvent::CloseRequested { .. } = event {
            if let Some(state) = event_app.try_state::<AppState>() {
                state.clear_plugin_window_surface(&event_label);
            }
        }
    });

    apply_persisted_window_state(handle, &window, state);
}

fn apply_persisted_window_state(
    handle: &tauri::AppHandle,
    window: &WebviewWindow,
    state: &PersistedWindowState,
) {
    if persisted_monitor_is_available(handle, state.monitor.as_ref()) {
        if let Some(size) = state.outer_size {
            let _ = window.set_size(PhysicalSize::new(size.width, size.height));
        }
        if let Some(position) = state.outer_position {
            let _ = window.set_position(PhysicalPosition::new(position.x, position.y));
        }
    } else {
        let _ = window.center();
    }

    if state.fullscreen {
        let _ = window.set_fullscreen(true);
    } else if state.maximized {
        let _ = window.maximize();
    }

    if state.visible {
        let _ = window.show();
    } else {
        let _ = window.hide();
    }
}

fn persisted_monitor_is_available(
    handle: &tauri::AppHandle,
    monitor: Option<&PersistedMonitorState>,
) -> bool {
    let Some(saved) = monitor else {
        return true;
    };
    let Ok(monitors) = handle.available_monitors() else {
        return false;
    };

    monitors.into_iter().any(|current| {
        if let Some(saved_name) = saved.name.as_deref() {
            if current.name().is_some_and(|name| name == saved_name) {
                return true;
            }
        }

        let position_matches = saved.position.is_some_and(|p| {
            let current_position = current.position();
            p.x == current_position.x && p.y == current_position.y
        });
        let size_matches = saved.size.is_some_and(|s| {
            let current_size = current.size();
            s.width == current_size.width && s.height == current_size.height
        });
        position_matches && size_matches
    })
}

#[cfg(test)]
mod tests {
    use super::plugin_workspace_invoke_handler;
    use super::{
        apply_main_close_decision, confirm_next_main_window_close, consume_exit_guard_confirmation,
        decide_main_close_action, persisted_view_identity_from_label,
        persisted_window_identity_from_label, should_install_exit_guard, MainCloseAction,
    };
    use super::{should_close_with_main_window, PersistedWindowKind};
    use application::AppExitWorkGuardState;
    use serde_json::json;
    use std::cell::Cell;
    use std::time::{SystemTime, UNIX_EPOCH};
    use tauri::test::{get_ipc_response, mock_builder, mock_context, noop_assets, INVOKE_KEY};

    #[test]
    fn main_close_without_work_allows_shutdown_without_preventing_close() {
        assert_eq!(
            decide_main_close_action(false, false),
            MainCloseAction::AllowShutdown
        );
    }

    #[test]
    fn main_close_with_work_prevents_and_notifies_before_shutdown() {
        assert_eq!(
            decide_main_close_action(true, false),
            MainCloseAction::PreventAndNotify
        );
    }

    #[test]
    fn main_close_with_work_emits_guard_payload_and_skips_shutdown() {
        let prevented = Cell::new(false);
        let shutdown = Cell::new(false);
        let emitted = Cell::new(None);
        let guard = AppExitWorkGuardState {
            has_work_in_progress: true,
            busy_agent_count: 2,
            active_background_task_count: 1,
            details: Vec::new(),
        };

        let action = apply_main_close_decision(
            guard,
            false,
            || prevented.set(true),
            |payload| {
                emitted.set(Some((
                    payload.busy_agent_count,
                    payload.active_background_task_count,
                )))
            },
            || shutdown.set(true),
        );

        assert_eq!(action, MainCloseAction::PreventAndNotify);
        assert!(prevented.get());
        assert_eq!(emitted.get(), Some((2, 1)));
        assert!(!shutdown.get());
    }

    #[test]
    fn main_close_without_work_runs_shutdown_without_prevent_or_emit() {
        let prevented = Cell::new(false);
        let shutdown = Cell::new(false);
        let emitted = Cell::new(false);
        let guard = AppExitWorkGuardState {
            has_work_in_progress: false,
            busy_agent_count: 0,
            active_background_task_count: 0,
            details: Vec::new(),
        };

        let action = apply_main_close_decision(
            guard,
            false,
            || prevented.set(true),
            |_| emitted.set(true),
            || shutdown.set(true),
        );

        assert_eq!(action, MainCloseAction::AllowShutdown);
        assert!(!prevented.get());
        assert!(!emitted.get());
        assert!(shutdown.get());
    }

    #[test]
    fn confirmed_main_close_bypasses_guard_once_then_rearms() {
        confirm_next_main_window_close();
        let first_attempt_confirmed = consume_exit_guard_confirmation();
        assert!(first_attempt_confirmed);
        assert_eq!(
            decide_main_close_action(true, first_attempt_confirmed),
            MainCloseAction::AllowShutdown
        );

        let second_attempt_confirmed = consume_exit_guard_confirmation();
        assert!(!second_attempt_confirmed);
        assert_eq!(
            decide_main_close_action(true, second_attempt_confirmed),
            MainCloseAction::PreventAndNotify
        );
    }

    #[test]
    fn exit_guard_is_scoped_to_main_window_only() {
        assert!(should_install_exit_guard("main"));
        assert!(!should_install_exit_guard(
            "view-work-00000000-0000-0000-0000-000000000001"
        ));
        assert!(!should_install_exit_guard("settings"));
    }

    #[test]
    fn main_window_close_does_not_target_main_again() {
        assert!(!should_close_with_main_window("main"));
    }

    #[test]
    fn main_window_close_targets_detached_view_windows() {
        assert!(should_close_with_main_window(
            "view-work-00000000-0000-0000-0000-000000000001"
        ));
    }

    #[test]
    fn main_window_close_targets_other_auxiliary_windows() {
        assert!(should_close_with_main_window("settings"));
    }

    #[test]
    fn persisted_identity_accepts_main_and_stable_view_labels() {
        let (kind, panel, project_id, plugin_layout, url) =
            persisted_window_identity_from_label("main").unwrap();
        assert_eq!(kind, PersistedWindowKind::Main);
        assert!(panel.is_none());
        assert!(project_id.is_none());
        assert!(plugin_layout.is_none());
        assert!(url.is_none());

        let (kind, panel, project_id, plugin_layout, url) =
            persisted_window_identity_from_label("view-tickets-0000000000000000000000000000002a")
                .unwrap();

        assert_eq!(kind, PersistedWindowKind::View);
        assert_eq!(panel.as_deref(), Some("tickets"));
        assert!(project_id.is_none());
        assert!(plugin_layout.is_none());
        assert_eq!(url.as_deref(), Some("index.html?panel=tickets"));
        assert_eq!(
            persisted_view_identity_from_label("view-tickets-0000000000000000000000000000002a")
                .unwrap()
                .as_str(),
            "tickets"
        );
    }

    #[test]
    fn persisted_identity_accepts_new_panel_only_view_labels() {
        let (kind, panel, project_id, plugin_layout, url) =
            persisted_window_identity_from_label("view-agents").unwrap();
        assert_eq!(kind, PersistedWindowKind::View);
        assert_eq!(panel.as_deref(), Some("agents"));
        assert!(project_id.is_none());
        assert!(plugin_layout.is_none());
        assert_eq!(url.as_deref(), Some("index.html?panel=agents"));
    }

    #[test]
    fn persisted_identity_accepts_plugin_layout_view_labels() {
        let plugin_id = domain::PluginId::new("dev.idea.android-plugin").unwrap();
        let layout_type = domain::PluginLayoutType::new("idea-android.health").unwrap();
        let label = crate::commands::plugin_layout_window_label(&plugin_id, &layout_type);

        let (kind, panel, project_id, plugin_layout, url) =
            persisted_window_identity_from_label(&label).unwrap();

        assert_eq!(kind, PersistedWindowKind::PluginLayout);
        assert!(panel.is_none());
        assert!(project_id.is_none());
        let plugin_layout = plugin_layout.unwrap();
        assert_eq!(plugin_layout.plugin_id, plugin_id);
        assert_eq!(plugin_layout.layout_type, layout_type);
        assert!(url
            .as_deref()
            .unwrap()
            .starts_with("index.html?pluginLayout=1&"));
    }

    #[test]
    fn persisted_identity_filters_unknown_or_headless_labels() {
        assert!(persisted_window_identity_from_label("mcp-server").is_none());
        assert!(persisted_window_identity_from_label("settings").is_none());
        assert!(persisted_window_identity_from_label(
            "view-unknown-0000000000000000000000000000002a"
        )
        .is_none());
        assert!(persisted_window_identity_from_label("view-tickets-not-a-project").is_none());
    }

    #[test]
    fn dto_plugins_workspace_commands_are_registered_in_tauri_invoke_handler() {
        let app_data = test_app_data_dir("plugin-workspace-commands");
        let app = mock_builder()
            .manage(crate::state::AppState::build(app_data.clone()))
            .invoke_handler(plugin_workspace_invoke_handler())
            .build(mock_context(noop_assets()))
            .expect("mock app builds");
        let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default())
            .build()
            .expect("mock webview builds");
        let missing_project = uuid::Uuid::from_u128(124).to_string();

        for command in [
            "plugin_workspace_read_text",
            "plugin_workspace_read_binary",
            "plugin_workspace_list_dir",
            "plugin_workspace_stat",
            "plugin_query_project_structure",
        ] {
            let err = invoke_plugin_command(
                &webview,
                command,
                json!({
                    "input": {
                        "projectId": missing_project.clone(),
                        "path": "src/main.rs",
                        "maxDepth": 2,
                        "maxEntries": 10
                    }
                }),
            )
            .expect_err("missing project must surface through the registered command");
            assert_eq!(err["code"], "NOT_FOUND", "{command}");
        }

        for (command, input) in [
            (
                "plugin_workspace_write_text",
                json!({
                    "projectId": missing_project.clone(),
                    "path": "generated.txt",
                    "content": "hello\n"
                }),
            ),
            (
                "plugin_workspace_write_binary",
                json!({
                    "projectId": missing_project.clone(),
                    "path": "generated.bin",
                    "bytes": [1, 2, 3]
                }),
            ),
        ] {
            let err = invoke_plugin_command(&webview, command, json!({ "input": input }))
                .expect_err("missing project must surface through the registered command");
            assert_eq!(err["code"], "NOT_FOUND", "{command}");
        }

        std::fs::remove_dir_all(app_data).ok();
    }

    #[test]
    fn dto_plugins_config_document_commands_are_registered_in_tauri_invoke_handler() {
        let app_data = test_app_data_dir("plugin-config-document-commands");
        let app = mock_builder()
            .manage(crate::state::AppState::build(app_data.clone()))
            .invoke_handler(plugin_workspace_invoke_handler())
            .build(mock_context(noop_assets()))
            .expect("mock app builds");
        let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default())
            .build()
            .expect("mock webview builds");
        let missing_project = uuid::Uuid::from_u128(130).to_string();

        let err = invoke_plugin_command(
            &webview,
            "plugin_config_read_document",
            json!({
                "input": {
                    "projectId": missing_project.clone(),
                    "path": "config/settings.json",
                    "format": "json"
                }
            }),
        )
        .expect_err("missing project must surface through the registered command");
        assert_eq!(err["code"], "NOT_FOUND");

        let err = invoke_plugin_command(
            &webview,
            "plugin_config_update_document",
            json!({
                "input": {
                    "projectId": missing_project,
                    "path": "config/settings.json",
                    "format": "json",
                    "mode": "mergePatch",
                    "value": {"enabled": true}
                }
            }),
        )
        .expect_err("missing project must surface through the registered command");
        assert_eq!(err["code"], "NOT_FOUND");

        std::fs::remove_dir_all(app_data).ok();
    }

    #[test]
    fn dto_plugins_storage_commands_are_registered_in_tauri_invoke_handler() {
        let app_data = test_app_data_dir("plugin-storage-commands");
        let app = mock_builder()
            .manage(crate::state::AppState::build(app_data.clone()))
            .invoke_handler(plugin_workspace_invoke_handler())
            .build(mock_context(noop_assets()))
            .expect("mock app builds");
        let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default())
            .build()
            .expect("mock webview builds");

        let get_err = invoke_plugin_command(
            &webview,
            "plugin_storage_get",
            json!({
                "input": {
                    "pluginId": "dev.acme.missing",
                    "key": "helloPlugin.launches"
                }
            }),
        )
        .expect_err("missing plugin must surface through the registered command");
        assert_eq!(get_err["code"], "NOT_FOUND");

        let set_err = invoke_plugin_command(
            &webview,
            "plugin_storage_set",
            json!({
                "input": {
                    "pluginId": "dev.acme.missing",
                    "key": "helloPlugin.launches",
                    "value": 1
                }
            }),
        )
        .expect_err("missing plugin must surface through the registered command");
        assert_eq!(set_err["code"], "NOT_FOUND");

        let delete_err = invoke_plugin_command(
            &webview,
            "plugin_storage_delete",
            json!({
                "input": {
                    "pluginId": "dev.acme.missing",
                    "key": "helloPlugin.launches"
                }
            }),
        )
        .expect_err("missing plugin must surface through the registered command");
        assert_eq!(delete_err["code"], "NOT_FOUND");

        std::fs::remove_dir_all(app_data).ok();
    }

    #[test]
    fn dto_plugins_command_task_commands_are_registered_in_tauri_invoke_handler() {
        let app_data = test_app_data_dir("plugin-task-commands");
        let app = mock_builder()
            .manage(crate::state::AppState::build(app_data.clone()))
            .invoke_handler(plugin_workspace_invoke_handler())
            .build(mock_context(noop_assets()))
            .expect("mock app builds");
        let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default())
            .build()
            .expect("mock webview builds");
        let missing_project = uuid::Uuid::from_u128(125).to_string();
        let owner = uuid::Uuid::from_u128(126).to_string();

        let err = invoke_plugin_command(
            &webview,
            "plugin_task_run_command",
            json!({
                "input": {
                    "projectId": missing_project,
                    "ownerAgentId": owner,
                    "label": "cargo test",
                    "command": "cargo",
                    "args": ["test"],
                    "cwd": ".",
                    "env": [["RUST_LOG", "debug"]],
                    "recordOnly": true
                }
            }),
        )
        .expect_err("missing project must surface through the registered command");
        assert_eq!(err["code"], "NOT_FOUND");

        let task_id = uuid::Uuid::from_u128(127).to_string();
        let value = invoke_plugin_command(
            &webview,
            "plugin_task_get_status",
            json!({
                "input": {
                    "taskId": task_id
                }
            }),
        )
        .expect("unknown task is a successful empty status");
        assert_eq!(value, serde_json::Value::Null);

        std::fs::remove_dir_all(app_data).ok();
    }

    #[test]
    fn dto_plugins_toolchain_diagnostic_command_is_registered_in_tauri_invoke_handler() {
        let app_data = test_app_data_dir("plugin-toolchain-diagnostic-command");
        let app = mock_builder()
            .manage(crate::state::AppState::build(app_data.clone()))
            .invoke_handler(plugin_workspace_invoke_handler())
            .build(mock_context(noop_assets()))
            .expect("mock app builds");
        let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default())
            .build()
            .expect("mock webview builds");
        let missing_project = uuid::Uuid::from_u128(126).to_string();

        let err = invoke_plugin_command(
            &webview,
            "plugin_toolchain_diagnose",
            json!({
                "input": {
                    "projectId": missing_project,
                    "cwd": ".",
                    "tools": [{
                        "id": "rust",
                        "executable": "cargo",
                        "versionArgs": ["--version"],
                        "required": true
                    }],
                    "env": [{
                        "name": "RUSTUP_HOME",
                        "required": false
                    }],
                    "files": [{
                        "path": "Cargo.toml",
                        "required": true,
                        "kind": "file"
                    }]
                }
            }),
        )
        .expect_err("missing project must surface through the registered command");
        assert_eq!(err["code"], "NOT_FOUND");

        std::fs::remove_dir_all(app_data).ok();
    }

    #[test]
    fn dto_plugins_event_commands_are_registered_in_tauri_invoke_handler() {
        let app_data = test_app_data_dir("plugin-event-commands");
        let app = mock_builder()
            .manage(crate::state::AppState::build(app_data.clone()))
            .invoke_handler(plugin_workspace_invoke_handler())
            .build(mock_context(noop_assets()))
            .expect("mock app builds");
        let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default())
            .build()
            .expect("mock webview builds");
        let missing_project = uuid::Uuid::from_u128(127).to_string();

        let err = invoke_plugin_command(
            &webview,
            "plugin_events_subscribe",
            json!({
                "input": {
                    "projectId": missing_project,
                    "eventTypes": ["workspaceFileChanged", "backgroundTaskChanged"],
                    "capacity": 10
                }
            }),
        )
        .expect_err("missing project must surface through the registered command");
        assert_eq!(err["code"], "NOT_FOUND");

        let subscription_id = uuid::Uuid::from_u128(128).to_string();
        let err = invoke_plugin_command(
            &webview,
            "plugin_events_poll",
            json!({
                "input": {
                    "subscriptionId": subscription_id.clone(),
                    "maxEvents": 10
                }
            }),
        )
        .expect_err("unknown subscription must surface through the registered command");
        assert_eq!(err["code"], "NOT_FOUND");

        let disposed = invoke_plugin_command(
            &webview,
            "plugin_events_unsubscribe",
            json!({
                "input": {
                    "subscriptionId": subscription_id
                }
            }),
        )
        .expect("unsubscribe is idempotent");
        assert_eq!(disposed["retention"], "disposed");

        std::fs::remove_dir_all(app_data).ok();
    }

    fn invoke_plugin_command<W: AsRef<tauri::Webview<tauri::test::MockRuntime>>>(
        webview: &W,
        command: &str,
        body: serde_json::Value,
    ) -> Result<serde_json::Value, serde_json::Value> {
        get_ipc_response(
            webview,
            tauri::webview::InvokeRequest {
                cmd: command.to_owned(),
                callback: tauri::ipc::CallbackFn(0),
                error: tauri::ipc::CallbackFn(1),
                url: "tauri://localhost".parse().unwrap(),
                body: tauri::ipc::InvokeBody::Json(body),
                headers: Default::default(),
                invoke_key: INVOKE_KEY.to_owned(),
            },
        )
        .map(|body| body.deserialize::<serde_json::Value>().unwrap())
    }

    fn test_app_data_dir(label: &str) -> std::path::PathBuf {
        let nanos = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        std::env::temp_dir().join(format!("idea-{label}-{}-{nanos}", std::process::id()))
    }
}
