//! Helpers for integrating clap into an application using _qsu_.
//!
//! The purpose of the argument parser is to allow a semi-standard command line
//! interface for registering, deregistering and allowing services to run both
//! in the foreground as well as run as actual services.  To accomplish this,
//! the argument parser wraps around the runtime.  Specifically, this means
//! that if the argument parser is used, the application does not need to
//! launch the runtime explicitly; the argument parser will do this if the
//! appropriate arguments have been passed to it.
//!
//! Assuming a service handler has been implemented already, a simple use-case
//! for the argument parser in a server application involves:
//! 1. Implement the [`ArgsProc`] trait on an application object, and implement
//!    the [`ArgsProc::build_apprt()`] method.  This method should return a
//!    [`SrvAppRt`] object, containing the service handler.
//! 2. Create an instance of the [`ArgParser`], passing in the service name and
//!    a reference to an [`ArgsProc`] object.
//! 3. Call [`ArgParser::proc()`] to process the command line arguments.
//!
//! The argument parser will determine whether the caller requested to
//! register/deregister the service, run the server application in foreground
//! mode or as a system service.
//!
//! # Customization
//! While the argument parser in _qsu_ is rather opinionated, it does allow
//! for _some_ customization.  The names of the subcommands to (de)register or
//! run the server application as a service can be modified by the
//! application.  It is possible to register custom subcommands, and the
//! command line parser specification can be modified by the [`ArgsProc`]
//! callbacks.

use clap::{Arg, ArgAction, ArgMatches, Args, Command, builder::Str};

use crate::{
  err::CbErr,
  installer::{self, RegSvc},
  lumberjack::LogLevel,
  rt::{RunCtx, SrvAppRt}
};


/// Register service.
#[derive(Debug, Args)]
struct RegSvcArgs {
  /// Autostart service at boot.
  #[arg(short = 's', long)]
  auto_start: bool,

  /// Set an optional display name for the service.
  ///
  /// Only used on Windows.
  #[arg(short = 'D', long, value_name = "DISPNAME")]
  display_name: Option<String>,

  /// Set an optional one-line service description.
  ///
  /// Only used on Windows and Linux with systemd.
  #[arg(short, long, value_name = "DESC")]
  description: Option<String>,

  /// Add a command line argument to the service command line.
  #[arg(short, long)]
  arg: Vec<String>,

  /// Add an environment variable to the service.
  #[arg(short, long, num_args(2), value_names=["KEY", "VALUE"])]
  env: Vec<String>,

  /// Set an optional directory the service runtime should start in.
  #[arg(short, long, value_name = "DIR")]
  workdir: Option<String>,

  #[arg(long, value_enum, value_name = "LEVEL")]
  log_level: Option<LogLevel>,

  #[arg(long, value_enum, value_name = "FILTER")]
  log_filter: Option<String>,

  #[arg(long, hide(true), value_name = "FILTER")]
  trace_filter: Option<String>,

  #[arg(long, value_enum, hide(true), value_name = "FNAME")]
  trace_file: Option<String>,

  /// Forcibly install service.
  ///
  /// On Windows, attempt to stop and uninstall service if it already exists.
  ///
  /// On macos/launchd and linux/systemd, overwrite the service specification
  /// file if it already exists.
  #[arg(long, short)]
  force: bool
}

/// Create a `clap` [`Command`] object that accepts service registration
/// arguments.
///
/// It is recommended that applications use the higher-level `ArgParser`
/// instead, but this call exists in case applications need finer grained
/// control.
#[must_use]
pub fn mk_inst_cmd(cmd: &str, svcname: &str) -> Command {
  let namearg = Arg::new("svcname")
    .short('n')
    .long("name")
    .action(ArgAction::Set)
    .value_name("SVCNAME")
    .default_value(Str::from(svcname.to_string()))
    .help("Set service name");

  //Command::new(cmd).arg(namearg).arg(autostartarg)
  let cli = Command::new(cmd.to_string()).arg(namearg);

  RegSvcArgs::augment_args(cli)
}


