use crate::event::{enqueue_signal, is_signal_observed};
use crate::prelude::*;
use crate::reader::{signal_safe_reader_handle_sigint, signal_safe_reader_set_exit_signal};
use crate::termsize::signal_safe_termsize_invalidate_tty;
use crate::topic_monitor::{Generation, GenerationsList, Topic, topic_monitor_principal};
use crate::tty_handoff::signal_safe_mark_tty_invalid;
use crate::wutil::fish_wcstoi;
use errno::{errno, set_errno};
use fish_common::exit_without_destructors;
use fish_util::perror;
use nix::sys::signal::kill;
use nix::{
    sys::signal::{SaFlags, SigAction, SigHandler, SigSet, SigmaskHow, sigprocmask},
    unistd::getpid,
};
use std::{
    mem::MaybeUninit,
    num::NonZeroI32,
    sync::{
        LazyLock,
        atomic::{AtomicI32, Ordering},
    },
};

/// Store the "main" pid. This allows us to reliably determine if we are in a forked child.
static MAIN_PID: AtomicI32 = AtomicI32::new(0);

/// It's possible that we receive a signal after we have forked, but before we have reset the signal
/// handlers (or even run the pthread_atfork calls). In that event we will do something dumb like
/// swallow SIGINT. Ensure that doesn't happen. Check if we are the main fish process; if not, reset
/// and re-raise the signal. Return whether we re-raised the signal.
fn reraise_if_forked_child(sig: i32) -> bool {
    // Don't use is_forked_child: it relies on atfork handlers which may have not yet run.
    if getpid().as_raw() == MAIN_PID.load(Ordering::Relaxed) {
        return false;
    }

    // Safety: signal() and raise() are async-signal-safe.
    unsafe {
        libc::signal(sig, libc::SIG_DFL);
        libc::raise(sig);
    }
    true
}

/// The cancellation signal we have received.
/// Of course this is modified from a signal handler.
static CANCELLATION_SIGNAL: AtomicI32 = AtomicI32::new(0);

/// Set the cancellation signal to zero.
/// In generally this should only be done in interactive sessions.
pub fn signal_clear_cancel() {
    CANCELLATION_SIGNAL.store(0, Ordering::Relaxed);
}

/// Return the most recent cancellation signal received by the fish process.
/// Currently only SIGINT is considered a cancellation signal.
/// This is thread safe.
pub fn signal_check_cancel() -> i32 {
    CANCELLATION_SIGNAL.load(Ordering::Relaxed)
}

/// The single signal handler. By centralizing signal handling we ensure that we can never install
/// the "wrong" signal handler (see #5969).
extern "C" fn fish_signal_handler(
    sig: i32,
    _info: *mut libc::siginfo_t,
    _context: *mut libc::c_void,
) {
    // Ensure we preserve errno.
    let saved_errno = errno();

    // Check if we are a forked child.
    if reraise_if_forked_child(sig) {
        set_errno(saved_errno);
        return;
    }

    // Check if fish script cares about this.
    let observed = is_signal_observed(sig);
    if observed {
        enqueue_signal(sig);
    }

    // Do some signal-specific stuff.
    match sig {
        libc::SIGWINCH => {
            // Respond to a winch signal by telling the termsize container.
            signal_safe_termsize_invalidate_tty();
        }
        libc::SIGHUP | libc::SIGTERM => {
            // Exit unless the signal was trapped.
            if !observed {
                signal_safe_reader_set_exit_signal(sig);
                if sig == libc::SIGHUP {
                    signal_safe_mark_tty_invalid();
                }
            }
            topic_monitor_principal().post(Topic::SigHupIntTerm);
        }
        libc::SIGINT => {
            // Cancel unless the signal was trapped.
            if !observed {
                CANCELLATION_SIGNAL.store(libc::SIGINT, Ordering::Relaxed);
            }
            signal_safe_reader_handle_sigint();
            topic_monitor_principal().post(Topic::SigHupIntTerm);
        }
        libc::SIGCHLD => {
            // A child process stopped or exited.
            topic_monitor_principal().post(Topic::SigChld);
        }
        libc::SIGALRM => {
            // We have a sigalarm handler that does nothing. This is used in the signal torture
            // test, to verify that we behave correctly when receiving lots of irrelevant signals.
        }
        _ => {}
    }

    set_errno(saved_errno);
}

/// Set all signal handlers to SIG_DFL.
/// This is called after fork - it should be async signal safe.
pub fn signal_reset_handlers() {
    let act = SigAction::new(SigHandler::SigDfl, SaFlags::empty(), SigSet::empty()).into();

    for data in SIGNAL_TABLE.iter() {
        if data.signal == libc::SIGHUP && sighup_disposition() == libc::SIG_IGN {
            continue;
        }
        unsafe {
            libc::sigaction(data.signal.code(), &act, std::ptr::null_mut());
        };
    }
}

// Wrapper around sigaction.
fn sigaction(sig: i32, act: &libc::sigaction, oact: *mut libc::sigaction) -> libc::c_int {
    // Note: historically many call sites have ignored return value of sigaction here.
    unsafe { libc::sigaction(sig, act, oact) }
}

fn sighup_disposition() -> libc::sighandler_t {
    let mut oact: libc::sigaction = unsafe { MaybeUninit::zeroed().assume_init() };
    unsafe { libc::sigaction(libc::SIGHUP, std::ptr::null_mut(), &mut oact) };
    oact.sa_sigaction
}

fn set_interactive_handlers() {
    let signal_handler: usize = fish_signal_handler as *const () as libc::sighandler_t;
    let mut act: libc::sigaction = unsafe { std::mem::zeroed() };
    act.sa_flags = 0;
    unsafe { libc::sigemptyset(&mut act.sa_mask) };

    let nullptr = std::ptr::null_mut();

    // Interactive mode. Ignore interactive signals.  We are a shell, we know what is best for
    // the user.
    act.sa_sigaction = libc::SIG_IGN;
    sigaction(libc::SIGTSTP, &act, nullptr);
    sigaction(libc::SIGTTOU, &act, nullptr);

    // We don't ignore SIGTTIN because we might send it to ourself.
    act.sa_sigaction = signal_handler;
    act.sa_flags = libc::SA_SIGINFO;
    sigaction(libc::SIGTTIN, &act, nullptr);

    // SIGTERM defers to allow graceful history save before exit.
    act.sa_sigaction = signal_handler;
    act.sa_flags = libc::SA_SIGINFO;
    sigaction(libc::SIGTERM, &act, nullptr);

    if sighup_disposition() == libc::SIG_DFL {
        act.sa_sigaction = signal_handler;
        act.sa_flags = libc::SA_SIGINFO;
        sigaction(libc::SIGHUP, &act, nullptr);
    }

    // SIGALARM as part of our signal torture test
    act.sa_sigaction = signal_handler;
    act.sa_flags = libc::SA_SIGINFO;
    sigaction(libc::SIGALRM, &act, nullptr);

    act.sa_sigaction = signal_handler;
    act.sa_flags = libc::SA_SIGINFO;
    sigaction(libc::SIGWINCH, &act, nullptr);
}

/// Set signal handlers to fish default handlers.
pub fn signal_set_handlers(interactive: bool) {
    // Mark our main pid.
    MAIN_PID.store(getpid().as_raw(), Ordering::Relaxed);

    use libc::SIG_IGN;
    let nullptr = std::ptr::null_mut();
    let mut act: libc::sigaction = unsafe { std::mem::zeroed() };

    act.sa_flags = 0;
    unsafe { libc::sigemptyset(&mut act.sa_mask) };

    // Ignore SIGPIPE. We'll detect failed writes and deal with them appropriately. We don't want
    // this signal interrupting other syscalls or terminating us.
    act.sa_sigaction = SIG_IGN;
    sigaction(libc::SIGPIPE, &act, nullptr);

    // Ignore SIGQUIT.
    act.sa_sigaction = SIG_IGN;
    sigaction(libc::SIGQUIT, &act, nullptr);

    // Apply our SIGINT handler.
    act.sa_sigaction = fish_signal_handler as *const () as libc::sighandler_t;
    act.sa_flags = libc::SA_SIGINFO;
    sigaction(libc::SIGINT, &act, nullptr);

    // Whether or not we're interactive we want SIGCHLD to not interrupt restartable syscalls.
    act.sa_sigaction = fish_signal_handler as *const () as libc::sighandler_t;
    act.sa_flags = libc::SA_SIGINFO | libc::SA_RESTART;
    if sigaction(libc::SIGCHLD, &act, nullptr) != 0 {
        perror("sigaction");
        exit_without_destructors(1);
    }

    if interactive {
        set_interactive_handlers();
    }

    if cfg!(feature = "tsan") {
        // Work around the following TSAN bug:
        // The structure containing signal information for a thread is lazily allocated by TSAN.
        // It is possible for the same thread to receive two allocations, if the signal handler
        // races with other allocation paths (e.g. a blocking call). This results in the first signal
        // being potentially dropped.
        // The workaround is to send ourselves a SIGCHLD signal now, to force the allocation to happen.
        // As no child is associated with this signal, it is OK if it is dropped, so long as the
        // allocation happens.
        let _ = kill(getpid(), nix::sys::signal::Signal::SIGCHLD);
    }
}

