//! Reusable systemd-service installer — the extracted, parameterized form of
//! `nornir install systemd`.
//!
//! nornir's own installer (`src/bin/nornir.rs::run_install_systemd`) hardcodes
//! everything for the `nornir` service: the `nornir` system user, the unit text,
//! the `/etc/nornir/nornir.env` env file, the daemon-reload/enable/start dance.
//! This module lifts those pieces out into a generic, spec-driven library so
//! **holger** (and any other nordisk product) can install itself the same way —
//! without nornir or holger being swapped over yet (create-only).
//!
//! Two layers, sharply split so the generators are unit-testable with NO root:
//!
//! 1. **Pure generators** ([`systemd_unit`], [`env_file`]) — deterministic
//!    `String` output, no filesystem, no subprocess, no privilege. These are the
//!    "generate" pieces the task calls for; they are what the tests assert on.
//! 2. **Privileged install** ([`install_systemd`]) — `#[cfg(unix)]`, root-only:
//!    create the system user if missing, write the unit + env file, daemon-reload,
//!    enable, start. A clear-error stub on non-unix, exactly like nornir.
//!
//! Design + guide: `.nornir/airgap-installer.md`.

use serde::{Deserialize, Serialize};

/// systemd `Restart=` policy for the rendered unit.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum RestartPolicy {
    /// `Restart=no` — never auto-restart.
    No,
    /// `Restart=on-failure` — restart on non-clean exit (nornir's default).
    #[default]
    OnFailure,
    /// `Restart=always` — restart whenever the process stops.
    Always,
}

impl RestartPolicy {
    /// The systemd `Restart=` keyword.
    pub fn as_systemd(self) -> &'static str {
        match self {
            RestartPolicy::No => "no",
            RestartPolicy::OnFailure => "on-failure",
            RestartPolicy::Always => "always",
        }
    }
}

/// Install scope — the generic form of nornir's `--scope`. Picks WHERE the unit
/// lives and WHO runs it:
///
/// - [`Scope::System`] — a root-owned unit in `/etc/systemd/system` running as a
///   dedicated system user ([`SystemdServiceSpec::user`]); the [`install_systemd`]
///   default. State under that user's home.
/// - [`Scope::User`] — a `systemctl --user` unit in `~/.config/systemd/user`
///   running as the invoking user. No root, no system user, no `/etc`; state in
///   that user's own home. The `User=`/`Group=` lines are OMITTED (systemd rejects
///   them in `--user` units).
///
/// [`Scope::resolve`] picks by privilege when the caller doesn't force one — root
/// → `System`, non-root → `User` — mirroring nornir's `resolve_scope`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum Scope {
    /// Root unit in `/etc/systemd/system`, runs as the spec's system user.
    #[default]
    System,
    /// `systemctl --user` unit in `~/.config/systemd/user`, runs as the caller.
    User,
}

impl Scope {
    /// Pick the scope: an explicit choice wins; otherwise `System` when running as
    /// root (euid 0), `User` otherwise. Mirrors nornir's `resolve_scope`.
    #[cfg(unix)]
    pub fn resolve(explicit: Option<Scope>) -> Scope {
        explicit
            .unwrap_or(if unsafe { geteuid() } == 0 { Scope::System } else { Scope::User })
    }

    /// Non-unix: the installers are linux-only, so only `System` is meaningful —
    /// but keep the signature so callers compile everywhere.
    #[cfg(not(unix))]
    pub fn resolve(explicit: Option<Scope>) -> Scope {
        explicit.unwrap_or(Scope::System)
    }

    fn is_user(self) -> bool {
        matches!(self, Scope::User)
    }
}

/// A single `KEY=VALUE` pair rendered as a unit `Environment=` line (and written
/// to the env file when [`SystemdServiceSpec::env_file`] is set).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EnvVar {
    pub key: String,
    pub value: String,
}

impl EnvVar {
    pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
        EnvVar {
            key: key.into(),
            value: value.into(),
        }
    }
}

/// Everything `run_install_systemd` currently hardcodes for nornir, but generic.
///
/// Build it with [`SystemdServiceSpec::new`] + the `with_*` chainers, or fill the
/// public fields directly. The two pure generators ([`systemd_unit`] /
/// [`env_file`]) and the privileged [`install_systemd`] all read from this one spec.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SystemdServiceSpec {
    /// The unit name WITHOUT the `.service` suffix (e.g. `"nornir"`, `"holger"`).
    /// `unit_file_name()` adds the suffix; the installer writes
    /// `/etc/systemd/system/<service_name>.service`.
    pub service_name: String,
    /// `[Unit] Description=` text.
    pub description: String,
    /// The binary `ExecStart=` runs. Should be an installed, on-system path
    /// (nornir copies to `/usr/local/bin` to avoid 203/EXEC off a removable drive).
    pub exec_path: String,
    /// Args appended to `exec_path` on the `ExecStart=` line (space-joined).
    pub exec_args: Vec<String>,
    /// System user to create + run as (`User=` / `Group=`). The installer creates
    /// it if missing.
    pub user: String,
    /// `Group=`. Defaults to `user` when `None`.
    pub group: Option<String>,
    /// `WorkingDirectory=` (and the user's home for `ensure_system_user`).
    pub working_dir: String,
    /// Inline `Environment=KEY=VALUE` lines AND the body of the env file.
    pub environment: Vec<EnvVar>,
    /// When set, the unit gets `EnvironmentFile=<path>` and [`install_systemd`]
    /// writes [`env_file`]'s output there (mode 0600). nornir uses
    /// `/etc/nornir/nornir.env`.
    pub env_file: Option<String>,
    /// `Restart=` policy.
    pub restart: RestartPolicy,
    /// `RestartSec=` (seconds). nornir uses `2`.
    pub restart_sec: u32,
    /// `Type=` (e.g. `simple`, `notify`).
    pub service_type: String,
    /// Units this one must start `After=` (network-online.target is always added).
    pub after: Vec<String>,
    /// Units this one `Wants=`.
    pub wants: Vec<String>,
    /// `WantedBy=` target in `[Install]`.
    pub wanted_by: String,
    /// Emit the always-on hardening line `NoNewPrivileges=true` (nornir does).
    pub no_new_privileges: bool,
}

