// Copyright(c) 2025 3NSoft Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

#![deny(clippy::all)]

use std::{sync::{atomic::{AtomicBool, Ordering}, Arc}, time::Duration};

use napi::{
  bindgen_prelude::*,
  threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}
};
use napi_derive::napi;
use tokio::{self, process::Command, runtime::{Builder, Runtime}, sync::mpsc::{self, Receiver, Sender}, time::timeout};

use ipc_duplex::{start_and_run_ipc_channel, SignalToSend, IpcServerEvent};


#[napi(js_name = "ChildWithIPC")]
pub struct JsChildWithIPC {
  tx_to_child: Sender<SignalToSend>,
  in_progress: Arc<AtomicBool>
}

#[napi]
impl JsChildWithIPC {
  
  #[napi]
  pub async fn send(&self, bytes: Buffer) -> Result<()> {
    self.absorb_signal_from_parent(SignalToSend::Data(bytes.into())).await
  }

  #[napi]
  pub async fn exit(&self) -> Result<()> {
    self.absorb_signal_from_parent(SignalToSend::Exit).await
  }

  async fn absorb_signal_from_parent(&self, sig: SignalToSend) -> Result<()> {
    match self.tx_to_child.send(sig).await {
      Ok(_) => Ok(()),
      Err(err) => Err(Error::from_reason(err.to_string()))
    }
  }

  #[napi(getter)]
  pub fn is_running(&self) -> Result<bool> {
    Ok(self.in_progress.load(Ordering::Relaxed))
  }

}

#[napi(js_name = "ChildrenWithIPC")]
pub struct JsChildrenWithIPC {
  rt: Arc<Runtime>
}

#[napi]
impl JsChildrenWithIPC {

  #[napi(constructor)]
  pub fn new() -> Self {
    let threaded_rt = Builder::new_multi_thread()
      .enable_io()
      .enable_time()
      .thread_name("Controllers of spawned child processes")
      .build()
      .unwrap();
    let rt = Arc::new(threaded_rt);
    JsChildrenWithIPC { rt }
  }

  #[napi]
  pub async fn spawn(
    &self, program: String, args: Vec<String>, timeout_millis: u32,
    send_to_parent: ThreadsafeFunction<(String, Option<Buffer>), ()>
  ) -> Result<JsChildWithIPC> {

    let (tx_to_child, rx_to_ipc) = mpsc::channel::<SignalToSend>(1);
    let (tx_from_ipc, rx_from_child) = mpsc::channel::<IpcServerEvent>(1);
    let in_progress = Arc::new(AtomicBool::new(true));

    self.rt.spawn(start_and_run_ipc_channel(tx_from_ipc, rx_to_ipc, timeout_millis.into(), in_progress.clone()));
    self.rt.spawn(do_passing_data_from_child_to_js(
      program, args, rx_from_child, send_to_parent, in_progress.clone()
    ));

    Ok(JsChildWithIPC {
      tx_to_child, in_progress
    })
  }

}

#[napi]
pub const CLIENT_STARTED_EVENT: &str = "client-started";

#[napi]
pub const CLIENT_CLOSED_EVENT: &str = "client-closed";

#[napi]
pub const DATA_EVENT: &str = "data";

async fn do_passing_data_from_child_to_js(
  program: String, args: Vec<String>,
  mut rx_from_child: Receiver<IpcServerEvent>,
  send_to_parent: ThreadsafeFunction<(String, Option<Buffer>), ()>,
  in_progress: Arc<AtomicBool>
) {
  macro_rules! send_to_parent {
    ($result:expr) => {
      send_to_parent.call($result, ThreadsafeFunctionCallMode::Blocking);
    };
  }
  while in_progress.load(Ordering::Relaxed) {
    match timeout(Duration::from_millis(100), rx_from_child.recv()).await {
      Ok(Some(IpcServerEvent::ServerCreated(server_path))) => {
        let args = [&[server_path], &args[..]].concat();
        tokio::spawn(run_child_process(program.clone(), args, in_progress.clone()));
      },
      Ok(Some(IpcServerEvent::ClientConnected)) => {
        send_to_parent!(Ok((CLIENT_STARTED_EVENT.to_string(), None)));
      },
      Ok(Some(IpcServerEvent::Data(bytes))) => {
        send_to_parent!(Ok((DATA_EVENT.to_string(), Some(bytes.into()))));
      },
      Ok(Some(IpcServerEvent::ClientClosed)) => {
        send_to_parent!(Ok((CLIENT_CLOSED_EVENT.to_string(), None)));
      },
      Ok(Some(IpcServerEvent::Error(err))) => {
        send_to_parent!(Err(Error::new(Status::GenericFailure, err.to_string())));
        break
      },
      Ok(None) => break,
      Err(_) => ()
    }
  }

}

async fn run_child_process(bin_path: String, args: Vec<String>, in_progress: Arc<AtomicBool>) {
  let status = Command::new(bin_path.clone())
    .args(args)
    .status().await;
  match status {
    Ok(status) => {
      match status.code() {
        Some(status) => {
          if status != 0 {
            eprintln!("{} exited with non-zero status code: {}", bin_path, status);
          }
        },
        None => ()
      };
    },
    Err(err) => {
      eprintln!("awaiting for {} process threw error: {}", bin_path, err);
    }
  }
  in_progress.store(false, Ordering::Relaxed);
}
