//! Windows service module.
//!
//! While Windows services have a normal `main()` entry point, the way they
//! work is that a (blocking) runtime is called, which calls an application
//! callback where the service application logic is normally implemented.
//!
//! qsu does it a little differently; it runs the service application logic in
//! a thread (called `svcapp`) and runs the Windows service subsystem on the
//! main thread.  Its (the service subsystem's) callback is used to monitor the
//! subsystem for events, which are passed on to the qsu event handler
//! application callback.

use std::{
  any::{Any, TypeId},
  ffi::OsString,
  sync::OnceLock,
  thread,
  time::Duration
};

use hashbrown::HashMap;

use parking_lot::Mutex;

use tokio::sync::{
  broadcast,
  mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel},
  oneshot
};

use windows_service::{
  define_windows_service,
  service::{
    ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState,
    ServiceStatus, ServiceType
  },
  service_control_handler::{
    self, ServiceControlHandlerResult, ServiceStatusHandle
  },
  service_dispatcher
};

use windows_registry::LOCAL_MACHINE;

#[cfg(feature = "wait-for-debugger")]
use dbgtools_win::debugger;

use crate::{
  err::{CbErr, Error},
  lumberjack::LumberJack,
  rt::{Demise, RunEnv, SrvAppRt, SvcEvt, rttype}
};


const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS;
//const SERVICE_STARTPENDING_TIME: Duration = Duration::from_secs(10);
const SERVICE_STARTPENDING_TIME: Duration = Duration::from_secs(300);
const SERVICE_STOPPENDING_TIME: Duration = Duration::from_secs(30);


/// Messages that are sent to the service subsystem thread from the
/// application.
enum ToSvcMsg {
  Starting(u32),
  Started,
  Stopping(u32),
  Stopped
}

/// Buffer passed from main thread to service subsystem thread via global
/// `OnceLock`.
pub(crate) struct Xfer {
  svcname: String,

  /// Used to send handhake message from the service handler.
  tx_fromsvc: oneshot::Sender<Result<HandshakeMsg, Error>>,

  /// Service iniitialization handler passthrough.
  passthrough_init: HashMap<TypeId, Box<dyn Any + Send + Sync>>,

  /// Service termination handler passthrough.
  passthrough_term: HashMap<TypeId, Box<dyn Any + Send + Sync>>
}

/// Used as a "bridge" send information to service thread.
static CELL: OnceLock<Mutex<Option<Xfer>>> = OnceLock::new();


/// Buffer passed back to the application thread from the service subsystem
/// thread.
struct HandshakeMsg {
  /// Channel end-point used to send messages to the service subsystem.
  tx: UnboundedSender<ToSvcMsg>,

  /// Channel end-point used to send service event messages to the application
  /// callback.
  tx_svcevt: broadcast::Sender<SvcEvt>,

  /// Channel end-point used to receive messages from the service subsystem.
  rx_svcevt: broadcast::Receiver<SvcEvt>,

  /// Service initialization passthrough.
  passthrough_init: HashMap<TypeId, Box<dyn Any + Send + Sync>>,

  /// Service termination passthrough.
  passthrough_term: HashMap<TypeId, Box<dyn Any + Send + Sync>>
}


/// A service reporter that forwards application state information to the
/// windows service subsystem.
pub struct ServiceReporter {
  tx: UnboundedSender<ToSvcMsg>
}

impl super::StateReporter for ServiceReporter {
  fn starting(&self, checkpoint: u32, _status: &str) {
    if let Err(e) = self.tx.send(ToSvcMsg::Starting(checkpoint)) {
      log::error!("Unable to send Starting message; {e}");
    }
  }

  fn started(&self) {
    if let Err(e) = self.tx.send(ToSvcMsg::Started) {
      log::error!("Unable to send Started message; {e}");
    }
  }

  fn stopping(&self, checkpoint: u32, _status: &str) {
    if let Err(e) = self.tx.send(ToSvcMsg::Stopping(checkpoint)) {
      log::error!("Unable to send Stopping message; {e}");
    }
  }