/// Deregister service.
#[derive(Debug, Args)]
struct DeregSvcArgs {}

/// Create a `clap` [`Command`] object that accepts service deregistration
/// arguments.
///
/// It is recommended that applications use the higher-level `ArgParser`
/// instead, but this call exists in case applications need finer grained
/// control.
#[must_use]
pub fn mk_rm_cmd(cmd: &str, svcname: &str) -> Command {
  let namearg = Arg::new("svcname")
    .short('n')
    .long("name")
    .action(ArgAction::Set)
    .value_name("SVCNAME")
    .default_value(svcname.to_string())
    .help("Name of service to remove");

  let cli = Command::new(cmd.to_string()).arg(namearg);

  DeregSvcArgs::augment_args(cli)
}


/// Parsed service deregistration arguments.
pub struct DeregSvc {
  pub svcname: String
}

impl DeregSvc {
  #[allow(
    clippy::missing_panics_doc,
    reason = "svcname should always have been set"
  )]
  #[must_use]
  pub fn from_cmd_match(matches: &ArgMatches) -> Self {
    // unwrap() should be okay here, because svcname should always be set.
    let svcname = matches.get_one::<String>("svcname").unwrap().to_owned();
    Self { svcname }
  }
}


pub(crate) enum ArgpRes<'cb, ApEr> {
  /// Run server application.
  RunApp(RunCtx, &'cb mut dyn ArgsProc<AppErr = ApEr>),

  /// Nothing to do (service was probably registered/deregistred).
  Quit
}


/// Parsed service running arguments.
pub struct RunSvc {
  pub as_service: bool,
  pub svcname: String
}

impl RunSvc {
  #[allow(
    clippy::missing_panics_doc,
    reason = "svcname should always have been set"
  )]
  #[must_use]
  pub fn from_cmd_match(matches: &ArgMatches) -> Self {
    let as_service = matches.get_one::<bool>("as_service").unwrap().to_owned();
    let svcname = matches
      .get_one::<String>("service_name")
      .unwrap()
      .to_owned();
    Self {
      as_service,
      svcname
    }
  }
}


/// Used to differentiate between running without a subcommand, or the
/// install/uninstall or run service subcommands.
#[derive(Debug, PartialEq, Eq)]
pub enum Cmd {
  /// The root `Command`.
  Root,

  /// The service registration/installation `Command`.
  Inst,

  /// The service deregistration/uninstallation `Command`.
  Rm,

  /// The service run `Command`.
  Run
}

/// Allow application to customise behavior of an [`ArgParser`] instance.
pub trait ArgsProc {
  type AppErr;

  /// Give the application an opportunity to modify the root and subcommand
  /// `Command`s.
  ///
  /// `cmdtype` indicates whether `cmd` is the root `Command` or one of the
  /// subcommand `Command`s.
  ///
  /// # Errors
  /// Application-defined error will be returned as `CbErr::Aop` to the
  /// original caller.
  #[allow(unused_variables)]
  fn conf_cmd(
    &mut self,
    cmdtype: Cmd,
    cmd: Command
  ) -> Result<Command, Self::AppErr> {
    Ok(cmd)
  }

  /// Callback allowing application to configure the service registration
  /// context just before the service is registered.
  ///
  /// This trait method can, among other things, be used by an application to:
  /// - Configure a service work directory.
  /// - Add environment variables.
  /// - Add command like arguments to the run command.
  ///
  /// The `sub_m` argument represents `clap`'s parsed subcommand context for
  /// the service registration subcommand.  Applications that want to add
  /// custom arguments to the parser should implement the
  /// [`ArgsProc::conf_cmd()`] trait method and perform the subcommand
  /// augmentation there.
  ///
  /// This callback is called after the system-defined command line arguments
  /// have been parsed and populated into `regsvc`.  Implementation should be
  /// careful not to overwrite settings that should be configurable via the
  /// command line, and can inspect the current value of fields before setting
  /// them if conditional reconfiguations are required.
  ///
  /// The default implementation does nothing but return `regsvc` unmodified.
  ///
  /// # Errors
  /// Application-defined error will be returned as `CbErr::Aop` to the
  /// original caller.
  #[allow(unused_variables)]
  fn proc_inst(
    &mut self,
    sub_m: &ArgMatches,
    regsvc: RegSvc
  ) -> Result<RegSvc, Self::AppErr> {
    Ok(regsvc)
  }

