pub mod acp;
mod commands;
pub mod core;
pub mod extensions;
pub mod gateway;
pub mod ipc;
pub mod model;
pub mod persistence;

use std::fs;
use std::path::PathBuf;
use std::str::FromStr;

use tauri::menu::{MenuBuilder, MenuItemBuilder};
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
use tauri::webview::PageLoadEvent;
use tauri::{Emitter, Manager, RunEvent, WindowEvent};
#[cfg(not(target_os = "macos"))]
use tauri_plugin_autostart::ManagerExt as AutoStartManagerExt;
use tauri_plugin_window_state::StateFlags;
use tracing_appender::rolling::{RollingFileAppender, Rotation};
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};

use crate::core::app_state::AppState;
use crate::core::desktop_runtime::{
    DesktopRuntimeState, LAUNCH_AT_LOGIN_SETTING_KEY, MINIMIZE_TO_TRAY_SETTING_KEY,
};
use crate::core::sleep_manager::PREVENT_SLEEP_WHILE_RUNNING_SETTING_KEY;

#[cfg(target_os = "macos")]
use crate::core::startup_manager;

const MAIN_WINDOW_LABEL: &str = "main";
const MAIN_TRAY_ID: &str = "main-tray";
const TRAY_SHOW_ID: &str = "tray-show";
const TRAY_QUIT_ID: &str = "tray-quit";
#[cfg(target_os = "macos")]
const TRAY_ICON: tauri::image::Image<'static> =
    tauri::include_image!("icons/statusbar-icon-template.png");
const LOG_FILE_PREFIX: &str = "tiycode";
const LOG_FILE_SUFFIX: &str = "log";

fn persisted_window_state_flags() -> StateFlags {
    StateFlags::SIZE | StateFlags::POSITION | StateFlags::MAXIMIZED | StateFlags::FULLSCREEN
}

/// Resolve the `$HOME/.tiy/` base directory.
fn tiy_home() -> PathBuf {
    dirs::home_dir()
        .expect("cannot resolve HOME directory")
        .join(".tiy")
}

/// Resolve the platform-native log directory for TiyCode.
fn log_dir() -> PathBuf {
    #[cfg(target_os = "macos")]
    {
        dirs::home_dir()
            .expect("cannot resolve HOME directory")
            .join("Library/Logs/TiyAgents")
    }
    #[cfg(target_os = "windows")]
    {
        dirs::data_local_dir()
            .expect("cannot resolve LOCALAPPDATA")
            .join("TiyAgents/logs")
    }
    #[cfg(not(any(target_os = "macos", target_os = "windows")))]
    {
        dirs::state_dir()
            .or_else(|| dirs::home_dir().map(|h| h.join(".local/state")))
            .expect("cannot resolve state directory")
            .join("tiy-agents/logs")
    }
}

/// Create all required directories under `$HOME/.tiy/` and the log directory.
fn init_directories(base: &PathBuf) -> std::io::Result<()> {
    let dirs_to_create = [
        base.join("db"),
        base.join("db/backups"),
        base.join("skills"),
        base.join("prompts"),
        base.join("plugins"),
        base.join("automations"),
        base.join("workspace"),
        base.join("cache"),
        base.join("cache/index"),
        base.join("catalog"),
    ];

    for dir in &dirs_to_create {
        fs::create_dir_all(dir)?;
    }

    // Log directory follows OS conventions
    fs::create_dir_all(log_dir())?;

    Ok(())
}

/// Initialize the tracing/logging subsystem.
fn init_logging() {
    let log_path = log_dir();

    let file_appender = RollingFileAppender::builder()
        .rotation(Rotation::DAILY)
        .max_log_files(5)
        .filename_prefix(LOG_FILE_PREFIX)
        .filename_suffix(LOG_FILE_SUFFIX)
        .build(&log_path)
        .expect("failed to create log appender");

    let env_filter = match std::env::var("RUST_LOG") {
        Ok(val) if !val.is_empty() => {
            EnvFilter::from_str(&val).unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn"))
        }
        _ => EnvFilter::new("info,sqlx=warn"),
    };

    tracing_subscriber::registry()
        .with(env_filter)
        .with(
            fmt::layer()
                .json()
                .with_writer(file_appender)
                .with_ansi(false),
        )
        .with(
            fmt::layer()
                .with_writer(std::io::stderr)
                .with_ansi(true)
                .compact(),
        )
        .init();
}