  fn stopped(&self) {
    if let Err(e) = self.tx.send(ToSvcMsg::Stopped) {
      log::error!("Unable to send Stopped message; {e}");
    }
  }
}


/// Run a service application under the Windows service subsystem.
///
/// # Errors
/// `Error::SubSystem` menas the service could not be started. `Error::IO`
/// means the internal worker could not be launched.
#[allow(clippy::missing_panics_doc)]
pub fn run<ApEr>(
  svcname: &str,
  st: SrvAppRt<ApEr>,
  passthrough_init: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
  passthrough_term: HashMap<TypeId, Box<dyn Any + Send + Sync>>
) -> Result<(), CbErr<ApEr>>
where
  ApEr: Send + 'static + std::fmt::Debug
{
  #[cfg(feature = "wait-for-debugger")]
  {
    debugger::wait_for_then_break();
    debugger::output("Hello, debugger");
  }

  // Create a one-shot channel used to receive a an initial handshake from the
  // service handler thread.
  let (tx_fromsvc, rx_fromsvc) = oneshot::channel();

  // Create a buffer that will be used to transfer data to the service
  // subsystem's callback function.
  let xfer = Xfer {
    svcname: svcname.into(),
    tx_fromsvc,
    passthrough_init,
    passthrough_term
  };

  // Store Xfer buffer in the shared state (so the service handler thread can
  // take it out).
  // This must be done _before_ launching the application runtime thread below.
  CELL.get_or_init(|| Mutex::new(Some(xfer)));

  // Launch main application thread.
  //
  // The server application must be run on its own thread because the service
  // dispatcher call below will block the thread.
  let svcnm = svcname.to_string();
  let jh = thread::Builder::new()
    .name("svcapp".into())
    .spawn(move || srvapp_thread(st, svcnm, rx_fromsvc))?;

  // Register generated `ffi_service_main` with the system and start the
  // service, blocking this thread until the service is stopped.
  service_dispatcher::start(svcname, ffi_service_main)?;

  // The return value should be hard-coded to `Result<(), Error>`, so this
  // unwrap should be okay.
  match jh.join() {
    Ok(res) => {
      tracing::trace!("srvapp_thread::join res={res:?}");
      match res {
        Ok(()) => Ok(()),
        Err(be) => Err(be)
      }
    }
    Err(e) => {
      tracing::error!("srvapp_thread() could not be joined; {e:?}");
      let msg = format!("Unable to join srvapp_thread(); {e:?}");
      Err(CbErr::Lib(Error::Internal(msg)))
    }
  }
}


/// Internal server application wrapper thread.
fn srvapp_thread<ApEr>(
  st: SrvAppRt<ApEr>,
  svcname: String,
  rx_fromsvc: oneshot::Receiver<Result<HandshakeMsg, Error>>
) -> Result<(), CbErr<ApEr>>
where
  ApEr: Send + std::fmt::Debug
{
  // Wait for the service subsystem to report that it has initialized.
  // It passes along a channel end-point that can be used to send events to
  // the service manager.
  let Ok(res) = rx_fromsvc.blocking_recv() else {
    panic!("Unable to receive handshake");
  };

  let Ok(HandshakeMsg {
    tx,
    tx_svcevt,
    rx_svcevt,
    passthrough_init,
    passthrough_term
  }) = res
  else {
    panic!("Unable to receive handshake");
  };

  let sr = ServiceReporter { tx };
  let sr = super::ServiceReporter::new(sr);


  let re = RunEnv::Service(Some(svcname));

  match st {
    SrvAppRt::Sync {
      svcevt_handler,
      rt_handler
    } => rttype::sync_main(rttype::SyncMainParams {
      re,
      svcevt_handler,
      rt_handler,
      sr,
      svcevt_ch: Some((tx_svcevt, rx_svcevt)),
      passthrough_init,
      passthrough_term,

      // Don't support test mode when running as a windows service
      test_mode: false
    }),
    #[cfg(feature = "tokio")]
    SrvAppRt::Tokio {
      rtbldr,
      svcevt_handler,
      rt_handler
    } => rttype::tokio_main(
      rtbldr,
      rttype::TokioMainParams {
        re,
        svcevt_handler,
        rt_handler,
        sr,
        svcevt_ch: Some((tx_svcevt, rx_svcevt)),
        passthrough_init,
        passthrough_term
      }
    ),
    #[cfg(feature = "rocket")]
    SrvAppRt::Rocket {
      svcevt_handler,
      rt_handler
    } => rttype::rocket_main(rttype::RocketMainParams {
      re,
      svcevt_handler,
      rt_handler,
      sr,
      svcevt_ch: Some((tx_svcevt, rx_svcevt)),
      passthrough_init,
      passthrough_term
    })
  }
}