impl SystemdServiceSpec {
    /// A spec with nornir-matching defaults filled in: `Type=simple`,
    /// `Restart=on-failure` / `RestartSec=2`, `After=`/`Wants=network-online.target`,
    /// `WantedBy=multi-user.target`, `NoNewPrivileges=true`. Set the rest with the
    /// `with_*` chainers.
    pub fn new(
        service_name: impl Into<String>,
        description: impl Into<String>,
        exec_path: impl Into<String>,
        user: impl Into<String>,
        working_dir: impl Into<String>,
    ) -> Self {
        SystemdServiceSpec {
            service_name: service_name.into(),
            description: description.into(),
            exec_path: exec_path.into(),
            exec_args: Vec::new(),
            user: user.into(),
            group: None,
            working_dir: working_dir.into(),
            environment: Vec::new(),
            env_file: None,
            restart: RestartPolicy::default(),
            restart_sec: 2,
            service_type: "simple".into(),
            after: Vec::new(),
            wants: Vec::new(),
            wanted_by: "multi-user.target".into(),
            no_new_privileges: true,
        }
    }

    /// Append an `ExecStart` argument.
    pub fn with_arg(mut self, arg: impl Into<String>) -> Self {
        self.exec_args.push(arg.into());
        self
    }

    /// Append an `Environment=`/env-file entry.
    pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.environment.push(EnvVar::new(key, value));
        self
    }

    /// Route environment to an `EnvironmentFile=` (mode 0600) instead of (well, in
    /// addition to) inline lines.
    pub fn with_env_file(mut self, path: impl Into<String>) -> Self {
        self.env_file = Some(path.into());
        self
    }

    /// Set the `Restart=` policy.
    pub fn with_restart(mut self, policy: RestartPolicy) -> Self {
        self.restart = policy;
        self
    }

    /// Add an `After=` unit edge.
    pub fn after(mut self, unit: impl Into<String>) -> Self {
        self.after.push(unit.into());
        self
    }

    /// Add a `Wants=` unit edge.
    pub fn wants(mut self, unit: impl Into<String>) -> Self {
        self.wants.push(unit.into());
        self
    }

    /// The unit file name (`<service_name>.service`).
    pub fn unit_file_name(&self) -> String {
        format!("{}.service", self.service_name)
    }

    /// The `Group=` value (`group` override, else `user`).
    pub fn group_name(&self) -> &str {
        self.group.as_deref().unwrap_or(&self.user)
    }

    /// The full `ExecStart=` value (`exec_path` + space-joined `exec_args`).
    pub fn exec_start(&self) -> String {
        if self.exec_args.is_empty() {
            self.exec_path.clone()
        } else {
            format!("{} {}", self.exec_path, self.exec_args.join(" "))
        }
    }
}

/// PURE generator — system-scope unit. Back-compat shim for
/// `systemd_unit_scoped(spec, Scope::System)`. No root, no IO.
pub fn systemd_unit(spec: &SystemdServiceSpec) -> String {
    systemd_unit_scoped(spec, Scope::System)
}

/// PURE generator — render the `.service` unit text from `spec` for `scope`. No
/// root, no IO; deterministic → unit-testable.
///
/// Mirrors the unit `run_install_systemd` builds for nornir, but every field is
/// driven by the spec. For [`Scope::User`] the `User=`/`Group=` lines are OMITTED
/// (a `systemctl --user` unit already runs as the invoking user — systemd rejects
/// those keys), and `[Install] WantedBy=` targets `default.target` (the user
/// manager's default) instead of the spec's system target.
pub fn systemd_unit_scoped(spec: &SystemdServiceSpec, scope: Scope) -> String {
    let mut s = String::new();

    // [Unit]
    s.push_str("[Unit]\n");
    s.push_str(&format!("Description={}\n", spec.description));
    // network-online is always wanted/after-ed (nornir does this unconditionally),
    // then any caller-supplied edges.
    let mut after: Vec<String> = vec!["network-online.target".to_string()];
    after.extend(spec.after.iter().cloned());
    s.push_str(&format!("After={}\n", after.join(" ")));
    let mut wants: Vec<String> = vec!["network-online.target".to_string()];
    wants.extend(spec.wants.iter().cloned());
    s.push_str(&format!("Wants={}\n", wants.join(" ")));
    s.push('\n');

    // [Service]
    s.push_str("[Service]\n");
    s.push_str(&format!("Type={}\n", spec.service_type));
    // User=/Group= are invalid in a `systemctl --user` unit (it runs as you).
    if !scope.is_user() {
        s.push_str(&format!("User={}\n", spec.user));
        s.push_str(&format!("Group={}\n", spec.group_name()));
    }
    s.push_str(&format!("WorkingDirectory={}\n", spec.working_dir));
    // Inline Environment= lines (env file is referenced separately).
    for e in &spec.environment {
        s.push_str(&format!("Environment={}={}\n", e.key, e.value));
    }
    if let Some(ef) = &spec.env_file {
        s.push_str(&format!("EnvironmentFile={}\n", ef));
    }
    s.push_str(&format!("ExecStart={}\n", spec.exec_start()));
    s.push_str(&format!("Restart={}\n", spec.restart.as_systemd()));
    s.push_str(&format!("RestartSec={}\n", spec.restart_sec));
    if spec.no_new_privileges {
        s.push_str("NoNewPrivileges=true\n");
    }
    s.push('\n');

    // [Install]
    s.push_str("[Install]\n");
    let wanted_by = if scope.is_user() { "default.target" } else { spec.wanted_by.as_str() };
    s.push_str(&format!("WantedBy={}\n", wanted_by));

    s
}

