//! Action to download Go module dependencies using `go mod download`.

#![allow(clippy::result_large_err)]

use std::path::PathBuf;

use serde::{Deserialize, Serialize};
use tempfile::tempdir;

use crate::{
    action::{ActionError, Context},
    action_impl::{spawn, spawn_in, ActionImpl},
    runlog::RunLogSource,
    util::{copy_file, mkdir, UtilError},
};

/// Download Go module dependencies using `go mod download`.
///
/// This action copies only `go.mod` and `go.sum` from the source tree
/// to a temporary directory, then runs `go mod download` with
/// `GOMODCACHE` pointing at the dependencies directory. This is safe
/// because `go mod download` only fetches and verifies module zip
/// files -- it never executes project code, `go generate`, or build
/// scripts.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GoModDownload;

impl GoModDownload {
    const ENV_CACHE: &str = "GOMODCACHE";
    const SUBDIR: &str = "gomod";

    fn cache_path(&self, context: &Context) -> PathBuf {
        context.deps_dir().join(Self::SUBDIR)
    }
}

impl ActionImpl for GoModDownload {
    fn execute(&self, context: &mut Context) -> Result<(), ActionError> {
        // First wait for network to be available. This is systemic
        // problem in Ambient, right now. It should do this automatically.
        // Note that is kludge assumed systemd.
        spawn(
            context,
            &["systemctl", "start", "systemd-networkd-wait-online"],
        )?;

        let gomod_dir = self.cache_path(context);
        mkdir(&gomod_dir).map_err(|err| GoModError::Mkdir(gomod_dir.clone(), err))?;

        // Copy only go.mod and go.sum to a temp dir. This avoids
        // exposing any project code to the go tool.
        let tmp = tempdir().map_err(GoModError::TempDir)?;
        let src = context.source_dir();

        let go_mod = src.join("go.mod");
        copy_file(&go_mod, &tmp.path().join("go.mod")).map_err(GoModError::CopyGoMod)?;

        let go_sum = src.join("go.sum");
        if go_sum.exists() {
            copy_file(&go_sum, &tmp.path().join("go.sum")).map_err(GoModError::CopyGoSum)?;
        }

        context.runlog().debug(
            RunLogSource::PrePlan,
            format!("go_mod_download: GOMODCACHE={}", gomod_dir.display()),
        );

        // Set GOMODCACHE for the download command so modules land in
        // the dependencies directory.
        context.set_env(Self::ENV_CACHE, &gomod_dir.display().to_string());

        spawn_in(
            context,
            &["go", "mod", "download"],
            tmp.path().to_path_buf(),
        )?;

        Ok(())
    }
}

/// Errors from GoModDownload action.
#[derive(Debug, thiserror::Error)]
pub enum GoModError {
    /// Can't create the Go module cache directory.
    #[error("failed to create Go module cache directory {0}")]
    Mkdir(PathBuf, #[source] UtilError),

    /// Can't create a temporary directory.
    #[error("failed to create temporary directory")]
    TempDir(#[source] std::io::Error),

    /// Can't copy go.mod.
    #[error("failed to copy go.mod")]
    CopyGoMod(#[source] UtilError),

    /// Can't copy go.sum.
    #[error("failed to copy go.sum")]
    CopyGoSum(#[source] UtilError),
}

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