use std::{cell::RefCell, io};

use nix::sys::stat::{Mode, umask};
use rar_common::{database::options::SBounding, util::activates_no_new_privs};
use rar_exec::orchestrator::{Orchestrator, PreExecContext, PreExecStep, Stage};

use crate::finder::de::cred::CredOwnedData;

struct PreExecConfig {
    umask: u16,
    cred: CredOwnedData,
    bounding: SBounding,
    set_no_new_privs: bool,
}

thread_local! {
    static PRE_EXEC_CONFIG: RefCell<Option<PreExecConfig>> = const { RefCell::new(None) };
}

pub fn configure_pre_exec(
    umask: u16,
    cred: CredOwnedData,
    bounding: SBounding,
    set_no_new_privs: bool,
) {
    PRE_EXEC_CONFIG.with(|cfg| {
        *cfg.borrow_mut() = Some(PreExecConfig {
            umask,
            cred,
            bounding,
            set_no_new_privs,
        });
    });
}

pub unsafe fn set_umask_step(_: PreExecContext) -> io::Result<()> {
    PRE_EXEC_CONFIG.with(|cfg| {
        if let Some(cfg) = cfg.borrow().as_ref() {
            umask(Mode::from_bits_truncate(u32::from(cfg.umask)));
        }
        Ok(())
    })
}

pub unsafe fn set_credentials_step(_: PreExecContext) -> io::Result<()> {
    PRE_EXEC_CONFIG.with(|cfg| {
        if let Some(cfg) = cfg.borrow().as_ref() {
            crate::setuid_setgid(&cfg.cred)
                .map_err(|e| io::Error::other(format!("Failed to set uid/gid: {e}")))?;
        }
        Ok(())
    })
}

pub unsafe fn set_capabilities_step(_: PreExecContext) -> io::Result<()> {
    PRE_EXEC_CONFIG.with(|cfg| {
        if let Some(cfg) = cfg.borrow().as_ref() {
            crate::set_capabilities(&cfg.cred, cfg.bounding)
                .map_err(|e| io::Error::other(format!("Failed to set capabilities: {e}")))?;
        }
        Ok(())
    })
}

pub unsafe fn set_no_new_privs_step(_: PreExecContext) -> io::Result<()> {
    PRE_EXEC_CONFIG.with(|cfg| {
        if cfg
            .borrow()
            .as_ref()
            .is_some_and(|cfg| cfg.set_no_new_privs)
        {
            activates_no_new_privs()
                .map_err(|e| io::Error::other(format!("Failed to set no_new_privs: {e}")))?;
        }
        Ok(())
    })
}

#[allow(clippy::unnecessary_wraps)]
pub unsafe fn close_fds_step(_: PreExecContext) -> io::Result<()> {
    let _ = crate::close_restrictive_fds();
    Ok(())
}

pub const SET_UMASK_STEP: PreExecStep = PreExecStep {
    stage: Stage::UMASK,
    run: set_umask_step,
};

pub const SET_CREDENTIALS_STEP: PreExecStep = PreExecStep {
    stage: Stage::PRIV_DROP,
    run: set_credentials_step,
};

pub const SET_CAPABILITIES_STEP: PreExecStep = PreExecStep {
    stage: Stage::CAPS,
    run: set_capabilities_step,
};

pub const CLOSE_FDS_STEP: PreExecStep = PreExecStep {
    stage: Stage::FD_CLEANUP,
    run: close_fds_step,
};

pub const SET_NO_NEW_PRIVS_STEP: PreExecStep = PreExecStep {
    stage: Stage::LOCKDOWN,
    run: set_no_new_privs_step,
};

pub static PRE_EXEC_STEPS: &[PreExecStep] = &[
    SET_UMASK_STEP,
    SET_CREDENTIALS_STEP,
    SET_CAPABILITIES_STEP,
    CLOSE_FDS_STEP,
    SET_NO_NEW_PRIVS_STEP,
    #[cfg(feature = "landlock")]
    crate::finder::api::landlock::LANDLOCK_STEP,
];

pub static PRE_EXEC_ORCHESTRATOR: Orchestrator = Orchestrator::new(PRE_EXEC_STEPS);

#[cfg(test)]
mod tests {
    use super::*;
    use crate::finder::de::cred::CredOwnedData;
    use rar_common::database::options::SBounding;

    #[test]
    fn pre_exec_steps_are_ordered_and_stable() {
        configure_pre_exec(0o022, CredOwnedData::default(), SBounding::Ignore, false);

        assert_eq!(SET_UMASK_STEP.stage, Stage::UMASK);
        assert_eq!(SET_CREDENTIALS_STEP.stage, Stage::PRIV_DROP);
        assert_eq!(SET_CAPABILITIES_STEP.stage, Stage::CAPS);
        assert_eq!(CLOSE_FDS_STEP.stage, Stage::FD_CLEANUP);
        assert_eq!(SET_NO_NEW_PRIVS_STEP.stage, Stage::LOCKDOWN);

        let stages: Vec<_> = PRE_EXEC_STEPS
            .iter()
            .map(|step| step.stage.order())
            .collect();
        assert!(stages.windows(2).all(|pair| pair[0] <= pair[1]));

        #[cfg(feature = "landlock")]
        assert_eq!(stages.last().copied(), Some(Stage::LOCKDOWN.order()));

        #[cfg(not(feature = "landlock"))]
        assert_eq!(stages, vec![45, 50, 55, 65, 70]);
    }
}
