// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

// spell-checker:ignore (ToDO) asid auditid auditinfo auid cstr egid rgid emod euid getaudit getlogin gflag nflag pline rflag termid uflag gsflag zflag cflag

// README:
// This was originally based on BSD's `id`
// (noticeable in functionality, usage text, options text, etc.)
// and synced with:
//  http://ftp-archive.freebsd.org/mirror/FreeBSD-Archive/old-releases/i386/1.0-RELEASE/ports/shellutils/src/id.c
//  http://www.opensource.apple.com/source/shell_cmds/shell_cmds-118/id/id.c
//
// * This was partially rewritten in order for stdout/stderr/exit_code
//   to be conform with GNU coreutils (8.32) test suite for `id`.
//
// * This supports multiple users (a feature that was introduced in coreutils 8.31)
//
// * This passes GNU's coreutils Test suite (8.32)
//   for "tests/id/uid.sh" and "tests/id/zero/sh".
//
// * Option '--zero' does not exist for BSD's `id`, therefore '--zero' is only
//   allowed together with other options that are available on GNU's `id`.
//
// * Help text based on BSD's `id` manpage and GNU's `id` manpage.
//
// * This passes GNU's coreutils Test suite (8.32) for "tests/id/context.sh" if compiled with
//   `--features feat_selinux`. It should also pass "tests/id/no-context.sh", but that depends on
//   `uu_ls -Z` being implemented and therefore fails at the moment
//

#![allow(non_camel_case_types)]
#![allow(dead_code)]

use clap::{Arg, ArgAction, Command};
use std::ffi::CStr;
use std::io::{self, Write};
use uucore::display::Quotable;
use uucore::entries::{self, Group, Locate, Passwd};
use uucore::error::UResult;
use uucore::error::{USimpleError, set_exit_code};
pub use uucore::libc;
use uucore::libc::getlogin;
use uucore::line_ending::LineEnding;
use uucore::translate;

use rustix::process::{Uid, getegid, geteuid, getgid, getuid};
use uucore::{format_usage, show_error};

macro_rules! cstr2cow {
    ($v:expr) => {
        unsafe {
            let ptr = $v;
            // Must be not null to call cstr2cow
            if ptr.is_null() {
                None
            } else {
                Some({ CStr::from_ptr(ptr) }.to_string_lossy())
            }
        }
    };
}

fn get_context_help_text() -> String {
    #[cfg(any(
        all(feature = "selinux", any(target_os = "android", target_os = "linux")),
        all(feature = "smack", target_os = "linux"),
    ))]
    return translate!("id-context-help-enabled");

    #[cfg(not(any(
        all(feature = "selinux", any(target_os = "android", target_os = "linux")),
        all(feature = "smack", target_os = "linux"),
    )))]
    return translate!("id-context-help-disabled");
}

mod options {
    pub const OPT_IGNORE: &str = "ignore";
    pub const OPT_AUDIT: &str = "audit"; // GNU's id does not have this
    pub const OPT_CONTEXT: &str = "context";
    pub const OPT_EFFECTIVE_USER: &str = "user";
    pub const OPT_GROUP: &str = "group";
    pub const OPT_GROUPS: &str = "groups";
    pub const OPT_HUMAN_READABLE: &str = "human-readable"; // GNU's id does not have this
    pub const OPT_NAME: &str = "name";
    pub const OPT_PASSWORD: &str = "password"; // GNU's id does not have this
    pub const OPT_REAL_ID: &str = "real";
    pub const OPT_ZERO: &str = "zero"; // BSD's id does not have this
    pub const ARG_USERS: &str = "USER";
}

struct Ids {
    uid: u32,  // user id
    gid: u32,  // group id
    euid: u32, // effective uid
    egid: u32, // effective gid
}

