Explore the GuruConnect agent codebase to understand how auto-update could be implemented. Focus on:

1. How the agent connects to the server (transport layer)
2. How the agent is installed (install.rs)
3. Current version/build info system (build_info module in main.rs)
4. The startup module - how the agent starts/restarts
5. Any existing update-related code or placeholders

Key files to examine:
- agent/src/main.rs - entry point, build_info
- agent/src/install.rs - installation logic
- agent/src/startup.rs - startup/restart logic
- agent/src/transport/ - server communication
- agent/src/config.rs - configuration

Report on:
- How version info is currently tracked
- How the agent could check for updates with the server
- How the binary could be replaced on Windows (challenges with replacing running exe)
- Any existing infrastructure that could support updates


I'll explore the GuruConnect agent codebase to understand the current architecture and how auto-update could be implemented. Let me start by examining the key files.


<tool_use_error>File does not exist.</tool_use_error>


Good! I can see there's a `guru-connect` project. Let me read the correct files:


     1→//! GuruConnect - Remote Desktop Agent and Viewer
     2→//!
     3→//! Single binary for both agent (receiving connections) and viewer (initiating connections).
     4→//!
     5→//! Usage:
     6→//!   guruconnect agent              - Run as background agent
     7→//!   guruconnect view <session_id>  - View a remote session
     8→//!   guruconnect install            - Install and register protocol handler
     9→//!   guruconnect launch <url>       - Handle guruconnect:// URL
    10→//!   guruconnect [support_code]     - Legacy: run agent with support code
    11→
    12→// Hide console window by default on Windows (release builds)
    13→#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
    14→
    15→mod capture;
    16→mod chat;
    17→mod config;
    18→mod encoder;
    19→mod input;
    20→mod install;
    21→mod sas_client;
    22→mod session;
    23→mod startup;
    24→mod transport;
    25→mod tray;
    26→mod viewer;
    27→
    28→pub mod proto {
    29→    include!(concat!(env!("OUT_DIR"), "/guruconnect.rs"));
    30→}
    31→
    32→/// Build information embedded at compile time
    33→pub mod build_info {
    34→    /// Cargo package version (from Cargo.toml)
    35→    pub const VERSION: &str = env!("CARGO_PKG_VERSION");
    36→
    37→    /// Git commit hash (short, 8 chars)
    38→    pub const GIT_HASH: &str = env!("GIT_HASH");
    39→
    40→    /// Git commit hash (full)
    41→    pub const GIT_HASH_FULL: &str = env!("GIT_HASH_FULL");
    42→
    43→    /// Git branch name
    44→    pub const GIT_BRANCH: &str = env!("GIT_BRANCH");
    45→
    46→    /// Git dirty state ("clean" or "dirty")
    47→    pub const GIT_DIRTY: &str = env!("GIT_DIRTY");
    48→
    49→    /// Git commit date
    50→    pub const GIT_COMMIT_DATE: &str = env!("GIT_COMMIT_DATE");
    51→
    52→    /// Build timestamp (UTC)
    53→    pub const BUILD_TIMESTAMP: &str = env!("BUILD_TIMESTAMP");
    54→
    55→    /// Build profile (debug/release)
    56→    pub const BUILD_PROFILE: &str = env!("BUILD_PROFILE");
    57→
    58→    /// Target triple (e.g., x86_64-pc-windows-msvc)
    59→    pub const BUILD_TARGET: &str = env!("BUILD_TARGET");
    60→
    61→    /// Short version string for display (version + git hash)
    62→    pub fn short_version() -> String {
    63→        if GIT_DIRTY == "dirty" {
    64→            format!("{}-{}-dirty", VERSION, GIT_HASH)
    65→        } else {
    66→            format!("{}-{}", VERSION, GIT_HASH)
    67→        }
    68→    }
    69→
    70→    /// Full version string with all details
    71→    pub fn full_version() -> String {
    72→        format!(
    73→            "GuruConnect v{}\n\
    74→             Git:     {} ({})\n\
    75→             Branch:  {}\n\
    76→             Commit:  {}\n\
    77→             Built:   {}\n\
    78→             Profile: {}\n\
    79→             Target:  {}",
    80→            VERSION,
    81→            GIT_HASH,
    82→            GIT_DIRTY,
    83→            GIT_BRANCH,
    84→            GIT_COMMIT_DATE,
    85→            BUILD_TIMESTAMP,
    86→            BUILD_PROFILE,
    87→            BUILD_TARGET
    88→        )
    89→    }
    90→}
    91→
    92→use anyhow::Result;
    93→use clap::{Parser, Subcommand};
    94→use tracing::{info, error, warn, Level};
    95→use tracing_subscriber::FmtSubscriber;
    96→
    97→#[cfg(windows)]
    98→use windows::Win32::UI::WindowsAndMessaging::{MessageBoxW, MB_OK, MB_ICONINFORMATION, MB_ICONERROR};
    99→#[cfg(windows)]
   100→use windows::core::PCWSTR;
   101→#[cfg(windows)]
   102→use windows::Win32::System::Console::{AllocConsole, GetConsoleWindow};
   103→#[cfg(windows)]
   104→use windows::Win32::UI::WindowsAndMessaging::{ShowWindow, SW_SHOW};
   105→
   106→/// GuruConnect Remote Desktop
   107→#[derive(Parser)]
   108→#[command(name = "guruconnect")]
   109→#[command(version = concat!(env!("CARGO_PKG_VERSION"), "-", env!("GIT_HASH")), about = "Remote desktop agent and viewer")]
   110→struct Cli {
   111→    #[command(subcommand)]
   112→    command: Option<Commands>,
   113→
   114→    /// Support code for legacy mode (runs agent with code)
   115→    #[arg(value_name = "SUPPORT_CODE")]
   116→    support_code: Option<String>,
   117→
   118→    /// Enable verbose logging
   119→    #[arg(short, long, global = true)]
   120→    verbose: bool,
   121→}
   122→
   123→#[derive(Subcommand)]
   124→enum Commands {
   125→    /// Run as background agent (receive remote connections)
   126→    Agent {
   127→        /// Support code for one-time session
   128→        #[arg(short, long)]
   129→        code: Option<String>,
   130→    },
   131→
   132→    /// View a remote session (connect to an agent)
   133→    View {
   134→        /// Session ID to connect to
   135→        session_id: String,
   136→
   137→        /// Server URL
   138→        #[arg(short, long, default_value = "wss://connect.azcomputerguru.com/ws/viewer")]
   139→        server: String,
   140→
   141→        /// API key for authentication
   142→        #[arg(short, long, default_value = "")]
   143→        api_key: String,
   144→    },
   145→
   146→    /// Install GuruConnect and register protocol handler
   147→    Install {
   148→        /// Skip UAC elevation, install for current user only
   149→        #[arg(long)]
   150→        user_only: bool,
   151→
   152→        /// Called internally when running elevated
   153→        #[arg(long, hide = true)]
   154→        elevated: bool,
   155→    },
   156→
   157→    /// Uninstall GuruConnect
   158→    Uninstall,
   159→
   160→    /// Handle a guruconnect:// protocol URL
   161→    Launch {
   162→        /// The guruconnect:// URL to handle
   163→        url: String,
   164→    },
   165→
   166→    /// Show detailed version and build information
   167→    #[command(name = "version-info")]
   168→    VersionInfo,
   169→}
   170→
   171→fn main() -> Result<()> {
   172→    let cli = Cli::parse();
   173→
   174→    // Initialize logging
   175→    let level = if cli.verbose { Level::DEBUG } else { Level::INFO };
   176→    FmtSubscriber::builder()
   177→        .with_max_level(level)
   178→        .with_target(true)
   179→        .with_thread_ids(true)
   180→        .init();
   181→
   182→    info!("GuruConnect {} ({})", build_info::short_version(), build_info::BUILD_TARGET);
   183→    info!("Built: {} | Commit: {}", build_info::BUILD_TIMESTAMP, build_info::GIT_COMMIT_DATE);
   184→
   185→    match cli.command {
   186→        Some(Commands::Agent { code }) => {
   187→            run_agent_mode(code)
   188→        }
   189→        Some(Commands::View { session_id, server, api_key }) => {
   190→            run_viewer_mode(&server, &session_id, &api_key)
   191→        }
   192→        Some(Commands::Install { user_only, elevated }) => {
   193→            run_install(user_only || elevated)
   194→        }
   195→        Some(Commands::Uninstall) => {
   196→            run_uninstall()
   197→        }
   198→        Some(Commands::Launch { url }) => {
   199→            run_launch(&url)
   200→        }
   201→        Some(Commands::VersionInfo) => {
   202→            // Show detailed version info (allocate console on Windows for visibility)
   203→            #[cfg(windows)]
   204→            show_debug_console();
   205→            println!("{}", build_info::full_version());
   206→            Ok(())
   207→        }
   208→        None => {
   209→            // Legacy mode: if a support code was provided, run as agent
   210→            if let Some(code) = cli.support_code {
   211→                run_agent_mode(Some(code))
   212→            } else {
   213→                // No args: check if protocol handler is installed
   214→                // If not, run install mode (user likely downloaded from web)
   215→                if !install::is_protocol_handler_registered() {
   216→                    info!("Protocol handler not registered, running installer");
   217→                    run_install(false)
   218→                } else {
   219→                    // Protocol handler exists, run as agent
   220→                    run_agent_mode(None)
   221→                }
   222→            }
   223→        }
   224→    }
   225→}
   226→
   227→/// Run in agent mode (receive remote connections)
   228→fn run_agent_mode(support_code: Option<String>) -> Result<()> {
   229→    info!("Running in agent mode");
   230→
   231→    // Check elevation status
   232→    if install::is_elevated() {
   233→        info!("Running with elevated (administrator) privileges");
   234→    } else {
   235→        info!("Running with standard user privileges");
   236→    }
   237→
   238→    // Also check for support code in filename (legacy compatibility)
   239→    let code = support_code.or_else(extract_code_from_filename);
   240→    if let Some(ref c) = code {
   241→        info!("Support code: {}", c);
   242→    }
   243→
   244→    // Load configuration
   245→    let mut config = config::Config::load()?;
   246→    config.support_code = code;
   247→    info!("Server: {}", config.server_url);
   248→
   249→    // Run the agent
   250→    let rt = tokio::runtime::Runtime::new()?;
   251→    rt.block_on(run_agent(config))
   252→}
   253→
   254→/// Run in viewer mode (connect to remote session)
   255→fn run_viewer_mode(server: &str, session_id: &str, api_key: &str) -> Result<()> {
   256→    info!("Running in viewer mode");
   257→    info!("Connecting to session: {}", session_id);
   258→
   259→    let rt = tokio::runtime::Runtime::new()?;
   260→    rt.block_on(viewer::run(server, session_id, api_key))
   261→}
   262→
   263→/// Handle guruconnect:// URL launch
   264→fn run_launch(url: &str) -> Result<()> {
   265→    info!("Handling protocol URL: {}", url);
   266→
   267→    match install::parse_protocol_url(url) {
   268→        Ok((server, session_id, token)) => {
   269→            let api_key = token.unwrap_or_default();
   270→            run_viewer_mode(&server, &session_id, &api_key)
   271→        }
   272→        Err(e) => {
   273→            error!("Failed to parse URL: {}", e);
   274→            show_error_box("GuruConnect", &format!("Invalid URL: {}", e));
   275→            Err(e)
   276→        }
   277→    }
   278→}
   279→
   280→/// Install GuruConnect
   281→fn run_install(force_user_install: bool) -> Result<()> {
   282→    info!("Installing GuruConnect...");
   283→
   284→    match install::install(force_user_install) {
   285→        Ok(()) => {
   286→            show_message_box("GuruConnect", "Installation complete!\n\nYou can now use guruconnect:// links.");
   287→            Ok(())
   288→        }
   289→        Err(e) => {
   290→            error!("Installation failed: {}", e);
   291→            show_error_box("GuruConnect", &format!("Installation failed: {}", e));
   292→            Err(e)
   293→        }
   294→    }
   295→}
   296→
   297→/// Uninstall GuruConnect
   298→fn run_uninstall() -> Result<()> {
   299→    info!("Uninstalling GuruConnect...");
   300→
   301→    // Remove from startup
   302→    if let Err(e) = startup::remove_from_startup() {
   303→        warn!("Failed to remove from startup: {}", e);
   304→    }
   305→
   306→    // TODO: Remove registry keys for protocol handler
   307→    // TODO: Remove install directory
   308→
   309→    show_message_box("GuruConnect", "Uninstall complete.");
   310→    Ok(())
   311→}
   312→
   313→/// Extract a 6-digit support code from the executable's filename
   314→fn extract_code_from_filename() -> Option<String> {
   315→    let exe_path = std::env::current_exe().ok()?;
   316→    let filename = exe_path.file_stem()?.to_str()?;
   317→
   318→    // Look for a 6-digit number in the filename
   319→    for part in filename.split(|c| c == '-' || c == '_' || c == '.') {
   320→        let trimmed = part.trim();
   321→        if trimmed.len() == 6 && trimmed.chars().all(|c| c.is_ascii_digit()) {
   322→            return Some(trimmed.to_string());
   323→        }
   324→    }
   325→
   326→    // Check if the last 6 characters are digits
   327→    if filename.len() >= 6 {
   328→        let last_six = &filename[filename.len() - 6..];
   329→        if last_six.chars().all(|c| c.is_ascii_digit()) {
   330→            return Some(last_six.to_string());
   331→        }
   332→    }
   333→
   334→    None
   335→}
   336→
   337→/// Show a message box (Windows only)
   338→#[cfg(windows)]
   339→fn show_message_box(title: &str, message: &str) {
   340→    use std::ffi::OsStr;
   341→    use std::os::windows::ffi::OsStrExt;
   342→
   343→    let title_wide: Vec<u16> = OsStr::new(title)
   344→        .encode_wide()
   345→        .chain(std::iter::once(0))
   346→        .collect();
   347→    let message_wide: Vec<u16> = OsStr::new(message)
   348→        .encode_wide()
   349→        .chain(std::iter::once(0))
   350→        .collect();
   351→
   352→    unsafe {
   353→        MessageBoxW(
   354→            None,
   355→            PCWSTR(message_wide.as_ptr()),
   356→            PCWSTR(title_wide.as_ptr()),
   357→            MB_OK | MB_ICONINFORMATION,
   358→        );
   359→    }
   360→}
   361→
   362→#[cfg(not(windows))]
   363→fn show_message_box(_title: &str, message: &str) {
   364→    println!("{}", message);
   365→}
   366→
   367→/// Show an error message box (Windows only)
   368→#[cfg(windows)]
   369→fn show_error_box(title: &str, message: &str) {
   370→    use std::ffi::OsStr;
   371→    use std::os::windows::ffi::OsStrExt;
   372→
   373→    let title_wide: Vec<u16> = OsStr::new(title)
   374→        .encode_wide()
   375→        .chain(std::iter::once(0))
   376→        .collect();
   377→    let message_wide: Vec<u16> = OsStr::new(message)
   378→        .encode_wide()
   379→        .chain(std::iter::once(0))
   380→        .collect();
   381→
   382→    unsafe {
   383→        MessageBoxW(
   384→            None,
   385→            PCWSTR(message_wide.as_ptr()),
   386→            PCWSTR(title_wide.as_ptr()),
   387→            MB_OK | MB_ICONERROR,
   388→        );
   389→    }
   390→}
   391→
   392→#[cfg(not(windows))]
   393→fn show_error_box(_title: &str, message: &str) {
   394→    eprintln!("ERROR: {}", message);
   395→}
   396→
   397→/// Show debug console window (Windows only)
   398→#[cfg(windows)]
   399→#[allow(dead_code)]
   400→fn show_debug_console() {
   401→    unsafe {
   402→        let hwnd = GetConsoleWindow();
   403→        if hwnd.0 == std::ptr::null_mut() {
   404→            let _ = AllocConsole();
   405→        } else {
   406→            let _ = ShowWindow(hwnd, SW_SHOW);
   407→        }
   408→    }
   409→}
   410→
   411→#[cfg(not(windows))]
   412→#[allow(dead_code)]
   413→fn show_debug_console() {}
   414→
   415→/// Clean up before exiting
   416→fn cleanup_on_exit() {
   417→    info!("Cleaning up before exit");
   418→    if let Err(e) = startup::remove_from_startup() {
   419→        warn!("Failed to remove from startup: {}", e);
   420→    }
   421→}
   422→
   423→/// Run the agent main loop
   424→async fn run_agent(config: config::Config) -> Result<()> {
   425→    let elevated = install::is_elevated();
   426→    let mut session = session::SessionManager::new(config.clone(), elevated);
   427→    let is_support_session = config.support_code.is_some();
   428→    let hostname = config.hostname();
   429→
   430→    // Add to startup
   431→    if let Err(e) = startup::add_to_startup() {
   432→        warn!("Failed to add to startup: {}", e);
   433→    }
   434→
   435→    // Create tray icon
   436→    let tray = match tray::TrayController::new(&hostname, config.support_code.as_deref(), is_support_session) {
   437→        Ok(t) => {
   438→            info!("Tray icon created");
   439→            Some(t)
   440→        }
   441→        Err(e) => {
   442→            warn!("Failed to create tray icon: {}", e);
   443→            None
   444→        }
   445→    };
   446→
   447→    // Create chat controller
   448→    let chat_ctrl = chat::ChatController::new();
   449→
   450→    // Connect to server and run main loop
   451→    loop {
   452→        info!("Connecting to server...");
   453→
   454→        if is_support_session {
   455→            if let Some(ref t) = tray {
   456→                if t.exit_requested() {
   457→                    info!("Exit requested by user");
   458→                    cleanup_on_exit();
   459→                    return Ok(());
   460→                }
   461→            }
   462→        }
   463→
   464→        match session.connect().await {
   465→            Ok(_) => {
   466→                info!("Connected to server");
   467→
   468→                if let Some(ref t) = tray {
   469→                    t.update_status("Status: Connected");
   470→                }
   471→
   472→                if let Err(e) = session.run_with_tray(tray.as_ref(), chat_ctrl.as_ref()).await {
   473→                    let error_msg = e.to_string();
   474→
   475→                    if error_msg.contains("USER_EXIT") {
   476→                        info!("Session ended by user");
   477→                        cleanup_on_exit();
   478→                        return Ok(());
   479→                    }
   480→
   481→                    if error_msg.contains("SESSION_CANCELLED") {
   482→                        info!("Session was cancelled by technician");
   483→                        cleanup_on_exit();
   484→                        show_message_box("Support Session Ended", "The support session was cancelled.");
   485→                        return Ok(());
   486→                    }
   487→
   488→                    if error_msg.contains("ADMIN_DISCONNECT") {
   489→                        info!("Session disconnected by administrator - uninstalling");
   490→                        if let Err(e) = startup::uninstall() {
   491→                            warn!("Uninstall failed: {}", e);
   492→                        }
   493→                        show_message_box("Remote Session Ended", "The session was ended by the administrator.");
   494→                        return Ok(());
   495→                    }
   496→
   497→                    if error_msg.contains("ADMIN_UNINSTALL") {
   498→                        info!("Uninstall command received from server - uninstalling");
   499→                        if let Err(e) = startup::uninstall() {
   500→                            warn!("Uninstall failed: {}", e);
   501→                        }
   502→                        show_message_box("GuruConnect Removed", "This computer has been removed from remote management.");
   503→                        return Ok(());
   504→                    }
   505→
   506→                    if error_msg.contains("ADMIN_RESTART") {
   507→                        info!("Restart command received - will reconnect");
   508→                        // Don't exit, just let the loop continue to reconnect
   509→                    } else {
   510→                        error!("Session error: {}", e);
   511→                    }
   512→                }
   513→            }
   514→            Err(e) => {
   515→                let error_msg = e.to_string();
   516→
   517→                if error_msg.contains("cancelled") {
   518→                    info!("Support code was cancelled");
   519→                    cleanup_on_exit();
   520→                    show_message_box("Support Session Cancelled", "This support session has been cancelled.");
   521→                    return Ok(());
   522→                }
   523→
   524→                error!("Connection failed: {}", e);
   525→            }
   526→        }
   527→
   528→        if is_support_session {
   529→            info!("Support session ended, not reconnecting");
   530→            cleanup_on_exit();
   531→            return Ok(());
   532→        }
   533→
   534→        info!("Reconnecting in 5 seconds...");
   535→        tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
   536→    }
   537→}
   538→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

