//! Named pipe helpers for the elevated Windows sandbox runner.
//!
//! This module generates paired pipe names, creates server‑side pipes with
//! sandbox-user-scoped ACLs, and waits for the runner to connect. It is
//! **elevated-path only** and is
//! used by the parent to establish the IPC channel for both unified_exec sessions
//! and elevated capture. The legacy restricted‑token path spawns the child directly
//! and does not use these helpers.

use crate::helper_materialization::HelperExecutable;
use crate::helper_materialization::resolve_helper_for_launch;
use crate::winutil::resolve_sid;
use crate::winutil::string_from_sid_bytes;
use crate::winutil::to_wide;
use rand::Rng;
use rand::SeedableRng;
use rand::rngs::SmallRng;
use std::io;
use std::path::Path;
use std::path::PathBuf;
use std::ptr;
use windows_sys::Win32::Foundation::GetLastError;
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Foundation::HLOCAL;
use windows_sys::Win32::Foundation::LocalFree;
use windows_sys::Win32::Security::Authorization::ConvertStringSecurityDescriptorToSecurityDescriptorW;
use windows_sys::Win32::Security::PSECURITY_DESCRIPTOR;
use windows_sys::Win32::Security::SECURITY_ATTRIBUTES;
use windows_sys::Win32::System::Pipes::ConnectNamedPipe;
use windows_sys::Win32::System::Pipes::CreateNamedPipeW;
use windows_sys::Win32::System::Pipes::GetNamedPipeClientProcessId;
use windows_sys::Win32::System::Pipes::PIPE_READMODE_BYTE;
use windows_sys::Win32::System::Pipes::PIPE_TYPE_BYTE;
use windows_sys::Win32::System::Pipes::PIPE_WAIT;

/// PIPE_ACCESS_INBOUND (win32 constant), not exposed in windows-sys 0.52.
pub const PIPE_ACCESS_INBOUND: u32 = 0x0000_0001;
/// PIPE_ACCESS_OUTBOUND (win32 constant), not exposed in windows-sys 0.52.
pub const PIPE_ACCESS_OUTBOUND: u32 = 0x0000_0002;

/// Resolves the elevated command runner path, preferring the copied helper under
/// `.sandbox-bin` and falling back to the legacy sibling lookup when needed.
pub fn find_runner_exe(codex_home: &Path, log_dir: Option<&Path>) -> PathBuf {
    resolve_helper_for_launch(HelperExecutable::CommandRunner, codex_home, log_dir)
}

/// Generates a unique named-pipe path used to communicate with the runner process.
pub fn pipe_pair() -> (String, String) {
    let mut rng = SmallRng::from_entropy();
    let nonce: u128 = rng.r#gen();
    let base = format!(r"\\.\pipe\codex-runner-{nonce:x}");
    (format!("{base}-in"), format!("{base}-out"))
}

/// Creates a named pipe whose DACL only allows the sandbox user to connect.
pub fn create_named_pipe(name: &str, access: u32, sandbox_username: &str) -> io::Result<HANDLE> {
    let sandbox_sid = resolve_sid(sandbox_username)
        .map_err(|err| io::Error::new(io::ErrorKind::PermissionDenied, err.to_string()))?;
    let sandbox_sid = string_from_sid_bytes(&sandbox_sid)
        .map_err(|err| io::Error::new(io::ErrorKind::PermissionDenied, err))?;
    let sddl = to_wide(format!("D:(A;;GA;;;{sandbox_sid})"));
    let mut sd: PSECURITY_DESCRIPTOR = ptr::null_mut();
    let ok = unsafe {
        ConvertStringSecurityDescriptorToSecurityDescriptorW(
            sddl.as_ptr(),
            1, // SDDL_REVISION_1
            &mut sd,
            ptr::null_mut(),
        )
    };
    if ok == 0 {
        return Err(io::Error::from_raw_os_error(unsafe {
            GetLastError() as i32
        }));
    }
    let mut sa = SECURITY_ATTRIBUTES {
        nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
        lpSecurityDescriptor: sd,
        bInheritHandle: 0,
    };
    let wide = to_wide(name);
    let h = unsafe {
        CreateNamedPipeW(
            wide.as_ptr(),
            access,
            PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
            1,
            65536,
            65536,
            0,
            &mut sa as *mut SECURITY_ATTRIBUTES,
        )
    };
    unsafe {
        LocalFree(sd as HLOCAL);
    }
    if h == 0 || h == windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE {
        return Err(io::Error::from_raw_os_error(unsafe {
            GetLastError() as i32
        }));
    }
    Ok(h)
}

/// Waits for the runner to connect to a parent-created server pipe.
///
/// This is parent-side only: the runner opens the pipe with `CreateFileW`, while the
/// parent calls `ConnectNamedPipe`, tolerates the already-connected case, and
/// verifies that the connected client is the runner process we just spawned.
pub fn connect_pipe(h: HANDLE, expected_runner_pid: u32) -> io::Result<()> {
    let ok = unsafe { ConnectNamedPipe(h, ptr::null_mut()) };
    if ok == 0 {
        let err = unsafe { GetLastError() };
        const ERROR_PIPE_CONNECTED: u32 = 535;
        if err != ERROR_PIPE_CONNECTED {
            return Err(io::Error::from_raw_os_error(err as i32));
        }
    }
    let mut client_pid = 0;
    let ok = unsafe { GetNamedPipeClientProcessId(h, &mut client_pid) };
    if ok == 0 {
        return Err(io::Error::from_raw_os_error(unsafe {
            GetLastError() as i32
        }));
    }
    if client_pid != expected_runner_pid {
        return Err(io::Error::new(
            io::ErrorKind::PermissionDenied,
            format!(
                "named pipe client pid {client_pid} did not match runner pid {expected_runner_pid}"
            ),
        ));
    }
    Ok(())
}