// Generate the windows service boilerplate.  The boilerplate contains the
// low-level service entry function (ffi_service_main) that parses incoming
// service arguments into Vec<OsString> and passes them to user defined service
// entry (my_service_main).
define_windows_service!(ffi_service_main, my_service_main);

fn take_shared_buffer() -> Xfer {
  let Some(x) = CELL.get() else {
    panic!("Unable to get shared buffer");
  };
  x.lock().take().unwrap()
}

/// The `Ok()` return value from [`svcinit()`].
struct InitRes {
  /// Value returned to the server application thread.
  handshake_reply: HandshakeMsg,

  rx_tosvc: UnboundedReceiver<ToSvcMsg>,

  status_handle: ServiceStatusHandle
}


/// Windows Service main entry point.
#[allow(clippy::needless_pass_by_value)]
fn my_service_main(_arguments: Vec<OsString>) {
  // Start by pulling out the service name and the channel sender.
  let Xfer {
    svcname,
    tx_fromsvc,
    passthrough_init,
    passthrough_term
  } = take_shared_buffer();

  match svcinit(&svcname, passthrough_init, passthrough_term) {
    Ok(InitRes {
      handshake_reply,
      rx_tosvc,
      status_handle
    }) => {
      // If svcinit() returned Ok(), it should have initialized logging.

      // Return Ok() to main server app thread so it will kick off the main
      // server application.
      if tx_fromsvc.send(Ok(handshake_reply)).is_err() {
        log::error!("Unable to send handshake message");
        return;
      }

      // Enter a loop that waits to receive a service termination event.
      svcloop(rx_tosvc, status_handle);
    }
    Err(e) => {
      // If svcinit() returns Err() we don't actually know if logging has been
      // enabled yet -- but we can't do much other than hope that it is and try
      // to output an error log.
      // ToDo: If dbgtools-win is used, then we should output to the debugger.
      if tx_fromsvc.send(Err(e)).is_err() {
        log::error!("Unable to send handshake message");
      }
    }
  }
}