struct State {
    nflag: bool,  // --name
    uflag: bool,  // --user
    gflag: bool,  // --group
    gsflag: bool, // --groups
    rflag: bool,  // --real
    zflag: bool,  // --zero
    cflag: bool,  // --context
    #[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
    selinux_supported: bool,
    #[cfg(all(feature = "smack", target_os = "linux"))]
    smack_supported: bool,
    ids: Option<Ids>,
    // The behavior for calling GNU's `id` and calling GNU's `id $USER` is similar but different.
    // * The SELinux context is only displayed without a specified user.
    // * The `getgroups` system call is only used without a specified user, this causes
    //   the order of the displayed groups to be different between `id` and `id $USER`.
    //
    // Example:
    // $ strace -e getgroups id -G $USER
    // 1000 10 975 968
    // +++ exited with 0 +++
    // $ strace -e getgroups id -G
    // getgroups(0, NULL)                      = 4
    // getgroups(4, [10, 968, 975, 1000])      = 4
    // 1000 10 968 975
    // +++ exited with 0 +++
    user_specified: bool,
}

#[uucore::main(no_signals)]
#[allow(clippy::cognitive_complexity)]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
    let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?;
    let mut lock = io::stdout().lock();

    let users: Vec<String> = matches
        .get_many::<String>(options::ARG_USERS)
        .map(|v| v.map(ToString::to_string).collect())
        .unwrap_or_default();

    let mut state = State {
        nflag: matches.get_flag(options::OPT_NAME),
        uflag: matches.get_flag(options::OPT_EFFECTIVE_USER),
        gflag: matches.get_flag(options::OPT_GROUP),
        gsflag: matches.get_flag(options::OPT_GROUPS),
        rflag: matches.get_flag(options::OPT_REAL_ID),
        zflag: matches.get_flag(options::OPT_ZERO),
        cflag: matches.get_flag(options::OPT_CONTEXT),
        #[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
        selinux_supported: uucore::selinux::is_selinux_enabled(),
        #[cfg(all(feature = "smack", target_os = "linux"))]
        smack_supported: uucore::smack::is_smack_enabled(),
        user_specified: !users.is_empty(),
        ids: None,
    };

    let default_format = {
        // "default format" is when none of '-ugG' was used
        !(state.uflag || state.gflag || state.gsflag)
    };

    if (state.nflag || state.rflag) && default_format && !state.cflag {
        return Err(USimpleError::new(
            1,
            translate!("id-error-names-real-ids-require-flags"),
        ));
    }
    if state.zflag && default_format && !state.cflag {
        // NOTE: GNU test suite "id/zero.sh" needs this stderr output:
        return Err(USimpleError::new(
            1,
            translate!("id-error-zero-not-permitted-default"),
        ));
    }
    if state.user_specified && state.cflag {
        return Err(USimpleError::new(
            1,
            translate!("id-error-cannot-print-context-with-user"),
        ));
    }

    let delimiter = if state.zflag { "\0" } else { " " };
    let line_ending = LineEnding::from_zero_flag(state.zflag);

    if state.cflag {
        // SELinux context
        #[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
        if state.selinux_supported {
            if let Ok(context) = selinux::SecurityContext::current(false) {
                let bytes = context.as_bytes();
                write!(lock, "{}{line_ending}", String::from_utf8_lossy(bytes))?;
                return Ok(());
            }
            return Err(USimpleError::new(
                1,
                translate!("id-error-cannot-get-context"),
            ));
        }

        // SMACK label
        #[cfg(all(feature = "smack", target_os = "linux"))]
        if state.smack_supported {
            match uucore::smack::get_smack_label_for_self() {
                Ok(label) => {
                    write!(lock, "{label}{line_ending}")?;
                    return Ok(());
                }
                Err(_) => {
                    return Err(USimpleError::new(
                        1,
                        translate!("id-error-cannot-get-context"),
                    ));
                }
            }
        }

        // Neither SELinux nor SMACK supported
        return Err(USimpleError::new(
            1,
            translate!("id-error-context-security-only"),
        ));
    }

    for i in 0..=users.len() {
        let possible_pw = if state.user_specified {
            if let Ok(p) = Passwd::locate(users[i].as_str()) {
                Some(p)
            } else {
                show_error!(
                    "{}",
                    translate!("id-error-no-such-user", "user" => users[i].quote())
                );
                set_exit_code(1);
                if i + 1 >= users.len() {
                    break;
                }

                continue;
            }
        } else {
            None
        };

        // GNU's `id` does not support the flags: -p/-P/-A.
        if matches.get_flag(options::OPT_PASSWORD) {
            // BSD's `id` ignores all but the first specified user
            pline(possible_pw.as_ref().map(|v| Uid::from_raw(v.uid)))?;
            return Ok(());
        }
        if matches.get_flag(options::OPT_HUMAN_READABLE) {
            // BSD's `id` ignores all but the first specified user
            pretty(possible_pw)?;
            return Ok(());
        }
        if matches.get_flag(options::OPT_AUDIT) {
            // BSD's `id` ignores specified users
            auditid()?;
            return Ok(());
        }

        let (uid, gid) = possible_pw.as_ref().map_or(
            {
                let use_effective = !state.rflag && (state.uflag || state.gflag || state.gsflag);
                if use_effective {
                    (geteuid().as_raw(), getegid().as_raw())
                } else {
                    (getuid().as_raw(), getgid().as_raw())
                }
            },
            |p| (p.uid, p.gid),
        );
        state.ids = Some(Ids {
            uid,
            gid,
            euid: geteuid().as_raw(),
            egid: getegid().as_raw(),
        });

        if state.gflag {
            write!(
                lock,
                "{}",
                if state.nflag {
                    entries::gid2grp(gid).unwrap_or_else(|_| {
                        show_error!(
                            "{}",
                            translate!("id-error-cannot-find-group-name", "gid" => gid)
                        );
                        set_exit_code(1);
                        gid.to_string()
                    })
                } else {
                    gid.to_string()
                }
            )?;
        }

        if state.uflag {
            write!(
                lock,
                "{}",
                if state.nflag {
                    entries::uid2usr(uid).unwrap_or_else(|_| {
                        show_error!(
                            "{}",
                            translate!("id-error-cannot-find-user-name", "uid" => uid)
                        );
                        set_exit_code(1);
                        uid.to_string()
                    })
                } else {
                    uid.to_string()
                }
            )?;
        }

        let groups = entries::get_groups_gnu(Some(gid))?;
        let groups = if state.user_specified {
            possible_pw.as_ref().map(Passwd::belongs_to).unwrap()
        } else {
            groups.clone()
        };

        if state.gsflag {
            write!(
                lock,
                "{}{}",
                groups
                    .iter()
                    .map(|&id| {
                        if state.nflag {
                            entries::gid2grp(id).unwrap_or_else(|_| {
                                show_error!(
                                    "{}",
                                    translate!("id-error-cannot-find-group-name", "gid" => id)
                                );
                                set_exit_code(1);
                                id.to_string()
                            })
                        } else {
                            id.to_string()
                        }
                    })
                    .collect::<Vec<_>>()
                    .join(delimiter),
                // NOTE: this is necessary to pass GNU's "tests/id/zero.sh":
                if state.zflag && state.user_specified && users.len() > 1 {
                    "\0"
                } else {
                    ""
                }
            )?;
        }

        if default_format {
            id_print(&state, &groups)?;
        }
        write!(lock, "{line_ending}")?;

        if i + 1 >= users.len() {
            break;
        }
    }

    Ok(())
}

