use std::{
    path::{Path, PathBuf},
    process::Command,
};

use clingwrap::runner::CommandRunner;
use serde::{Deserialize, Serialize};

use crate::{
    action::{ActionError, Context},
    action_impl::ActionImpl,
};

/// Upload build artifacts using `rsync` to configured server.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Rsync {
    artifacts: PathBuf,
    rsync_target: Option<String>,
}

impl Rsync {
    /// Create a new `Rsync` action.
    pub fn new<P: AsRef<Path>>(artifacts: P, rsync_target: Option<String>) -> Self {
        Self {
            artifacts: artifacts.as_ref().to_path_buf(),
            rsync_target,
        }
    }
}

impl ActionImpl for Rsync {
    fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
        if let Some(target) = &self.rsync_target {
            rsync_server(&context.artifacts_dir().join(&self.artifacts), target)?;
        }
        Ok(())
    }
}

fn rsync_server(src: &Path, target: &str) -> Result<(), RsyncError> {
    let src = src.join(".");
    let target = format!("{target}/.");
    let mut cmd = Command::new("rsync");
    cmd.arg("-a").arg("--del").arg(&src).arg(&target);

    let mut runner = CommandRunner::new(cmd);
    runner.capture_stdout();
    runner.capture_stderr();

    let output = runner
        .execute()
        .map_err(|err| RsyncError::Execute("rsync", err))?;

    if output.status.code() != Some(0) {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(RsyncError::Rsync(
            src.clone(),
            target.clone(),
            stderr.into(),
        ));
    }

    Ok(())
}

/// Errors from Rsync action.
#[derive(Debug, thiserror::Error)]
pub enum RsyncError {
    /// `rsync` failed.
    #[error("rsync failed to synchronize {0} to {1}:\n{2}")]
    Rsync(PathBuf, String, String),

    /// Can't run program.
    #[error("failed to run program {0}")]
    Execute(&'static str, #[source] clingwrap::runner::CommandError),
}

impl From<RsyncError> for ActionError {
    fn from(value: RsyncError) -> Self {
        Self::Rsync(value)
    }
}