fn svcinit(
  svcname: &str,
  passthrough_init: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
  passthrough_term: HashMap<TypeId, Box<dyn Any + Send + Sync>>
) -> Result<InitRes, Error> {
  // Set up logging *before* telling sending SvcRunning to caller
  // ToDo: Respect request not to initialize logging
  LumberJack::from_winsvc(svcname)?.service().init()?;

  // If the service has a WorkDir configured under it's Parameters subkey, then
  // retreive it and attempt to change directory to it.
  // This must be done _before_ sending the HandskageMsg back to the service
  // main thread.
  // ToDo: Need proper error handling:
  //       - If the Paramters subkey can not be loaded, do we abort?
  //       - If the cwd can not be changed to the WorkDir we should abort.
  if let Ok(svcparams) = get_service_params_subkey(svcname)
    && let Ok(wd) = svcparams.get_string("WorkDir")
  {
    std::env::set_current_dir(wd).map_err(|e| {
      Error::internal(format!("Unable to switch to WorkDir; {e}"))
    })?;
  }

  // Create channel that will be used to receive messages from the application.
  let (tx_tosvc, rx_tosvc) = unbounded_channel();

  // Create channel that will be used to send messages to the application.
  let (tx_svcevt, rx_svcevt) = broadcast::channel(16);

  //
  // Define system service event handler that will be receiving service events.
  //
  // ToDo: autoclone
  let tx_svcevt2 = tx_svcevt.clone();
  let event_handler = move |control_event| -> ServiceControlHandlerResult {
    match control_event {
      ServiceControl::Interrogate => {
        tracing::debug!("svc signal recieved: interrogate");
        // Notifies a service to report its current status information to the
        // service control manager.  Always return NoError even if not
        // implemented.
        ServiceControlHandlerResult::NoError
      }
      ServiceControl::Stop => {
        tracing::debug!("svc signal recieved: stop");

        // Message application that it's time to shutdown
        if let Err(e) = tx_svcevt2.send(SvcEvt::Shutdown(Demise::Terminated)) {
          log::error!("Unable to send SvcEvt::Shutdown from winsvc; {e}");
        }

        ServiceControlHandlerResult::NoError
      }
      ServiceControl::Continue => {
        tracing::debug!("svc signal recieved: continue");
        ServiceControlHandlerResult::NotImplemented
      }
      ServiceControl::Pause => {
        tracing::debug!("svc signal recieved: pause");
        ServiceControlHandlerResult::NotImplemented
      }
      _ => {
        tracing::debug!("svc signal recieved: other");
        ServiceControlHandlerResult::NotImplemented
      }
    }
  };


  let status_handle =
    service_control_handler::register(svcname, event_handler)?;

  if let Err(e) = status_handle.set_service_status(ServiceStatus {
    service_type: SERVICE_TYPE,
    current_state: ServiceState::StartPending,
    controls_accepted: ServiceControlAccept::empty(),
    exit_code: ServiceExitCode::Win32(0),
    checkpoint: 0,
    wait_hint: SERVICE_STARTPENDING_TIME,
    process_id: None
  }) {
    log::error!("Unable to set the sevice status to 'start pending 0'; {e}");
    Err(e)?;
  }

  Ok(InitRes {
    handshake_reply: HandshakeMsg {
      tx: tx_tosvc,
      tx_svcevt,
      rx_svcevt,
      passthrough_init,
      passthrough_term
    },
    rx_tosvc,
    status_handle
  })
}

/// Internal service state loop.
///
/// Receives the current service application state from an internal channel and
/// uses it to report the state to the windows service subsystem.
///
/// Once a _Stopped_ state is received, the state will be reported to winsvc
/// subsystem, and the loop will be broken out of so the thread exits.
fn svcloop(
  mut rx_tosvc: UnboundedReceiver<ToSvcMsg>,
  status_handle: ServiceStatusHandle
) {
  //
  // Enter loop that waits for application state changes that should be
  // reported to the service subsystem.
  // Once the application reports that it has stopped, then break out of the
  // loop.
  //
  tracing::trace!("enter app state monitoring loop");
  loop {
    let Some(ev) = rx_tosvc.blocking_recv() else {
      // All the sender halves have been deallocated
      log::error!("Sender endpoints unexpectedly disappeared");
      break;
    };
    match ev {
      ToSvcMsg::Starting(checkpoint) => {
        tracing::debug!("app reported that it is running");
        if let Err(e) = status_handle.set_service_status(ServiceStatus {
          service_type: SERVICE_TYPE,
          current_state: ServiceState::StartPending,
          controls_accepted: ServiceControlAccept::empty(),
          exit_code: ServiceExitCode::Win32(0),
          checkpoint,
          wait_hint: SERVICE_STARTPENDING_TIME,
          process_id: None
        }) {
          log::error!(
            "Unable to set service status to 'start pending {checkpoint}'; \
             {e}"
          );
        }
      }
      ToSvcMsg::Started => {
        if let Err(e) = status_handle.set_service_status(ServiceStatus {
          service_type: SERVICE_TYPE,
          current_state: ServiceState::Running,
          controls_accepted: ServiceControlAccept::STOP,
          exit_code: ServiceExitCode::Win32(0),
          checkpoint: 0,
          wait_hint: Duration::default(),
          process_id: None
        }) {
          log::error!("Unable to set service status to 'started'; {e}");
        }
      }
      ToSvcMsg::Stopping(checkpoint) => {
        tracing::debug!("app is shutting down");
        if let Err(e) = status_handle.set_service_status(ServiceStatus {
          service_type: SERVICE_TYPE,
          current_state: ServiceState::StopPending,
          controls_accepted: ServiceControlAccept::empty(),
          exit_code: ServiceExitCode::Win32(0),
          checkpoint,
          wait_hint: SERVICE_STOPPENDING_TIME,
          process_id: None
        }) {
          log::error!(
            "Unable to set service status to 'stop pending {checkpoint}'; {e}"
          );
        }
      }
      ToSvcMsg::Stopped => {
        if let Err(e) = status_handle.set_service_status(ServiceStatus {
          service_type: SERVICE_TYPE,
          current_state: ServiceState::Stopped,
          controls_accepted: ServiceControlAccept::empty(),
          exit_code: ServiceExitCode::Win32(0),
          checkpoint: 0,
          wait_hint: Duration::default(),
          process_id: None
        }) {
          log::error!("Unable to set service status to 'stopped'; {e}");
        }

        // Break out of loop to terminate service subsystem
        break;
      }
    }
  }

  tracing::trace!("service terminated");
}


