// Copyright 2018-2026 the Deno authors. MIT license.

use std::collections::HashMap;
use std::sync::Mutex;
use std::sync::OnceLock;
use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;

use signal_hook::consts::*;
use tokio::sync::watch;

mod dict;
pub use dict::*;

#[cfg(windows)]
static SIGHUP: i32 = 1;
#[cfg(windows)]
static SIGWINCH: i32 = 28;

static COUNTER: AtomicU32 = AtomicU32::new(0);

type Handler = Box<dyn Fn() + Send>;
type Handlers = HashMap<i32, Vec<(u32, bool, Handler)>>;
static HANDLERS: OnceLock<(Handle, Mutex<Handlers>)> = OnceLock::new();

#[cfg(unix)]
struct Handle(signal_hook::iterator::Handle);

#[cfg(windows)]
struct Handle;

fn handle_signal(signal: i32) -> bool {
  let Some((_, handlers)) = HANDLERS.get() else {
    return false;
  };
  let handlers = handlers.lock().unwrap();
  let Some(handlers) = handlers.get(&signal) else {
    return false;
  };

  let mut handled = false;

  for (_, prevent_default, f) in handlers {
    if *prevent_default {
      handled = true;
    }

    f();
  }

  handled
}

#[cfg(unix)]
fn init() -> Handle {
  use signal_hook::iterator::Signals;

  let mut signals = Signals::new([SIGHUP, SIGTERM, SIGINT]).unwrap();
  let handle = signals.handle();

  std::thread::spawn(move || {
    for signal in signals.forever() {
      let handled = handle_signal(signal);
      if !handled {
        if signal == SIGHUP || signal == SIGTERM || signal == SIGINT {
          run_exit();
        }
        signal_hook::low_level::emulate_default_handler(signal).unwrap();
      }
    }
  });

  Handle(handle)
}

#[cfg(windows)]
fn init() -> Handle {
  unsafe extern "system" fn handle(ctrl_type: u32) -> i32 {
    let signal = match ctrl_type {
      0 => SIGINT,
      1 => SIGBREAK,
      2 => SIGHUP,
      5 => SIGTERM,
      6 => SIGTERM,
      _ => return 0,
    };
    let handled = handle_signal(signal);
    handled as _
  }

  // SAFETY: Registering handler
  unsafe {
    windows_sys::Win32::System::Console::SetConsoleCtrlHandler(Some(handle), 1);
  }

  Handle
}

#[cfg(windows)]
fn start_sigwinch_polling() {
  static STARTED: OnceLock<()> = OnceLock::new();
  STARTED.get_or_init(|| {
    std::thread::spawn(|| {
      // SAFETY: Win32 calls to open CONOUT$ and poll console size
      unsafe {
        let conout_name: Vec<u16> =
          "CONOUT$".encode_utf16().chain(Some(0)).collect();
        let handle = windows_sys::Win32::Storage::FileSystem::CreateFileW(
          conout_name.as_ptr(),
          windows_sys::Win32::Foundation::GENERIC_READ,
          windows_sys::Win32::Storage::FileSystem::FILE_SHARE_READ
            | windows_sys::Win32::Storage::FileSystem::FILE_SHARE_WRITE,
          std::ptr::null(),
          windows_sys::Win32::Storage::FileSystem::OPEN_EXISTING,
          0,
          std::ptr::null_mut(),
        );
        if handle == windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE {
          return;
        }

        let mut prev_cols: i32 = 0;
        let mut prev_rows: i32 = 0;

        // Read initial size
        let mut bufinfo: windows_sys::Win32::System::Console::CONSOLE_SCREEN_BUFFER_INFO =
          std::mem::zeroed();
        if windows_sys::Win32::System::Console::GetConsoleScreenBufferInfo(handle, &mut bufinfo)
          != 0
        {
          prev_cols =
            bufinfo.srWindow.Right as i32 - bufinfo.srWindow.Left as i32 + 1;
          prev_rows =
            bufinfo.srWindow.Bottom as i32 - bufinfo.srWindow.Top as i32 + 1;
        }

        loop {
          windows_sys::Win32::System::Threading::Sleep(250);

          let mut bufinfo: windows_sys::Win32::System::Console::CONSOLE_SCREEN_BUFFER_INFO =
            std::mem::zeroed();
          if windows_sys::Win32::System::Console::GetConsoleScreenBufferInfo(
            handle,
            &mut bufinfo,
          ) == 0
          {
            continue;
          }

          let cols =
            bufinfo.srWindow.Right as i32 - bufinfo.srWindow.Left as i32 + 1;
          let rows =
            bufinfo.srWindow.Bottom as i32 - bufinfo.srWindow.Top as i32 + 1;

          if cols != prev_cols || rows != prev_rows {
            prev_cols = cols;
            prev_rows = rows;
            handle_signal(SIGWINCH);
          }
        }
      }
    });
  });
}