fn parse_bool_setting(record: Option<crate::model::settings::SettingRecord>, key: &str) -> bool {
    match record {
        Some(setting) => match serde_json::from_str::<bool>(&setting.value_json) {
            Ok(enabled) => enabled,
            Err(error) => {
                tracing::warn!(setting = key, error = %error, "failed to parse boolean setting");
                false
            }
        },
        None => false,
    }
}

fn show_main_window<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
    if let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) {
        let _ = window.unminimize();
        let _ = window.show();
        let _ = window.set_focus();
    }
}

fn sync_tray_visibility<R: tauri::Runtime>(app: &tauri::AppHandle<R>, visible: bool) {
    if let Some(tray) = app.tray_by_id(MAIN_TRAY_ID) {
        if let Err(error) = tray.set_visible(visible) {
            tracing::warn!(error = %error, "failed to update tray visibility");
        }
    }
}

fn build_tray<R: tauri::Runtime>(
    app: &tauri::App<R>,
    visible: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    let show = MenuItemBuilder::with_id(TRAY_SHOW_ID, "Show").build(app)?;
    let quit = MenuItemBuilder::with_id(TRAY_QUIT_ID, "Quit").build(app)?;
    let menu = MenuBuilder::new(app).items(&[&show, &quit]).build()?;

    let mut tray_builder = TrayIconBuilder::with_id(MAIN_TRAY_ID)
        .menu(&menu)
        .show_menu_on_left_click(false)
        .tooltip("TiyCode");

    #[cfg(target_os = "macos")]
    {
        tray_builder = tray_builder.icon(TRAY_ICON).icon_as_template(true);
    }

    #[cfg(not(target_os = "macos"))]
    if let Some(icon) = app.default_window_icon().cloned() {
        tray_builder = tray_builder.icon(icon);
    }

    let tray = tray_builder.build(app)?;
    tray.set_visible(visible)?;

    Ok(())
}

pub fn run_acp_stdio() -> anyhow::Result<()> {
    let tiy_home = tiy_home();
    init_directories(&tiy_home)?;
    init_logging();
    tracing::info!(path = %tiy_home.display(), "tiy ACP stdio server starting");

    let db_path = tiy_home.join("db/tiycode.db");
    let runtime = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()?;

    runtime.block_on(async move {
        let pool = persistence::init_database(&db_path).await?;
        let state = acp::AcpServerState::new_headless(pool);
        if let Err(error) = state.prompt_command_manager.ensure_builtin_seeded() {
            tracing::warn!(error = %error, "failed to seed builtin prompts during ACP startup");
        }
        if let Err(error) = state.workspace_manager.validate_all().await {
            tracing::warn!(error = %error, "ACP startup workspace validation failed");
        }
        if let Err(error) = state.thread_manager.recover_interrupted_runs().await {
            tracing::warn!(error = %error, "ACP startup run recovery failed");
        }
        if let Err(error) = state.terminal_manager.recover_orphaned_sessions().await {
            tracing::warn!(error = %error, "ACP startup terminal session recovery failed");
        }
        acp::run_stdio(state).await.map_err(anyhow::Error::from)
    })
}

pub fn run_acp_http(addr: &str) -> anyhow::Result<()> {
    let tiy_home = tiy_home();
    init_directories(&tiy_home)?;
    init_logging();
    tracing::info!(path = %tiy_home.display(), addr = %addr, "tiy ACP HTTP server starting");

    let db_path = tiy_home.join("db/tiycode.db");
    let runtime = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()?;

    let addr_owned = addr.to_string();
    runtime.block_on(async move {
        let pool = persistence::init_database(&db_path).await?;
        let state = acp::AcpServerState::new_headless(pool);
        if let Err(error) = state.prompt_command_manager.ensure_builtin_seeded() {
            tracing::warn!(error = %error, "failed to seed builtin prompts during ACP startup");
        }
        if let Err(error) = state.workspace_manager.validate_all().await {
            tracing::warn!(error = %error, "ACP startup workspace validation failed");
        }
        if let Err(error) = state.thread_manager.recover_interrupted_runs().await {
            tracing::warn!(error = %error, "ACP startup run recovery failed");
        }
        if let Err(error) = state.terminal_manager.recover_orphaned_sessions().await {
            tracing::warn!(error = %error, "ACP startup terminal session recovery failed");
        }
        acp::run_http(state, &addr_owned).await
    })
}

