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

use serde::{Deserialize, Serialize};
use walkdir::WalkDir;

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

const DPUT_CF: &str = "/ci/secrets/dput.cf";

/// 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> {
        context
            .runlog()
            .debug(crate::runlog::RunLogSource::PostPlan, "DPUT: action call");
        if let Some(target) = &self.dput_target {
            context.runlog().debug(
                crate::runlog::RunLogSource::PostPlan,
                format!("DPUT: target={target}"),
            );

            let changes = changes_file(context.artifacts_dir());

            context.runlog().debug(
                crate::runlog::RunLogSource::PostPlan,
                format!("DPUT: changes={:?}", changes),
            );

            spawn(context, &["find", "/ci/secrets", "-ls"]).ok();
            spawn(context, &["dput", "-p", "-c", DPUT_CF])?;

            let changes = changes?;
            let argv = &[
                "dput",
                "-d",
                "-c",
                DPUT_CF,
                target,
                &changes.display().to_string(),
            ];
            spawn(context, argv)?;
        }
        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)
    }
}

/// 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)
    }
}
