use std::path::PathBuf;

use serde::{Deserialize, Serialize};

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

/// Create a `tar` archive from a directory.
///
/// This is meant for internal use by Ambient. It can't be used in any kind
/// of plan, pre-plan, or post-plan. It can be used in a runnable plan.
/// It is generated by Ambient to set up execution of a runnable plan.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TarCreate {
    archive: PathBuf,
    directory: PathBuf,
}

impl TarCreate {
    /// Create a new `TarCreate` action.
    pub fn new(archive: PathBuf, directory: PathBuf) -> Self {
        Self { archive, directory }
    }
}

impl ActionImpl for TarCreate {
    fn execute(&self, _context: &mut Context) -> Result<(), ActionError> {
        VirtualDriveBuilder::default()
            .filename(&self.archive)
            .root_directory(&self.directory)
            .create()
            .map_err(|e| TarError::TarCreate(self.archive.clone(), self.directory.clone(), e))?;
        Ok(())
    }
}

/// Extract a tar archive into a directory.
///
/// This is meant for internal use by Ambient. It can't be used in any kind
/// of plan, pre-plan, or post-plan. It can be used in a runnable plan.
/// It is generated by Ambient to set up execution of a runnable plan.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TarExtract {
    archive: PathBuf,
    directory: PathBuf,
}

impl TarExtract {
    /// Create a new `TarExtract` action.
    pub fn new(archive: PathBuf, directory: PathBuf) -> Self {
        Self { archive, directory }
    }
}

impl ActionImpl for TarExtract {
    fn execute(&self, _context: &mut Context) -> Result<(), ActionError> {
        let tar = VirtualDriveBuilder::default()
            .filename(&self.archive)
            .root_directory(&self.directory)
            .open()
            .map_err(|e| TarError::TarOpen(self.archive.clone(), e))?;
        tar.extract_to(&self.directory)
            .map_err(|e| TarError::TarExtract(self.archive.clone(), self.directory.clone(), e))?;
        Ok(())
    }
}

/// Errors from tar actions.
#[derive(Debug, thiserror::Error)]
pub enum TarError {
    /// Can't create a tar archive.
    #[error("failed to create tar archive {0} from {1}")]
    TarCreate(PathBuf, PathBuf, #[source] crate::vdrive::VirtualDriveError),

    /// Can't open a tar archive to extract it.
    #[error("failed to open tar archive {0}")]
    TarOpen(PathBuf, #[source] crate::vdrive::VirtualDriveError),

    /// Can't extract a tar archive.
    #[error("failed to extract tar archive {0} into {1}")]
    TarExtract(PathBuf, PathBuf, #[source] crate::vdrive::VirtualDriveError),
}

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

#[cfg(test)]
mod test {
    use crate::{
        action::{RunnableAction, UnsafeAction},
        plan::RunnablePlan,
        runlog::RunLog,
    };

    use super::*;
    use tempfile::tempdir;

    fn plan() -> RunnablePlan {
        let mut plan = RunnablePlan::default();
        plan.set_cache_dir("/tmp");
        plan.set_deps_dir("/tmp");
        plan.set_source_dir("/tmp");
        plan.set_artifacts_dir("/tmp");
        plan
    }

    #[test]
    fn tar_create_action() -> Result<(), Box<dyn std::error::Error>> {
        let tmp = tempdir()?;
        let src = tmp.path().join("src");
        let tar = tmp.path().join("src.tar");

        std::fs::create_dir(&src)?;
        let action = RunnableAction::from_unsafe_action(&UnsafeAction::tar_create(&tar, &src));
        let plan = plan();
        let mut runlog = RunLog::default();
        let mut context = Context::new(&mut runlog);
        context.set_envs_from_plan(&plan)?;

        assert!(!tar.exists());
        assert!(action.execute(&mut context).is_ok());
        assert!(tar.exists());
        Ok(())
    }

    #[test]
    fn tar_extract_action() -> Result<(), Box<dyn std::error::Error>> {
        let tmp = tempdir()?;
        let src = tmp.path().join("src");
        let tar = tmp.path().join("src.tar");
        let extracted = tmp.path().join("extracted");

        std::fs::create_dir(&src)?;
        std::fs::File::create(src.join("file.dat"))?;
        let plan = plan();
        let mut runlog = RunLog::default();
        let mut context = Context::new(&mut runlog);
        context.set_envs_from_plan(&plan)?;

        RunnableAction::from_unsafe_action(&UnsafeAction::tar_create(&tar, &src))
            .execute(&mut context)?;

        let action =
            RunnableAction::from_unsafe_action(&UnsafeAction::tar_extract(&tar, &extracted));
        assert!(action.execute(&mut context).is_ok());
        assert!(extracted.join("file.dat").exists());
        Ok(())
    }
}
