//! Helpers for installing/uninstalling services.

#[cfg(windows)]
pub mod winsvc;

#[cfg(target_os = "macos")]
pub mod launchd;

#[cfg(all(target_os = "linux", feature = "systemd"))]
#[cfg_attr(
  docsrs,
  doc(cfg(all(all(target_os = "linux", feature = "installer"))))
)]
pub mod systemd;

//use std::{fmt, path::PathBuf};

#[cfg(feature = "clap")]
use clap::ArgMatches;

use itertools::Itertools;

use crate::{err::Error, lumberjack::LogLevel};


/*
#[cfg(any(
  target_os = "macos",
  all(target_os = "linux", feature = "systemd")
))]
pub enum InstallDir {
  #[cfg(target_os = "macos")]
  UserAgent,

  #[cfg(target_os = "macos")]
  GlobalAgent,

  #[cfg(target_os = "macos")]
  GlobalDaemon,

  #[cfg(all(target_os = "linux", feature = "systemd"))]
  System,

  #[cfg(all(target_os = "linux", feature = "systemd"))]
  PublicUser,

  #[cfg(all(target_os = "linux", feature = "systemd"))]
  PrivateUser
}

#[cfg(any(
  target_os = "macos",
  all(target_os = "linux", feature = "systemd")
))]
impl InstallDir {
  fn path(self) -> PathBuf {
    PathBuf::from(self.to_string())
  }

  fn path_str(self) -> String {
    self.to_string()
  }
}

#[cfg(any(
  target_os = "macos",
  all(target_os = "linux", feature = "systemd")
))]
impl Default for InstallDir {
  fn default() -> Self {
    #[cfg(target_os = "macos")]
    return InstallDir::GlobalDaemon;

    #[cfg(all(target_os = "linux", feature = "systemd"))]
    return InstallDir::System;
  }
}

#[cfg(any(
  target_os = "macos",
  all(target_os = "linux", feature = "systemd")
))]
impl fmt::Display for InstallDir {
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    let s = match self {
      #[cfg(target_os = "macos")]
      InstallDir::UserAgent => "~/Library/LaunchAgents",
      #[cfg(target_os = "macos")]
      InstallDir::GlobalAgent => "/Library/LaunchAgents",
      #[cfg(target_os = "macos")]
      InstallDir::GlobalDaemon => "/Library/LaunchDaemons",

      #[cfg(all(target_os = "linux", feature = "systemd"))]
      InstallDir::System => "/etc/systemd/system",
      #[cfg(all(target_os = "linux", feature = "systemd"))]
      InstallDir::PublicUser => "/etc/systemd/user",
      #[cfg(all(target_os = "linux", feature = "systemd"))]
      InstallDir::PrivateUser => "~/.config/systemd/user"
    };
    write!(f, "{}", s)
  }
}
*/


/// What account to run the service as.
///
/// # Windows
#[derive(Default)]
pub enum Account {
  /// Run as the highest privileged user available on system.
  ///
  /// On unixy systems, this means `root`.  On Windows, this means the
  /// [LocalSystem](https://learn.microsoft.com/en-us/windows/win32/services/localsystem-account) account.
  #[default]
  System,

  /// On Windows systems, run the service as the [LocalService](https://learn.microsoft.com/en-us/windows/win32/services/localservice-account) account.
  #[cfg(windows)]
  #[cfg_attr(docsrs, doc(cfg(windows)))]
  Service,

  /// On Windows systems, run the service as the [NetworkService](https://learn.microsoft.com/en-us/windows/win32/services/networkservice-account) account.
  #[cfg(windows)]
  #[cfg_attr(docsrs, doc(cfg(windows)))]
  Network,

  #[cfg(unix)]
  User(String),

  #[cfg(windows)]
  UserAndPass(String, String)
}


#[derive(Debug, Default)]
pub struct RunAs {
  user: Option<String>,
  group: Option<String>,

  #[cfg(target_os = "macos")]
  initgroups: bool,