//! GuruConnect - Remote Desktop Agent and Viewer
//!
//! Single binary for both agent (receiving connections) and viewer (initiating connections).
//!
//! Usage:
//!   guruconnect agent              - Run as background agent
//!   guruconnect view <session_id>  - View a remote session
//!   guruconnect install            - Install and register protocol handler
//!   guruconnect launch <url>       - Handle guruconnect:// URL
//!   guruconnect [support_code]     - Legacy: run agent with support code

// Hide console window by default on Windows (release builds)
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

mod capture;
mod chat;
mod config;
mod encoder;
mod input;
mod install;
mod sas_client;
mod session;
mod startup;
mod transport;
mod tray;
mod viewer;

pub mod proto {
    include!(concat!(env!("OUT_DIR"), "/guruconnect.rs"));
}

/// Build information embedded at compile time
pub mod build_info {
    /// Cargo package version (from Cargo.toml)
    pub const VERSION: &str = env!("CARGO_PKG_VERSION");

    /// Git commit hash (short, 8 chars)
    pub const GIT_HASH: &str = env!("GIT_HASH");

    /// Git commit hash (full)
    pub const GIT_HASH_FULL: &str = env!("GIT_HASH_FULL");

    /// Git branch name
    pub const GIT_BRANCH: &str = env!("GIT_BRANCH");

