// SPDX-License-Identifier: AGPL-3.0-or-later
//! Parsed command representation for [de]serialization

use std::{
    fmt::Display,
    process::{Output, Stdio},
    time::{Duration, Instant},
};

use anyhow::Context;
use bitcode::{Decode, Encode};
use tokio::process::Command;

#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
pub struct Cmd {
    command: String,
    args: Vec<String>,
    timeout: Option<Duration>,
}

impl Cmd {
    pub fn new(command: String, args: Vec<String>) -> Self {
        Self {
            command,
            args,
            timeout: None,
        }
    }

    pub fn program(&self) -> &str {
        &self.command
    }

    pub fn args(&self) -> &[String] {
        self.args.as_slice()
    }

    pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
        self.timeout = Some(timeout);
        self
    }

    pub fn default_timeout(&mut self, timeout: Duration) -> &mut Self {
        if self.timeout.is_none() {
            self.timeout(timeout);
        }
        self
    }

    fn as_command(&self) -> Command {
        let mut c = Command::new(&self.command);
        c.args(self.args.as_slice());
        c
    }

    pub async fn run(self) -> CmdResult {
        let start = Instant::now();
        let result = self.run_inner().await;
        let duration = Instant::now().duration_since(start);

        match result {
            Ok(output) => CmdResult::Ok(CmdOutput {
                cmd: self,
                output,
                duration,
            }),
            Err(err) => CmdResult::Err(CmdError {
                cmd: self,
                err: format!("{err:#}"),
            }),
        }
    }

    async fn run_inner(&self) -> Result<Output, anyhow::Error> {
        let cmd = self.as_command().stdin(Stdio::null()).output();
        let output = if let Some(timeout) = self.timeout {
            tokio::time::timeout(timeout, cmd).await.with_context(|| {
                format!(
                    "timeout ({} s) while running command: {}",
                    timeout.as_secs(),
                    self.command
                )
            })?
        } else {
            cmd.await
        }
        .with_context(|| format!("while spawning command: {}", self.command))?;

        Ok(output)
    }
}

impl PartialOrd for Cmd {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.cmp(other))
    }
}
impl Ord for Cmd {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        match self.command.cmp(&other.command) {
            core::cmp::Ordering::Equal => {}
            ord => return ord,
        }
        self.args.cmp(&other.args)
    }
}

impl Display for Cmd {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} {}", self.program(), self.args().join(" "))
    }
}

pub enum CmdResult {
    Ok(CmdOutput),
    Err(CmdError),
}

#[derive(Clone)]
pub struct CmdError {
    pub cmd: Cmd,
    pub err: String,
}

pub struct CmdOutput {
    pub cmd: Cmd,
    pub output: Output,
    pub duration: Duration,
}