  #[cfg(any(
    target_os = "macos",
    all(target_os = "linux", feature = "systemd")
  ))]
  umask: Option<String>
}


#[cfg(windows)]
pub type BoxRegCb =
  Box<dyn FnOnce(&str, &mut windows_registry::Key) -> Result<(), Error>>;


#[allow(clippy::struct_excessive_bools)]
pub struct RegSvc {
  /// If `true`, then attempt to forcibly install service.
  pub force: bool,

  /// Set to `true` if this service uses the qsu service argument parser.
  ///
  /// This will ensure that `--as-service` is  passed to the service
  /// executable.
  pub qsu_argp: bool,

  pub svcname: String,

  /// Service's display name.
  ///
  /// Only used on Windows.
  pub display_name: Option<String>,

  /// Service's description.
  ///
  /// Only used on Windows and on linux/systemd.
  pub description: Option<String>,

  /// Set to `true` if service supports configuration reloading.
  pub conf_reload: bool,

  /// Set to `true` if this is a network service.
  ///
  /// Note that this does not magically solve startup dependencies.
  pub netservice: bool,

  #[cfg(windows)]
  pub regconf: Option<BoxRegCb>,

  /// Command line arguments.
  pub args: Vec<String>,

  /// Environment variables.
  pub envs: Vec<(String, String)>,

  /// Set service to auto-start.
  ///
  /// By default the service will be registered, but needs to be started
  /// manually.
  pub autostart: bool,

  pub(crate) workdir: Option<String>,

  /// List of service dependencies.
  deps: Vec<Depend>,

  log_level: Option<LogLevel>,

  log_filter: Option<String>,

  trace_filter: Option<String>,

  trace_file: Option<String>,

  runas: RunAs
}

pub enum Depend {
  Network,
  Custom(Vec<String>)
}

impl RegSvc {
  #[must_use]
  pub fn new(svcname: &str) -> Self {
    Self {
      force: false,

      qsu_argp: false,

      svcname: svcname.to_string(),

      display_name: None,

      description: None,

      conf_reload: false,

      netservice: false,

      #[cfg(windows)]
      regconf: None,

      args: Vec::new(),

      envs: Vec::new(),

      autostart: false,

      workdir: None,

      deps: Vec::new(),

      log_level: None,

      log_filter: None,

      trace_filter: None,

      trace_file: None,

      runas: RunAs::default()
    }
  }

  #[cfg(feature = "clap")]
  #[allow(clippy::missing_panics_doc)]
  pub fn from_cmd_match(matches: &ArgMatches) -> Self {
    let force = matches.get_flag("force");

    // unwrap should be okay, because svcname is mandatory
    let svcname = matches.get_one::<String>("svcname").unwrap().to_owned();
    let autostart = matches.get_flag("auto_start");

    let dispname = matches.get_one::<String>("display_name");

    let descr = matches.get_one::<String>("description");
    let args: Vec<String> = matches
      .get_many::<String>("arg")
      .map_or_else(Vec::new, |vr| vr.map(String::from).collect());

    let envs: Vec<String> = matches
      .get_many::<String>("env")
      .map_or_else(Vec::new, |vr| vr.map(String::from).collect());

    /*
      if let Some(vr) = matches.get_many::<String>("env")
    {
      vr.map(String::from).collect()
    } else {
      Vec::new()
    };
    */
    let workdir = matches.get_one::<String>("workdir");

    let mut environ = Vec::new();
    let mut it = envs.into_iter();
    while let Some((key, value)) = it.next_tuple() {
      environ.push((key, value));
    }

    let log_level = matches.get_one::<LogLevel>("log_level").copied();
    let log_filter = matches.get_one::<String>("log_filter").cloned();
    let trace_filter = matches.get_one::<String>("trace_filter").cloned();
    let trace_file = matches.get_one::<String>("trace_file").cloned();

    let runas = RunAs::default();

    Self {
      force,
      qsu_argp: true,
      svcname,
      display_name: dispname.cloned(),
      description: descr.cloned(),
      conf_reload: false,
      netservice: false,
      #[cfg(windows)]
      regconf: None,
      args,
      envs: environ,
      autostart,
      workdir: workdir.cloned(),
      deps: Vec::new(),
      log_level,
      log_filter,
      trace_filter,
      trace_file,
      runas
    }
  }