/// PURE generator — render the env-file body (`KEY=VALUE\n` per entry) from
/// `spec.environment`. This is the generic form of nornir's
/// `/etc/nornir/nornir.env` body. No root, no IO; deterministic → unit-testable.
pub fn env_file(spec: &SystemdServiceSpec) -> String {
    let mut s = String::new();
    for e in &spec.environment {
        s.push_str(&format!("{}={}\n", e.key, e.value));
    }
    s
}

/// Example constructor: how nornir's own service would be expressed as a spec.
/// NOT wired into nornir's binary (create-only) — it shows the mapping from
/// `run_install_systemd`'s hardcoded values onto the generic spec, and doubles as
/// a stable fixture for the tests.
pub fn nornir_spec(server_bin: &str, addr: &str) -> SystemdServiceSpec {
    SystemdServiceSpec::new(
        "nornir",
        "nornir server",
        server_bin,
        "nornir",
        "/home/nornir",
    )
    .with_env("HOME", "/home/nornir")
    .with_env_file("/etc/nornir/nornir.env")
    .with_restart(RestartPolicy::OnFailure)
    // The env file carries the token + addr; surfaced here as env entries so the
    // env-file generator emits them. (HOME stays an inline Environment= for the
    // unit, matching nornir.)
    .with_env("NORNIR_SERVER_ADDR", addr)
}

/// Example constructor showing how **holger** would build a spec to reuse this
/// installer. NOT wired into holger's binary (create-only) — holger keeps its own
/// install command until INSTALL-4 swaps it over.
pub fn holger_spec(holger_bin: &str, data_dir: &str) -> SystemdServiceSpec {
    SystemdServiceSpec::new(
        "holger",
        "Holger artifact repository",
        holger_bin,
        "_holger",
        data_dir,
    )
    .with_arg("serve")
    .with_arg("--config")
    .with_arg("/etc/holger/holger.toml")
    .after("nornir-server.service")
    .with_env("HOLGER_DATA", data_dir)
    .with_env_file("/etc/holger/holger.env")
    .with_restart(RestartPolicy::OnFailure)
}

// ───────────────────────── privileged install (unix) ─────────────────────────

#[cfg(unix)]
unsafe extern "C" {
    fn geteuid() -> u32;
    fn chown(path: *const std::os::raw::c_char, owner: u32, group: u32) -> i32;
}

/// Recursively `chown` a tree via libc — no `chown` subprocess (mirrors nornir).
#[cfg(unix)]
fn chown_recursive(path: &std::path::Path, uid: u32, gid: u32) -> anyhow::Result<()> {
    use anyhow::Context;
    use std::os::unix::ffi::OsStrExt;
    let c = std::ffi::CString::new(path.as_os_str().as_bytes())
        .with_context(|| format!("path has interior NUL: {}", path.display()))?;
    if unsafe { chown(c.as_ptr(), uid, gid) } != 0 {
        anyhow::bail!(
            "chown {} failed: {}",
            path.display(),
            std::io::Error::last_os_error()
        );
    }
    if path.is_dir() {
        for entry in
            std::fs::read_dir(path).with_context(|| format!("read {}", path.display()))?
        {
            chown_recursive(&entry?.path(), uid, gid)?;
        }
    }
    Ok(())
}

/// Create a system user if it doesn't already exist (idempotent). 100% Rust — no
/// `useradd` subprocess: allocate a free system id and append matching
/// `/etc/group` + `/etc/passwd` + `/etc/shadow` entries.
#[cfg(unix)]
fn ensure_system_user(user: &str, home: &str) -> anyhow::Result<()> {
    use anyhow::Context;
    use std::path::Path;
    let (uid, gid) = create_system_user_in(
        Path::new("/etc/passwd"),
        Path::new("/etc/group"),
        Path::new("/etc/shadow"),
        user,
        home,
    )?;
    std::fs::create_dir_all(home).context("create service home")?;
    chown_recursive(Path::new(home), uid, gid).ok();
    Ok(())
}

/// Pure-Rust system-user creation (the `useradd --system` replacement), with the
/// three system files as params so it is unit-testable against temp fixtures.
/// Idempotent: if `user` is already in passwd, returns its `(uid, gid)` and writes
/// nothing. Otherwise allocates the lowest id free in **both** passwd and group
/// within `[100, 1000)`, and appends a group, a passwd, and a (locked) shadow row.
#[cfg(unix)]
fn create_system_user_in(
    passwd_path: &std::path::Path,
    group_path: &std::path::Path,
    shadow_path: &std::path::Path,
    user: &str,
    home: &str,
) -> anyhow::Result<(u32, u32)> {
    use anyhow::{anyhow, Context};
    use std::io::Write;

    let passwd = std::fs::read_to_string(passwd_path).unwrap_or_default();
    for l in passwd.lines() {
        let f: Vec<&str> = l.split(':').collect();
        if f.first() == Some(&user) && f.len() >= 4 {
            return Ok((f[2].parse().unwrap_or(0), f[3].parse().unwrap_or(0)));
        }
    }
    let used_in = |s: &str| -> std::collections::BTreeSet<u32> {
        s.lines()
            .filter_map(|l| l.split(':').nth(2).and_then(|v| v.parse::<u32>().ok()))
            .filter(|&n| (100..1000).contains(&n))
            .collect()
    };
    let used_uid = used_in(&passwd);
    let group = std::fs::read_to_string(group_path).unwrap_or_default();
    let used_gid = used_in(&group);
    let id = (100u32..1000)
        .find(|i| !used_uid.contains(i) && !used_gid.contains(i))
        .ok_or_else(|| anyhow!("no free system uid/gid in [100,1000) for `{user}`"))?;

    let append = |path: &std::path::Path, line: String| -> anyhow::Result<()> {
        let mut f = std::fs::OpenOptions::new()
            .create(true)
            .append(true)
            .open(path)
            .with_context(|| format!("open {}", path.display()))?;
        writeln!(f, "{line}").with_context(|| format!("append {}", path.display()))?;
        Ok(())
    };
    append(group_path, format!("{user}:x:{id}:"))?;
    append(passwd_path, format!("{user}:x:{id}:{id}::{home}:/usr/sbin/nologin"))?;
    append(shadow_path, format!("{user}:!::0:99999:7:::"))?;
    Ok((id, id))
}