pub fn run_gateway(config_path: Option<&str>) -> anyhow::Result<()> {
    let tiy_home = tiy_home();
    init_directories(&tiy_home)?;
    init_logging();

    let config_file = config_path
        .map(PathBuf::from)
        .unwrap_or_else(gateway::config::default_config_path);

    tracing::info!(
        path = %tiy_home.display(),
        config_file = %config_file.display(),
        "tiy IM gateway starting"
    );

    let db_path = tiy_home.join("db/tiycode.db");
    let runtime = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()?;

    runtime.block_on(async move {
        let pool = persistence::init_database(&db_path).await?;
        let state = gateway::GatewayState::new_headless(pool);
        if let Err(error) = state.prompt_command_manager.ensure_builtin_seeded() {
            tracing::warn!(error = %error, "failed to seed builtin prompts during gateway startup");
        }
        if let Err(error) = state.workspace_manager.validate_all().await {
            tracing::warn!(error = %error, "gateway startup workspace validation failed");
        }
        if let Err(error) = state.thread_manager.recover_interrupted_runs().await {
            tracing::warn!(error = %error, "gateway startup run recovery failed");
        }
        if let Err(error) = state.terminal_manager.recover_orphaned_sessions().await {
            tracing::warn!(error = %error, "gateway startup terminal session recovery failed");
        }
        gateway::run(state, config_file).await
    })
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    let tiy_home = tiy_home();

    // 1. Initialize directories
    init_directories(&tiy_home).expect("failed to initialize $HOME/.tiy/ directories");

    // 2. Initialize logging
    init_logging();
    tracing::info!(path = %tiy_home.display(), "tiy agent starting");

    // 3. Build Tauri app
    let build_start = std::time::Instant::now();
    let builder = tauri::Builder::default()
        .plugin(tauri_plugin_dialog::init())
        .plugin(tauri_plugin_opener::init())
        .plugin(
            tauri_plugin_window_state::Builder::default()
                .with_state_flags(persisted_window_state_flags())
                .build(),
        );

    let builder = builder
        .plugin(tauri_plugin_updater::Builder::new().build())
        .plugin(tauri_plugin_process::init());

    #[cfg(not(target_os = "macos"))]
    let builder = builder.plugin(tauri_plugin_autostart::init(
        tauri_plugin_autostart::MacosLauncher::LaunchAgent,
        None::<Vec<&'static str>>,
    ));

    #[cfg(target_os = "macos")]
    let builder = builder;

    tracing::info!(
        elapsed_ms = build_start.elapsed().as_millis(),
        "⏱ [startup] plugins registered"
    );

    let app = builder
        .invoke_handler(tauri::generate_handler![
            commands::attachment::attachment_read_files,
            // File operations
            commands::file::file_read,
            commands::file::file_write,
            commands::file::file_create,
            commands::file::file_delete,
            commands::file::file_rename,
            // System
            commands::system::get_system_metadata,
            commands::system::is_homebrew_installed,
            commands::system::is_cli_installed,
            commands::system::install_cli_in_path,
            commands::system::uninstall_cli_from_path,
            commands::system::get_workspace_open_apps,
            commands::system::open_workspace_in_app,
            commands::system::open_tree_path_in_app,
            // Workspace
            commands::workspace::workspace_list,
            commands::workspace::workspace_add,
            commands::workspace::workspace_ensure_default,
            commands::workspace::workspace_remove,
            commands::workspace::workspace_set_default,
            commands::workspace::workspace_validate,
            commands::workspace::workspace_list_worktrees,
            commands::workspace::workspace_create_worktree,
            commands::workspace::workspace_remove_worktree,
            commands::workspace::workspace_prune_worktrees,
            // Settings & Policies
            commands::settings::settings_get,
            commands::settings::settings_get_all,
            commands::settings::settings_set,
            commands::settings::policy_get,
            commands::settings::policy_get_all,
            commands::settings::policy_set,
            // Providers
            commands::settings::provider_catalog_list,
            commands::settings::provider_settings_get_all,
            commands::settings::provider_settings_fetch_models,
            commands::settings::provider_settings_upsert_builtin,
            commands::settings::provider_settings_create_custom,
            commands::settings::provider_settings_update_custom,
            commands::settings::provider_settings_delete_custom,
            commands::settings::provider_model_test_connection,
            // Agent Profiles
            commands::settings::profile_list,
            commands::settings::profile_create,
            commands::settings::profile_update,
            commands::settings::profile_delete,
            // Custom Subagents
            commands::settings::custom_subagent_list,
            commands::settings::custom_subagent_create,
            commands::settings::custom_subagent_update,
            commands::settings::custom_subagent_delete,
            commands::settings::profile_subagent_access_get,
            commands::settings::profile_subagent_access_set,
            // Prompt Commands
            commands::prompts::prompt_command_list,
            commands::prompts::prompt_command_create,
            commands::prompts::prompt_command_update,
            commands::prompts::prompt_command_delete,
            // Extensions
            commands::extensions::extensions_list,
            commands::extensions::config_list_diagnostics,
            commands::extensions::extension_get_detail,
            commands::extensions::extension_enable,
            commands::extensions::extension_disable,
            commands::extensions::extension_uninstall,
            commands::extensions::extensions_list_commands,
            commands::extensions::extensions_list_activity,
            commands::extensions::marketplace_list_sources,
            commands::extensions::marketplace_add_source,
            commands::extensions::marketplace_remove_source,
            commands::extensions::marketplace_get_remove_source_plan,
            commands::extensions::marketplace_refresh_source,
            commands::extensions::marketplace_list_items,
            commands::extensions::marketplace_install_item,
            commands::extensions::plugin_validate_dir,
            commands::extensions::plugin_install_from_dir,
            commands::extensions::plugin_update_config,
            commands::extensions::mcp_list_servers,
            commands::extensions::mcp_add_server,
            commands::extensions::mcp_update_server,
            commands::extensions::mcp_remove_server,
            commands::extensions::mcp_restart_server,
            commands::extensions::mcp_get_server_state,
            commands::extensions::skill_list,
            commands::extensions::skill_rescan,
            commands::extensions::skill_enable,
            commands::extensions::skill_disable,
            commands::extensions::skill_preview,
            // Threads
            commands::thread::thread_list,
            commands::thread::thread_create,
            commands::thread::thread_load,
            commands::thread::thread_update_title,
            commands::thread::thread_update_profile,
            commands::thread::thread_regenerate_title,
            commands::thread::thread_delete,
            commands::thread::thread_add_message,
            // Agent Run
            commands::agent::thread_start_run,
            commands::agent::thread_subscribe_run,
            commands::agent::thread_execute_approved_plan,
            commands::agent::thread_enqueue_queue_message,
            commands::agent::thread_clear_runtime_queue,
            commands::agent::thread_cancel_runtime_queue_message,
            commands::agent::thread_promote_runtime_queue_message,
            commands::agent::thread_clear_context,
            commands::agent::thread_compact_context,
            commands::agent::thread_cancel_run,
            commands::agent::tool_approval_respond,
            commands::agent::tool_clarify_respond,
            // Goal
            commands::agent::goal_get_state,
            commands::agent::goal_set,
            commands::agent::goal_pause,
            commands::agent::goal_resume,
            commands::agent::goal_clear,
            commands::agent::goal_evaluate,
            // Index
            commands::index::index_get_tree,
            commands::index::index_get_children,
            commands::index::index_filter_files,
            commands::index::index_reveal_path,
            commands::index::index_search,
            commands::index::index_search_stream,
            commands::index::index_cancel_search_stream,
            // Git
            commands::git::git_get_snapshot,
            commands::git::git_get_history,
            commands::git::git_get_diff,
            commands::git::git_get_conflict_diff,
            commands::git::git_get_file_status,
            commands::git::git_subscribe,
            commands::git::git_unsubscribe,
            commands::git::git_refresh,
            commands::git::git_stage,
            commands::git::git_unstage,
            commands::git::git_generate_commit_message,
            commands::git::git_commit,
            commands::git::git_fetch,
            commands::git::git_pull,
            commands::git::git_push,
            commands::git::git_list_branches,
            commands::git::git_checkout_branch,
            commands::git::git_create_branch,
            commands::git::git_restore,
            commands::git::git_clean,
            commands::git::git_generate_branch_name,
            // Terminal
            commands::terminal::terminal_create_or_attach,
            commands::terminal::terminal_write_input,
            commands::terminal::terminal_resize,
            commands::terminal::terminal_restart,
            commands::terminal::terminal_close,
            commands::terminal::terminal_list,
            commands::terminal::terminal_list_available_shells,
            // Gateway
            commands::gateway::gateway_start,
            commands::gateway::gateway_stop,
            commands::gateway::gateway_restart,
            commands::gateway::gateway_status,
            commands::gateway::gateway_weixin_qr_login,
            commands::gateway::gateway_weixin_login_poll,
            commands::gateway::gateway_weixin_session,
            commands::gateway::gateway_weixin_logout,
            commands::gateway::gateway_get_config,
            commands::gateway::gateway_save_config,
        ])
        .setup(move |app| {
            tracing::info!(elapsed_ms = build_start.elapsed().as_millis(), "⏱ [startup] builder configured (plugins + invoke_handler), entering setup()");
            let setup_start = std::time::Instant::now();

            // 4. Initialize database (async, on the tokio runtime that Tauri provides)
            let db_path = tiy_home.join("db/tiycode.db");

            let t0 = std::time::Instant::now();
            let pool = tauri::async_runtime::block_on(async {
                persistence::init_database(&db_path).await
            })?;
            tracing::info!(elapsed_ms = t0.elapsed().as_millis(), db = %db_path.display(), "⏱ [startup] database ready");

            // 5. Construct and manage AppState
            let t0 = std::time::Instant::now();
            let state = AppState::new(pool, app.handle().clone());
            tracing::info!(elapsed_ms = t0.elapsed().as_millis(), "⏱ [startup] AppState constructed");

            let t0 = std::time::Instant::now();
            if let Err(error) = state.prompt_command_manager.ensure_builtin_seeded() {
                tracing::warn!(error = %error, "failed to seed builtin prompts during startup");
            }
            tracing::info!(elapsed_ms = t0.elapsed().as_millis(), "⏱ [startup] ensure_builtin_seeded");

            let desktop_runtime = DesktopRuntimeState::default();

            let t0 = std::time::Instant::now();
            let (prevent_sleep_while_running, launch_at_login, minimize_to_tray) =
                tauri::async_runtime::block_on(async {
                    let prevent_sleep = state
                        .settings_manager
                        .get_setting(PREVENT_SLEEP_WHILE_RUNNING_SETTING_KEY)
                        .await?;
                    let launch_at_login = state
                        .settings_manager
                        .get_setting(LAUNCH_AT_LOGIN_SETTING_KEY)
                        .await?;
                    let minimize_to_tray = state
                        .settings_manager
                        .get_setting(MINIMIZE_TO_TRAY_SETTING_KEY)
                        .await?;

                    Ok::<_, crate::model::errors::AppError>((
                        parse_bool_setting(prevent_sleep, PREVENT_SLEEP_WHILE_RUNNING_SETTING_KEY),
                        parse_bool_setting(launch_at_login, LAUNCH_AT_LOGIN_SETTING_KEY),
                        parse_bool_setting(minimize_to_tray, MINIMIZE_TO_TRAY_SETTING_KEY),
                    ))
                })?;
            tracing::info!(elapsed_ms = t0.elapsed().as_millis(), "⏱ [startup] read 3 settings (prevent_sleep, launch_at_login, minimize_to_tray)");

            let t0 = std::time::Instant::now();
            tauri::async_runtime::block_on(async {
                state
                    .sleep_manager
                    .set_user_preference(prevent_sleep_while_running)
                    .await;
            });
            tracing::info!(elapsed_ms = t0.elapsed().as_millis(), "⏱ [startup] sleep_manager.set_user_preference");

            desktop_runtime.set_minimize_to_tray(minimize_to_tray);

            #[cfg(target_os = "macos")]
            if let Some(system_enabled) = startup_manager::launch_at_login_enabled() {
                if system_enabled != launch_at_login {
                    let value_json =
                        serde_json::to_string(&system_enabled).unwrap_or_else(|_| "false".to_string());
                    let sync_result = state.settings_manager.set_setting(
                        LAUNCH_AT_LOGIN_SETTING_KEY,
                        &value_json,
                    );

                    if let Err(error) = tauri::async_runtime::block_on(sync_result) {
                        tracing::warn!(error = %error, "failed to sync launch at login setting from system state");
                    }
                }
            } else if let Err(error) = startup_manager::set_launch_at_login(launch_at_login) {
                tracing::warn!(error = %error, "failed to sync launch at login state");
            }

            #[cfg(not(target_os = "macos"))]
            {
                let autolaunch = app.handle().autolaunch();
                match autolaunch.is_enabled() {
                    Ok(system_enabled) if system_enabled != launch_at_login => {
                        let value_json = serde_json::to_string(&system_enabled)
                            .unwrap_or_else(|_| "false".to_string());
                        if let Err(error) = tauri::async_runtime::block_on(
                            state.settings_manager.set_setting(
                                LAUNCH_AT_LOGIN_SETTING_KEY,
                                &value_json,
                            ),
                        ) {
                            tracing::warn!(error = %error, "failed to sync launch at login setting from system state");
                        }
                    }
                    Ok(_) => {} // already in sync
                    Err(error) => {
                        tracing::warn!(error = %error, "failed to query autolaunch state, applying DB value");
                        let result = if launch_at_login {
                            autolaunch.enable()
                        } else {
                            autolaunch.disable()
                        };
                        if let Err(error) = result {
                            tracing::warn!(error = %error, "failed to sync launch at login state");
                        }
                    }
                }
            }

            // Apply bundled catalog snapshot if it is newer than the local cache.
            // This ensures a usable catalog is available even without network access
            // (e.g. fresh install or app update in an offline environment).
            let t0 = std::time::Instant::now();
            crate::core::settings_manager::apply_bundled_catalog_if_newer(app.handle());
            tracing::info!(elapsed_ms = t0.elapsed().as_millis(), "⏱ [startup] apply_bundled_catalog_if_newer");

            app.manage(state);
            app.manage(desktop_runtime);
            app.manage(crate::core::gateway_supervisor::GatewaySupervisorHandle::new());

            // 6. Startup recovery: validate workspaces + interrupt dangling runs.
            // Spawned asynchronously so the window can render without waiting for
            // potentially slow filesystem checks (especially on Windows where NTFS
            // metadata + antivirus hooks add significant latency per workspace).
            let recovery_handle = app.handle().clone();
            tauri::async_runtime::spawn(async move {
                let recovery_start = std::time::Instant::now();
                let state = recovery_handle.state::<AppState>();

                let t0 = std::time::Instant::now();
                if let Err(e) = state.workspace_manager.validate_all().await {
                    tracing::warn!(error = %e, "startup workspace validation failed");
                }
                tracing::info!(elapsed_ms = t0.elapsed().as_millis(), "⏱ [startup-recovery] validate_all workspaces");

                let t0 = std::time::Instant::now();
                if let Err(e) = state.thread_manager.recover_interrupted_runs().await {
                    tracing::warn!(error = %e, "startup run recovery failed");
                }
                tracing::info!(elapsed_ms = t0.elapsed().as_millis(), "⏱ [startup-recovery] recover_interrupted_runs");

                let t0 = std::time::Instant::now();
                if let Err(e) = state.terminal_manager.recover_orphaned_sessions().await {
                    tracing::warn!(error = %e, "startup terminal session recovery failed");
                }
                tracing::info!(elapsed_ms = t0.elapsed().as_millis(), "⏱ [startup-recovery] recover_orphaned_sessions");

                tracing::info!(elapsed_ms = recovery_start.elapsed().as_millis(), "⏱ [startup-recovery] total");
            });

            // 7. Auto-start gateway if config exists (or restart on version upgrade).
            let gateway_handle = app.handle().clone();
            tauri::async_runtime::spawn(async move {
                let supervisor = gateway_handle.state::<crate::core::gateway_supervisor::GatewaySupervisorHandle>();
                match supervisor.ensure_started().await {
                    Ok(true) => tracing::info!("gateway process auto-started"),
                    Ok(false) => tracing::debug!("gateway already running"),
                    Err(e) => tracing::debug!(error = %e, "gateway auto-start skipped"),
                }
            });

            tauri::async_runtime::spawn(async {
                crate::core::settings_manager::SettingsManager::refresh_catalog_snapshot_silently()
                    .await;
            });
            build_tray(app, minimize_to_tray)?;

            // 7. Platform-specific window setup
            #[cfg(target_os = "windows")]
            if let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) {
                let _ = window.set_decorations(false);
            }

            tracing::info!(elapsed_ms = setup_start.elapsed().as_millis(), "⏱ [startup] setup() total");

            Ok(())
        })
        .on_page_load(|webview, payload| {
            if webview.label() != MAIN_WINDOW_LABEL || payload.event() != PageLoadEvent::Finished {
                return;
            }

            tracing::info!("⏱ [startup] on_page_load Finished — showing main window");
            let window = webview.window();
            let _ = window.show();
            let _ = window.set_focus();
            let _ = webview.emit("backend-ready", ());
            tracing::info!("⏱ [startup] backend-ready event emitted");
        })
        .on_window_event(|window, event| {
            if window.label() != MAIN_WINDOW_LABEL {
                return;
            }

            match event {
                WindowEvent::CloseRequested { api, .. } => {
                    let runtime = window.state::<DesktopRuntimeState>();
                    if runtime.minimize_to_tray_enabled() && !runtime.is_quitting() {
                        api.prevent_close();
                        let _ = window.hide();
                    }
                }
                _ => {}
            }
        })
        .on_menu_event(|app, event| match event.id().as_ref() {
            TRAY_SHOW_ID => show_main_window(app),
            TRAY_QUIT_ID => {
                app.state::<DesktopRuntimeState>().mark_quitting();
                app.exit(0);
            }
            _ => {}
        })
        .on_tray_icon_event(|app, event| {
            if event.id().as_ref() != MAIN_TRAY_ID {
                return;
            }

            if let TrayIconEvent::Click {
                button: MouseButton::Left,
                button_state: MouseButtonState::Up,
                ..
            } = event
            {
                show_main_window(app);
            }
        })
        .build(tauri::generate_context!())
        .expect("error while building tauri application");

    tracing::info!(
        elapsed_ms = build_start.elapsed().as_millis(),
        "⏱ [startup] app.build() complete (window created), entering run loop"
    );

    app.run(|app, event| match event {
        RunEvent::ExitRequested { .. } => {
            app.state::<DesktopRuntimeState>().mark_quitting();
            sync_tray_visibility(app, false);
        }
        #[cfg(target_os = "macos")]
        RunEvent::Reopen { .. } => {
            show_main_window(app);
        }
        _ => {}
    });
}