pub fn uu_app() -> Command {
    Command::new("id")
        .version(uucore::crate_version!())
        .help_template(uucore::localized_help_template("id"))
        .about(translate!("id-about"))
        .override_usage(format_usage(&translate!("id-usage")))
        .infer_long_args(true)
        .args_override_self(true)
        .after_help(translate!("id-after-help"))
        .arg(
            Arg::new(options::OPT_IGNORE)
                .short('a')
                .long(options::OPT_IGNORE)
                .help(translate!("id-help-ignore"))
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new(options::OPT_AUDIT)
                .short('A')
                .conflicts_with_all([
                    options::OPT_GROUP,
                    options::OPT_EFFECTIVE_USER,
                    options::OPT_HUMAN_READABLE,
                    options::OPT_PASSWORD,
                    options::OPT_GROUPS,
                    options::OPT_ZERO,
                ])
                .help(translate!("id-help-audit"))
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new(options::OPT_EFFECTIVE_USER)
                .short('u')
                .long(options::OPT_EFFECTIVE_USER)
                .conflicts_with(options::OPT_GROUP)
                .help(translate!("id-help-user"))
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new(options::OPT_GROUP)
                .short('g')
                .long(options::OPT_GROUP)
                .conflicts_with(options::OPT_EFFECTIVE_USER)
                .help(translate!("id-help-group"))
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new(options::OPT_GROUPS)
                .short('G')
                .long(options::OPT_GROUPS)
                .conflicts_with_all([
                    options::OPT_GROUP,
                    options::OPT_EFFECTIVE_USER,
                    options::OPT_CONTEXT,
                    options::OPT_HUMAN_READABLE,
                    options::OPT_PASSWORD,
                    options::OPT_AUDIT,
                ])
                .help(translate!("id-help-groups"))
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new(options::OPT_HUMAN_READABLE)
                .short('p')
                .help(translate!("id-help-human-readable"))
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new(options::OPT_NAME)
                .short('n')
                .long(options::OPT_NAME)
                .help(translate!("id-help-name"))
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new(options::OPT_PASSWORD)
                .short('P')
                .help(translate!("id-help-password"))
                .conflicts_with(options::OPT_HUMAN_READABLE)
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new(options::OPT_REAL_ID)
                .short('r')
                .long(options::OPT_REAL_ID)
                .help(translate!("id-help-real"))
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new(options::OPT_ZERO)
                .short('z')
                .long(options::OPT_ZERO)
                .help(translate!("id-help-zero"))
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new(options::OPT_CONTEXT)
                .short('Z')
                .long(options::OPT_CONTEXT)
                .conflicts_with_all([options::OPT_GROUP, options::OPT_EFFECTIVE_USER])
                .help(get_context_help_text())
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new(options::ARG_USERS)
                .action(ArgAction::Append)
                .value_name(options::ARG_USERS)
                .value_hint(clap::ValueHint::Username),
        )
}