    /// Git dirty state ("clean" or "dirty")
    pub const GIT_DIRTY: &str = env!("GIT_DIRTY");

    /// Git commit date
    pub const GIT_COMMIT_DATE: &str = env!("GIT_COMMIT_DATE");

    /// Build timestamp (UTC)
    pub const BUILD_TIMESTAMP: &str = env!("BUILD_TIMESTAMP");

    /// Build profile (debug/release)
    pub const BUILD_PROFILE: &str = env!("BUILD_PROFILE");

    /// Target triple (e.g., x86_64-pc-windows-msvc)
    pub const BUILD_TARGET: &str = env!("BUILD_TARGET");

    /// Short version string for display (version + git hash)
    pub fn short_version() -> String {
        if GIT_DIRTY == "dirty" {
            format!("{}-{}-dirty", VERSION, GIT_HASH)
        } else {
            format!("{}-{}", VERSION, GIT_HASH)
        }
    }

    /// Full version string with all details
    pub fn full_version() -> String {
        format!(
            "GuruConnect v{}\n\
             Git:     {} ({})\n\
             Branch:  {}\n\
             Commit:  {}\n\
             Built:   {}\n\
             Profile: {}\n\
             Target:  {}",
            VERSION,
            GIT_HASH,
            GIT_DIRTY,
            GIT_BRANCH,
            GIT_COMMIT_DATE,
            BUILD_TIMESTAMP,
            BUILD_PROFILE,
            BUILD_TARGET
        )
    }
}

use anyhow::Result;
use clap::{Parser, Subcommand};
use tracing::{info, error, warn, Level};
use tracing_subscriber::FmtSubscriber;

#[cfg(windows)]
use windows::Win32::UI::WindowsAndMessaging::{MessageBoxW, MB_OK, MB_ICONINFORMATION, MB_ICONERROR};
#[cfg(windows)]
use windows::core::PCWSTR;
#[cfg(windows)]
use windows::Win32::System::Console::{AllocConsole, GetConsoleWindow};
#[cfg(windows)]
use windows::Win32::UI::WindowsAndMessaging::{ShowWindow, SW_SHOW};

/// GuruConnect Remote Desktop
#[derive(Parser)]
#[command(name = "guruconnect")]
#[command(version = concat!(env!("CARGO_PKG_VERSION"), "-", env!("GIT_HASH")), about = "Remote desktop agent and viewer")]
struct Cli {
    #[command(subcommand)]
    command: Option<Commands>,

    /// Support code for legacy mode (runs agent with code)
    #[arg(value_name = "SUPPORT_CODE")]
    support_code: Option<String>,

    /// Enable verbose logging
    #[arg(short, long, global = true)]
    verbose: bool,
}

#[derive(Subcommand)]
enum Commands {
    /// Run as background agent (receive remote connections)
    Agent {
        /// Support code for one-time session
        #[arg(short, long)]
        code: Option<String>,
    },

    /// View a remote session (connect to an agent)
    View {
        /// Session ID to connect to
        session_id: String,

        /// Server URL
        #[arg(short, long, default_value = "wss://connect.azcomputerguru.com/ws/viewer")]
        server: String,

        /// API key for authentication
        #[arg(short, long, default_value = "")]
        api_key: String,
    },

    /// Install GuruConnect and register protocol handler
    Install {
        /// Skip UAC elevation, install for current user only
        #[arg(long)]
        user_only: bool,

        /// Called internally when running elevated
        #[arg(long, hide = true)]
        elevated: bool,
    },

    /// Uninstall GuruConnect
    Uninstall,

    /// Handle a guruconnect:// protocol URL
    Launch {
        /// The guruconnect:// URL to handle
        url: String,
    },

    /// Show detailed version and build information
    #[command(name = "version-info")]
    VersionInfo,
}

fn main() -> Result<()> {
    let cli = Cli::parse();

    // Initialize logging
    let level = if cli.verbose { Level::DEBUG } else { Level::INFO };
    FmtSubscriber::builder()
        .with_max_level(level)
        .with_target(true)
        .with_thread_ids(true)
        .init();

    info!("GuruConnect {} ({})", build_info::short_version(), build_info::BUILD_TARGET);
    info!("Built: {} | Commit: {}", build_info::BUILD_TIMESTAMP, build_info::GIT_COMMIT_DATE);

    match cli.command {
        Some(Commands::Agent { code }) => {
            run_agent_mode(code)
        }
        Some(Commands::View { session_id, server, api_key }) => {
            run_viewer_mode(&server, &session_id, &api_key)
        }
        Some(Commands::Install { user_only, elevated }) => {
            run_install(user_only || elevated)
        }
        Some(Commands::Uninstall) => {
            run_uninstall()
        }
        Some(Commands::Launch { url }) => {
            run_launch(&url)
        }
        Some(Commands::VersionInfo) => {
            // Show detailed version info (allocate console on Windows for visibility)
            #[cfg(windows)]
            show_debug_console();
            println!("{}", build_info::full_version());
            Ok(())
        }
        None => {
            // Legacy mode: if a support code was provided, run as agent
            if let Some(code) = cli.support_code {
                run_agent_mode(Some(code))
            } else {
                // No args: check if protocol handler is installed
                // If not, run install mode (user likely downloaded from web)
                if !install::is_protocol_handler_registered() {
                    info!("Protocol handler not registered, running installer");
                    run_install(false)
                } else {
                    // Protocol handler exists, run as agent
                    run_agent_mode(None)
                }
            }
        }
    }
}

/// Run in agent mode (receive remote connections)
fn run_agent_mode(support_code: Option<String>) -> Result<()> {
    info!("Running in agent mode");

    // Check elevation status
    if install::is_elevated() {
        info!("Running with elevated (administrator) privileges");
    } else {
        info!("Running with standard user privileges");
    }

    // Also check for support code in filename (legacy compatibility)
    let code = support_code.or_else(extract_code_from_filename);
    if let Some(ref c) = code {
        info!("Support code: {}", c);
    }

    // Load configuration
    let mut config = config::Config::load()?;
    config.support_code = code;
    info!("Server: {}", config.server_url);

    // Run the agent
    let rt = tokio::runtime::Runtime::new()?;
    rt.block_on(run_agent(config))
}

/// Run in viewer mode (connect to remote session)
fn run_viewer_mode(server: &str, session_id: &str, api_key: &str) -> Result<()> {
    info!("Running in viewer mode");
    info!("Connecting to session: {}", session_id);

    let rt = tokio::runtime::Runtime::new()?;
    rt.block_on(viewer::run(server, session_id, api_key))
}

/// Handle guruconnect:// URL launch
fn run_launch(url: &str) -> Result<()> {
    info!("Handling protocol URL: {}", url);

    match install::parse_protocol_url(url) {
        Ok((server, session_id, token)) => {
            let api_key = token.unwrap_or_default();
            run_viewer_mode(&server, &session_id, &api_key)
        }
        Err(e) => {
            error!("Failed to parse URL: {}", e);
            show_error_box("GuruConnect", &format!("Invalid URL: {}", e));
            Err(e)
        }
    }
}

/// Install GuruConnect
fn run_install(force_user_install: bool) -> Result<()> {
    info!("Installing GuruConnect...");

    match install::install(force_user_install) {
        Ok(()) => {
            show_message_box("GuruConnect", "Installation complete!\n\nYou can now use guruconnect:// links.");
            Ok(())
        }
        Err(e) => {
            error!("Installation failed: {}", e);
            show_error_box("GuruConnect", &format!("Installation failed: {}", e));
            Err(e)
        }
    }
}

/// Uninstall GuruConnect
fn run_uninstall() -> Result<()> {
    info!("Uninstalling GuruConnect...");

    // Remove from startup
    if let Err(e) = startup::remove_from_startup() {
        warn!("Failed to remove from startup: {}", e);
    }

    // TODO: Remove registry keys for protocol handler
    // TODO: Remove install directory

    show_message_box("GuruConnect", "Uninstall complete.");
    Ok(())
}

/// Extract a 6-digit support code from the executable's filename
fn extract_code_from_filename() -> Option<String> {
    let exe_path = std::env::current_exe().ok()?;
    let filename = exe_path.file_stem()?.to_str()?;

    // Look for a 6-digit number in the filename
    for part in filename.split(|c| c == '-' || c == '_' || c == '.') {
        let trimmed = part.trim();
        if trimmed.len() == 6 && trimmed.chars().all(|c| c.is_ascii_digit()) {
            return Some(trimmed.to_string());
        }
    }

    // Check if the last 6 characters are digits
    if filename.len() >= 6 {
        let last_six = &filename[filename.len() - 6..];
        if last_six.chars().all(|c| c.is_ascii_digit()) {
            return Some(last_six.to_string());
        }
    }

    None
}

/// Show a message box (Windows only)
#[cfg(windows)]
fn show_message_box(title: &str, message: &str) {
    use std::ffi::OsStr;
    use std::os::windows::ffi::OsStrExt;

    let title_wide: Vec<u16> = OsStr::new(title)
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();
    let message_wide: Vec<u16> = OsStr::new(message)
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();

    unsafe {
        MessageBoxW(
            None,
            PCWSTR(message_wide.as_ptr()),
            PCWSTR(title_wide.as_ptr()),
            MB_OK | MB_ICONINFORMATION,
        );
    }
}