#[cfg(test)]
mod tests {
    use super::{
        init_directories, log_dir, parse_bool_setting, persisted_window_state_flags, tiy_home,
        LOG_FILE_PREFIX, LOG_FILE_SUFFIX,
    };
    use crate::model::settings::SettingRecord;
    use std::io::Write;
    use tauri_plugin_window_state::StateFlags;
    use tracing_appender::rolling::{RollingFileAppender, Rotation};

    fn setting(value_json: &str) -> SettingRecord {
        SettingRecord {
            key: "test.setting".to_string(),
            value_json: value_json.to_string(),
            updated_at: "2026-04-25T00:00:00Z".to_string(),
        }
    }

    #[test]
    fn tiy_home_uses_dot_tiy_under_user_home() {
        let path = tiy_home();
        assert!(
            path.ends_with(".tiy"),
            "expected tiy home to end with .tiy, got {}",
            path.display()
        );
    }

    #[test]
    fn persisted_window_state_flags_include_only_restorable_window_state() {
        let flags = persisted_window_state_flags();

        assert!(flags.contains(StateFlags::SIZE));
        assert!(flags.contains(StateFlags::POSITION));
        assert!(flags.contains(StateFlags::MAXIMIZED));
        assert!(flags.contains(StateFlags::FULLSCREEN));
        assert!(!flags.contains(StateFlags::VISIBLE));
        assert!(!flags.contains(StateFlags::DECORATIONS));
    }

