#![deny(missing_docs)]
#![deny(clippy::missing_docs_in_private_items)]
// SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
// SPDX-License-Identifier: BSD-2-Clause
//! Limit a child process's absolute execution time.

#![doc(html_root_url = "https://docs.rs/timelimit-cli/1.9.5")]

use core::result::Result as CoreResult;

use std::process::{ExitCode as StdExitCode, Termination};

use anyhow::{Context as _, anyhow};
use clap::Parser as _;
use clap_derive::Parser;
use itertools::Itertools as _;
use log::{LevelFilter, warn};
use roundlet::cli_basic;
use simple_logger::SimpleLogger;
use sysexits::ExitCode;

use timelimit::exitcode::{ExitCodeReport, WrappedReport};
use timelimit::{Config, Signal};

/// The top-level program result.
#[derive(Debug)]
enum MainResult {
    /// Everything is fine.
    Ok,

    /// Something is wrong.
    Err(ExitCodeReport),
}

impl Termination for MainResult {
    #[inline]
    #[expect(clippy::print_stderr, reason = "this is the whole point")]
    #[expect(clippy::use_debug, reason = "diagnostic aid")]
    fn report(self) -> StdExitCode {
        match self {
            Self::Ok => StdExitCode::SUCCESS,
            Self::Err(err) => {
                match *err.report() {
                    WrappedReport::Exact(_) | WrappedReport::Signal(_) => (),
                    WrappedReport::Anyhow(ref anyport) => eprintln!("{anyport:#}"),
                    ref other => eprintln!("Unexpected wrapped error report: {other:?}"),
                }
                err.exitcode().into()
            }
        }
    }
}

impl<E: Into<ExitCodeReport>> From<CoreResult<(), E>> for MainResult {
    #[inline]
    fn from(res: CoreResult<(), E>) -> Self {
        match res {
            Ok(()) => Self::Ok,
            Err(err) => Self::Err(err.into()),
        }
    }
}

#[derive(Debug, Parser)]
struct Timelimit {
    /// List the supported signal names.
    #[clap(short = 'l')]
    list: bool,

    /// Propagate the signal the child process was terminated by.
    #[clap(short = 'p')]
    propagate: bool,

    /// Only display warning and error messages, no diagnostic or informational ones.
    #[clap(short = 'q')]
    quiet: bool,

    /// The kill signal to send.
    #[clap(short = 'S', default_value = "KILL")]
    killsig: Signal,

    /// The warning signal to send.
    #[clap(short = 's', default_value = "TERM")]
    warnsig: Signal,

    /// The time to wait before sending the kill signal after the warning signal has been sent.
    #[clap(short = 'T', default_value = "120")]
    killtime: f32,

    /// The time to wait before sending the warning signal.
    #[clap(short = 't', default_value = "3600")]
    warntime: f32,

    /// The program to run.
    #[clap(num_args = 1.., trailing_var_arg = true)]
    command: Vec<String>,
}

#[expect(clippy::print_stdout, reason = "this is the whole point")]
fn list_signals() {
    println!("{sigs}", sigs = timelimit::list_signals().join(" "));
}

fn real_main() -> Result<(), ExitCodeReport> {
    if cli_basic::handle_basic_options(
        "timelimit",
        env!("CARGO_PKG_VERSION"),
        timelimit::FEATURES.into_iter(),
        "Usage:	timelimit [-pq] [-S killsig] [-s warnsig] [-T killtime] [-t warntime] command
	timelimit -l
	timelimit --help | -h | --version | -V | --features
",
    ) {
        return Ok(());
    }

    let args = Timelimit::try_parse()
        .context("Could not parse the command-line arguments")
        .map_err(|err| (ExitCode::Usage, err))?;

    if args.list {
        list_signals();
        return Ok(());
    }
    if args.command.is_empty() {
        return Err((ExitCode::Usage, anyhow!("No command to run specified")).into());
    }

    let cfg = Config::builder()
        .warntime(args.warntime)
        .warnsig(args.warnsig)
        .killtime(args.killtime)
        .killsig(args.killsig)
        .build();

    SimpleLogger::new()
        .with_level(if args.quiet {
            LevelFilter::Warn
        } else {
            LevelFilter::Debug
        })
        .init()
        .context("Could not initialize the logging subsystem")
        .map_err(|err| ExitCodeReport::from((ExitCode::OsErr, err)))?;

    let res = smol::block_on(timelimit::limit(cfg, &args.command));
    if args.propagate
        && let WrappedReport::Signal(sig) = *res.report()
        && let Err(err) = sig.raise()
    {
        warn!("Could not raise a signal within the timelimit process: {err}");
    }
    if res.exitcode() == 0_u8 {
        Ok(())
    } else {
        Err(res)
    }
}

fn main() -> MainResult {
    real_main().into()
}