#[cfg(not(windows))]
fn show_message_box(_title: &str, message: &str) {
    println!("{}", message);
}

/// Show an error message box (Windows only)
#[cfg(windows)]
fn show_error_box(title: &str, message: &str) {
    use std::ffi::OsStr;
    use std::os::windows::ffi::OsStrExt;

    let title_wide: Vec<u16> = OsStr::new(title)
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();
    let message_wide: Vec<u16> = OsStr::new(message)
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();

    unsafe {
        MessageBoxW(
            None,
            PCWSTR(message_wide.as_ptr()),
            PCWSTR(title_wide.as_ptr()),
            MB_OK | MB_ICONERROR,
        );
    }
}

#[cfg(not(windows))]
fn show_error_box(_title: &str, message: &str) {
    eprintln!("ERROR: {}", message);
}

/// Show debug console window (Windows only)
#[cfg(windows)]
#[allow(dead_code)]
fn show_debug_console() {
    unsafe {
        let hwnd = GetConsoleWindow();
        if hwnd.0 == std::ptr::null_mut() {
            let _ = AllocConsole();
        } else {
            let _ = ShowWindow(hwnd, SW_SHOW);
        }
    }
}

#[cfg(not(windows))]
#[allow(dead_code)]
fn show_debug_console() {}

/// Clean up before exiting
fn cleanup_on_exit() {
    info!("Cleaning up before exit");
    if let Err(e) = startup::remove_from_startup() {
        warn!("Failed to remove from startup: {}", e);
    }
}