fn pretty(possible_pw: Option<Passwd>) -> io::Result<()> {
    let mut lock = io::stdout().lock();

    if let Some(p) = possible_pw {
        writeln!(
            lock,
            "{}\t{}\n{}\t",
            translate!("id-output-uid"),
            p.name,
            translate!("id-output-groups")
        )?;
        writeln!(
            lock,
            "{}",
            p.belongs_to()
                .iter()
                .map(|&gr| entries::gid2grp(gr).unwrap_or_else(|_| gr.to_string()))
                .collect::<Vec<_>>()
                .join(" ")
        )?;
    } else {
        let login = cstr2cow!(getlogin().cast_const());
        let uid = getuid().as_raw();
        if let Ok(p) = Passwd::locate(uid) {
            if let Some(user_name) = login {
                writeln!(lock, "{}\t{user_name}", translate!("id-output-login"))?;
            }
            writeln!(lock, "{}\t{}", translate!("id-output-uid"), p.name)?;
        } else {
            writeln!(lock, "{}\t{uid}", translate!("id-output-uid"))?;
        }

        let euid = geteuid().as_raw();
        if euid != uid {
            if let Ok(p) = Passwd::locate(euid) {
                writeln!(lock, "{}\t{}", translate!("id-output-euid"), p.name)?;
            } else {
                writeln!(lock, "{}\t{euid}", translate!("id-output-euid"))?;
            }
        }

        let rgid = getgid().as_raw();
        let egid = getegid().as_raw();
        if egid != rgid {
            if let Ok(g) = Group::locate(rgid) {
                writeln!(lock, "{}\t{}", translate!("id-output-rgid"), g.name)?;
            } else {
                writeln!(lock, "{}\t{rgid}", translate!("id-output-rgid"))?;
            }
        }

        writeln!(
            lock,
            "{}\t{}",
            translate!("id-output-groups"),
            entries::get_groups_gnu(None)?
                .iter()
                .map(|&gr| entries::gid2grp(gr).unwrap_or_else(|_| gr.to_string()))
                .collect::<Vec<_>>()
                .join(" ")
        )?;
    }

    Ok(())
}