/// Run `systemctl <args>` (annotated deploy subprocess). Mirrors nornir.
#[cfg(unix)]
/// 100%-Rust systemd control via **D-Bus** (`org.freedesktop.systemd1`) — the
/// replacement for shelling `systemctl`. Pure-Rust `zbus` over the system bus (no
/// libdbus, no subprocess). Privileged ops (start/enable/…) need root, exactly as
/// `systemctl` did; the connection itself needs the system bus present.
#[cfg(unix)]
pub struct Systemd {
    conn: zbus::blocking::Connection,
}

#[cfg(unix)]
impl Systemd {
    pub fn connect() -> anyhow::Result<Self> {
        Self::connect_scoped(Scope::System)
    }

    /// Connect to the systemd manager for `scope`: the SYSTEM bus for
    /// [`Scope::System`] (root ops), the per-user SESSION bus for [`Scope::User`]
    /// (`systemctl --user`). The session manager exposes the same
    /// `org.freedesktop.systemd1` interface, so every method below is unchanged.
    pub fn connect_scoped(scope: Scope) -> anyhow::Result<Self> {
        let conn = if scope.is_user() {
            zbus::blocking::Connection::session()
                .map_err(|e| anyhow::anyhow!("connect user D-Bus (systemd --user): {e}"))?
        } else {
            zbus::blocking::Connection::system()
                .map_err(|e| anyhow::anyhow!("connect system D-Bus (systemd): {e}"))?
        };
        Ok(Systemd { conn })
    }

    fn mgr<B>(&self, method: &str, body: &B) -> anyhow::Result<zbus::Message>
    where
        B: serde::Serialize + zbus::zvariant::DynamicType,
    {
        self.conn
            .call_method(
                Some("org.freedesktop.systemd1"),
                "/org/freedesktop/systemd1",
                Some("org.freedesktop.systemd1.Manager"),
                method,
                body,
            )
            .map_err(|e| anyhow::anyhow!("systemd `{method}` D-Bus call: {e}"))
    }

    pub fn reload(&self) -> anyhow::Result<()> { self.mgr("Reload", &())?; Ok(()) }
    /// `EnableUnitFiles(files: as, runtime: b, force: b)`.
    pub fn enable(&self, unit: &str) -> anyhow::Result<()> {
        self.mgr("EnableUnitFiles", &(vec![unit], false, true))?;
        Ok(())
    }
    /// `DisableUnitFiles(files: as, runtime: b)`.
    pub fn disable(&self, unit: &str) -> anyhow::Result<()> {
        self.mgr("DisableUnitFiles", &(vec![unit], false))?;
        Ok(())
    }
    pub fn start(&self, unit: &str) -> anyhow::Result<()> {
        self.mgr("StartUnit", &(unit, "replace"))?;
        Ok(())
    }
    pub fn stop(&self, unit: &str) -> anyhow::Result<()> {
        self.mgr("StopUnit", &(unit, "replace"))?;
        Ok(())
    }
    /// `TryRestartUnit(name: s, mode: s)` — restart the unit **only if it is already
    /// running** (a no-op when stopped), matching `systemctl try-restart`.
    pub fn try_restart(&self, unit: &str) -> anyhow::Result<()> {
        self.mgr("TryRestartUnit", &(unit, "replace"))?;
        Ok(())
    }
    pub fn reset_failed(&self, unit: &str) -> anyhow::Result<()> {
        self.mgr("ResetFailedUnit", &(unit,))?;
        Ok(())
    }
    /// `true` iff the unit's `ActiveState == "active"` (GetUnit → property). An
    /// unloaded/unknown unit is inactive, not an error.
    pub fn is_active(&self, unit: &str) -> anyhow::Result<bool> {
        let msg = match self.mgr("GetUnit", &(unit,)) {
            Ok(m) => m,
            Err(_) => return Ok(false), // not loaded → inactive
        };
        let unit_body = msg.body();
        let path: zbus::zvariant::OwnedObjectPath = unit_body
            .deserialize()
            .map_err(|e| anyhow::anyhow!("decode GetUnit path: {e}"))?;
        let reply = self
            .conn
            .call_method(
                Some("org.freedesktop.systemd1"),
                path.as_str(),
                Some("org.freedesktop.DBus.Properties"),
                "Get",
                &("org.freedesktop.systemd1.Unit", "ActiveState"),
            )
            .map_err(|e| anyhow::anyhow!("get ActiveState: {e}"))?;
        let state_body = reply.body();
        let v: zbus::zvariant::Value = state_body
            .deserialize()
            .map_err(|e| anyhow::anyhow!("decode ActiveState: {e}"))?;
        // Properties.Get returns a VARIANT; the content is the ActiveState string.
        // Handle both an unwrapped Str and a nested Value(Str).
        let is = |s: &zbus::zvariant::Str| s.as_str() == "active";
        let active = match &v {
            zbus::zvariant::Value::Str(s) => is(s),
            zbus::zvariant::Value::Value(inner) => {
                matches!(&**inner, zbus::zvariant::Value::Str(s) if is(s))
            }
            _ => false,
        };
        Ok(active)
    }
}