/// Run the agent main loop
async fn run_agent(config: config::Config) -> Result<()> {
    let elevated = install::is_elevated();
    let mut session = session::SessionManager::new(config.clone(), elevated);
    let is_support_session = config.support_code.is_some();
    let hostname = config.hostname();

    // Add to startup
    if let Err(e) = startup::add_to_startup() {
        warn!("Failed to add to startup: {}", e);
    }

    // Create tray icon
    let tray = match tray::TrayController::new(&hostname, config.support_code.as_deref(), is_support_session) {
        Ok(t) => {
            info!("Tray icon created");
            Some(t)
        }
        Err(e) => {
            warn!("Failed to create tray icon: {}", e);
            None
        }
    };

    // Create chat controller
    let chat_ctrl = chat::ChatController::new();

    // Connect to server and run main loop
    loop {
        info!("Connecting to server...");

        if is_support_session {
            if let Some(ref t) = tray {
                if t.exit_requested() {
                    info!("Exit requested by user");
                    cleanup_on_exit();
                    return Ok(());
                }
            }
        }

        match session.connect().await {
            Ok(_) => {
                info!("Connected to server");

                if let Some(ref t) = tray {
                    t.update_status("Status: Connected");
                }

                if let Err(e) = session.run_with_tray(tray.as_ref(), chat_ctrl.as_ref()).await {
                    let error_msg = e.to_string();

                    if error_msg.contains("USER_EXIT") {
                        info!("Session ended by user");
                        cleanup_on_exit();
                        return Ok(());
                    }

                    if error_msg.contains("SESSION_CANCELLED") {
                        info!("Session was cancelled by technician");
                        cleanup_on_exit();
                        show_message_box("Support Session Ended", "The support session was cancelled.");
                        return Ok(());
                    }

                    if error_msg.contains("ADMIN_DISCONNECT") {
                        info!("Session disconnected by administrator - uninstalling");
                        if let Err(e) = startup::uninstall() {
                            warn!("Uninstall failed: {}", e);
                        }
                        show_message_box("Remote Session Ended", "The session was ended by the administrator.");
                        return Ok(());
                    }

                    if error_msg.contains("ADMIN_UNINSTALL") {
                        info!("Uninstall command received from server - uninstalling");
                        if let Err(e) = startup::uninstall() {
                            warn!("Uninstall failed: {}", e);
                        }
                        show_message_box("GuruConnect Removed", "This computer has been removed from remote management.");
                        return Ok(());
                    }

                    if error_msg.contains("ADMIN_RESTART") {
                        info!("Restart command received - will reconnect");
                        // Don't exit, just let the loop continue to reconnect
                    } else {
                        error!("Session error: {}", e);
                    }
                }
            }
            Err(e) => {
                let error_msg = e.to_string();

                if error_msg.contains("cancelled") {
                    info!("Support code was cancelled");
                    cleanup_on_exit();
                    show_message_box("Support Session Cancelled", "This support session has been cancelled.");
                    return Ok(());
                }

                error!("Connection failed: {}", e);
            }
        }

        if is_support_session {
            info!("Support session ended, not reconnecting");
            cleanup_on_exit();
            return Ok(());
        }

        info!("Reconnecting in 5 seconds...");
        tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
    }
}


     1→//! Installation and protocol handler registration
     2→//!
     3→//! Handles:
     4→//! - Self-installation to Program Files (with UAC) or LocalAppData (fallback)
     5→//! - Protocol handler registration (guruconnect://)
     6→//! - UAC elevation with graceful fallback
     7→
     8→use anyhow::{anyhow, Result};
     9→use tracing::{info, warn, error};
    10→
    11→#[cfg(windows)]
    12→use windows::{
    13→    core::PCWSTR,
    14→    Win32::Foundation::HANDLE,
    15→    Win32::Security::{GetTokenInformation, TokenElevation, TOKEN_ELEVATION, TOKEN_QUERY},
    16→    Win32::System::Threading::{GetCurrentProcess, OpenProcessToken},
    17→    Win32::System::Registry::{
    18→        RegCreateKeyExW, RegSetValueExW, RegCloseKey, HKEY, HKEY_CLASSES_ROOT,
    19→        HKEY_CURRENT_USER, KEY_WRITE, REG_SZ, REG_OPTION_NON_VOLATILE,
    20→    },
    21→    Win32::UI::Shell::ShellExecuteW,
    22→    Win32::UI::WindowsAndMessaging::SW_SHOWNORMAL,
    23→};
    24→
    25→#[cfg(windows)]
    26→use std::ffi::OsStr;
    27→#[cfg(windows)]
    28→use std::os::windows::ffi::OsStrExt;
    29→
    30→/// Install locations
    31→pub const SYSTEM_INSTALL_PATH: &str = r"C:\Program Files\GuruConnect";
    32→pub const USER_INSTALL_PATH: &str = r"GuruConnect"; // Relative to %LOCALAPPDATA%
    33→
    34→/// Check if running with elevated privileges
    35→#[cfg(windows)]
    36→pub fn is_elevated() -> bool {
    37→    unsafe {
    38→        let mut token_handle = HANDLE::default();
    39→        if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token_handle).is_err() {
    40→            return false;
    41→        }
    42→
    43→        let mut elevation = TOKEN_ELEVATION::default();
    44→        let mut size = std::mem::size_of::<TOKEN_ELEVATION>() as u32;
    45→
    46→        let result = GetTokenInformation(
    47→            token_handle,
    48→            TokenElevation,
    49→            Some(&mut elevation as *mut _ as *mut _),
    50→            size,
    51→            &mut size,
    52→        );
    53→
    54→        let _ = windows::Win32::Foundation::CloseHandle(token_handle);
    55→
    56→        result.is_ok() && elevation.TokenIsElevated != 0
    57→    }
    58→}
    59→
    60→#[cfg(not(windows))]
    61→pub fn is_elevated() -> bool {
    62→    unsafe { libc::geteuid() == 0 }
    63→}
    64→
    65→/// Get the install path based on elevation status
    66→pub fn get_install_path(elevated: bool) -> std::path::PathBuf {
    67→    if elevated {
    68→        std::path::PathBuf::from(SYSTEM_INSTALL_PATH)
    69→    } else {
    70→        let local_app_data = std::env::var("LOCALAPPDATA")
    71→            .unwrap_or_else(|_| {
    72→                let home = std::env::var("USERPROFILE").unwrap_or_else(|_| ".".to_string());
    73→                format!(r"{}\AppData\Local", home)
    74→            });
    75→        std::path::PathBuf::from(local_app_data).join(USER_INSTALL_PATH)
    76→    }
    77→}
    78→
    79→/// Get the executable path
    80→pub fn get_exe_path(install_path: &std::path::Path) -> std::path::PathBuf {
    81→    install_path.join("guruconnect.exe")
    82→}
    83→
    84→/// Attempt to elevate and re-run with install command
    85→#[cfg(windows)]
    86→pub fn try_elevate_and_install() -> Result<bool> {
    87→    let exe_path = std::env::current_exe()?;
    88→    let exe_path_wide: Vec<u16> = OsStr::new(exe_path.as_os_str())
    89→        .encode_wide()
    90→        .chain(std::iter::once(0))
    91→        .collect();
    92→
    93→    let verb: Vec<u16> = OsStr::new("runas")
    94→        .encode_wide()
    95→        .chain(std::iter::once(0))
    96→        .collect();
    97→
    98→    let params: Vec<u16> = OsStr::new("install --elevated")
    99→        .encode_wide()
   100→        .chain(std::iter::once(0))
   101→        .collect();
   102→
   103→    unsafe {
   104→        let result = ShellExecuteW(
   105→            None,
   106→            PCWSTR(verb.as_ptr()),
   107→            PCWSTR(exe_path_wide.as_ptr()),
   108→            PCWSTR(params.as_ptr()),
   109→            PCWSTR::null(),
   110→            SW_SHOWNORMAL,
   111→        );
   112→
   113→        // ShellExecuteW returns > 32 on success
   114→        if result.0 as usize > 32 {
   115→            info!("UAC elevation requested");
   116→            Ok(true)
   117→        } else {
   118→            warn!("UAC elevation denied or failed");
   119→            Ok(false)
   120→        }
   121→    }
   122→}
   123→
   124→#[cfg(not(windows))]
   125→pub fn try_elevate_and_install() -> Result<bool> {
   126→    Ok(false)
   127→}
   128→
   129→/// Register the guruconnect:// protocol handler
   130→#[cfg(windows)]
   131→pub fn register_protocol_handler(elevated: bool) -> Result<()> {
   132→    let install_path = get_install_path(elevated);
   133→    let exe_path = get_exe_path(&install_path);
   134→    let exe_path_str = exe_path.to_string_lossy();
   135→
   136→    // Command to execute: "C:\...\guruconnect.exe" "launch" "%1"
   137→    let command = format!("\"{}\" launch \"%1\"", exe_path_str);
   138→
   139→    // Choose registry root based on elevation
   140→    let root_key = if elevated {
   141→        HKEY_CLASSES_ROOT
   142→    } else {
   143→        // User-level registration under Software\Classes
   144→        HKEY_CURRENT_USER
   145→    };
   146→
   147→    let base_path = if elevated {
   148→        "guruconnect"
   149→    } else {
   150→        r"Software\Classes\guruconnect"
   151→    };
   152→
   153→    unsafe {
   154→        // Create guruconnect key
   155→        let mut protocol_key = HKEY::default();
   156→        let key_path = to_wide(base_path);
   157→        let result = RegCreateKeyExW(
   158→            root_key,
   159→            PCWSTR(key_path.as_ptr()),
   160→            0,
   161→            PCWSTR::null(),
   162→            REG_OPTION_NON_VOLATILE,
   163→            KEY_WRITE,
   164→            None,
   165→            &mut protocol_key,
   166→            None,
   167→        );
   168→        if result.is_err() {
   169→            return Err(anyhow!("Failed to create protocol key: {:?}", result));
   170→        }
   171→
   172→        // Set default value (protocol description)
   173→        let description = to_wide("GuruConnect Protocol");
   174→        let result = RegSetValueExW(
   175→            protocol_key,
   176→            PCWSTR::null(),
   177→            0,
   178→            REG_SZ,
   179→            Some(&description_to_bytes(&description)),
   180→        );
   181→        if result.is_err() {
   182→            let _ = RegCloseKey(protocol_key);
   183→            return Err(anyhow!("Failed to set protocol description: {:?}", result));
   184→        }
   185→
   186→        // Set URL Protocol (empty string indicates this is a protocol handler)
   187→        let url_protocol = to_wide("URL Protocol");
   188→        let empty = to_wide("");
   189→        let result = RegSetValueExW(
   190→            protocol_key,
   191→            PCWSTR(url_protocol.as_ptr()),
   192→            0,
   193→            REG_SZ,
   194→            Some(&description_to_bytes(&empty)),
   195→        );
   196→        if result.is_err() {
   197→            let _ = RegCloseKey(protocol_key);
   198→            return Err(anyhow!("Failed to set URL Protocol: {:?}", result));
   199→        }
   200→
   201→        let _ = RegCloseKey(protocol_key);
   202→
   203→        // Create shell\open\command key
   204→        let command_path = if elevated {
   205→            r"guruconnect\shell\open\command"
   206→        } else {
   207→            r"Software\Classes\guruconnect\shell\open\command"
   208→        };
   209→        let command_key_path = to_wide(command_path);
   210→        let mut command_key = HKEY::default();
   211→        let result = RegCreateKeyExW(
   212→            root_key,
   213→            PCWSTR(command_key_path.as_ptr()),
   214→            0,
   215→            PCWSTR::null(),
   216→            REG_OPTION_NON_VOLATILE,
   217→            KEY_WRITE,
   218→            None,
   219→            &mut command_key,
   220→            None,
   221→        );
   222→        if result.is_err() {
   223→            return Err(anyhow!("Failed to create command key: {:?}", result));
   224→        }
   225→
   226→        // Set the command
   227→        let command_wide = to_wide(&command);
   228→        let result = RegSetValueExW(
   229→            command_key,
   230→            PCWSTR::null(),
   231→            0,
   232→            REG_SZ,
   233→            Some(&description_to_bytes(&command_wide)),
   234→        );
   235→        if result.is_err() {
   236→            let _ = RegCloseKey(command_key);
   237→            return Err(anyhow!("Failed to set command: {:?}", result));
   238→        }
   239→
   240→        let _ = RegCloseKey(command_key);
   241→    }
   242→
   243→    info!("Protocol handler registered: guruconnect://");
   244→    Ok(())
   245→}
   246→
   247→#[cfg(not(windows))]
   248→pub fn register_protocol_handler(_elevated: bool) -> Result<()> {
   249→    warn!("Protocol handler registration not supported on this platform");
   250→    Ok(())
   251→}
   252→
   253→/// Install the application
   254→pub fn install(force_user_install: bool) -> Result<()> {
   255→    let elevated = is_elevated();
   256→
   257→    // If not elevated and not forcing user install, try to elevate
   258→    if !elevated && !force_user_install {
   259→        info!("Attempting UAC elevation for system-wide install...");
   260→        match try_elevate_and_install() {
   261→            Ok(true) => {
   262→                // Elevation was requested, exit this instance
   263→                // The elevated instance will continue the install
   264→                info!("Elevated process started, exiting current instance");
   265→                std::process::exit(0);
   266→            }
   267→            Ok(false) => {
   268→                info!("UAC denied, falling back to user install");
   269→            }
   270→            Err(e) => {
   271→                warn!("Elevation failed: {}, falling back to user install", e);
   272→            }
   273→        }
   274→    }
   275→
   276→    let install_path = get_install_path(elevated);
   277→    let exe_path = get_exe_path(&install_path);
   278→
   279→    info!("Installing to: {}", install_path.display());
   280→
   281→    // Create install directory
   282→    std::fs::create_dir_all(&install_path)?;
   283→
   284→    // Copy ourselves to install location
   285→    let current_exe = std::env::current_exe()?;
   286→    if current_exe != exe_path {
   287→        std::fs::copy(&current_exe, &exe_path)?;
   288→        info!("Copied executable to: {}", exe_path.display());
   289→    }
   290→
   291→    // Register protocol handler
   292→    register_protocol_handler(elevated)?;
   293→
   294→    info!("Installation complete!");
   295→    if elevated {
   296→        info!("Installed system-wide to: {}", install_path.display());
   297→    } else {
   298→        info!("Installed for current user to: {}", install_path.display());
   299→    }
   300→
   301→    Ok(())
   302→}
   303→
   304→/// Check if the guruconnect:// protocol handler is registered
   305→#[cfg(windows)]
   306→pub fn is_protocol_handler_registered() -> bool {
   307→    use windows::Win32::System::Registry::{
   308→        RegOpenKeyExW, RegCloseKey, HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, KEY_READ,
   309→    };
   310→
   311→    unsafe {
   312→        // Check system-wide registration (HKCR\guruconnect)
   313→        let mut key = HKEY::default();
   314→        let key_path = to_wide("guruconnect");
   315→        if RegOpenKeyExW(
   316→            HKEY_CLASSES_ROOT,
   317→            PCWSTR(key_path.as_ptr()),
   318→            0,
   319→            KEY_READ,
   320→            &mut key,
   321→        ).is_ok() {
   322→            let _ = RegCloseKey(key);
   323→            return true;
   324→        }
   325→
   326→        // Check user-level registration (HKCU\Software\Classes\guruconnect)
   327→        let key_path = to_wide(r"Software\Classes\guruconnect");
   328→        if RegOpenKeyExW(
   329→            HKEY_CURRENT_USER,
   330→            PCWSTR(key_path.as_ptr()),
   331→            0,
   332→            KEY_READ,
   333→            &mut key,
   334→        ).is_ok() {
   335→            let _ = RegCloseKey(key);
   336→            return true;
   337→        }
   338→    }
   339→
   340→    false
   341→}
   342→
   343→#[cfg(not(windows))]
   344→pub fn is_protocol_handler_registered() -> bool {
   345→    // On non-Windows, assume not registered (or check ~/.local/share/applications)
   346→    false
   347→}
   348→
   349→/// Parse a guruconnect:// URL and extract session parameters
   350→pub fn parse_protocol_url(url: &str) -> Result<(String, String, Option<String>)> {
   351→    // Expected formats:
   352→    // guruconnect://view/SESSION_ID
   353→    // guruconnect://view/SESSION_ID?token=API_KEY
   354→    // guruconnect://connect/SESSION_ID?server=wss://...&token=API_KEY
   355→    //
   356→    // Note: In URL parsing, "view" becomes the host, SESSION_ID is the path
   357→
   358→    let url = url::Url::parse(url)
   359→        .map_err(|e| anyhow!("Invalid URL: {}", e))?;
   360→
   361→    if url.scheme() != "guruconnect" {
   362→        return Err(anyhow!("Invalid scheme: expected guruconnect://"));
   363→    }
   364→
   365→    // The "action" (view/connect) is parsed as the host
   366→    let action = url.host_str()
   367→        .ok_or_else(|| anyhow!("Missing action in URL"))?;
   368→
   369→    // The session ID is the first path segment
   370→    let path = url.path().trim_start_matches('/');
   371→    let session_id = if path.is_empty() {
   372→        return Err(anyhow!("Missing session ID"));
   373→    } else {
   374→        path.split('/').next().unwrap_or("").to_string()
   375→    };
   376→
   377→    if session_id.is_empty() {
   378→        return Err(anyhow!("Missing session ID"));
   379→    }
   380→
   381→    // Extract query parameters
   382→    let mut server = None;
   383→    let mut token = None;
   384→
   385→    for (key, value) in url.query_pairs() {
   386→        match key.as_ref() {
   387→            "server" => server = Some(value.to_string()),
   388→            "token" | "api_key" => token = Some(value.to_string()),
   389→            _ => {}
   390→        }
   391→    }
   392→
   393→    // Default server if not specified
   394→    let server = server.unwrap_or_else(|| "wss://connect.azcomputerguru.com/ws/viewer".to_string());
   395→
   396→    match action {
   397→        "view" | "connect" => Ok((server, session_id, token)),
   398→        _ => Err(anyhow!("Unknown action: {}", action)),
   399→    }
   400→}
   401→
   402→// Helper functions for Windows registry operations
   403→#[cfg(windows)]
   404→fn to_wide(s: &str) -> Vec<u16> {
   405→    OsStr::new(s)
   406→        .encode_wide()
   407→        .chain(std::iter::once(0))
   408→        .collect()
   409→}
   410→
   411→#[cfg(windows)]
   412→fn description_to_bytes(wide: &[u16]) -> Vec<u8> {
   413→    wide.iter()
   414→        .flat_map(|w| w.to_le_bytes())
   415→        .collect()
   416→}
   417→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

//! Installation and protocol handler registration
//!
//! Handles:
//! - Self-installation to Program Files (with UAC) or LocalAppData (fallback)
//! - Protocol handler registration (guruconnect://)
//! - UAC elevation with graceful fallback

use anyhow::{anyhow, Result};
use tracing::{info, warn, error};

#[cfg(windows)]
use windows::{
    core::PCWSTR,
    Win32::Foundation::HANDLE,
    Win32::Security::{GetTokenInformation, TokenElevation, TOKEN_ELEVATION, TOKEN_QUERY},
    Win32::System::Threading::{GetCurrentProcess, OpenProcessToken},
    Win32::System::Registry::{
        RegCreateKeyExW, RegSetValueExW, RegCloseKey, HKEY, HKEY_CLASSES_ROOT,
        HKEY_CURRENT_USER, KEY_WRITE, REG_SZ, REG_OPTION_NON_VOLATILE,
    },
    Win32::UI::Shell::ShellExecuteW,
    Win32::UI::WindowsAndMessaging::SW_SHOWNORMAL,
};

#[cfg(windows)]
use std::ffi::OsStr;
#[cfg(windows)]
use std::os::windows::ffi::OsStrExt;

/// Install locations
pub const SYSTEM_INSTALL_PATH: &str = r"C:\Program Files\GuruConnect";
pub const USER_INSTALL_PATH: &str = r"GuruConnect"; // Relative to %LOCALAPPDATA%

/// Check if running with elevated privileges
#[cfg(windows)]
pub fn is_elevated() -> bool {
    unsafe {
        let mut token_handle = HANDLE::default();
        if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token_handle).is_err() {
            return false;
        }

        let mut elevation = TOKEN_ELEVATION::default();
        let mut size = std::mem::size_of::<TOKEN_ELEVATION>() as u32;

        let result = GetTokenInformation(
            token_handle,
            TokenElevation,
            Some(&mut elevation as *mut _ as *mut _),
            size,
            &mut size,
        );

        let _ = windows::Win32::Foundation::CloseHandle(token_handle);

        result.is_ok() && elevation.TokenIsElevated != 0
    }
}

#[cfg(not(windows))]
pub fn is_elevated() -> bool {
    unsafe { libc::geteuid() == 0 }
}

/// Get the install path based on elevation status
pub fn get_install_path(elevated: bool) -> std::path::PathBuf {
    if elevated {
        std::path::PathBuf::from(SYSTEM_INSTALL_PATH)
    } else {
        let local_app_data = std::env::var("LOCALAPPDATA")
            .unwrap_or_else(|_| {
                let home = std::env::var("USERPROFILE").unwrap_or_else(|_| ".".to_string());
                format!(r"{}\AppData\Local", home)
            });
        std::path::PathBuf::from(local_app_data).join(USER_INSTALL_PATH)
    }
}

/// Get the executable path
pub fn get_exe_path(install_path: &std::path::Path) -> std::path::PathBuf {
    install_path.join("guruconnect.exe")
}

/// Attempt to elevate and re-run with install command
#[cfg(windows)]
pub fn try_elevate_and_install() -> Result<bool> {
    let exe_path = std::env::current_exe()?;
    let exe_path_wide: Vec<u16> = OsStr::new(exe_path.as_os_str())
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();

    let verb: Vec<u16> = OsStr::new("runas")
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();

    let params: Vec<u16> = OsStr::new("install --elevated")
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();

    unsafe {
        let result = ShellExecuteW(
            None,
            PCWSTR(verb.as_ptr()),
            PCWSTR(exe_path_wide.as_ptr()),
            PCWSTR(params.as_ptr()),
            PCWSTR::null(),
            SW_SHOWNORMAL,
        );

        // ShellExecuteW returns > 32 on success
        if result.0 as usize > 32 {
            info!("UAC elevation requested");
            Ok(true)
        } else {
            warn!("UAC elevation denied or failed");
            Ok(false)
        }
    }
}

#[cfg(not(windows))]
pub fn try_elevate_and_install() -> Result<bool> {
    Ok(false)
}

/// Register the guruconnect:// protocol handler
#[cfg(windows)]
pub fn register_protocol_handler(elevated: bool) -> Result<()> {
    let install_path = get_install_path(elevated);
    let exe_path = get_exe_path(&install_path);
    let exe_path_str = exe_path.to_string_lossy();

    // Command to execute: "C:\...\guruconnect.exe" "launch" "%1"
    let command = format!("\"{}\" launch \"%1\"", exe_path_str);

    // Choose registry root based on elevation
    let root_key = if elevated {
        HKEY_CLASSES_ROOT
    } else {
        // User-level registration under Software\Classes
        HKEY_CURRENT_USER
    };

    let base_path = if elevated {
        "guruconnect"
    } else {
        r"Software\Classes\guruconnect"
    };

    unsafe {
        // Create guruconnect key
        let mut protocol_key = HKEY::default();
        let key_path = to_wide(base_path);
        let result = RegCreateKeyExW(
            root_key,
            PCWSTR(key_path.as_ptr()),
            0,
            PCWSTR::null(),
            REG_OPTION_NON_VOLATILE,
            KEY_WRITE,
            None,
            &mut protocol_key,
            None,
        );
        if result.is_err() {
            return Err(anyhow!("Failed to create protocol key: {:?}", result));
        }

        // Set default value (protocol description)
        let description = to_wide("GuruConnect Protocol");
        let result = RegSetValueExW(
            protocol_key,
            PCWSTR::null(),
            0,
            REG_SZ,
            Some(&description_to_bytes(&description)),
        );
        if result.is_err() {
            let _ = RegCloseKey(protocol_key);
            return Err(anyhow!("Failed to set protocol description: {:?}", result));
        }

        // Set URL Protocol (empty string indicates this is a protocol handler)
        let url_protocol = to_wide("URL Protocol");
        let empty = to_wide("");
        let result = RegSetValueExW(
            protocol_key,
            PCWSTR(url_protocol.as_ptr()),
            0,
            REG_SZ,
            Some(&description_to_bytes(&empty)),
        );
        if result.is_err() {
            let _ = RegCloseKey(protocol_key);
            return Err(anyhow!("Failed to set URL Protocol: {:?}", result));
        }

        let _ = RegCloseKey(protocol_key);

        // Create shell\open\command key
        let command_path = if elevated {
            r"guruconnect\shell\open\command"
        } else {
            r"Software\Classes\guruconnect\shell\open\command"
        };
        let command_key_path = to_wide(command_path);
        let mut command_key = HKEY::default();
        let result = RegCreateKeyExW(
            root_key,
            PCWSTR(command_key_path.as_ptr()),
            0,
            PCWSTR::null(),
            REG_OPTION_NON_VOLATILE,
            KEY_WRITE,
            None,
            &mut command_key,
            None,
        );
        if result.is_err() {
            return Err(anyhow!("Failed to create command key: {:?}", result));
        }

        // Set the command
        let command_wide = to_wide(&command);
        let result = RegSetValueExW(
            command_key,
            PCWSTR::null(),
            0,
            REG_SZ,
            Some(&description_to_bytes(&command_wide)),
        );
        if result.is_err() {
            let _ = RegCloseKey(command_key);
            return Err(anyhow!("Failed to set command: {:?}", result));
        }

        let _ = RegCloseKey(command_key);
    }

    info!("Protocol handler registered: guruconnect://");
    Ok(())
}

#[cfg(not(windows))]
pub fn register_protocol_handler(_elevated: bool) -> Result<()> {
    warn!("Protocol handler registration not supported on this platform");
    Ok(())
}

/// Install the application
pub fn install(force_user_install: bool) -> Result<()> {
    let elevated = is_elevated();

    // If not elevated and not forcing user install, try to elevate
    if !elevated && !force_user_install {
        info!("Attempting UAC elevation for system-wide install...");
        match try_elevate_and_install() {
            Ok(true) => {
                // Elevation was requested, exit this instance
                // The elevated instance will continue the install
                info!("Elevated process started, exiting current instance");
                std::process::exit(0);
            }
            Ok(false) => {
                info!("UAC denied, falling back to user install");
            }
            Err(e) => {
                warn!("Elevation failed: {}, falling back to user install", e);
            }
        }
    }

    let install_path = get_install_path(elevated);
    let exe_path = get_exe_path(&install_path);

    info!("Installing to: {}", install_path.display());

    // Create install directory
    std::fs::create_dir_all(&install_path)?;

    // Copy ourselves to install location
    let current_exe = std::env::current_exe()?;
    if current_exe != exe_path {
        std::fs::copy(&current_exe, &exe_path)?;
        info!("Copied executable to: {}", exe_path.display());
    }

    // Register protocol handler
    register_protocol_handler(elevated)?;

    info!("Installation complete!");
    if elevated {
        info!("Installed system-wide to: {}", install_path.display());
    } else {
        info!("Installed for current user to: {}", install_path.display());
    }

    Ok(())
}

/// Check if the guruconnect:// protocol handler is registered
#[cfg(windows)]
pub fn is_protocol_handler_registered() -> bool {
    use windows::Win32::System::Registry::{
        RegOpenKeyExW, RegCloseKey, HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, KEY_READ,
    };

    unsafe {
        // Check system-wide registration (HKCR\guruconnect)
        let mut key = HKEY::default();
        let key_path = to_wide("guruconnect");
        if RegOpenKeyExW(
            HKEY_CLASSES_ROOT,
            PCWSTR(key_path.as_ptr()),
            0,
            KEY_READ,
            &mut key,
        ).is_ok() {
            let _ = RegCloseKey(key);
            return true;
        }

        // Check user-level registration (HKCU\Software\Classes\guruconnect)
        let key_path = to_wide(r"Software\Classes\guruconnect");
        if RegOpenKeyExW(
            HKEY_CURRENT_USER,
            PCWSTR(key_path.as_ptr()),
            0,
            KEY_READ,
            &mut key,
        ).is_ok() {
            let _ = RegCloseKey(key);
            return true;
        }
    }

    false
}

#[cfg(not(windows))]
pub fn is_protocol_handler_registered() -> bool {
    // On non-Windows, assume not registered (or check ~/.local/share/applications)
    false
}