  #[must_use]
  pub fn svcname(&self) -> &str {
    &self.svcname
  }

  /// Set the service's display name.
  ///
  /// This only has an effect on Windows.
  #[must_use]
  pub fn display_name(mut self, name: impl ToString) -> Self {
    self.display_name_ref(name);
    self
  }

  /// Set the service's _display name_.
  ///
  /// This only has an effect on Windows.
  #[allow(clippy::needless_pass_by_value)]
  pub fn display_name_ref(&mut self, name: impl ToString) -> &mut Self {
    self.display_name = Some(name.to_string());
    self
  }

  /// Set the service's description.
  ///
  /// This only has an effect on Windows and linux/systemd.
  #[must_use]
  pub fn description(mut self, text: impl ToString) -> Self {
    self.description_ref(text);
    self
  }

  /// Set the service's description.
  ///
  /// This only has an effect on Windows and linux/systemd.
  #[allow(clippy::needless_pass_by_value)]
  pub fn description_ref(&mut self, text: impl ToString) -> &mut Self {
    self.description = Some(text.to_string());
    self
  }

  /// Mark service as able to live reload its configuration.
  #[must_use]
  pub const fn conf_reload(mut self) -> Self {
    self.conf_reload_ref();
    self
  }

  /// Mark service as able to live reload its configuration.
  pub const fn conf_reload_ref(&mut self) -> &mut Self {
    self.conf_reload = true;
    self
  }

  /// Mark service as a network application.
  ///
  /// # Windows
  /// Calling this will implicitly add a `Tcpip` service dependency.
  #[must_use]
  pub fn netservice(mut self) -> Self {
    self.netservice_ref();
    self
  }

  /// Mark service as a network application.
  ///
  /// # Windows
  /// Calling this will implicitly add a `Tcpip` service dependency.
  #[cfg_attr(not(windows), allow(clippy::missing_const_for_fn))]
  pub fn netservice_ref(&mut self) -> &mut Self {
    self.netservice = true;

    #[cfg(windows)]
    self.deps.push(Depend::Network);

    self
  }

  /// Register a callback that will be used to set service registry keys.
  #[cfg(windows)]
  #[cfg_attr(docsrs, doc(cfg(windows)))]
  #[must_use]
  pub fn regconf<F>(mut self, f: F) -> Self
  where
    F: FnOnce(&str, &mut windows_registry::Key) -> Result<(), Error> + 'static
  {
    self.regconf = Some(Box::new(f));
    self
  }

  /// Register a callback that will be used to set service registry keys.
  #[cfg(windows)]
  #[cfg_attr(docsrs, doc(cfg(windows)))]
  pub fn regconf_ref<F>(&mut self, f: F) -> &mut Self
  where
    F: FnOnce(&str, &mut windows_registry::Key) -> Result<(), Error> + 'static
  {
    self.regconf = Some(Box::new(f));
    self
  }

  /// Append a service command line argument.
  #[allow(clippy::needless_pass_by_value)]
  #[must_use]
  pub fn arg(mut self, arg: impl ToString) -> Self {
    self.args.push(arg.to_string());
    self
  }

  /// Append a service command line argument.
  #[allow(clippy::needless_pass_by_value)]
  pub fn arg_ref(&mut self, arg: impl ToString) -> &mut Self {
    self.args.push(arg.to_string());
    self
  }

  /// Append service command line arguments.
  #[must_use]
  pub fn args<I, S>(mut self, args: I) -> Self
  where
    I: IntoIterator<Item = S>,
    S: ToString
  {
    for arg in args {
      self.args.push(arg.to_string());
    }
    self
  }

  /// Append service command line arguments.
  pub fn args_ref<I, S>(&mut self, args: I) -> &mut Self
  where
    I: IntoIterator<Item = S>,
    S: ToString
  {
    for arg in args {
      self.arg_ref(arg.to_string());
    }
    self
  }