/// Privileged install driven by `spec` — the generic form of `run_install_systemd`.
///
/// Steps (root-only): create the system user if missing, write the env file
/// (mode 0600) when `spec.env_file` is set, write the unit to
/// `/etc/systemd/system/<service>.service`, `daemon-reload`, `enable`, `start`.
///
/// The pure generators ([`systemd_unit`] / [`env_file`]) produce the file bodies;
/// this function only performs the privileged side effects. Tests cover the
/// generators, not this (which needs root).
#[cfg(unix)]
pub fn install_systemd(spec: &SystemdServiceSpec) -> anyhow::Result<()> {
    install_systemd_scoped(spec, Scope::System)
}

/// Privileged/per-user install driven by `spec` + `scope` — the generic, dual-scope
/// form of nornir's `run_install_systemd`.
///
/// - [`Scope::System`] (root): create the system user if missing, write the env
///   file (0600) + the unit to `/etc/systemd/system/<svc>.service`, then
///   reload/enable/start on the SYSTEM bus.
/// - [`Scope::User`] (no root): write the unit to
///   `~/.config/systemd/user/<svc>.service` (NO system user, NO `/etc`; the env
///   file, if set, is written under that dir too), then reload/enable/start on the
///   per-user SESSION bus. Running as root here is rejected — a `--user` unit must
///   run as a real login user.
///
/// The pure generators ([`systemd_unit_scoped`] / [`env_file`]) produce the file
/// bodies; this performs only the side effects.
#[cfg(unix)]
pub fn install_systemd_scoped(spec: &SystemdServiceSpec, scope: Scope) -> anyhow::Result<()> {
    use anyhow::Context;
    use std::os::unix::fs::PermissionsExt;
    use std::path::{Path, PathBuf};

    let user_scope = scope.is_user();
    let euid = unsafe { geteuid() };
    if user_scope {
        if euid == 0 {
            anyhow::bail!(
                "`--scope user` installs a `systemctl --user` service that runs as YOU — \
                 don't run it as root. Drop to your normal user, or use `--scope system`."
            );
        }
    } else if euid != 0 {
        anyhow::bail!(
            "installing the `{}` systemd service in system scope must run as root — it creates \
             the `{}` system user and writes /etc/systemd/system. Re-run with sudo (or use \
             `--scope user`).",
            spec.service_name,
            spec.user
        );
    }

    // System scope creates the dedicated user; user scope runs as the caller.
    if !user_scope {
        ensure_system_user(&spec.user, &spec.working_dir)?;
    }

    // Where the unit (and, in user scope, the env file) live.
    let unit_dir: PathBuf = if user_scope {
        let home = std::env::var_os("HOME")
            .map(PathBuf::from)
            .ok_or_else(|| anyhow::anyhow!("$HOME is not set — needed for a --user unit"))?;
        home.join(".config/systemd/user")
    } else {
        PathBuf::from("/etc/systemd/system")
    };
    std::fs::create_dir_all(&unit_dir)
        .with_context(|| format!("create unit dir {}", unit_dir.display()))?;

    // Env file (mode 0600). System scope honors the spec's absolute path (e.g.
    // /etc/<svc>/<svc>.env); user scope relocates it next to the unit so an
    // unprivileged install never touches /etc.
    if let Some(ef) = &spec.env_file {
        let ef_path = if user_scope {
            unit_dir.join(
                Path::new(ef).file_name().unwrap_or_else(|| std::ffi::OsStr::new("service.env")),
            )
        } else {
            PathBuf::from(ef)
        };
        if let Some(parent) = ef_path.parent() {
            std::fs::create_dir_all(parent)
                .with_context(|| format!("create env-file dir {}", parent.display()))?;
        }
        std::fs::write(&ef_path, env_file(spec))
            .with_context(|| format!("write env file {}", ef_path.display()))?;
        std::fs::set_permissions(&ef_path, std::fs::Permissions::from_mode(0o600))
            .with_context(|| format!("chmod 600 {}", ef_path.display()))?;
    }

    // Unit file.
    let unit_path = unit_dir.join(spec.unit_file_name());
    let updating = unit_path.exists();
    std::fs::write(&unit_path, systemd_unit_scoped(spec, scope))
        .with_context(|| format!("write unit {}", unit_path.display()))?;

    let sd = Systemd::connect_scoped(scope)?;
    let unit = spec.unit_file_name();
    sd.reload()?;
    let _ = sd.reset_failed(&unit); // clear any prior crash-loop start-limit; best-effort
    sd.enable(&unit)?;
    sd.start(&unit)?;

    if user_scope {
        println!(
            "✓ {} {} systemd --user service (runs as you)",
            if updating { "updated" } else { "installed" },
            spec.service_name
        );
    } else {
        println!(
            "✓ {} {} systemd service (user {})",
            if updating { "updated" } else { "installed" },
            spec.service_name,
            spec.user
        );
    }

    // Mint-or-load the install identity into the spec's data dir. Best-effort:
    // identity is transparent to existing callers (nornir/holger), so a failure
    // here logs a warning and never fails the install.
    match crate::identity::ensure(Path::new(&spec.working_dir), &spec.service_name, scope, &spec.user)
    {
        Ok(id) => println!("identity: {id}"),
        Err(e) => eprintln!("warning: identity minting skipped ({e})"),
    }

    Ok(())
}

/// Non-unix stub — clear error, exactly like nornir's `#[cfg(not(unix))]` arm.
#[cfg(not(unix))]
pub fn install_systemd(spec: &SystemdServiceSpec) -> anyhow::Result<()> {
    anyhow::bail!(
        "installing the `{}` systemd service is linux-only",
        spec.service_name
    )
}

/// Non-unix stub for the scoped install — mirrors [`install_systemd`].
#[cfg(not(unix))]
pub fn install_systemd_scoped(spec: &SystemdServiceSpec, _scope: Scope) -> anyhow::Result<()> {
    install_systemd(spec)
}

