use std::thread;

use nix::sys::signal::{SigSet, SigmaskHow, Signal};

#[cfg(feature = "tokio")]
use tokio::signal::unix::{SignalKind, signal};

#[cfg(feature = "tokio")]
use killswitch::KillSwitch;

use crate::err::Error;


/// Async task used to wait for SIGINT/SIGTERM.
///
/// Whenever a SIGINT or SIGTERM is signalled the closure in `f` is called and
/// the task is terminated.
#[cfg(feature = "tokio")]
pub async fn wait_shutdown<F>(f: F, ks: KillSwitch)
where
  F: FnOnce() + Send
{
  wait_oneshot_signal(SignalKind::interrupt(), f, ks).await;
}

#[cfg(feature = "tokio")]
pub async fn wait_term<F>(f: F, ks: KillSwitch)
where
  F: FnOnce() + Send
{
  wait_oneshot_signal(SignalKind::terminate(), f, ks).await;
}

/// Async task used to wait for SIGHUP
///
/// Whenever a SIGHUP is signalled the closure in `f` is called.
#[cfg(feature = "tokio")]
pub async fn wait_reload<F>(f: F, ks: KillSwitch)
where
  F: Fn() + Send
{
  wait_repeating_signal(SignalKind::hangup(), f, ks).await;
}

#[cfg(feature = "tokio")]
pub async fn wait_user1<F>(f: F, ks: KillSwitch)
where
  F: Fn() + Send
{
  wait_repeating_signal(SignalKind::user_defined1(), f, ks).await;
}

#[cfg(feature = "tokio")]
pub async fn wait_user2<F>(f: F, ks: KillSwitch)
where
  F: Fn() + Send
{
  wait_repeating_signal(SignalKind::user_defined2(), f, ks).await;
}


#[cfg(feature = "tokio")]
pub async fn wait_repeating_signal<F>(
  sigkind: SignalKind,
  f: F,
  ks: KillSwitch
) where
  F: Fn() + Send
{
  tracing::trace!("Repeating {:?} task launched", sigkind);

  let Ok(mut sig) = signal(sigkind) else {
    log::error!("Unable to create {sigkind:?} Future");
    return;
  };

  loop {
    tokio::select! {
      _ = sig.recv() => {
        tracing::debug!("Received {:?} -- running closure", sigkind);
        f();
      }
      () = ks.wait() => {
        tracing::debug!("killswitch triggered");
        break;
      }
    }
  }

  tracing::trace!("{:?} terminating", sigkind);
}


#[cfg(feature = "tokio")]
pub async fn wait_oneshot_signal<F>(sigkind: SignalKind, f: F, ks: KillSwitch)
where
  F: FnOnce() + Send
{
  tracing::trace!("One-shot {:?} task launched", sigkind);

  let Ok(mut sig) = signal(sigkind) else {
    log::error!("Unable to create {sigkind:?} Future");
    return;
  };

  // Wait for either SIGUSR1.
  tokio::select! {
    _ = sig.recv() => {
      tracing::debug!("Received {sigkind:?} -- running closure" );
      f();
    }
    () = ks.wait() => {
      tracing::debug!("killswitch triggered");
    }
  }

  tracing::trace!("{:?} terminating", sigkind);
}


pub enum SigType {
  Usr1,
  Usr2,
  Int,
  Term,
  Hup
}

pub fn sync_sigmon<F>(f: F) -> Result<thread::JoinHandle<()>, Error>
where
  F: Fn(SigType) + Send + 'static
{
  //
  // Block signals-of-interest on main thread.
  //
  let mut ss = SigSet::empty();
  ss.add(Signal::SIGINT);
  ss.add(Signal::SIGTERM);
  ss.add(Signal::SIGHUP);
  ss.add(Signal::SIGUSR1);
  ss.add(Signal::SIGUSR2);

  let mut oldset = SigSet::empty();
  nix::sys::signal::pthread_sigmask(
    SigmaskHow::SIG_SETMASK,
    Some(&ss),
    Some(&mut oldset)
  )
  .unwrap();

  let jh = thread::Builder::new()
    .name("sigmon".into())
    .spawn(move || {
      // Note: Don't need to unblock signals in this thread, because sigwait()
      // does it implicitly.
      let mask = unsafe {
        let mut mask: libc::sigset_t = std::mem::zeroed();
        libc::sigemptyset(&raw mut mask);
        libc::sigaddset(&raw mut mask, libc::SIGINT);
        libc::sigaddset(&raw mut mask, libc::SIGTERM);
        libc::sigaddset(&raw mut mask, libc::SIGHUP);
        libc::sigaddset(&raw mut mask, libc::SIGUSR1);
        libc::sigaddset(&raw mut mask, libc::SIGUSR2);
        mask
      };

      loop {
        let mut sig: libc::c_int = 0;
        let ret = unsafe { libc::sigwait(&raw const mask, &raw mut sig) };
        if ret == 0 {
          let signal = Signal::try_from(sig).unwrap();
          match signal {
            Signal::SIGUSR1 => {
              f(SigType::Usr1);
            }
            Signal::SIGUSR2 => {
              f(SigType::Usr2);
            }
            Signal::SIGINT => {
              f(SigType::Int);
              break;
            }
            Signal::SIGTERM => {
              f(SigType::Term);
              break;
            }
            Signal::SIGHUP => {
              f(SigType::Hup);
            }
            _ => {}
          }
        }
      }
    })?;

  Ok(jh)
}

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