const SVCPATH: &str = "SYSTEM\\CurrentControlSet\\Services";
const PARAMS: &str = "Parameters";


/// Create a read-only handle service's registry subkey.
///
/// `HKLM:SYSTEM\CurrentControlSet\Services\[servicename]`
///
/// # Errors
/// Registry errors are returned as `Error::SubSystem`.
pub fn read_service_subkey(
  service_name: &str
) -> Result<windows_registry::Key, Error> {
  let key = LOCAL_MACHINE.open(SVCPATH)?.open(service_name)?;
  Ok(key)
}

/// Create a read/write handle for service's registry subkey.
///
/// `HKLM:SYSTEM\CurrentControlSet\Services\[servicename]`
///
/// # Errors
/// Registry errors are returned as `Error::SubSystem`.
pub fn write_service_subkey(
  service_name: &str
) -> Result<windows_registry::Key, Error> {
  let key = LOCAL_MACHINE.open(SVCPATH)?.create(service_name)?;
  Ok(key)
}

/// Create a `Parameters` subkey for a service, and return a handle to it
///
/// `HKLM:SYSTEM\CurrentControlSet\Services\[servicename]\Parameters`
///
/// # Errors
/// Registry errors are returned as `Error::SubSystem`.
pub fn create_service_params(
  service_name: &str
) -> Result<windows_registry::Key, Error> {
  let key = LOCAL_MACHINE
    .open(SVCPATH)?
    .open(service_name)?
    .create(PARAMS)?;
  Ok(key)
}

/// Get a read/write handle for the `Parameters` subkey for a service.
///
/// `HKLM:SYSTEM\CurrentControlSet\Services\[servicename]\Parameters`
///
/// # Errors
/// Registry errors are returned as `Error::SubSystem`.
pub fn get_service_params_subkey(
  service_name: &str
) -> Result<windows_registry::Key, Error> {
  let key = LOCAL_MACHINE
    .open(SVCPATH)?
    .open(service_name)?
    .create(PARAMS)?;
  Ok(key)
}

/// Load a service `Parameter` from the registry.
///
/// `HKLM:SYSTEM\CurrentControlSet\Services\[servicename]\Parameters`
///
/// # Errors
/// Registry errors are returned as `Error::SubSystem`.
pub fn get_service_param(
  service_name: &str
) -> Result<windows_registry::Key, Error> {
  let key = LOCAL_MACHINE
    .open(SVCPATH)?
    .open(service_name)?
    .open(PARAMS)?;
  Ok(key)
}

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