/// Privileged uninstall driven by `spec` — the `delete` verb to pair with
/// [`install_systemd`]'s `add`/`update`. Stops + disables the unit, removes the
/// unit file from `/etc/systemd/system`, then `daemon-reload`. Idempotent: a unit
/// that is already stopped / already gone is not an error (stop/disable are
/// best-effort). The system user and data home are deliberately **kept** — only
/// the service definition is removed. Root-only.
#[cfg(unix)]
pub fn remove_systemd(spec: &SystemdServiceSpec) -> anyhow::Result<()> {
    remove_systemd_scoped(spec, Scope::System)
}

/// Privileged/per-user uninstall driven by `spec` + `scope` — the dual-scope
/// `delete` verb. Stops + disables the unit on the right bus, removes the unit
/// file from `/etc/systemd/system` (system) or `~/.config/systemd/user` (user),
/// then reloads. Idempotent. The system user and data home are deliberately
/// **kept** — only the service definition is removed.
#[cfg(unix)]
pub fn remove_systemd_scoped(spec: &SystemdServiceSpec, scope: Scope) -> anyhow::Result<()> {
    use anyhow::Context;
    use std::path::PathBuf;

    let user_scope = scope.is_user();
    if !user_scope && unsafe { geteuid() } != 0 {
        anyhow::bail!(
            "removing the `{}` systemd service in system scope must run as root — it writes \
             /etc/systemd/system. Re-run with sudo (or use `--scope user`).",
            spec.service_name
        );
    }

    let unit = spec.unit_file_name();
    let sd = Systemd::connect_scoped(scope)?;
    // Best-effort: the unit may not be running or enabled.
    let _ = sd.stop(&unit);
    let _ = sd.disable(&unit);

    let unit_path: PathBuf = if user_scope {
        let home = std::env::var_os("HOME")
            .map(PathBuf::from)
            .ok_or_else(|| anyhow::anyhow!("$HOME is not set — needed for a --user unit"))?;
        home.join(".config/systemd/user").join(&unit)
    } else {
        PathBuf::from("/etc/systemd/system").join(&unit)
    };
    if unit_path.exists() {
        std::fs::remove_file(&unit_path)
            .with_context(|| format!("remove unit {}", unit_path.display()))?;
    }
    sd.reload()?;
    let _ = sd.reset_failed(&unit);

    if user_scope {
        println!("✓ removed {} systemd --user service (data kept)", spec.service_name);
    } else {
        println!("✓ removed {} systemd service (user {} + data kept)", spec.service_name, spec.user);
    }
    Ok(())
}

/// Non-unix stub — clear error, mirrors [`install_systemd`].
#[cfg(not(unix))]
pub fn remove_systemd(spec: &SystemdServiceSpec) -> anyhow::Result<()> {
    anyhow::bail!(
        "removing the `{}` systemd service is linux-only",
        spec.service_name
    )
}

/// Non-unix stub for the scoped remove — mirrors [`remove_systemd`].
#[cfg(not(unix))]
pub fn remove_systemd_scoped(spec: &SystemdServiceSpec, _scope: Scope) -> anyhow::Result<()> {
    remove_systemd(spec)
}

/// Is the unit currently `active`? (systemd D-Bus `ActiveState`.) Returns `false`
/// for an unknown / inactive / failed unit. System-bus variant.
#[cfg(unix)]
pub fn status_systemd(spec: &SystemdServiceSpec) -> anyhow::Result<bool> {
    status_systemd_scoped(spec, Scope::System)
}

/// Scoped status — queries the SYSTEM bus for [`Scope::System`], the per-user
/// SESSION bus for [`Scope::User`].
#[cfg(unix)]
pub fn status_systemd_scoped(spec: &SystemdServiceSpec, scope: Scope) -> anyhow::Result<bool> {
    Systemd::connect_scoped(scope)?.is_active(&spec.unit_file_name())
}

/// Non-unix stub — clear error, mirrors [`install_systemd`].
#[cfg(not(unix))]
pub fn status_systemd(spec: &SystemdServiceSpec) -> anyhow::Result<bool> {
    anyhow::bail!(
        "querying the `{}` systemd service is linux-only",
        spec.service_name
    )
}