  /// Callback allowing application to configure the service deregistration
  /// context just before the service is deregistered.
  ///
  /// # Errors
  /// Application-defined error will be returned as `CbErr::Aop` to the
  /// original caller.
  #[allow(unused_variables)]
  fn proc_rm(
    &mut self,
    sub_m: &ArgMatches,
    deregsvc: DeregSvc
  ) -> Result<DeregSvc, Self::AppErr> {
    Ok(deregsvc)
  }

  /// Callback allowing application to configure the run context before
  /// launching the server application.
  ///
  /// qsu will have performed all its own initialization of the [`RunCtx`]
  /// before calling this function.
  ///
  /// The application can differentiate between running in a service mode and
  /// running as a foreground by calling [`RunCtx::is_service()`].
  ///
  /// # Errors
  /// Application-defined error will be returned as `CbErr::Aop` to the
  /// original caller.
  #[allow(unused_variables)]
  fn proc_run(
    &mut self,
    matches: &ArgMatches,
    runctx: RunCtx
  ) -> Result<RunCtx, Self::AppErr> {
    Ok(runctx)
  }

  /// Called when a subcommand is encountered that is not one of the three
  /// subcommands regognized by qsu.
  ///
  /// # Errors
  /// Application-defined error will be returned as `CbErr::Aop` to the
  /// original caller.
  #[allow(unused_variables)]
  fn proc_other(
    &mut self,
    subcmd: &str,
    sub_m: &ArgMatches
  ) -> Result<(), Self::AppErr> {
    Ok(())
  }

  /// Construct an server application runtime.
  ///
  /// The implementor must return a service handler wrapped in a `SrvAppRt`.
  ///
  /// # Errors
  /// Application-defined error will be returned as `CbErr::Aop` to the
  /// original caller.
  fn build_apprt(
    &mut self,
    runctx: &mut RunCtx
  ) -> Result<SrvAppRt<Self::AppErr>, Self::AppErr>;
}


/// Add some common package metadata to a clap `Command`.
///
/// Sets:
/// - `name` from `CARGO_PKG_NAME`.
/// - `about` from `CARGO_PKG_DESCRIPTION`.
/// - `version` from `CARGO_PKG_VERSION`.
///
/// ```
/// use qsu::{clap::Command, init_command};
///
/// let mut cmd = Command::new("");
/// cmd = init_command!(cmd);
/// ```
#[macro_export]
macro_rules! init_command {
  ($cmd:expr) => {
    $cmd
      .name(env!("CARGO_PKG_NAME"))
      .about(env!("CARGO_PKG_DESCRIPTION"))
      .version(env!("CARGO_PKG_VERSION"))
  };
}

/// High-level argument parser.
///
/// This is suitable for applications that follow a specific pattern:
/// - It has subcommands for:
///   - Registering a service
///   - Deregistering a service
///   - Running as a service
/// - Running without any subcommands should run the server application as a
///   foreground process.
pub struct ArgParser<'cb, ApEr> {
  svcname: String,
  reg_subcmd: String,
  dereg_subcmd: String,
  cli: Command,
  cb: &'cb mut dyn ArgsProc<AppErr = ApEr>,
  runcb: Option<Box<dyn FnOnce(RunCtx) -> RunCtx>>,
  regcb: Option<Box<dyn FnOnce(RegSvc) -> RegSvc>>
}