pub fn signal_set_handlers_once(interactive: bool) {
    static NONINTER_ONCE: std::sync::Once = std::sync::Once::new();
    NONINTER_ONCE.call_once(|| signal_set_handlers(false));

    if interactive {
        static INTER_ONCE: std::sync::Once = std::sync::Once::new();
        INTER_ONCE.call_once(set_interactive_handlers);
    }
}

/// Mark that a signal is being handled.
pub fn signal_handle(sig: RawSignal) {
    let sig = sig.code();
    let mut act: libc::sigaction = unsafe { std::mem::zeroed() };

    // These should always be handled.
    if sig == libc::SIGINT
        || sig == libc::SIGQUIT
        || sig == libc::SIGTSTP
        || sig == libc::SIGTTIN
        || sig == libc::SIGTTOU
        || sig == libc::SIGCHLD
    {
        return;
    }

    act.sa_flags = 0;
    unsafe { libc::sigemptyset(&mut act.sa_mask) };
    act.sa_flags = libc::SA_SIGINFO;
    act.sa_sigaction = fish_signal_handler as *const () as libc::sighandler_t;
    sigaction(sig, &act, std::ptr::null_mut());
}

pub static SIGNALS_TO_DEFAULT: LazyLock<libc::sigset_t> = LazyLock::new(|| {
    let mut set = MaybeUninit::uninit();
    unsafe { libc::sigemptyset(set.as_mut_ptr()) };
    for data in SIGNAL_TABLE.iter() {
        // If SIGHUP is being ignored (e.g., because were were run via `nohup`) don't reset it.
        // We don't special case other signals because if they're being ignored that shouldn't
        // affect processes we spawn. They should get the default behavior for those signals.
        if data.signal == libc::SIGHUP && sighup_disposition() == libc::SIG_IGN {
            continue;
        }
        unsafe { libc::sigaddset(set.as_mut_ptr(), data.signal.code()) };
    }
    unsafe { set.assume_init() }
});

/// Ensure we did not inherit any blocked signals. See issue #3964.
pub fn signal_unblock_all() {
    sigprocmask(SigmaskHow::SIG_SETMASK, Some(&SigSet::empty()), None).unwrap();
}

/// A Sigchecker can be used to check if a SIGINT (or SIGHUP) has been delivered.
pub struct SigChecker {
    topic: Topic,
    r#gen: Generation,
}