/// Parse a guruconnect:// URL and extract session parameters
pub fn parse_protocol_url(url: &str) -> Result<(String, String, Option<String>)> {
    // Expected formats:
    // guruconnect://view/SESSION_ID
    // guruconnect://view/SESSION_ID?token=API_KEY
    // guruconnect://connect/SESSION_ID?server=wss://...&token=API_KEY
    //
    // Note: In URL parsing, "view" becomes the host, SESSION_ID is the path

    let url = url::Url::parse(url)
        .map_err(|e| anyhow!("Invalid URL: {}", e))?;

    if url.scheme() != "guruconnect" {
        return Err(anyhow!("Invalid scheme: expected guruconnect://"));
    }

    // The "action" (view/connect) is parsed as the host
    let action = url.host_str()
        .ok_or_else(|| anyhow!("Missing action in URL"))?;

    // The session ID is the first path segment
    let path = url.path().trim_start_matches('/');
    let session_id = if path.is_empty() {
        return Err(anyhow!("Missing session ID"));
    } else {
        path.split('/').next().unwrap_or("").to_string()
    };

    if session_id.is_empty() {
        return Err(anyhow!("Missing session ID"));
    }

    // Extract query parameters
    let mut server = None;
    let mut token = None;

    for (key, value) in url.query_pairs() {
        match key.as_ref() {
            "server" => server = Some(value.to_string()),
            "token" | "api_key" => token = Some(value.to_string()),
            _ => {}
        }
    }

    // Default server if not specified
    let server = server.unwrap_or_else(|| "wss://connect.azcomputerguru.com/ws/viewer".to_string());

    match action {
        "view" | "connect" => Ok((server, session_id, token)),
        _ => Err(anyhow!("Unknown action: {}", action)),
    }
}

// Helper functions for Windows registry operations
#[cfg(windows)]
fn to_wide(s: &str) -> Vec<u16> {
    OsStr::new(s)
        .encode_wide()
        .chain(std::iter::once(0))
        .collect()
}

#[cfg(windows)]
fn description_to_bytes(wide: &[u16]) -> Vec<u8> {
    wide.iter()
        .flat_map(|w| w.to_le_bytes())
        .collect()
}


     1→//! Startup persistence for the agent
     2→//!
     3→//! Handles adding/removing the agent from Windows startup.
     4→
     5→use anyhow::Result;
     6→use tracing::{info, warn, error};
     7→
     8→#[cfg(windows)]
     9→use windows::Win32::System::Registry::{
    10→    RegOpenKeyExW, RegSetValueExW, RegDeleteValueW, RegCloseKey,
    11→    HKEY_CURRENT_USER, KEY_WRITE, REG_SZ,
    12→};
    13→#[cfg(windows)]
    14→use windows::core::PCWSTR;
    15→
    16→const STARTUP_KEY: &str = r"Software\Microsoft\Windows\CurrentVersion\Run";
    17→const STARTUP_VALUE_NAME: &str = "GuruConnect";
    18→
    19→/// Add the current executable to Windows startup
    20→#[cfg(windows)]
    21→pub fn add_to_startup() -> Result<()> {
    22→    use std::ffi::OsStr;
    23→    use std::os::windows::ffi::OsStrExt;
    24→
    25→    // Get the path to the current executable
    26→    let exe_path = std::env::current_exe()?;
    27→    let exe_path_str = exe_path.to_string_lossy();
    28→
    29→    info!("Adding to startup: {}", exe_path_str);
    30→
    31→    // Convert strings to wide strings
    32→    let key_path: Vec<u16> = OsStr::new(STARTUP_KEY)
    33→        .encode_wide()
    34→        .chain(std::iter::once(0))
    35→        .collect();
    36→    let value_name: Vec<u16> = OsStr::new(STARTUP_VALUE_NAME)
    37→        .encode_wide()
    38→        .chain(std::iter::once(0))
    39→        .collect();
    40→    let value_data: Vec<u16> = OsStr::new(&*exe_path_str)
    41→        .encode_wide()
    42→        .chain(std::iter::once(0))
    43→        .collect();
    44→
    45→    unsafe {
    46→        let mut hkey = windows::Win32::Foundation::HANDLE::default();
    47→
    48→        // Open the Run key
    49→        let result = RegOpenKeyExW(
    50→            HKEY_CURRENT_USER,
    51→            PCWSTR(key_path.as_ptr()),
    52→            0,
    53→            KEY_WRITE,
    54→            &mut hkey as *mut _ as *mut _,
    55→        );
    56→
    57→        if result.is_err() {
    58→            anyhow::bail!("Failed to open registry key: {:?}", result);
    59→        }
    60→
    61→        let hkey_raw = std::mem::transmute::<_, windows::Win32::System::Registry::HKEY>(hkey);
    62→
    63→        // Set the value
    64→        let data_bytes = std::slice::from_raw_parts(
    65→            value_data.as_ptr() as *const u8,
    66→            value_data.len() * 2,
    67→        );
    68→
    69→        let set_result = RegSetValueExW(
    70→            hkey_raw,
    71→            PCWSTR(value_name.as_ptr()),
    72→            0,
    73→            REG_SZ,
    74→            Some(data_bytes),
    75→        );
    76→
    77→        let _ = RegCloseKey(hkey_raw);
    78→
    79→        if set_result.is_err() {
    80→            anyhow::bail!("Failed to set registry value: {:?}", set_result);
    81→        }
    82→    }
    83→
    84→    info!("Successfully added to startup");
    85→    Ok(())
    86→}
    87→
    88→/// Remove the agent from Windows startup
    89→#[cfg(windows)]
    90→pub fn remove_from_startup() -> Result<()> {
    91→    use std::ffi::OsStr;
    92→    use std::os::windows::ffi::OsStrExt;
    93→
    94→    info!("Removing from startup");
    95→
    96→    let key_path: Vec<u16> = OsStr::new(STARTUP_KEY)
    97→        .encode_wide()
    98→        .chain(std::iter::once(0))
    99→        .collect();
   100→    let value_name: Vec<u16> = OsStr::new(STARTUP_VALUE_NAME)
   101→        .encode_wide()
   102→        .chain(std::iter::once(0))
   103→        .collect();
   104→
   105→    unsafe {
   106→        let mut hkey = windows::Win32::Foundation::HANDLE::default();
   107→
   108→        let result = RegOpenKeyExW(
   109→            HKEY_CURRENT_USER,
   110→            PCWSTR(key_path.as_ptr()),
   111→            0,
   112→            KEY_WRITE,
   113→            &mut hkey as *mut _ as *mut _,
   114→        );
   115→
   116→        if result.is_err() {
   117→            warn!("Failed to open registry key for removal: {:?}", result);
   118→            return Ok(()); // Not an error if key doesn't exist
   119→        }
   120→
   121→        let hkey_raw = std::mem::transmute::<_, windows::Win32::System::Registry::HKEY>(hkey);
   122→
   123→        let delete_result = RegDeleteValueW(hkey_raw, PCWSTR(value_name.as_ptr()));
   124→
   125→        let _ = RegCloseKey(hkey_raw);
   126→
   127→        if delete_result.is_err() {
   128→            warn!("Registry value may not exist: {:?}", delete_result);
   129→        } else {
   130→            info!("Successfully removed from startup");
   131→        }
   132→    }
   133→
   134→    Ok(())
   135→}
   136→
   137→/// Full uninstall: remove from startup and delete the executable
   138→#[cfg(windows)]
   139→pub fn uninstall() -> Result<()> {
   140→    use std::ffi::OsStr;
   141→    use std::os::windows::ffi::OsStrExt;
   142→    use windows::Win32::Storage::FileSystem::{MoveFileExW, MOVEFILE_DELAY_UNTIL_REBOOT};
   143→
   144→    info!("Uninstalling agent");
   145→
   146→    // First remove from startup
   147→    let _ = remove_from_startup();
   148→
   149→    // Get the path to the current executable
   150→    let exe_path = std::env::current_exe()?;
   151→    let exe_path_str = exe_path.to_string_lossy();
   152→
   153→    info!("Scheduling deletion of: {}", exe_path_str);
   154→
   155→    // Convert path to wide string
   156→    let exe_wide: Vec<u16> = OsStr::new(&*exe_path_str)
   157→        .encode_wide()
   158→        .chain(std::iter::once(0))
   159→        .collect();
   160→
   161→    // Schedule the file for deletion on next reboot
   162→    // This is necessary because the executable is currently running
   163→    unsafe {
   164→        let result = MoveFileExW(
   165→            PCWSTR(exe_wide.as_ptr()),
   166→            PCWSTR::null(),
   167→            MOVEFILE_DELAY_UNTIL_REBOOT,
   168→        );
   169→
   170→        if result.is_err() {
   171→            warn!("Failed to schedule file deletion: {:?}. File may need manual removal.", result);
   172→        } else {
   173→            info!("Executable scheduled for deletion on reboot");
   174→        }
   175→    }
   176→
   177→    Ok(())
   178→}
   179→
   180→/// Install the SAS service if the binary is available
   181→/// This allows the agent to send Ctrl+Alt+Del even without SYSTEM privileges
   182→#[cfg(windows)]
   183→pub fn install_sas_service() -> Result<()> {
   184→    info!("Attempting to install SAS service...");
   185→
   186→    // Check if the SAS service binary exists alongside the agent
   187→    let exe_path = std::env::current_exe()?;
   188→    let exe_dir = exe_path.parent().ok_or_else(|| anyhow::anyhow!("No parent directory"))?;
   189→    let sas_binary = exe_dir.join("guruconnect-sas-service.exe");
   190→
   191→    if !sas_binary.exists() {
   192→        // Also check in Program Files
   193→        let program_files = std::path::PathBuf::from(r"C:\Program Files\GuruConnect\guruconnect-sas-service.exe");
   194→        if !program_files.exists() {
   195→            warn!("SAS service binary not found");
   196→            return Ok(());
   197→        }
   198→    }
   199→
   200→    // Run the install command
   201→    let sas_path = if sas_binary.exists() {
   202→        sas_binary
   203→    } else {
   204→        std::path::PathBuf::from(r"C:\Program Files\GuruConnect\guruconnect-sas-service.exe")
   205→    };
   206→
   207→    let output = std::process::Command::new(&sas_path)
   208→        .arg("install")
   209→        .output();
   210→
   211→    match output {
   212→        Ok(result) => {
   213→            if result.status.success() {
   214→                info!("SAS service installed successfully");
   215→            } else {
   216→                let stderr = String::from_utf8_lossy(&result.stderr);
   217→                warn!("SAS service install failed: {}", stderr);
   218→            }
   219→        }
   220→        Err(e) => {
   221→            warn!("Failed to run SAS service installer: {}", e);
   222→        }
   223→    }
   224→
   225→    Ok(())
   226→}
   227→
   228→/// Uninstall the SAS service
   229→#[cfg(windows)]
   230→pub fn uninstall_sas_service() -> Result<()> {
   231→    info!("Attempting to uninstall SAS service...");
   232→
   233→    // Try to find and run the uninstall command
   234→    let paths = [
   235→        std::env::current_exe().ok().and_then(|p| p.parent().map(|d| d.join("guruconnect-sas-service.exe"))),
   236→        Some(std::path::PathBuf::from(r"C:\Program Files\GuruConnect\guruconnect-sas-service.exe")),
   237→    ];
   238→
   239→    for path_opt in paths.iter() {
   240→        if let Some(ref path) = path_opt {
   241→            if path.exists() {
   242→                let output = std::process::Command::new(path)
   243→                    .arg("uninstall")
   244→                    .output();
   245→
   246→                if let Ok(result) = output {
   247→                    if result.status.success() {
   248→                        info!("SAS service uninstalled successfully");
   249→                        return Ok(());
   250→                    }
   251→                }
   252→            }
   253→        }
   254→    }
   255→
   256→    warn!("SAS service binary not found for uninstall");
   257→    Ok(())
   258→}
   259→
   260→/// Check if the SAS service is installed and running
   261→#[cfg(windows)]
   262→pub fn check_sas_service() -> bool {
   263→    use crate::sas_client;
   264→    sas_client::is_service_available()
   265→}
   266→
   267→#[cfg(not(windows))]
   268→pub fn add_to_startup() -> Result<()> {
   269→    warn!("Startup persistence not implemented for this platform");
   270→    Ok(())
   271→}
   272→
   273→#[cfg(not(windows))]
   274→pub fn remove_from_startup() -> Result<()> {
   275→    Ok(())
   276→}
   277→
   278→#[cfg(not(windows))]
   279→pub fn uninstall() -> Result<()> {
   280→    warn!("Uninstall not implemented for this platform");
   281→    Ok(())
   282→}
   283→
   284→#[cfg(not(windows))]
   285→pub fn install_sas_service() -> Result<()> {
   286→    warn!("SAS service only available on Windows");
   287→    Ok(())
   288→}
   289→
   290→#[cfg(not(windows))]
   291→pub fn uninstall_sas_service() -> Result<()> {
   292→    Ok(())
   293→}
   294→
   295→#[cfg(not(windows))]
   296→pub fn check_sas_service() -> bool {
   297→    false
   298→}
   299→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

//! Startup persistence for the agent
//!
//! Handles adding/removing the agent from Windows startup.

use anyhow::Result;
use tracing::{info, warn, error};

#[cfg(windows)]
use windows::Win32::System::Registry::{
    RegOpenKeyExW, RegSetValueExW, RegDeleteValueW, RegCloseKey,
    HKEY_CURRENT_USER, KEY_WRITE, REG_SZ,
};
#[cfg(windows)]
use windows::core::PCWSTR;

const STARTUP_KEY: &str = r"Software\Microsoft\Windows\CurrentVersion\Run";
const STARTUP_VALUE_NAME: &str = "GuruConnect";

/// Add the current executable to Windows startup
#[cfg(windows)]
pub fn add_to_startup() -> Result<()> {
    use std::ffi::OsStr;
    use std::os::windows::ffi::OsStrExt;

    // Get the path to the current executable
    let exe_path = std::env::current_exe()?;
    let exe_path_str = exe_path.to_string_lossy();

    info!("Adding to startup: {}", exe_path_str);

    // Convert strings to wide strings
    let key_path: Vec<u16> = OsStr::new(STARTUP_KEY)
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();
    let value_name: Vec<u16> = OsStr::new(STARTUP_VALUE_NAME)
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();
    let value_data: Vec<u16> = OsStr::new(&*exe_path_str)
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();

    unsafe {
        let mut hkey = windows::Win32::Foundation::HANDLE::default();

        // Open the Run key
        let result = RegOpenKeyExW(
            HKEY_CURRENT_USER,
            PCWSTR(key_path.as_ptr()),
            0,
            KEY_WRITE,
            &mut hkey as *mut _ as *mut _,
        );

        if result.is_err() {
            anyhow::bail!("Failed to open registry key: {:?}", result);
        }

        let hkey_raw = std::mem::transmute::<_, windows::Win32::System::Registry::HKEY>(hkey);

        // Set the value
        let data_bytes = std::slice::from_raw_parts(
            value_data.as_ptr() as *const u8,
            value_data.len() * 2,
        );

        let set_result = RegSetValueExW(
            hkey_raw,
            PCWSTR(value_name.as_ptr()),
            0,
            REG_SZ,
            Some(data_bytes),
        );

        let _ = RegCloseKey(hkey_raw);

        if set_result.is_err() {
            anyhow::bail!("Failed to set registry value: {:?}", set_result);
        }
    }

    info!("Successfully added to startup");
    Ok(())
}

/// Remove the agent from Windows startup
#[cfg(windows)]
pub fn remove_from_startup() -> Result<()> {
    use std::ffi::OsStr;
    use std::os::windows::ffi::OsStrExt;

    info!("Removing from startup");

    let key_path: Vec<u16> = OsStr::new(STARTUP_KEY)
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();
    let value_name: Vec<u16> = OsStr::new(STARTUP_VALUE_NAME)
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();

    unsafe {
        let mut hkey = windows::Win32::Foundation::HANDLE::default();

        let result = RegOpenKeyExW(
            HKEY_CURRENT_USER,
            PCWSTR(key_path.as_ptr()),
            0,
            KEY_WRITE,
            &mut hkey as *mut _ as *mut _,
        );

        if result.is_err() {
            warn!("Failed to open registry key for removal: {:?}", result);
            return Ok(()); // Not an error if key doesn't exist
        }

        let hkey_raw = std::mem::transmute::<_, windows::Win32::System::Registry::HKEY>(hkey);

        let delete_result = RegDeleteValueW(hkey_raw, PCWSTR(value_name.as_ptr()));

        let _ = RegCloseKey(hkey_raw);

        if delete_result.is_err() {
            warn!("Registry value may not exist: {:?}", delete_result);
        } else {
            info!("Successfully removed from startup");
        }
    }

    Ok(())
}

/// Full uninstall: remove from startup and delete the executable
#[cfg(windows)]
pub fn uninstall() -> Result<()> {
    use std::ffi::OsStr;
    use std::os::windows::ffi::OsStrExt;
    use windows::Win32::Storage::FileSystem::{MoveFileExW, MOVEFILE_DELAY_UNTIL_REBOOT};

    info!("Uninstalling agent");

    // First remove from startup
    let _ = remove_from_startup();

    // Get the path to the current executable
    let exe_path = std::env::current_exe()?;
    let exe_path_str = exe_path.to_string_lossy();

    info!("Scheduling deletion of: {}", exe_path_str);

    // Convert path to wide string
    let exe_wide: Vec<u16> = OsStr::new(&*exe_path_str)
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();

    // Schedule the file for deletion on next reboot
    // This is necessary because the executable is currently running
    unsafe {
        let result = MoveFileExW(
            PCWSTR(exe_wide.as_ptr()),
            PCWSTR::null(),
            MOVEFILE_DELAY_UNTIL_REBOOT,
        );

        if result.is_err() {
            warn!("Failed to schedule file deletion: {:?}. File may need manual removal.", result);
        } else {
            info!("Executable scheduled for deletion on reboot");
        }
    }

    Ok(())
}

/// Install the SAS service if the binary is available
/// This allows the agent to send Ctrl+Alt+Del even without SYSTEM privileges
#[cfg(windows)]
pub fn install_sas_service() -> Result<()> {
    info!("Attempting to install SAS service...");

    // Check if the SAS service binary exists alongside the agent
    let exe_path = std::env::current_exe()?;
    let exe_dir = exe_path.parent().ok_or_else(|| anyhow::anyhow!("No parent directory"))?;
    let sas_binary = exe_dir.join("guruconnect-sas-service.exe");

    if !sas_binary.exists() {
        // Also check in Program Files
        let program_files = std::path::PathBuf::from(r"C:\Program Files\GuruConnect\guruconnect-sas-service.exe");
        if !program_files.exists() {
            warn!("SAS service binary not found");
            return Ok(());
        }
    }

    // Run the install command
    let sas_path = if sas_binary.exists() {
        sas_binary
    } else {
        std::path::PathBuf::from(r"C:\Program Files\GuruConnect\guruconnect-sas-service.exe")
    };

    let output = std::process::Command::new(&sas_path)
        .arg("install")
        .output();

    match output {
        Ok(result) => {
            if result.status.success() {
                info!("SAS service installed successfully");
            } else {
                let stderr = String::from_utf8_lossy(&result.stderr);
                warn!("SAS service install failed: {}", stderr);
            }
        }
        Err(e) => {
            warn!("Failed to run SAS service installer: {}", e);
        }
    }

    Ok(())
}

/// Uninstall the SAS service
#[cfg(windows)]
pub fn uninstall_sas_service() -> Result<()> {
    info!("Attempting to uninstall SAS service...");

    // Try to find and run the uninstall command
    let paths = [
        std::env::current_exe().ok().and_then(|p| p.parent().map(|d| d.join("guruconnect-sas-service.exe"))),
        Some(std::path::PathBuf::from(r"C:\Program Files\GuruConnect\guruconnect-sas-service.exe")),
    ];

    for path_opt in paths.iter() {
        if let Some(ref path) = path_opt {
            if path.exists() {
                let output = std::process::Command::new(path)
                    .arg("uninstall")
                    .output();

                if let Ok(result) = output {
                    if result.status.success() {
                        info!("SAS service uninstalled successfully");
                        return Ok(());
                    }
                }
            }
        }
    }

    warn!("SAS service binary not found for uninstall");
    Ok(())
}

/// Check if the SAS service is installed and running
#[cfg(windows)]
pub fn check_sas_service() -> bool {
    use crate::sas_client;
    sas_client::is_service_available()
}

#[cfg(not(windows))]
pub fn add_to_startup() -> Result<()> {
    warn!("Startup persistence not implemented for this platform");
    Ok(())
}

#[cfg(not(windows))]
pub fn remove_from_startup() -> Result<()> {
    Ok(())
}

#[cfg(not(windows))]
pub fn uninstall() -> Result<()> {
    warn!("Uninstall not implemented for this platform");
    Ok(())
}

#[cfg(not(windows))]
pub fn install_sas_service() -> Result<()> {
    warn!("SAS service only available on Windows");
    Ok(())
}

#[cfg(not(windows))]
pub fn uninstall_sas_service() -> Result<()> {
    Ok(())
}

#[cfg(not(windows))]
pub fn check_sas_service() -> bool {
    false
}