/// Non-unix stub for the scoped status — mirrors [`status_systemd`].
#[cfg(not(unix))]
pub fn status_systemd_scoped(spec: &SystemdServiceSpec, _scope: Scope) -> anyhow::Result<bool> {
    status_systemd(spec)
}

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

    /// Pure-Rust user creation (no `useradd`): allocates the lowest free system id
    /// >= 100, appends well-formed passwd/group/shadow rows, and is idempotent.
    #[cfg(unix)]
    #[test]
    fn create_system_user_allocates_consistent_id_and_is_idempotent() {
        let dir = tempfile::tempdir().unwrap();
        let (passwd, group, shadow) =
            (dir.path().join("passwd"), dir.path().join("group"), dir.path().join("shadow"));
        // Seed id 100 as taken in both → allocation must skip to 101.
        std::fs::write(&passwd, "root:x:0:0::/root:/bin/sh\n_taken:x:100:100::/h:/n\n").unwrap();
        std::fs::write(&group, "root:x:0:\n_taken:x:100:\n").unwrap();
        std::fs::write(&shadow, "root:!::::::\n").unwrap();

        let (uid, gid) =
            create_system_user_in(&passwd, &group, &shadow, "_holger", "/var/lib/holger").unwrap();
        assert_eq!(uid, gid, "system user gets a matching group");
        assert_eq!(uid, 101, "lowest free id >= 100 (100 is taken)");
        assert!(std::fs::read_to_string(&passwd)
            .unwrap()
            .contains("_holger:x:101:101::/var/lib/holger:/usr/sbin/nologin"));
        assert!(std::fs::read_to_string(&group).unwrap().contains("_holger:x:101:"));
        assert!(std::fs::read_to_string(&shadow).unwrap().contains("_holger:!::0:99999:7:::"));

        // Idempotent: same ids, no duplicate passwd row.
        let again =
            create_system_user_in(&passwd, &group, &shadow, "_holger", "/var/lib/holger").unwrap();
        assert_eq!(again, (101, 101));
        assert_eq!(
            std::fs::read_to_string(&passwd).unwrap().matches("_holger:x:").count(),
            1,
            "no duplicate user row"
        );
    }

    /// LAW 1 (inject-assert): build a real spec, render the unit, assert every
    /// section/line carries the injected values — not just "didn't panic".
    #[test]
    fn systemd_unit_contains_injected_fields() {
        let spec = SystemdServiceSpec::new(
            "holger",
            "Holger artifact repository",
            "/usr/local/bin/holger",
            "_holger",
            "/var/lib/holger",
        )
        .with_arg("serve")
        .with_arg("--config")
        .with_arg("/etc/holger/holger.toml")
        .with_env("HOLGER_DATA", "/var/lib/holger")
        .after("nornir-server.service")
        .with_restart(RestartPolicy::Always);

        let unit = systemd_unit(&spec);

        // [Unit]
        assert!(unit.contains("[Unit]"));
        assert!(unit.contains("Description=Holger artifact repository"));
        assert!(unit.contains("After=network-online.target nornir-server.service"));
        assert!(unit.contains("Wants=network-online.target"));
        // [Service]
        assert!(unit.contains("[Service]"));
        assert!(unit.contains("Type=simple"));
        assert!(unit.contains("User=_holger"));
        // Group defaults to user.
        assert!(unit.contains("Group=_holger"));
        assert!(unit.contains("WorkingDirectory=/var/lib/holger"));
        assert!(unit.contains("Environment=HOLGER_DATA=/var/lib/holger"));
        assert!(unit.contains(
            "ExecStart=/usr/local/bin/holger serve --config /etc/holger/holger.toml"
        ));
        assert!(unit.contains("Restart=always"));
        assert!(unit.contains("RestartSec=2"));
        assert!(unit.contains("NoNewPrivileges=true"));
        // [Install]
        assert!(unit.contains("[Install]"));
        assert!(unit.contains("WantedBy=multi-user.target"));
    }

    /// EnvironmentFile= is emitted only when set, and the env-file generator
    /// renders the exact KEY=VALUE body.
    #[test]
    fn env_file_generator_and_reference() {
        let spec = SystemdServiceSpec::new(
            "nornir",
            "nornir server",
            "/usr/local/bin/nornir-server",
            "nornir",
            "/home/nornir",
        )
        .with_env("NORNIR_SERVER_TOKEN", "deadbeef")
        .with_env("NORNIR_SERVER_ADDR", "127.0.0.1:7878")
        .with_env_file("/etc/nornir/nornir.env");

        let body = env_file(&spec);
        assert_eq!(
            body,
            "NORNIR_SERVER_TOKEN=deadbeef\nNORNIR_SERVER_ADDR=127.0.0.1:7878\n"
        );

        let unit = systemd_unit(&spec);
        assert!(unit.contains("EnvironmentFile=/etc/nornir/nornir.env"));
    }

    /// When no env file is set, the unit must NOT carry an EnvironmentFile= line.
    #[test]
    fn no_env_file_no_reference() {
        let spec = SystemdServiceSpec::new("svc", "d", "/bin/svc", "svc", "/tmp");
        let unit = systemd_unit(&spec);
        assert!(!unit.contains("EnvironmentFile="));
        // env_file body is empty with no env vars.
        assert_eq!(env_file(&spec), "");
    }

    /// RestartPolicy maps to the right systemd keyword.
    #[test]
    fn restart_policy_keywords() {
        assert_eq!(RestartPolicy::No.as_systemd(), "no");
        assert_eq!(RestartPolicy::OnFailure.as_systemd(), "on-failure");
        assert_eq!(RestartPolicy::Always.as_systemd(), "always");
        assert_eq!(RestartPolicy::default(), RestartPolicy::OnFailure);
    }

    /// Group override is honored (distinct from the run user).
    #[test]
    fn group_override() {
        let mut spec = SystemdServiceSpec::new("svc", "d", "/bin/svc", "runuser", "/tmp");
        spec.group = Some("rungroup".into());
        let unit = systemd_unit(&spec);
        assert!(unit.contains("User=runuser"));
        assert!(unit.contains("Group=rungroup"));
    }

    /// The example holger_spec constructor produces a unit holger could install
    /// (the reuse path the doc describes), with real injected values.
    #[test]
    fn holger_spec_example_renders() {
        let spec = holger_spec("/opt/holger/bin/holger", "/var/lib/holger");
        let unit = systemd_unit(&spec);
        assert_eq!(spec.unit_file_name(), "holger.service");
        assert!(unit.contains("User=_holger"));
        assert!(unit.contains(
            "ExecStart=/opt/holger/bin/holger serve --config /etc/holger/holger.toml"
        ));
        assert!(unit.contains("After=network-online.target nornir-server.service"));
        assert!(unit.contains("EnvironmentFile=/etc/holger/holger.env"));
        assert_eq!(env_file(&spec), "HOLGER_DATA=/var/lib/holger\n");
    }

    /// User scope OMITS `User=`/`Group=` (invalid in a `systemctl --user` unit) and
    /// targets `default.target`; system scope keeps them and the spec's target.
    #[test]
    fn user_scope_omits_user_group_and_targets_default() {
        let spec = SystemdServiceSpec::new(
            "holger",
            "Holger artifact repository",
            "/home/me/.cargo/bin/holger-server",
            "holger",
            "/home/me/.holger",
        )
        .with_arg("start")
        .with_arg("--config")
        .with_arg("/home/me/.holger/holger.ron");

        let user = systemd_unit_scoped(&spec, Scope::User);
        assert!(!user.contains("User="), "no User= in a --user unit");
        assert!(!user.contains("Group="), "no Group= in a --user unit");
        assert!(user.contains("WantedBy=default.target"));
        assert!(user.contains(
            "ExecStart=/home/me/.cargo/bin/holger-server start --config /home/me/.holger/holger.ron"
        ));

        let system = systemd_unit_scoped(&spec, Scope::System);
        assert!(system.contains("User=holger"));
        assert!(system.contains("Group=holger"));
        assert!(system.contains("WantedBy=multi-user.target"));
        // The bare `systemd_unit` shim stays system-scope (back-compat).
        assert_eq!(systemd_unit(&spec), system);
    }

    /// `Scope::resolve` honors an explicit choice and defaults sensibly.
    #[cfg(unix)]
    #[test]
    fn scope_resolve_honors_explicit() {
        assert_eq!(Scope::resolve(Some(Scope::User)), Scope::User);
        assert_eq!(Scope::resolve(Some(Scope::System)), Scope::System);
        // Unforced: matches current privilege (root → System, else User).
        let euid = unsafe { geteuid() };
        let expected = if euid == 0 { Scope::System } else { Scope::User };
        assert_eq!(Scope::resolve(None), expected);
    }

    /// SystemdServiceSpec round-trips through JSON (the spec is serde — a product
    /// can embed it in its airgap bundle, as the LakeSpec does for Product).
    #[test]
    fn spec_json_roundtrip() {
        let spec = nornir_spec("/usr/local/bin/nornir-server", "0.0.0.0:7878");
        let back: SystemdServiceSpec =
            serde_json::from_str(&serde_json::to_string(&spec).unwrap()).unwrap();
        assert_eq!(back, spec);
        assert!(back.environment.iter().any(|e| e.key == "NORNIR_SERVER_ADDR"
            && e.value == "0.0.0.0:7878"));
    }

    /// The `delete`/`status` verbs act on `<service_name>.service` — assert the
    /// unit name they target is derived from the spec (no root needed for the
    /// name; the privileged side effects are covered by an ignored integration
    /// test that needs a real systemd).
    #[test]
    fn remove_and_status_target_the_unit_name() {
        // A holger spec as holger itself would build it: the real CLI verb is
        // `start --config` (NOT `serve`), so a product owns its own spec and uses
        // this crate only as the engine.
        let spec = SystemdServiceSpec::new(
            "holger",
            "Holger artifact repository",
            "/usr/local/bin/holger-server",
            "_holger",
            "/var/lib/holger",
        )
        .with_arg("start")
        .with_arg("--config")
        .with_arg("/etc/holger/holger.toml");

        // install_systemd writes, and remove_systemd/status_systemd act on, exactly this.
        assert_eq!(spec.unit_file_name(), "holger.service");
        let unit = systemd_unit(&spec);
        assert!(unit.contains(
            "ExecStart=/usr/local/bin/holger-server start --config /etc/holger/holger.toml"
        ));
    }

    /// `Restart=no` renders, and an arg-less spec's `ExecStart` is exactly the
    /// binary path (no trailing args) — the `exec_start()` boundary.
    #[test]
    fn restart_no_and_argless_exec_start() {
        let spec = SystemdServiceSpec::new("svc", "d", "/bin/svc", "svc", "/tmp")
            .with_restart(RestartPolicy::No);
        assert_eq!(spec.exec_start(), "/bin/svc");
        let unit = systemd_unit(&spec);
        assert!(unit.contains("Restart=no"));
        assert!(unit.contains("ExecStart=/bin/svc\n"), "exec line is just the path");
    }

    /// `no_new_privileges = false` OMITS the hardening line (it is opt-out); the
    /// default spec keeps it on.
    #[test]
    fn no_new_privileges_false_omits_line() {
        let mut spec = SystemdServiceSpec::new("svc", "d", "/bin/svc", "svc", "/tmp");
        assert!(systemd_unit(&spec).contains("NoNewPrivileges=true"));
        spec.no_new_privileges = false;
        assert!(!systemd_unit(&spec).contains("NoNewPrivileges"));
    }

    /// User scope still emits inline `Environment=` AND `EnvironmentFile=` (only
    /// `User=`/`Group=` are dropped); the env-file body is scope-independent and
    /// matches the injected pairs exactly.
    #[test]
    fn user_scope_keeps_environment_and_env_file() {
        let spec = SystemdServiceSpec::new("holger", "d", "/bin/h", "holger", "/h")
            .with_env("A", "1")
            .with_env("B", "2")
            .with_env_file("/etc/holger/holger.env");

        let user = systemd_unit_scoped(&spec, Scope::User);
        assert!(!user.contains("User="), "no User= in --user unit");
        assert!(!user.contains("Group="), "no Group= in --user unit");
        assert!(user.contains("Environment=A=1"));
        assert!(user.contains("Environment=B=2"));
        assert!(user.contains("EnvironmentFile=/etc/holger/holger.env"));
        assert!(user.contains("WantedBy=default.target"));
        assert_eq!(env_file(&spec), "A=1\nB=2\n");
    }

    /// Multiple `After=`/`Wants=` edges are space-joined AFTER the always-on
    /// `network-online.target`, preserving declared order.
    #[test]
    fn multiple_after_and_wants_edges_joined_in_order() {
        let spec = SystemdServiceSpec::new("svc", "d", "/bin/svc", "svc", "/tmp")
            .after("a.target")
            .after("b.service")
            .wants("c.target");
        let unit = systemd_unit(&spec);
        assert!(unit.contains("After=network-online.target a.target b.service"));
        assert!(unit.contains("Wants=network-online.target c.target"));
    }

    /// `Scope::default()` is `System` (the privileged install default).
    #[test]
    fn scope_default_is_system() {
        assert_eq!(Scope::default(), Scope::System);
    }
}
