// Approval popup, 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"])`:
//       ▼
//   --cm (user session, USER token) --- this module
//       │  binds `_cm`, accepts one connection from the parent's start_ipc,
//       │  reads frames until it sees Data::Login{authorized:false, …},
//       │  shows MessageBoxW (works cleanly because USER token + interactive
//       │  desktop), replies Data::Authorize / Data::Close, drains the
//       │  stream until the server closes it, exits.
//
// The previous design (run cm_popup as a thread inside the SYSTEM-token
// --server worker) hit Windows' UI-isolation rules — `MessageBoxW` from a
// SYSTEM-token process technically returns successfully but draws on a
// desktop the logged-in user can't see, so the popup was invisible.
// Spawning as a USER child sidesteps the whole class of issues.

use anyhow::Result;
use librustdesk::ipc;

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

const POSTFIX: &str = "_cm";

/// 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());
        }
    }
}

/// Run the popup loop forever on a freshly-created Tokio runtime.
/// Safe to call from a `std::thread::spawn` body.
pub fn run_blocking() {
    trace("run_blocking entered");

    let rt = match tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
    {
        Ok(rt) => rt,
        Err(e) => {
            trace(&format!("build runtime: {e}"));
            return;
        }
    };
    trace("runtime built; entering serve()");

    if let Err(e) = rt.block_on(serve()) {
        trace(&format!("serve exited: {e:#}"));
    } else {
        trace("serve returned cleanly");
    }
}

/// Bind `_cm`, accept connections from `--server`'s `start_ipc` for as
/// long as the user session lasts. Each connection corresponds to one
/// peer requesting approval; we handle them concurrently.
async fn serve() -> Result<()> {
    trace(&format!("calling new_listener({POSTFIX})"));
    let mut incoming = match ipc::new_listener(POSTFIX).await {
        Ok(i) => {
            trace("new_listener succeeded");
            i
        }
        Err(e) => {
            trace(&format!("new_listener failed: {e}"));
            return Err(anyhow::anyhow!("new_listener({POSTFIX}): {e}"));
        }
    };

    trace("entering accept loop");
    while let Some(result) = incoming.next().await {
        match result {
            Ok(stream) => {
                trace("accepted incoming connection");
                let conn = ipc::Connection::new(stream);
                tokio::spawn(async move {
                    if let Err(e) = handle_one(conn).await {
                        trace(&format!("handle_one error: {e:#}"));
                    }
                });
            }
            Err(e) => {
                trace(&format!("accept error: {e}"));
            }
        }
    }
    trace("accept loop exited");
    Ok(())
}

async fn handle_one(mut conn: ipc::Connection) -> Result<()> {
    // Frame ordering on the `_cm` pipe is NOT "Login first, then chatter".
    // For an installed/portable controlled side, the server first emits
    // `Data::DataPortableService(CmShowElevation(...))` so the Flutter CM
    // can render its elevation banner. The `Data::Login` we care about
    // arrives a moment later. We loop through frames, ignore everything
    // until we see Login{authorized:false}, decide once, and from then on
    // just drain the stream so the server's `tx_to_cm.send()` calls don't
    // back up.
    //
    // We use `conn.next()` (no timeout). A long active session can sit
    // quiet for tens of minutes — `tx_to_cm` only fires on Login, FS
    // transfers, and connection-close — so a short read timeout would
    // false-positive into "session ended" UX during normal use.
    trace("handle_one: entering frame loop");
    let mut decided = false;
    // Set when the user clicks Yes on the approval popup. Carries the
    // peer's id / name for the matching "session ended" notification we
    // fire after the server tears the connection down.
    let mut approved_peer: Option<(String, String)> = None;

    loop {
        match conn.next().await {
            Ok(Some(ipc::Data::Login {
                peer_id,
                name,
                authorized: false,
                ..
            })) if !decided => {
                trace(&format!(
                    "handle_one: Login peer_id={peer_id} name={name} authorized=false"
                ));
                decided = true;

                let approved = ask_user_blocking(&peer_id, &name).await;
                trace(&format!(
                    "handle_one: MessageBox returned approved={approved}"
                ));

                if approved {
                    let _ = conn.send(&ipc::Data::Authorize).await;
                    trace("handle_one: sent Authorize");
                    approved_peer = Some((peer_id, name));
                } else {
                    let _ = conn.send(&ipc::Data::Close).await;
                    trace("handle_one: sent Close — exiting handler");
                    return Ok(());
                }
            }
            Ok(Some(ipc::Data::Close)) | Ok(Some(ipc::Data::Disconnected)) => {
                // Server signals the supporter has left (or the
                // connection failed). Fall through to the post-loop
                // notification path.
                trace("handle_one: server sent Close/Disconnected");
                break;
            }
            Ok(Some(other)) => {
                // Pre-login chatter (CmShowElevation), or post-Authorize
                // chatter (chat, file transfer events, voice call). We
                // don't act on any of it — the Flutter CM would, we just
                // need to consume frames so the server's send buffer
                // drains.
                trace(&format!("handle_one: ignoring frame: {other:?}"));
                continue;
            }
            Ok(None) => {
                trace("handle_one: stream closed by peer");
                break;
            }
            Err(e) => {
                trace(&format!("handle_one: stream error: {e}"));
                break;
            }
        }
    }

    // Tell the user the supporter is gone. Only fires when we approved
    // the connection — denied/cancelled connections already returned
    // above, and pre-approval Close from the server (e.g., auth failure
    // before the popup even fired) shouldn't show a "session ended"
    // banner the user has no context for.
    if let Some((peer_id, name)) = approved_peer {
        notify_session_ended(&peer_id, &name).await;
    }
    trace("handle_one: returning");
    Ok(())
}

/// Show a native MessageBox in the calling (user) session. Runs the dialog
/// on tokio's blocking thread pool so we don't park the reactor while it
/// waits for the user to click.
async fn ask_user_blocking(peer_id: &str, name: &str) -> bool {
    let peer_id = peer_id.to_string();
    let name = name.to_string();
    tokio::task::spawn_blocking(move || show_messagebox(&peer_id, &name))
        .await
        .unwrap_or(false)
}

/// Inform the user that the remote support session has ended. Best-effort:
/// errors out of the OS dialog APIs are logged (via `trace`) and otherwise
/// ignored — failing to show the post-session banner shouldn't block the
/// handler from cleaning up.
async fn notify_session_ended(peer_id: &str, name: &str) {
    let peer_id = peer_id.to_string();
    let name = name.to_string();
    let _ = tokio::task::spawn_blocking(move || show_session_ended(&peer_id, &name)).await;
}

#[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
}