  #[must_use]
  pub const fn have_args(&self) -> bool {
    !self.args.is_empty()
  }

  /// Add a service environment variable.
  #[allow(clippy::needless_pass_by_value)]
  #[must_use]
  pub fn env<K, V>(mut self, key: K, val: V) -> Self
  where
    K: ToString,
    V: ToString
  {
    self.envs.push((key.to_string(), val.to_string()));
    self
  }

  /// Add a service environment variable.
  #[allow(clippy::needless_pass_by_value)]
  pub fn env_ref<K, V>(&mut self, key: K, val: V) -> &mut Self
  where
    K: ToString,
    V: ToString
  {
    self.envs.push((key.to_string(), val.to_string()));
    self
  }

  /// Add service environment variables.
  #[must_use]
  pub fn envs<I, K, V>(mut self, envs: I) -> Self
  where
    I: IntoIterator<Item = (K, V)>,
    K: ToString,
    V: ToString
  {
    for (key, val) in envs {
      self.envs.push((key.to_string(), val.to_string()));
    }
    self
  }

  /// Add service environment variables.
  pub fn envs_ref<I, K, V>(&mut self, args: I) -> &mut Self
  where
    I: IntoIterator<Item = (K, V)>,
    K: ToString,
    V: ToString
  {
    for (key, val) in args {
      self.env_ref(key.to_string(), val.to_string());
    }
    self
  }

  #[must_use]
  pub const fn have_envs(&self) -> bool {
    !self.envs.is_empty()
  }

  /// Mark service to auto-start on boot.
  #[must_use]
  pub const fn autostart(mut self) -> Self {
    self.autostart = true;
    self
  }

  /// Mark service to auto-start on boot.
  pub const fn autostart_ref(&mut self) -> &mut Self {
    self.autostart = true;
    self
  }

  /// Sets the work directory that the service should start in.
  ///
  /// This is a utf-8 string rather than a `Path` or `PathBuf` because the
  /// directory tends to end up in places that have an utf-8 constraint.
  #[allow(clippy::needless_pass_by_value)]
  #[must_use]
  pub fn workdir(mut self, workdir: impl ToString) -> Self {
    self.workdir = Some(workdir.to_string());
    self
  }

  /// In-place version of [`Self::workdir()`].
  #[allow(clippy::needless_pass_by_value)]
  pub fn workdir_ref(&mut self, workdir: impl ToString) -> &mut Self {
    self.workdir = Some(workdir.to_string());
    self
  }

  /// Add a service dependency.
  ///
  /// Has no effect on macos.
  #[must_use]
  pub fn depend(mut self, dep: Depend) -> Self {
    self.deps.push(dep);
    self
  }

  /// Add a service dependency.
  ///
  /// Has no effect on macos.
  pub fn depend_ref(&mut self, dep: Depend) -> &mut Self {
    self.deps.push(dep);
    self
  }

  /// Perform the service registration.
  ///
  /// # Errors
  /// The error may be system/service subsystem specific.
  pub fn register(self) -> Result<(), Error> {
    #[cfg(windows)]
    winsvc::install(self)?;

    #[cfg(target_os = "macos")]
    launchd::install(self)?;

    #[cfg(all(target_os = "linux", feature = "systemd"))]
    systemd::install(self)?;

    Ok(())
  }
}


/// Deregister a service from a service subsystem.
///
/// # Errors
/// The error may be system/service subsystem specific.
#[allow(unreachable_code)]
pub fn uninstall(svcname: &str) -> Result<(), Error> {
  #[cfg(windows)]
  {
    winsvc::uninstall(svcname)?;
    return Ok(());
  }

  #[cfg(target_os = "macos")]
  {
    launchd::uninstall(svcname)?;
    return Ok(());
  }

  #[cfg(all(target_os = "linux", feature = "systemd"))]
  {
    systemd::uninstall(svcname)?;
    return Ok(());
  }

  Err(Error::Unsupported)
}

// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :
