use anyhow::{Result, anyhow};
use tokio::{io::{AsyncBufReadExt, BufReader}, select, sync::mpsc};
use yazi_binding::Permit;
use yazi_term::YIELD_TO_SUBPROCESS;

use super::{ProcessInBg, ProcessInBlock, ProcessInOrphan, ShellOpt};
use crate::{AppProxy, NotifyProxy, TaskOp, TaskOps, process::{ProcessIn, ProcessOutBg, ProcessOutBlock, ProcessOutOrphan}};

pub(crate) struct Process {
	ops: TaskOps,
	tx:  async_priority_channel::Sender<ProcessIn, u8>,
}

impl Process {
	pub(crate) fn new(
		ops: &mpsc::UnboundedSender<TaskOp>,
		tx: async_priority_channel::Sender<ProcessIn, u8>,
	) -> Self {
		Self { ops: ops.into(), tx }
	}

	pub(crate) async fn block(&self, task: ProcessInBlock) -> Result<(), ProcessOutBlock> {
		let _permit = Permit::new(YIELD_TO_SUBPROCESS.acquire().await.unwrap(), AppProxy::resume());
		AppProxy::stop().await;

		let (id, cmd) = (task.id, task.cmd.clone());
		let result = super::shell(task.into()).await;
		if let Err(e) = result {
			NotifyProxy::push_warn(
				cmd.to_string_lossy().into_owned(),
				format!("Failed to start process: {e}"),
			);
			return Ok(self.ops.out(id, ProcessOutBlock::Succ));
		}

		let status = result.unwrap().wait().await?;
		if !status.success() {
			let content = match status.code() {
				Some(130) => return Ok(self.ops.out(id, ProcessOutBlock::Succ)), // Ctrl-C pressed by user
				Some(code) => format!("Process exited with status code: {code}"),
				None => "Process terminated by signal".to_string(),
			};
			NotifyProxy::push_warn(cmd.to_string_lossy().into_owned(), content);
		}

		Ok(self.ops.out(id, ProcessOutBlock::Succ))
	}

	pub(crate) async fn orphan(&self, task: ProcessInOrphan) -> Result<(), ProcessOutOrphan> {
		let id = task.id;

		super::shell(task.into()).await?;
		Ok(self.ops.out(id, ProcessOutOrphan::Succ))
	}

	pub(crate) async fn bg(&self, task: ProcessInBg) -> Result<(), ProcessOutBg> {
		let mut child = super::shell(ShellOpt {
			cwd:    task.cwd,
			cmd:    task.cmd,
			args:   task.args,
			piped:  true,
			orphan: false,
		})
		.await?;

		let done = task.done;
		let mut stdout = BufReader::new(child.stdout.take().unwrap()).lines();
		let mut stderr = BufReader::new(child.stderr.take().unwrap()).lines();
		loop {
			select! {
				false = done.future() => {
					child.start_kill().ok();
					break;
				}
				Ok(Some(line)) = stdout.next_line() => {
					self.ops.out(task.id, ProcessOutBg::Log(line));
				}
				Ok(Some(line)) = stderr.next_line() => {
					self.ops.out(task.id, ProcessOutBg::Log(line));
				}
				Ok(status) = child.wait() => {
					self.ops.out(task.id, ProcessOutBg::Log(match status.code() {
						Some(code) => format!("Exited with status code: {code}"),
						None => "Process terminated by signal".to_string(),
					}));
					if !status.success() {
						Err(anyhow!("Process failed"))?;
					}
					break;
				}
			}
		}

		Ok(self.ops.out(task.id, ProcessOutBg::Succ))
	}
}

impl Process {
	#[inline]
	pub(crate) fn submit(&self, r#in: impl Into<ProcessIn>, priority: u8) {
		_ = self.tx.try_send(r#in.into(), priority);
	}
}