impl<'cb, ApEr> ArgParser<'cb, ApEr>
where
  ApEr: Send + 'static
{
  fn setup_run_options(mut cli: Command, svcname: &str) -> Command {
    // Add -S,--as-service to argument parser
    let as_service = Arg::new("as_service")
      .short('S')
      .long("as-service")
      .action(ArgAction::SetTrue)
      .help("Run as a system service");
    cli = cli.arg(as_service);

    // Add -N,--service-name to argument parser
    let svcnamearg = Arg::new("service_name")
      .short('N')
      .long("service-name")
      .action(ArgAction::Set)
      .value_name("SVCNAME")
      .default_value(Str::from(svcname.to_string()))
      .help("Set service name");
    cli = cli.arg(svcnamearg);

    cli
  }

  /// Create a new argument parser.
  ///
  /// `svcname` is the _default_ service name.  It may be overridden using
  /// command line arguments.
  pub fn new(svcname: &str, cb: &'cb mut dyn ArgsProc<AppErr = ApEr>) -> Self {
    let mut cli = Command::new("");

    cli = Self::setup_run_options(cli, svcname);

    Self {
      svcname: svcname.to_string(),
      reg_subcmd: "register-service".into(),
      dereg_subcmd: "deregister-service".into(),
      cli,
      cb,
      runcb: None,
      regcb: None
    }
  }

  /// Create a new argument parser, basing the root command parser on an
  /// application-supplied `Command`.
  ///
  /// `svcname` is the _default_ service name.  It may be overridden using
  /// command line arguments.
  pub fn with_cmd(
    svcname: &str,
    mut cli: Command,
    cb: &'cb mut dyn ArgsProc<AppErr = ApEr>
  ) -> Self {
    cli = Self::setup_run_options(cli, svcname);

    Self {
      svcname: svcname.to_string(),
      reg_subcmd: "register-service".into(),
      dereg_subcmd: "deregister-service".into(),
      cli,
      cb,
      runcb: None,
      regcb: None
    }
  }

  /// Call a closure which is passed the argument parser's internal root
  /// `Command`.
  ///
  /// The application can implement [`ArgsProc::conf_cmd()`] and use the root
  /// callback to accomplish the same functionallity.  The main difference is
  /// that `setup_cmd()` is called immediately, while the `conf_cmd` method is
  /// implemented when argument parsing has been requested.
  ///
  /// ```
  /// use qsu::{
  ///   rt::{RunCtx, SrvAppRt},
  ///   argp::{ArgParser, ArgsProc},
  ///   clap::Command
  /// };
  ///
  /// struct AP {}
  ///
  /// impl ArgsProc for AP {
  ///   type AppErr = String;
  ///
  ///   fn build_apprt(
  ///     &mut self,
  ///     runctx: &mut RunCtx
  ///   ) -> Result<SrvAppRt<Self::AppErr>, Self::AppErr> {
  ///     todo!()
  ///   }
  /// }
  ///
  /// let f = |cmd: Command| {
  ///   cmd
  ///     .name(env!("CARGO_PKG_NAME"))
  ///     .about(env!("CARGO_PKG_DESCRIPTION"))
  ///     .version(env!("CARGO_PKG_VERSION"))
  /// };
  /// let mut argproc = AP {};
  /// let ap = ArgParser::new("myservice", &mut argproc).setup_cmd(f);
  /// ```
  #[must_use]
  pub fn setup_cmd<F>(mut self, f: F) -> Self
  where
    F: FnOnce(Command) -> Command
  {
    self.cli = f(self.cli);
    self
  }

  /// Rename the service registration subcommand.
  #[must_use]
  pub fn reg_subcmd(mut self, nm: &str) -> Self {
    self.reg_subcmd = nm.to_string();
    self
  }

  /// Rename the service deregistration subcommand.
  #[must_use]
  pub fn dereg_subcmd(mut self, nm: &str) -> Self {
    self.dereg_subcmd = nm.to_string();
    self
  }

  fn inner_proc(self) -> Result<ArgpRes<'cb, ApEr>, CbErr<ApEr>> {
    let matches = match self.cli.try_get_matches() {
      Ok(m) => m,
      Err(e) => match e.kind() {
        clap::error::ErrorKind::DisplayHelp
        | clap::error::ErrorKind::DisplayVersion => {
          e.exit();
        }
        _ => {
          // ToDo: Convert error to Error::ArgP, pass along the error type so
          // that the Termination handler can output the specific error.
          //Err(e)?;
          e.exit();
        }
      }
    };
    match matches.subcommand() {
      Some((subcmd, sub_m)) if subcmd == self.reg_subcmd => {
        //println!("{:#?}", sub_m);

        // Convert known register-service command line arguments to a RegSvc
        // context.
        let mut regsvc = RegSvc::from_cmd_match(sub_m);

        // To trigger the server to run in service mode, run with the
        // option "--as-service".
        //
        // We're making a pretty safe assumption that the service will
        // be run though qsu's argument processor because it is being
        // registered using it.
        //
        // If the service name is different that the name drived from the
        // executable's name, then add "--service-name <svcname>" arguments.
        let mut args = vec![String::from("--as-service")];
        if regsvc.svcname() != self.svcname {
          args.push(String::from("--service-name"));
          args.push(regsvc.svcname().to_string());
        }
        regsvc.args_ref(args);

        // Call application call-back, to allow application-specific
        // service configuration.
        //
        // This is a good place to stick custom environment, arguments,
        // registry changes that may depend on command line arguments.
        let regsvc = self
          .cb
          .proc_inst(sub_m, regsvc)
          .map_err(|ae| CbErr::App(ae))?;

        // Give the application a final chance to modify the service
        // registration parameters.
        //
        // This can be used to set hardcoded parameters like service display
        // name, description, etc.
        let regsvc = if let Some(cb) = self.regcb {
          cb(regsvc)
        } else {
          regsvc
        };

        // Register the service with the operating system's service subsystem.
        regsvc.register()?;

        Ok(ArgpRes::Quit)
      }
      Some((subcmd, sub_m)) if subcmd == self.dereg_subcmd => {
        // Get arguments relating to service deregistration.
        let args = DeregSvc::from_cmd_match(sub_m);

        let args =
          self.cb.proc_rm(sub_m, args).map_err(|ae| CbErr::App(ae))?;

        installer::uninstall(&args.svcname)?;

        Ok(ArgpRes::Quit)
      }
      Some((subcmd, sub_m)) => {
        // Call application callback for processing "other" subcmd
        self
          .cb
          .proc_other(subcmd, sub_m)
          .map_err(|ae| CbErr::App(ae))?;

        // Return a run context for a background service process.
        Ok(ArgpRes::Quit)
      }
      _ => {
        let args = RunSvc::from_cmd_match(&matches);

        // Prepare a service run context
        let mut rctx = RunCtx::new(&self.svcname);

        // If user passed `-S`/`--as-service`, then mark run context as a
        // service
        if args.as_service {
          rctx = rctx.service();
        }

        // Allow application the opportunity to modify the run context based on
        // the parsed command line.
        rctx = self
          .cb
          .proc_run(&matches, rctx)
          .map_err(|ae| CbErr::App(ae))?;

        // Tell caller to run the service runtime
        Ok(ArgpRes::RunApp(rctx, self.cb))
      }
    }
  }

  /// Register a callback function to be called when running a service.
  #[must_use]
  pub fn runsvc_proc(
    mut self,
    f: impl FnOnce(RunCtx) -> RunCtx + 'static
  ) -> Self {
    self.runcb = Some(Box::new(f));
    self
  }

  /// Register a closure that will be called before service registration.
  ///
  /// This callback serves a different purpose than implementing the
  /// [`ArgsProc::proc_inst()`] method, which is primarily meant to tranlate
  /// command line arguments to service registration parameters.
  ///
  /// The `regsvc_proc()` callback on the other hand is called just before
  /// actual registration and is intended to overwrite service registration
  /// parametmer.  It is suitable to use this callback to set hard-coded
  /// application-specific service parameters, like service display name,
  /// description, dependencies, and other parameters that the user should
  /// not need to specify manually.
  ///
  /// Just as with `ArgsProc::proc_inst()` this callback is called after the
  /// system-defined command line arguments have been parsed and populated
  /// into the [`RegSvc`] context.  Closures should be careful not to overwrite
  /// settings that should be configurable via the command line, and can
  /// inspect the current value of fields before setting them if conditional
  /// reconfiguations are required.
  #[must_use]
  pub fn regsvc_proc(
    mut self,
    f: impl FnOnce(RegSvc) -> RegSvc + 'static
  ) -> Self {
    self.regcb = Some(Box::new(f));
    self
  }

  /// Process command line arguments.
  ///
  /// Calling this method will initialize the command line parser, parse the
  /// command line, using the associated [`ArgsProc`] as appropriate to modify
  /// the argument parser, and then take the appropriate action:
  ///
  /// - If a service registration was requested, the service will be registered
  ///   and then the function will return.
  /// - If a service deregistration was requested, the service will be
  ///   deregistered and then the function will return.
  /// - If a service run was requested, then set up the service subsystem and
  ///   launch the server application under it.
  /// - If an application-defined subcommand was called, then process it using
  ///   [`ArgsProc::proc_other()`] and then exit.
  /// - If none of the above subcommands where issued, then run the server
  ///   application as a foreground process.
  ///
  /// The `bldr` is a closure that will be called to yield the `SrvAppRt` in
  /// case the service was requested to run.
  ///
  /// # Service registration behavior
  /// The default service registration behavior in qsu will:
  /// - Assume that the executable being used to register the service is the
  ///   same one that will run the service.
  /// - Add the "run service" subcommand to the service's command line
  ///   arguments.
  /// - If the specified service name is different than the default service
  ///   name (determined by
  ///   [`default_service_name()`](crate::default_service_name), then the
  ///   aguments `--name <service name>` will be added.
  ///
  /// # Errors
  /// Application-defined error will be returned as `CbErr::Aop` to the
  /// original caller.
  pub fn proc(mut self) -> Result<(), CbErr<ApEr>>
  where
    ApEr: std::fmt::Debug
  {
    // Give application the opportunity to modify root Command
    self.cli = self
      .cb
      .conf_cmd(Cmd::Root, self.cli)
      .map_err(|ae| CbErr::App(ae))?;

    let runcb = self.runcb.take();

    // Create registration subcommand and give application the opportunity to
    // modify the subcommand's Command
    let sub = mk_inst_cmd(&self.reg_subcmd, &self.svcname);
    let sub = self
      .cb
      .conf_cmd(Cmd::Inst, sub)
      .map_err(|ae| CbErr::App(ae))?;
    self.cli = self.cli.subcommand(sub);

    // Create deregistration subcommand
    let sub = mk_rm_cmd(&self.dereg_subcmd, &self.svcname);
    let sub = self
      .cb
      .conf_cmd(Cmd::Rm, sub)
      .map_err(|ae| CbErr::App(ae))?;
    self.cli = self.cli.subcommand(sub);

    // Parse command line arguments.  Run the service application if requiested
    // to do so.
    if let ArgpRes::RunApp(mut runctx, cb) = self.inner_proc()? {
      // Argument parser asked us to run, so call the application to ask it to
      // create the service handler, and then kick off the service runtime.

      let st = cb.build_apprt(&mut runctx).map_err(|ae| CbErr::App(ae))?;

      // If application registered a pre-run callback, then call it now to let
      // it perform updates to the run context.
      if let Some(runcb) = runcb {
        runctx = runcb(runctx);
      }

      runctx.run(st)?;
    }
    Ok(())
  }
}

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