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

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

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

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

#[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<'a>,
}

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: nix::libc::c_int = -1;
        let clone_flags = nix::libc::SIGCHLD | nix::libc::CLONE_PIDFD;
        let seccomp_pipefd = (
            (
                self.seccomp_pipefd.0 .0.as_fd(),
                self.seccomp_pipefd.0 .1.as_fd(),
            ),
            (
                self.seccomp_pipefd.1 .0.as_fd(),
                self.seccomp_pipefd.1 .1.as_fd(),
            ),
        );
        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 nix::libc::c_void =
            Box::into_raw(child_info) as *mut nix::libc::c_void;
        // SAFETY:
        // 1. We use a 2M stack by default, see config.rs.
        // 2. We randomize the contents of the stack.
        let mut stack = [0u8; CHLD_STACK_SIZE];
        fillrandom(&mut stack)?;

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

        // SAFETY: clone with CLONE_PIDFD returns a valid FD.
        let pid_fd = unsafe { BorrowedFd::borrow_raw(pid_fd) };
        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_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, nix::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, nix::libc::SIGSTOP, nix::libc::PTRACE_EVENT_STOP)
            );
            // SAFETY: nix does not have a wrapper for PTRACE_LISTEN.
            Errno::result(unsafe {
                nix::libc::ptrace(nix::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, nix::libc::SIGTRAP, nix::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, nix::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)?;
        }

        // Close the read end of the first pipe
        // Close the write end of the second pipe
        drop(self.seccomp_pipefd.0 .0);
        drop(self.seccomp_pipefd.1 .1);
        let pipe_ro = self.seccomp_pipefd.1 .0;
        let pipe_rw = self.seccomp_pipefd.0 .1;

        // 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),
        };

        // SAFETY: In libc we trust.
        let fd = match unsafe {
            nix::libc::syscall(nix::libc::SYS_pidfd_getfd, pid_fd.as_raw_fd(), fd, 0)
        } {
            e if e < 0 => return Err(Errno::last()),
            fd => fd as RawFd,
        };

        // Send success notification to the child.
        // Handle interrupts, partial write is impossible.
        // EOF means process died before reading from the pipe.
        let ack = [0u8];
        loop {
            match write(&pipe_rw, &ack) {
                Ok(0) => return Err(Errno::EIO),
                Ok(_) => break,
                Err(Errno::EINTR | Errno::EAGAIN) => continue,
                Err(errno) => return Err(errno),
            }
        }

        Ok(fd)
    }
}
