// Approval popup + connection-manager process body, run in a dedicated
// `--cm` child process.
//
// Architecture (matches stock rustdesk):
//
//   --service (Session 0, SYSTEM)
//       │ launches into active console session as SYSTEM token
//       ▼
//   --server (user session, SYSTEM token) --- screen capture, rendezvous, …
//       │  on incoming peer requiring approval, librustdesk's start_ipc
//       │  tries `ipc::connect("_cm")`, fails (no listener), then falls
//       │  back to `run_as_user(["--cm"])`. Either way, a `--cm` process
//       │  must be holding the `_cm` named pipe in the user's session:
//       ▼
//   --cm (user session, USER token) --- this module
//       │  binds `_cm`, accepts connections from `--server`, hands each one
//       │  to upstream's `IpcTaskRunner` (via `start_ipc`). Our only role
//       │  on top of that is to plug in an `InvokeUiCM` impl that renders
//       │  the approval popup with `MessageBoxW` and forwards the user's
//       │  decision back via `authorize(id)` / `close(id)`.
//
// Why this delegates to upstream's `start_ipc` instead of running its own
// frame loop: on Windows the `--server` process forwards *every* filesystem
// operation (ReadDir, ReadFile, WriteBlock, …) over the `_cm` pipe and
// expects the CM to execute them in the user's security context. Reading
// only the Login frame and discarding the rest — what an earlier version of
// this module did — meant the supporter could open a file-transfer session
// and get the request approved, but the directory listing never arrived
// because the `Data::FS(ReadDir)` frame was being silently dropped. The
// upstream `IpcTaskRunner` implements all of that machinery (handle_fs +
// the file_timer for streaming read jobs); we just provide the popup.

use librustdesk::ui_cm_interface::{self, Client, ConnectionManager, InvokeUiCM};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};

#[cfg(target_os = "windows")]
use std::os::windows::ffi::OsStrExt;

/// Diagnostic trace: writes to stderr AND a debug log file.
/// Bypasses `log` so we still see output even when env_logger / flexi_logger
/// init went wrong. Drop these calls once the popup mechanism is stable.
fn trace(msg: &str) {
    let line = format!(
        "[{:?}] cm_popup: {msg}\n",
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map(|d| d.as_millis())
            .unwrap_or(0),
    );
    let _ = std::io::Write::write_all(&mut std::io::stderr(), line.as_bytes());

    if let Ok(temp) = std::env::var("TEMP") {
        let path = format!("{temp}\\hello-agent-cm.log");
        if let Ok(mut f) = std::fs::OpenOptions::new()
            .create(true)
            .append(true)
            .open(&path)
        {
            let _ = std::io::Write::write_all(&mut f, line.as_bytes());
        }
    }
}

/// Connection-manager process entry point: bind `_cm`, accept connections
/// from the `--server` worker forever, run upstream's IpcTaskRunner on each.
///
/// `start_ipc` is `#[tokio::main(flavor = "current_thread")]` — it builds
/// its own runtime internally — so this is callable from sync context.
pub fn run_blocking() {
    trace("run_blocking entered");
    let cm = ConnectionManager {
        ui_handler: HeadlessCm::default(),
    };
    // Returns only on listener error (e.g. another --cm already holds the
    // pipe) or process shutdown. Either way there's nothing to do after.
    ui_cm_interface::start_ipc(cm);
    trace("start_ipc returned");
}

/// `InvokeUiCM` adapter for hello-agent. Stateless except for a small map
/// of `(connection id) -> (peer_id, name)` we keep so we can render a
/// "session ended" notification at remove time (the `Client` is dropped
/// from upstream's `CLIENTS` registry before our `remove_connection` hook
/// is called, so we can't fish the peer info out of there).
#[derive(Clone, Default)]
struct HeadlessCm {
    /// Tracks peers we approved. Connections that the user denied are not
    /// inserted here, so they don't trigger a "session ended" banner the
    /// user has no context for.
    approved: Arc<Mutex<HashMap<i32, (String, String)>>>,
}

impl InvokeUiCM for HeadlessCm {
    /// Called by upstream's IPC loop the moment a peer's Login frame is
    /// received and the client has been registered in the global CLIENTS
    /// map. We must NOT block here — the same task that called us is
    /// also the one that pumps the `_cm` pipe, so blocking on a user
    /// click would prevent the IPC loop from ever delivering the
    /// `Data::Authorize` we send back.
    fn add_connection(&self, client: &Client) {
        trace(&format!(
            "add_connection: id={} peer_id={} name={} authorized={}",
            client.id, client.peer_id, client.name, client.authorized
        ));

        if client.authorized {
            // Already authorized (e.g. password-based auth). No popup,
            // but track so remove_connection can show "session ended".
            self.approved
                .lock()
                .unwrap()
                .insert(client.id, (client.peer_id.clone(), client.name.clone()));
            return;
        }

        // Render the approval MessageBox on a fresh OS thread so the IPC
        // task that called us stays responsive. On Yes we register the
        // peer in `approved` and call `authorize(id)` which sends
        // `Data::Authorize` back to `--server`; on No we call `close(id)`
        // which sends `Data::Close` and the server tears the session down.
        let id = client.id;
        let peer_id = client.peer_id.clone();
        let name = client.name.clone();
        let approved_map = self.approved.clone();
        std::thread::spawn(move || {
            let approved = show_messagebox(&peer_id, &name);
            trace(&format!("add_connection: MessageBox approved={approved}"));
            if approved {
                approved_map
                    .lock()
                    .unwrap()
                    .insert(id, (peer_id, name));
                ui_cm_interface::authorize(id);
            } else {
                ui_cm_interface::close(id);
            }
        });
    }

    fn remove_connection(&self, id: i32, _close: bool) {
        trace(&format!("remove_connection: id={id}"));
        let entry = self.approved.lock().unwrap().remove(&id);
        if let Some((peer_id, name)) = entry {
            std::thread::spawn(move || show_session_ended(&peer_id, &name));
        }
    }

    // The remaining InvokeUiCM hooks fire for chat / theme / voice-call /
    // file-transfer-log / privacy-mode-elevation events. Hello-agent
    // doesn't surface any of them — file transfers complete silently in
    // the background (the supporter's UI shows progress on their end),
    // chat is unsupported, voice call is unsupported. Stubs only.
    fn new_message(&self, _id: i32, _text: String) {}
    fn change_theme(&self, _dark: String) {}
    fn change_language(&self) {}
    fn show_elevation(&self, _show: bool) {}
    fn update_voice_call_state(&self, _client: &Client) {}
    fn file_transfer_log(&self, action: &str, log: &str) {
        // Useful breadcrumb for debugging file-transfer failures, gated
        // behind trace() to keep production stderr quiet.
        trace(&format!("file_transfer_log: action={action} log={log}"));
    }
}

#[cfg(target_os = "windows")]
fn show_session_ended(peer_id: &str, name: &str) {
    use std::ffi::OsStr;
    use winapi::um::winuser::{MB_ICONINFORMATION, MB_OK};

    let display_name = if name.is_empty() { "Unknown" } else { name };
    let body = format!(
        "{display_name} ({peer_id}) has ended the remote support session.\n\nThe supporter is no longer connected."
    );
    let caption = "HelloAgent — Remote session ended";

    let body_w: Vec<u16> = OsStr::new(&body).encode_wide().chain(Some(0)).collect();
    let caption_w: Vec<u16> = OsStr::new(caption).encode_wide().chain(Some(0)).collect();
    let style = MB_OK | MB_ICONINFORMATION;

    // Same dual-path rendering as the approval popup: SYSTEM-token
    // callers route through `WTSSendMessageW` to land on the user's
    // interactive desktop, user-token callers go straight to MessageBoxW.
    let res = if librustdesk::platform::is_root() {
        match wts_send_message(&caption_w, &body_w, style) {
            Ok(r) => Some(r),
            Err(e) => {
                trace(&format!(
                    "show_session_ended: WTSSendMessageW failed ({e}); falling back to MessageBoxW"
                ));
                messagebox_w(&caption_w, &body_w, style)
            }
        }
    } else {
        messagebox_w(&caption_w, &body_w, style)
    };
    trace(&format!("show_session_ended: dialog returned {res:?}"));
}

#[cfg(not(target_os = "windows"))]
fn show_session_ended(_peer_id: &str, _name: &str) {}