#[cfg(any(target_vendor = "apple", target_os = "freebsd"))]
fn pline(possible_uid: Option<Uid>) -> io::Result<()> {
    let uid = possible_uid.unwrap_or_else(getuid);
    let pw = Passwd::locate(uid.as_raw())?;

    writeln!(
        io::stdout().lock(),
        "{}:{}:{}:{}:{}:{}:{}:{}:{}:{}",
        pw.name,
        pw.user_passwd.unwrap_or_default(),
        pw.uid,
        pw.gid,
        pw.user_access_class.unwrap_or_default(),
        pw.passwd_change_time,
        pw.expiration,
        pw.user_info.unwrap_or_default(),
        pw.user_dir.unwrap_or_default(),
        pw.user_shell.unwrap_or_default()
    )
}

#[cfg(any(
    target_os = "linux",
    target_os = "android",
    target_os = "openbsd",
    target_os = "cygwin",
    target_os = "netbsd"
))]
fn pline(possible_uid: Option<Uid>) -> io::Result<()> {
    let uid = possible_uid.unwrap_or_else(getuid);
    let pw = Passwd::locate(uid.as_raw())?;

    writeln!(
        io::stdout().lock(),
        "{}:{}:{}:{}:{}:{}:{}",
        pw.name,
        pw.user_passwd.unwrap_or_default(),
        pw.uid,
        pw.gid,
        pw.user_info.unwrap_or_default(),
        pw.user_dir.unwrap_or_default(),
        pw.user_shell.unwrap_or_default()
    )?;
    Ok(())
}

#[cfg(any(
    target_os = "linux",
    target_os = "android",
    target_os = "openbsd",
    target_os = "cygwin",
    target_os = "netbsd"
))]
#[allow(clippy::unnecessary_wraps)]
fn auditid() -> io::Result<()> {
    Ok(())
}

#[cfg(not(any(
    target_os = "linux",
    target_os = "android",
    target_os = "openbsd",
    target_os = "cygwin",
    target_os = "netbsd"
)))]
fn auditid() -> io::Result<()> {
    use std::mem::MaybeUninit;
    let mut lock = io::stdout().lock();

    let mut auditinfo: MaybeUninit<audit::c_auditinfo_addr_t> = MaybeUninit::uninit();
    let address = auditinfo.as_mut_ptr();
    if unsafe { audit::getaudit(address) } < 0 {
        writeln!(lock, "{}", translate!("id-error-audit-retrieve"))?;
        return Ok(());
    }

    // SAFETY: getaudit wrote a valid struct to auditinfo
    let auditinfo = unsafe { auditinfo.assume_init() };

    writeln!(lock, "auid={}", auditinfo.ai_auid)?;
    writeln!(lock, "mask.success=0x{:x}", auditinfo.ai_mask.am_success)?;
    writeln!(lock, "mask.failure=0x{:x}", auditinfo.ai_mask.am_failure)?;
    writeln!(lock, "termid.port=0x{:x}", auditinfo.ai_termid.port)?;
    writeln!(lock, "asid={}", auditinfo.ai_asid)?;
    Ok(())
}

