// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

// spell-checker:ignore catchable forwardable

//! Windows implementation of `timeout`'s platform facade, built on the signal
//! emulation in [`uucore::process`] (which maps POSIX signal numbers to
//! Windows primitives). Non-foreground mode spawns the child in a new console
//! process group tracked by a Job Object, so a lethal signal kills the whole
//! tree and `INT`/`QUIT` arrive as a catchable `CTRL_BREAK_EVENT`.

use std::io;
use std::process::Child;

use uucore::process::{
    Job, configure_process_group, enable_ctrl_forwarding, send_signal_to_console_group,
    send_signal_to_process, send_signal_to_tree,
};

const SIGNAL_INT: usize = 2;
const SIGNAL_QUIT: usize = 3;

/// Per-spawn platform state, threaded through [`send_signal`]: the job object
/// tracking the child's process tree (non-foreground mode only; also `None`
/// when job assignment failed and we degraded to per-process signalling).
/// Stack ownership means repeated in-process invocations of `uumain`
/// (benchmarks, fuzzing) can never signal a previous run's job.
pub(crate) struct SpawnState {
    job: Option<Job>,
}

/// Configure the child's spawn attributes and console-event forwarding, right
/// before the child is spawned.
pub(crate) fn prepare(
    cmd_builder: &mut std::process::Command,
    foreground: bool,
    _signal: usize,
) -> io::Result<()> {
    // Record Ctrl-C/Ctrl-Break/console-close as forwardable signal numbers
    // instead of dying to them (the unix code blocks a signal set here).
    enable_ctrl_forwarding()?;

    if !foreground {
        // The analog of unix setpgid(0, 0): the child leads its own console
        // group, so the console's Ctrl-C no longer reaches it directly and
        // CTRL_BREAK can target exactly its group.
        configure_process_group(cmd_builder);
    }
    Ok(())
}

/// Put the freshly spawned child in a job so a lethal signal can terminate
/// its entire process tree. Degrades silently to per-process signalling when
/// jobs are unavailable (mirroring the ignored `setpgid` result on unix).
///
/// A child that spawns a grandchild before the assignment completes escapes
/// the job; the window is sub-millisecond and unix has the analogous escape
/// (a child can leave the process group via setpgid).
pub(crate) fn post_spawn(child: &Child, foreground: bool) -> SpawnState {
    let job = if foreground {
        None
    } else {
        Job::new().ok().filter(|job| job.assign(child).is_ok())
    };
    SpawnState { job }
}

/// `external` is the console event that interrupted the wait, when this send
/// forwards that event rather than a timeout expiry.
pub(crate) fn send_signal(
    process: &mut Child,
    signal: usize,
    foreground: bool,
    external: Option<usize>,
    state: &SpawnState,
) {
    // GNU timeout ignores signal-send errors (the child may have just exited),
    // hence the discarded results below.
    if foreground {
        if matches!(signal, SIGNAL_INT | SIGNAL_QUIT) && external == Some(signal) {
            // A foreground child shares our console group: the console already
            // delivered this externally received event to it, and re-sending
            // cannot be targeted without hitting ourselves.
            return;
        }
        let _ = send_signal_to_process(process, signal);
    } else if matches!(signal, SIGNAL_INT | SIGNAL_QUIT) {
        // Deliverable as a catchable CTRL_BREAK to the child's group; without
        // a console, fall back to termination so the signal is never lost.
        if send_signal_to_console_group(process.id(), signal).is_err() {
            let _ = send_signal_to_tree(process, state.job.as_ref(), signal);
        }
    } else {
        let _ = send_signal_to_tree(process, state.job.as_ref(), signal);
    }
}

/// Windows exit statuses carry no signal information: terminated children
/// report their forced `128 + signal` value through `status.code()` instead.
pub(crate) fn status_signal(_status: std::process::ExitStatus) -> Option<i32> {
    None
}

/// No two-channel exit status exists on Windows; the exit code already
/// carries the signal information (`128 + signal`), so there is nothing to
/// preserve by re-signalling ourselves like the unix implementation does.
pub(crate) fn preserve_signal_info(signal: i32) -> i32 {
    signal
}