impl SigChecker {
    /// Create a new checker for the given topic.
    pub fn new(topic: Topic) -> Self {
        let mut res = SigChecker { topic, r#gen: 0 };
        // Call check() to update our generation.
        res.check();
        res
    }

    /// Create a new checker for SIGHUP and SIGINT.
    pub fn new_sighupintterm() -> Self {
        Self::new(Topic::SigHupIntTerm)
    }

    /// Check if a sigint has been delivered since the last call to check(), or since the detector
    /// was created.
    pub fn check(&mut self) -> bool {
        let tm = topic_monitor_principal();
        let r#gen = tm.generation_for_topic(self.topic);
        let changed = self.r#gen != r#gen;
        self.r#gen = r#gen;
        changed
    }

    /// Wait until a sigint is delivered.
    pub fn wait(&self) {
        let tm = topic_monitor_principal();
        let gens = GenerationsList::invalid();
        gens.set(self.topic, self.r#gen);
        tm.check(&gens, true /* wait */);
    }
}

/// Struct describing an entry for the lookup table used to convert between signal names and signal
/// ids, etc.
struct LookupEntry {
    signal: RawSignal,
    name: &'static wstr,
    desc: LocalizableString, // Note: this needs to be localized via gettext before presenting it to the user.
}

impl LookupEntry {
    const fn new(signal: i32, name: &'static wstr, desc: LocalizableString) -> Self {
        Self {
            signal: RawSignal::new(signal),
            name,
            desc,
        }
    }
}

macro_rules! signal_entry {
    ($name:ident, $desc:literal) => {
        LookupEntry::new(
            libc::$name,
            L!(stringify!($name)),
            localizable_string!($desc),
        )
    };
    ($name:ident, $desc:expr) => {
        LookupEntry::new(libc::$name, L!(stringify!($name)), $desc)
    };
}

// Lookup table used to convert between signal names and signal ids, etc.
#[rustfmt::skip]
const SIGNAL_TABLE : &[LookupEntry] = &[
    signal_entry!(SIGHUP, "Terminal hung up"),
    signal_entry!(SIGINT, "Quit request from job control (^C)"),
    signal_entry!(SIGQUIT, "Quit request from job control with core dump (^\\)"),
    signal_entry!(SIGILL, "Illegal instruction"),
    signal_entry!(SIGTRAP, "Trace or breakpoint trap"),
    signal_entry!(SIGABRT, "Abort"),
    signal_entry!(SIGBUS, "Misaligned address error"),
    signal_entry!(SIGFPE, "Floating point exception"),
    signal_entry!(SIGKILL, "Forced quit"),
    signal_entry!(SIGUSR1, "User defined signal 1"),
    signal_entry!(SIGUSR2, "User defined signal 2"),
    signal_entry!(SIGSEGV, "Address boundary error"),
    signal_entry!(SIGPIPE, "Broken pipe"),
    signal_entry!(SIGALRM, "Timer expired"),
    signal_entry!(SIGTERM, "Polite quit request"),
    signal_entry!(SIGCHLD, "Child process status changed"),
    signal_entry!(SIGCONT, "Continue previously stopped process"),
    signal_entry!(SIGSTOP, "Forced stop"),
    signal_entry!(SIGTSTP, "Stop request from job control (^Z)"),
    signal_entry!(SIGTTIN, "Stop from terminal input"),
    signal_entry!(SIGTTOU, "Stop from terminal output"),
    signal_entry!(SIGURG, "Urgent socket condition"),
    signal_entry!(SIGXCPU, "CPU time limit exceeded"),
    signal_entry!(SIGXFSZ, "File size limit exceeded"),
    signal_entry!(SIGVTALRM, "Virtual timer expired"),
    signal_entry!(SIGPROF, "Profiling timer expired"),
    signal_entry!(SIGWINCH, "Window size change"),
    signal_entry!(SIGIO, "I/O on asynchronous file descriptor is possible"),
    signal_entry!(SIGSYS, "Bad system call"),
    signal_entry!(SIGIOT, "Abort (Alias for SIGABRT)"),

    #[cfg(any(apple, bsd))]
    signal_entry!(SIGEMT, SIGEMT_DESC),

    #[cfg(any(apple, bsd))]
    signal_entry!(SIGINFO, SIGINFO_DESC),

    #[cfg(all(
        target_os = "linux",
        not(any(
            target_arch = "mips",
            target_arch = "mips32r6",
            target_arch = "mips64",
            target_arch = "mips64r6",
            target_arch = "sparc",
            target_arch = "sparc64",
        ))
    ))]
    signal_entry!(SIGSTKFLT, SIGSTKFLT_DESC),

    #[cfg(target_os = "linux")]
    signal_entry!(SIGPWR, SIGPWR_DESC),

    // TODO: determine whether SIGWIND is defined on any platform.
    //signal_entry!(SIGWIND, "Window size change"),
];

localizable_consts!(
    #[allow(dead_code)]
    SIGEMT_DESC "Unused signal"

    #[allow(dead_code)]
    SIGINFO_DESC "Information request"

    #[allow(dead_code)]
    SIGSTKFLT_DESC "Stack fault"

    #[allow(dead_code)]
    SIGPWR_DESC "Power failure"
);

// Return true if two strings are equal, ignoring ASCII case.
fn equals_ascii_icase(left: &wstr, right: &wstr) -> bool {
    if left.len() != right.len() {
        return false;
    }
    for (lc, rc) in left.chars().zip(right.chars()) {
        if !lc.eq_ignore_ascii_case(&rc) {
            return false;
        }
    }
    true
}

