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

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

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

/// Upload a built `deb` package using the `dput` command.
///
/// The host must have `dput` configured to support the specified upload
/// target.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Dput {
    artifacts: PathBuf,
    dput_target: Option<String>,
}

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

impl ActionImpl for Dput {
    fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
        if let Some(target) = &self.dput_target {
            let changes = changes_file(context.artifacts_dir())?;
            dput(target, &changes)?;
        }
        Ok(())
    }
}

fn changes_file(dir: &Path) -> Result<PathBuf, DputError> {
    let mut result = None;
    for entry in WalkDir::new(dir).into_iter() {
        let path = entry.map_err(DputError::WalkDir)?.path().to_path_buf();
        if path.display().to_string().ends_with(".changes") {
            if result.is_some() {
                return Err(DputError::ManyChanges);
            }
            result = Some(path);
        }
    }

    if let Some(path) = result {
        Ok(path)
    } else {
        Err(DputError::NoChanges)
    }
}

fn dput(dest: &str, changes: &Path) -> Result<(), DputError> {
    let mut cmd = Command::new("dput");
    cmd.arg(dest).arg(changes);

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

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

    if output.status.code() != Some(0) {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(DputError::Dput(
            dest.into(),
            changes.to_path_buf(),
            stderr.into(),
        ));
    }

    Ok(())
}

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

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

    /// Can't list files.
    #[error("failed to list contents of upload directory")]
    WalkDir(#[source] walkdir::Error),

    /// Can't find a .changes file.
    #[error("no *.changes file built for deb project")]
    NoChanges,

    /// Found more than one .changes file.
    #[error("more than one *.changes file built for deb project")]
    ManyChanges,
}

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