    #[test]
    fn init_directories_creates_expected_tiy_subdirectories() {
        let tempdir = tempfile::tempdir().expect("should create tempdir");
        let base = tempdir.path().join(".tiy-test");

        init_directories(&base).expect("should create app directories");

        for relative in [
            "db",
            "db/backups",
            "skills",
            "prompts",
            "plugins",
            "automations",
            "workspace",
            "cache",
            "cache/index",
            "catalog",
        ] {
            let path = base.join(relative);
            assert!(
                path.is_dir(),
                "expected {} to be a directory",
                path.display()
            );
        }
    }

    #[test]
    fn parse_bool_setting_accepts_only_json_booleans() {
        assert!(parse_bool_setting(Some(setting("true")), "test.setting"));
        assert!(!parse_bool_setting(Some(setting("false")), "test.setting"));
        assert!(!parse_bool_setting(
            Some(setting("\"true\"")),
            "test.setting"
        ));
        assert!(!parse_bool_setting(
            Some(setting("not-json")),
            "test.setting"
        ));
        assert!(!parse_bool_setting(None, "test.setting"));
    }

    #[test]
    fn log_dir_uses_platform_native_location() {
        let path = log_dir();

        #[cfg(target_os = "macos")]
        assert!(
            path.ends_with("Library/Logs/TiyAgents"),
            "expected macOS logs under ~/Library/Logs/TiyAgents, got {}",
            path.display()
        );

        #[cfg(target_os = "windows")]
        assert!(
            path.ends_with("TiyAgents/logs"),
            "expected Windows logs under %LOCALAPPDATA%/TiyAgents/logs, got {}",
            path.display()
        );

        #[cfg(not(any(target_os = "macos", target_os = "windows")))]
        assert!(
            path.ends_with("tiy-agents/logs"),
            "expected Unix logs under state dir/tiy-agents/logs, got {}",
            path.display()
        );
    }

    #[test]
    fn log_file_uses_tiycode_prefix_and_log_suffix() {
        let temp_dir = tempfile::tempdir().expect("should create tempdir");
        let mut appender = RollingFileAppender::builder()
            .rotation(Rotation::DAILY)
            .filename_prefix(LOG_FILE_PREFIX)
            .filename_suffix(LOG_FILE_SUFFIX)
            .build(temp_dir.path())
            .expect("should create rolling file appender");

        writeln!(appender, "test log line").expect("should write log line");
        appender.flush().expect("should flush log output");

        let entries = std::fs::read_dir(temp_dir.path())
            .expect("should read log directory")
            .collect::<Result<Vec<_>, _>>()
            .expect("should collect log directory entries");

        assert_eq!(entries.len(), 1, "expected a single log file");

        let file_name = entries[0].file_name();
        let file_name = file_name.to_string_lossy();
        assert!(
            file_name.starts_with(LOG_FILE_PREFIX),
            "expected log file to start with {LOG_FILE_PREFIX}, got {file_name}"
        );
        assert!(
            file_name.ends_with(&format!(".{LOG_FILE_SUFFIX}")),
            "expected log file to end with .{LOG_FILE_SUFFIX}, got {file_name}"
        );
    }
}
