use std::{
    cmp::Ordering,
    ffi::CString,
    hint::unreachable_unchecked,
    os::{
        fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd},
        unix::io::RawFd,
    },
    ptr,
};

use nix::{
    errno::Errno,
    libc::c_char,
    sys::{
        ptrace::{cont, seize, Options},
        signal::{kill, Signal},
        wait::{Id, WaitPidFlag},
    },
    unistd::{close, read, setpgid, tcsetpgrp, write, Pid},
};

use crate::{
    compat::{waitid, WaitStatus},
    config::CHLD_STACK_SIZE,
    fs::duprand,
    libseccomp::ScmpFilterContext,
    unshare::{child, config::Config, Child, Command, Executable},
};

type ChildPreExecFunc = Box<dyn Fn() -> Result<(), Errno>>;
type PipePair = ((OwnedFd, OwnedFd), (OwnedFd, OwnedFd));

#[derive(Debug)]
pub enum Exe<'a> {
    Library(&'a libloading::os::unix::Library),
    Program((*const c_char, Vec<*const c_char>)),
}

pub struct ChildInfo<'a> {
    pub exe: Exe<'a>,
    pub cfg: Config,
    pub pre_exec: Option<ChildPreExecFunc>,
    pub seccomp_filter: Option<ScmpFilterContext>,
    pub seccomp_pipefd: PipePair,
}

fn raw_with_null(arr: &Vec<CString>) -> Vec<*const c_char> {
    let mut vec = Vec::with_capacity(arr.len().saturating_add(1));
    for i in arr {
        vec.push(i.as_ptr());
    }
    vec.push(ptr::null());
    vec
}

impl Command {
    /// Spawn the command and return a handle that can be waited for
    pub fn spawn(mut self) -> Result<Child, Errno> {
        let exe = match self.exe {
            Executable::Library(ref lib) => Exe::Library(lib),
            Executable::Program((ref filename, ref args)) => {
                let c_args = raw_with_null(args);
                Exe::Program((filename.as_ptr(), c_args))
            }
        };

        let mut pid_fd: libc::c_int = -1;
        let clone_flags = libc::SIGCHLD | libc::CLONE_FILES | libc::CLONE_PIDFD;
        // SAFETY: CLONE_FILES:
        // Child owns the pipes and is responsible for closing them.
        let seccomp_pipefd = unsafe {
            (
                (
                    OwnedFd::from_raw_fd(self.seccomp_pipefd.0 .0),
                    OwnedFd::from_raw_fd(self.seccomp_pipefd.0 .1),
                ),
                (
                    OwnedFd::from_raw_fd(self.seccomp_pipefd.1 .0),
                    OwnedFd::from_raw_fd(self.seccomp_pipefd.1 .1),
                ),
            )
        };
        let child_info = Box::new(ChildInfo {
            exe,
            cfg: self.config,
            pre_exec: std::mem::take(&mut self.pre_exec),
            seccomp_filter: std::mem::take(&mut self.seccomp_filter),
            seccomp_pipefd,
        });
        let child_info_ptr: *mut libc::c_void = Box::into_raw(child_info) as *mut libc::c_void;
        // 2M stack by default, see config.rs.
        let mut stack = [0u8; CHLD_STACK_SIZE];

        // SAFETY: nix's clone does not support CLONE_PIDFD,
        // so we use libc::clone instead.
        let child = unsafe {
            let ptr = stack.as_mut_ptr().add(stack.len());
            let ptr_aligned = ptr.sub(ptr as usize % 16);
            libc::clone(
                child::child_after_clone as extern "C" fn(*mut libc::c_void) -> libc::c_int,
                ptr_aligned as *mut libc::c_void,
                clone_flags,
                child_info_ptr,
                &mut pid_fd,
            )
        };

        // SAFETY: Randomize the pid FD for hardening.
        // The created fd is O_CLOEXEC too, and we'll
        // send the number to the child to close it.
        let pid_fd_rand = duprand(pid_fd)?;
        let _ = close(pid_fd);

        // SAFETY: duprand returns a valid FD on success.
        let pid_fd = unsafe { BorrowedFd::borrow_raw(pid_fd_rand) };

        match child.cmp(&0) {
            Ordering::Less => {
                // SAFETY: Reconstruct and drop.
                let _ = unsafe { Box::from_raw(child_info_ptr as *mut ChildInfo) };
                Err(Errno::last())
            }
            Ordering::Greater => {
                let child = Pid::from_raw(child);

                let seccomp_fd = match self.after_start(child, &pid_fd) {
                    Ok(seccomp_fd) => seccomp_fd,
                    Err(e) => loop {
                        match waitid(Id::PIDFd(pid_fd.as_fd()), WaitPidFlag::WEXITED) {
                            Ok(WaitStatus::Exited(_, errno)) => return Err(Errno::from_raw(errno)),
                            Err(Errno::EINTR) => {}
                            _ => return Err(e),
                        }
                    },
                };

                Ok(Child {
                    pid: child.into(),
                    pid_fd: pid_fd.as_raw_fd(),
                    seccomp_fd,
                    status: None,
                })
            }
            // SAFETY: This can never happen because clone child
            // jumps to the specified function.
            _ => unsafe { unreachable_unchecked() },
        }
    }

    #[allow(clippy::cognitive_complexity)]
    fn after_start<F: AsRawFd>(mut self, pid: Pid, pid_fd: &F) -> Result<RawFd, Errno> {
        if self.config.stop {
            // Seize the process for tracing.
            // This must happen before reading the seccomp fd.
            // TODO: Make ptrace options configurable.
            let ptrace_options: Options = Options::PTRACE_O_TRACEFORK
                | Options::PTRACE_O_TRACEVFORK
                | Options::PTRACE_O_TRACECLONE
                | Options::PTRACE_O_TRACEEXEC     // used by Exec TOCTOU mitigator.
                | Options::PTRACE_O_TRACEEXIT     // used by SegvGuard.
                | Options::PTRACE_O_TRACESECCOMP  // used by chdir and exec hooks.
                | Options::PTRACE_O_TRACESYSGOOD  // ditto.
                | Options::PTRACE_O_EXITKILL; // we also set PDEATHSIG so this is the second layer.

            // SAFETY: Prefer to use the PIDFd rather than the PID
            // for waitid(2) calls. This gives us safety against
            // e.g. PID recycling and ensures a secure attach process.
            let pid_fd = unsafe { BorrowedFd::borrow_raw(pid_fd.as_raw_fd()) };
            // Step 1: Wait for the process to stop itself.
            // Note, we also wait for EXITED so that if the process is
            // interrupted, and the wait will fall through to the assert
            // to fail.
            let status = waitid(
                Id::PIDFd(pid_fd),
                WaitPidFlag::WEXITED | WaitPidFlag::WSTOPPED | WaitPidFlag::__WNOTHREAD,
            )?;
            assert_eq!(status, WaitStatus::Stopped(pid, libc::SIGSTOP));
            // Step 2: Seize the process.
            // We use PTRACE_SEIZE in the parent rather than
            // PTRACE_TRACEME in the child for its improved
            // behaviour/API.  This also gives us the chance to deny
            // PTRACE_TRACEME and further confine the sandbox against
            // e.g. trivial ptrace detectors.
            seize(pid, ptrace_options)?;
            let status = waitid(
                Id::PIDFd(pid_fd),
                WaitPidFlag::WEXITED | WaitPidFlag::WSTOPPED | WaitPidFlag::__WNOTHREAD,
            )?;
            assert_eq!(
                status,
                WaitStatus::PtraceEvent(pid, libc::SIGSTOP, libc::PTRACE_EVENT_STOP)
            );
            // SAFETY: nix does not have a wrapper for PTRACE_LISTEN.
            Errno::result(unsafe { libc::ptrace(libc::PTRACE_LISTEN, pid.as_raw(), 0, 0) })?;
            // Step 3: Successfully attached, resume the process.
            // We have to do a simple signal ping-pong here but
            // it's done once and it's worth the trouble.
            kill(pid, Signal::SIGCONT)?;
            let status = waitid(
                Id::PIDFd(pid_fd),
                WaitPidFlag::WEXITED | WaitPidFlag::WSTOPPED | WaitPidFlag::__WNOTHREAD,
            )?;
            assert_eq!(
                status,
                WaitStatus::PtraceEvent(pid, libc::SIGTRAP, libc::PTRACE_EVENT_STOP)
            );
            cont(pid, None)?;
            let status = waitid(
                Id::PIDFd(pid_fd),
                WaitPidFlag::WEXITED | WaitPidFlag::WSTOPPED | WaitPidFlag::__WNOTHREAD,
            )?;
            assert_eq!(status, WaitStatus::PtraceEvent(pid, libc::SIGCONT, 0));
            cont(pid, Some(Signal::SIGCONT))?;
        }

        if self.config.make_group_leader {
            setpgid(pid, pid)?;
            tcsetpgrp(std::io::stderr(), pid)?;
        }

        if let Some(ref mut callback) = self.before_unfreeze {
            #[allow(clippy::cast_sign_loss)]
            callback(i32::from(pid) as u32)?;
        }

        // SAFETY: CLONE_FILES:
        // Child owns the pipes and is responsible for closing them.
        let pipe_rw = unsafe { BorrowedFd::borrow_raw(self.seccomp_pipefd.0 .1) };
        let pipe_ro = self.seccomp_pipefd.1 .0;

        // Read the value of the file descriptor from the pipe.
        // Handle interrupts and partial reads.
        // EOF means process died before writing to the pipe.
        let mut buf = vec![0u8; std::mem::size_of::<RawFd>()];
        let mut nread = 0;
        while nread < buf.len() {
            #[allow(clippy::arithmetic_side_effects)]
            match read(pipe_ro.as_raw_fd(), &mut buf[nread..]) {
                Ok(0) => return Err(Errno::EIO),
                Ok(n) => nread += n,
                Err(Errno::EINTR | Errno::EAGAIN) => continue,
                Err(errno) => return Err(errno),
            }
        }
        let fd = match buf.as_slice().try_into() {
            Ok(bytes) => RawFd::from_le_bytes(bytes),
            Err(_) => return Err(Errno::EINVAL),
        };

        // Send the number of PIDFD to the child as reply.
        // Because we used CLONE_FILES, they own a copy of it,
        // however they cannot reliably determine its number.
        // Handle interrupts.
        // EOF means process died before reading from the pipe.
        let pid_fd_bytes = pid_fd.as_raw_fd().to_le_bytes();
        let mut nwrite = 0;
        while nwrite < pid_fd_bytes.len() {
            #[allow(clippy::arithmetic_side_effects)]
            match write(pipe_rw, &pid_fd_bytes[nwrite..]) {
                Ok(0) => return Err(Errno::EIO),
                Ok(n) => nwrite += n,
                Err(Errno::EINTR | Errno::EAGAIN) => continue,
                Err(errno) => return Err(errno),
            }
        }

        // SAFETY: We have used CLONE_FILES:
        // 1. fd is a valid FD in our process.
        // 2. Child is going to close down the pipe for us ;)
        // 3. After unshare(CLONE_FILES), child will close their copy of
        //    the seccomp-fd and the pid-fd, such that we leak no fds to
        //    the sandbox process.
        Ok(fd)
    }
}