fn id_print(state: &State, groups: &[u32]) -> io::Result<()> {
    let uid = state.ids.as_ref().unwrap().uid;
    let gid = state.ids.as_ref().unwrap().gid;
    let euid = state.ids.as_ref().unwrap().euid;
    let egid = state.ids.as_ref().unwrap().egid;

    let mut lock = io::stdout().lock();

    write!(
        lock,
        "uid={uid}({})",
        entries::uid2usr(uid).unwrap_or_else(|_| {
            show_error!(
                "{}",
                translate!("id-error-cannot-find-user-name", "uid" => uid)
            );
            set_exit_code(1);
            uid.to_string()
        })
    )?;
    write!(
        lock,
        " gid={gid}({})",
        entries::gid2grp(gid).unwrap_or_else(|_| {
            show_error!(
                "{}",
                translate!("id-error-cannot-find-group-name", "gid" => gid)
            );
            set_exit_code(1);
            gid.to_string()
        })
    )?;
    if !state.user_specified && (euid != uid) {
        write!(
            lock,
            " euid={euid}({})",
            entries::uid2usr(euid).unwrap_or_else(|_| {
                show_error!(
                    "{}",
                    translate!("id-error-cannot-find-user-name", "uid" => euid)
                );
                set_exit_code(1);
                euid.to_string()
            })
        )?;
    }
    if !state.user_specified && (egid != gid) {
        // BUG?  printing egid={euid} ?
        write!(
            lock,
            " egid={egid}({})",
            entries::gid2grp(egid).unwrap_or_else(|_| {
                show_error!(
                    "{}",
                    translate!("id-error-cannot-find-group-name", "gid" => egid)
                );
                set_exit_code(1);
                egid.to_string()
            })
        )?;
    }
    write!(
        lock,
        " groups={}",
        groups
            .iter()
            .map(|&gr| format!(
                "{gr}({})",
                entries::gid2grp(gr).unwrap_or_else(|_| {
                    show_error!(
                        "{}",
                        translate!("id-error-cannot-find-group-name", "gid" => gr)
                    );
                    set_exit_code(1);
                    gr.to_string()
                })
            ))
            .collect::<Vec<_>>()
            .join(",")
    )?;

    #[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
    if state.selinux_supported
        && !state.user_specified
        && std::env::var_os("POSIXLY_CORRECT").is_none()
    {
        // print SElinux context (does not depend on "-Z")
        if let Ok(context) = selinux::SecurityContext::current(false) {
            let bytes = context.as_bytes();
            write!(lock, " context={}", String::from_utf8_lossy(bytes))?;
        }
    }

    #[cfg(all(feature = "smack", target_os = "linux"))]
    if state.smack_supported
        && !state.user_specified
        && std::env::var_os("POSIXLY_CORRECT").is_none()
    {
        // print SMACK label (does not depend on "-Z")
        if let Ok(label) = uucore::smack::get_smack_label_for_self() {
            write!(lock, " context={label}")?;
        }
    }

    Ok(())
}

#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "openbsd")))]
mod audit {
    use super::libc::{c_int, c_uint, dev_t, pid_t, uid_t};

    pub type au_id_t = uid_t;
    pub type au_asid_t = pid_t;
    pub type au_event_t = c_uint;
    pub type au_emod_t = c_uint;
    pub type au_class_t = c_int;
    pub type au_flag_t = u64;

    #[repr(C)]
    pub struct au_mask {
        pub am_success: c_uint,
        pub am_failure: c_uint,
    }
    pub type au_mask_t = au_mask;

    #[repr(C)]
    pub struct au_tid_addr {
        pub port: dev_t,
    }
    pub type au_tid_addr_t = au_tid_addr;

    #[repr(C)]
    #[expect(clippy::struct_field_names)]
    pub struct c_auditinfo_addr {
        pub ai_auid: au_id_t,         // Audit user ID
        pub ai_mask: au_mask_t,       // Audit masks.
        pub ai_termid: au_tid_addr_t, // Terminal ID.
        pub ai_asid: au_asid_t,       // Audit session ID.
        pub ai_flags: au_flag_t,      // Audit session flags
    }
    pub type c_auditinfo_addr_t = c_auditinfo_addr;

    unsafe extern "C" {
        pub fn getaudit(auditinfo_addr: *mut c_auditinfo_addr_t) -> c_int;
    }
}