pub fn register(
  signal: i32,
  prevent_default: bool,
  f: Box<dyn Fn() + Send>,
) -> Result<u32, std::io::Error> {
  if is_forbidden(signal) {
    return Err(std::io::Error::other(format!(
      "Refusing to register signal {signal}"
    )));
  }

  let (handle, handlers) = HANDLERS.get_or_init(|| {
    let handle = init();

    let handlers = Mutex::new(HashMap::new());

    (handle, handlers)
  });

  let id = COUNTER.fetch_add(1, Ordering::Relaxed);
  let mut handlers = handlers.lock().unwrap();
  match handlers.entry(signal) {
    std::collections::hash_map::Entry::Occupied(mut v) => {
      v.get_mut().push((id, prevent_default, f))
    }
    std::collections::hash_map::Entry::Vacant(v) => {
      v.insert(vec![(id, prevent_default, f)]);

      #[cfg(unix)]
      {
        handle.0.add_signal(signal).map_err(|e| {
          std::io::Error::other(format!(
            "Failed to register signal {signal}: {e}"
          ))
        })?;
      }
      #[cfg(windows)]
      {
        let _ = handle;
        if signal == SIGWINCH {
          start_sigwinch_polling();
        }
      }
    }
  }

  Ok(id)
}

pub fn unregister(signal: i32, id: u32) {
  let Some((_, handlers)) = HANDLERS.get() else {
    return;
  };
  let mut handlers = handlers.lock().unwrap();
  let Some(handlers) = handlers.get_mut(&signal) else {
    return;
  };
  let Some(index) = handlers.iter().position(|v| v.0 == id) else {
    return;
  };
  let _ = handlers.swap_remove(index);
}

static BEFORE_EXIT: OnceLock<Mutex<Vec<Handler>>> = OnceLock::new();

pub fn before_exit(f: fn()) {
  BEFORE_EXIT
    .get_or_init(|| Mutex::new(vec![]))
    .lock()
    .unwrap()
    .push(Box::new(f));
}

pub fn run_exit() {
  if let Some(fns) = BEFORE_EXIT.get() {
    let fns = fns.lock().unwrap();
    for f in fns.iter() {
      f();
    }
  }
}

pub const SIGINT: i32 = 2;
pub const SIGTERM: i32 = 15;

/// Synthetically raise a signal, triggering all registered JS handlers.
///
/// This does NOT use OS-level signal delivery — it directly invokes the
/// handler functions under a mutex, making it safe to call from any async
/// or sync context on all platforms (including Windows).
///
/// Returns true if any handler prevented the default behavior.
pub fn raise(signal: i32) -> bool {
  handle_signal(signal)
}

pub fn is_forbidden(signo: i32) -> bool {
  if FORBIDDEN.contains(&signo) {
    return true;
  }
  // On Windows, signal_hook's FORBIDDEN list doesn't include SIGKILL/SIGABRT
  // (they're Unix-specific in the crate). Add them here since listening for
  // uncatchable/fatal signals doesn't make sense on any platform.
  #[cfg(windows)]
  if signo == 9 || signo == 22 {
    // SIGKILL (9) and SIGABRT (22, Windows CRT value)
    return true;
  }
  false
}

pub struct SignalStream {
  signo: i32,
  id: u32,
  rx: watch::Receiver<()>,
}

impl SignalStream {
  pub async fn recv(&mut self) -> Option<()> {
    self.rx.changed().await.ok()
  }
}

impl Drop for SignalStream {
  fn drop(&mut self) {
    unregister(self.signo, self.id);
  }
}

pub fn signal_stream(signo: i32) -> Result<SignalStream, std::io::Error> {
  signal_stream_inner(signo, true)
}

/// Like [`signal_stream`], but does not prevent the default signal behavior.
///
/// The stream only observes the signal: if no other registered handler
/// (e.g. a JS signal listener) prevents the default, the default action
/// still runs, terminating the process for signals like SIGINT. This is
/// important for consumers that poll the stream from the main event loop,
/// which may be blocked in synchronous JS execution and unable to react
/// to the notification.
pub fn signal_stream_allow_default(
  signo: i32,
) -> Result<SignalStream, std::io::Error> {
  signal_stream_inner(signo, false)
}

fn signal_stream_inner(
  signo: i32,
  prevent_default: bool,
) -> Result<SignalStream, std::io::Error> {
  let (tx, rx) = watch::channel(());
  let cb = Box::new(move || {
    tx.send_replace(());
  });
  let id = register(signo, prevent_default, cb)?;
  Ok(SignalStream { signo, id, rx })
}

pub async fn ctrl_c() -> std::io::Result<()> {
  ctrl_c_inner(signal_stream(libc::SIGINT)?).await
}

/// Like [`ctrl_c`], but does not prevent the default SIGINT behavior.
/// See [`signal_stream_allow_default`].
pub async fn ctrl_c_allow_default() -> std::io::Result<()> {
  ctrl_c_inner(signal_stream_allow_default(libc::SIGINT)?).await
}

async fn ctrl_c_inner(mut stream: SignalStream) -> std::io::Result<()> {
  match stream.recv().await {
    Some(_) => Ok(()),
    None => Err(std::io::Error::other("failed to receive SIGINT signal")),
  }
}