/// Test if \c name is a string describing the signal named \c canonical.
fn match_signal_name(canonical: &wstr, mut name: &wstr) -> bool {
    // Skip the "SIG" prefix if it exists.
    if name.char_count() >= 3 && equals_ascii_icase(name.slice_to(3), L!("sig")) {
        name = name.slice_from(3);
    }
    equals_ascii_icase(canonical.slice_from(3), name)
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
/// A wrapper around the system signal code.
pub struct RawSignal(NonZeroI32);

impl RawSignal {
    /// Creates a new `RawSignal` to represent the passed system signal code `sig`.
    /// Panics if `sig` is zero.
    pub const fn new(sig: i32) -> Self {
        match NonZeroI32::new(sig) {
            None => panic!("Invalid zero signal value!"),
            Some(result) => RawSignal(result),
        }
    }

    /// Return the LookupEntry for ourself.
    fn get_lookup_entry(&self) -> Option<&'static LookupEntry> {
        SIGNAL_TABLE
            .iter()
            .find(|entry| entry.signal == self.code())
    }

    /// Get string representation of a signal.
    /// Previously sig2wcs().
    pub fn name(&self) -> &'static wstr {
        match self.get_lookup_entry() {
            Some(entry) => entry.name,
            None => wgettext!("Unknown"),
        }
    }

    /// Returns a description of the specified signal.
    /// Previously signal_get_desc().
    pub fn desc(&self) -> &'static wstr {
        match self.get_lookup_entry() {
            Some(entry) => entry.desc.localize(),
            None => wgettext!("Unknown"),
        }
    }

    pub fn code(&self) -> i32 {
        self.0.into()
    }
    /// Parses a string into the equivalent [`RawSignal`] sharing the same name.
    /// Accepts both `SIGABC` and `ABC` to match against `Signal::SIGABC`. If the signal name is not
    /// recognized, `None` is returned.
    /// This also accepts integer codes via fish_wcstoi().
    /// Previously sig2wcs().
    pub fn parse(name: &wstr) -> Option<RawSignal> {
        for entry in SIGNAL_TABLE.iter() {
            if match_signal_name(entry.name, name) {
                return Some(entry.signal);
            }
        }

        if let Ok(num) = fish_wcstoi(name) {
            if num > 0 {
                return Some(RawSignal::new(num));
            }
        }
        None
    }
}

// Allow signals to be compared against i32.
impl PartialEq<i32> for RawSignal {
    fn eq(&self, other: &i32) -> bool {
        self.code() == *other
    }
}

impl From<RawSignal> for i32 {
    fn from(value: RawSignal) -> Self {
        value.code()
    }
}

impl From<RawSignal> for usize {
    fn from(value: RawSignal) -> Self {
        usize::try_from(value.code()).unwrap()
    }
}

impl From<RawSignal> for NonZeroI32 {
    fn from(value: RawSignal) -> Self {
        value.0
    }
}

#[cfg(test)]
mod tests {
    use super::RawSignal;
    use crate::prelude::*;

    #[test]
    fn test_signal_name() {
        let sig = RawSignal::new(libc::SIGINT);
        assert_eq!(sig.name(), "SIGINT");
    }

    #[rustfmt::skip]
    #[test]
    fn test_signal_parse() {
        assert_eq!(RawSignal::parse(L!("SIGHUP")), Some(RawSignal::new(libc::SIGHUP)));
        assert_eq!(RawSignal::parse(L!("sigwinch")), Some(RawSignal::new(libc::SIGWINCH)));
        assert_eq!(RawSignal::parse(L!("TSTP")), Some(RawSignal::new(libc::SIGTSTP)));
        assert_eq!(RawSignal::parse(L!("TstP")), Some(RawSignal::new(libc::SIGTSTP)));
        assert_eq!(RawSignal::parse(L!("sigCONT")), Some(RawSignal::new(libc::SIGCONT)));
        assert_eq!(RawSignal::parse(L!("SIGFOO")), None);
        assert_eq!(RawSignal::parse(L!("")), None);
        assert_eq!(RawSignal::parse(L!("SIG")), None);
        assert_eq!(RawSignal::parse(L!("سلام")), None);

        assert_eq!(RawSignal::parse(&libc::SIGINT.to_wstring()), Some(RawSignal::new(libc::SIGINT)));
        assert_eq!(RawSignal::parse(L!("0")), None);
        assert_eq!(RawSignal::parse(L!("-0")), None);
        assert_eq!(RawSignal::parse(L!("-1")), None);
    }

    #[test]
    #[cfg(any(target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))]
    /// Verify bsd feature is detected on the known BSDs, which gives us greater confidence it'll work
    /// for the unknown ones too. We don't need to do this for Linux and macOS because we're using
    /// rust's native OS targeting for those.
    fn bsd_signals() {
        assert_eq!(
            RawSignal::parse(L!("SIGEMT")),
            Some(RawSignal::new(libc::SIGEMT))
        );
        assert_eq!(
            RawSignal::parse(L!("SIGINFO")),
            Some(RawSignal::new(libc::SIGINFO))
        );
    }
}