#[cfg(target_os = "windows")]
fn show_messagebox(peer_id: &str, name: &str) -> bool {
    use std::ffi::OsStr;
    use winapi::um::winuser::{IDYES, MB_DEFBUTTON2, MB_ICONQUESTION, MB_YESNO};

    let display_name = if name.is_empty() { "Unknown" } else { name };
    let body = format!(
        "{display_name} ({peer_id}) is requesting remote control of this computer.\n\nAllow?"
    );
    let caption = "HelloAgent — Allow remote support?";

    // Pick the right rendering path. When the worker runs under the SYSTEM
    // token (the service-launched case), a direct MessageBoxW call usually
    // *does* succeed but draws on a desktop the logged-in user can't see —
    // the call returns IDNO/IDCANCEL with no user input. WTSSendMessageW is
    // the supported way for a SYSTEM caller to ask the *interactive* user
    // a question: Windows itself renders the dialog on the user's session's
    // active input desktop and ferries the click result back.
    //
    // For standalone (user-context) runs we keep the simple MessageBoxW
    // path — the calling thread already owns the right desktop.
    let body_w: Vec<u16> = OsStr::new(&body).encode_wide().chain(Some(0)).collect();
    let caption_w: Vec<u16> = OsStr::new(caption).encode_wide().chain(Some(0)).collect();
    let style = MB_YESNO | MB_ICONQUESTION | MB_DEFBUTTON2;

    let response: Option<i32> = if librustdesk::platform::is_root() {
        match wts_send_message(&caption_w, &body_w, style) {
            Ok(r) => Some(r),
            Err(e) => {
                trace(&format!(
                    "show_messagebox: WTSSendMessageW failed ({e}); falling back to MessageBoxW"
                ));
                messagebox_w(&caption_w, &body_w, style)
            }
        }
    } else {
        messagebox_w(&caption_w, &body_w, style)
    };

    response.map(|r| r == IDYES).unwrap_or(false)
}

#[cfg(target_os = "windows")]
fn messagebox_w(caption_w: &[u16], body_w: &[u16], style: u32) -> Option<i32> {
    use winapi::um::winuser::{MessageBoxW, MB_SETFOREGROUND, MB_SYSTEMMODAL, MB_TOPMOST};
    let flags = style | MB_TOPMOST | MB_SETFOREGROUND | MB_SYSTEMMODAL;
    trace("show_messagebox: calling MessageBoxW (user-context path)");
    let result = unsafe {
        MessageBoxW(
            std::ptr::null_mut(),
            body_w.as_ptr(),
            caption_w.as_ptr(),
            flags,
        )
    };
    trace(&format!("show_messagebox: MessageBoxW returned {result}"));
    Some(result)
}

/// `WTSSendMessageW` from `wtsapi32.dll`. Not exposed by `winapi 0.3`, so we
/// declare it manually. The link to `WtsApi32.lib` comes from the vendored
/// rustdesk `build.rs` (`cargo:rustc-link-lib=WtsApi32`), which is already
/// linked into our final binary because we depend on `librustdesk`.
#[cfg(target_os = "windows")]
fn wts_send_message(
    caption_w: &[u16],
    body_w: &[u16],
    style: u32,
) -> std::result::Result<i32, String> {
    use winapi::shared::ntdef::HANDLE;
    use winapi::um::winbase::WTSGetActiveConsoleSessionId;

    extern "system" {
        fn WTSSendMessageW(
            h_server: HANDLE,
            session_id: u32,
            p_title: *const u16,
            title_length: u32,
            p_message: *const u16,
            message_length: u32,
            style: u32,
            timeout: u32,
            p_response: *mut u32,
            b_wait: i32,
        ) -> i32;
    }

    // WTS_CURRENT_SERVER_HANDLE is `(HANDLE)NULL` per the SDK header.
    const WTS_CURRENT_SERVER_HANDLE: HANDLE = std::ptr::null_mut();

    let session_id = unsafe { WTSGetActiveConsoleSessionId() };
    if session_id == 0xFFFF_FFFF {
        return Err("no active console session (lock screen?)".into());
    }
    trace(&format!(
        "show_messagebox: calling WTSSendMessageW (session {session_id})"
    ));

    // Lengths are in BYTES (despite the wide-char strings). Subtract the
    // trailing null terminator we appended.
    let title_bytes = ((caption_w.len().saturating_sub(1)) * 2) as u32;
    let body_bytes = ((body_w.len().saturating_sub(1)) * 2) as u32;

    let mut response: u32 = 0;
    let ok = unsafe {
        WTSSendMessageW(
            WTS_CURRENT_SERVER_HANDLE,
            session_id,
            caption_w.as_ptr() as *const u16,
            title_bytes,
            body_w.as_ptr() as *const u16,
            body_bytes,
            style,
            0,    // timeout=0 → no timeout (block until user responds)
            &mut response,
            1,    // bWait=TRUE → block until response
        )
    };
    if ok == 0 {
        let err = std::io::Error::last_os_error();
        return Err(format!("WTSSendMessageW returned 0 (GetLastError: {err})"));
    }
    trace(&format!(
        "show_messagebox: WTSSendMessageW returned response={response}"
    ));
    Ok(response as i32)
}

#[cfg(not(target_os = "windows"))]
fn show_messagebox(_peer_id: &str, _name: &str) -> bool {
    // Non-Windows is a stub. The whole module is only wired in when
    // cfg(windows), so this branch should be unreachable in practice.
    false
}
