//! Different kinds of CI plans.

#![allow(clippy::result_large_err)]

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

use serde::{Deserialize, Serialize};

use crate::{
    action::{Context, PostPlanAction, PrePlanAction, RunnableAction, UnsafeAction},
    config::Config,
    project::{Project, State},
    qemu,
};

/// A complete plan, as used by users. This can only contain
/// unsafe actions that are to be executed in a virtual machine.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Plan {
    steps: Vec<UnsafeAction>,
}

impl Plan {
    /// Load plan from a file.
    pub fn from_file(filename: &Path) -> Result<Self, PlanError> {
        let plan = std::fs::read(filename).map_err(|e| PlanError::PlanOpen(filename.into(), e))?;
        let plan = serde_norway::from_slice(&plan)
            .map_err(|e| PlanError::PlanParse(filename.into(), e))?;
        Ok(plan)
    }

    /// Write plan to a file.
    pub fn to_file(&self, filename: &Path) -> Result<(), PlanError> {
        let plan = serde_norway::to_string(&self).map_err(PlanError::PlanSerialize)?;
        std::fs::write(filename, plan).map_err(|e| PlanError::PlanWrite(filename.into(), e))?;
        Ok(())
    }

    /// Append another action to the plan.
    pub fn push(&mut self, action: UnsafeAction) {
        self.steps.push(action);
    }

    /// Iterator over actions in a plan.
    pub fn iter(&self) -> impl Iterator<Item = &UnsafeAction> {
        self.steps.iter()
    }
}

/// A runnable plan. This contains actual actions that can be executed.
/// It does not guard against safe vs unsafe actions, anything goes.
#[derive(Debug, Default, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct RunnablePlan {
    steps: Vec<RunnableAction>,
    executor_drive: Option<String>,
    source_drive: Option<String>,
    artifact_drive: Option<String>,
    cache_drive: Option<String>,
    deps_drive: Option<String>,
    workspace_dir: Option<String>,
    source_dir: Option<String>,
    deps_dir: Option<String>,
    cache_dir: Option<String>,
    artifacts_dir: Option<String>,
}

impl RunnablePlan {
    #[cfg(test)]
    fn parse_str(yaml: &str) -> Result<Self, PlanError> {
        serde_norway::from_str(yaml).map_err(PlanError::PlanParseStr)
    }

    /// Load plan from a file.
    pub fn from_file(filename: &Path) -> Result<Self, PlanError> {
        let plan = std::fs::read(filename).map_err(|e| PlanError::PlanOpen(filename.into(), e))?;
        let plan = String::from_utf8_lossy(&plan);
        let plan: Self =
            serde_norway::from_str(&plan).map_err(|e| PlanError::PlanParse(filename.into(), e))?;

        for step in plan.steps.iter() {
            if let RunnableAction::HttpGet(x) = step {
                for item in x.items() {
                    if item
                        .filename()
                        .as_os_str()
                        .as_encoded_bytes()
                        .contains(&b'/')
                    {
                        return Err(PlanError::FilenameIsNotBasename(filename.to_path_buf()));
                    }
                }
            }
        }

        Ok(plan)
    }

    /// Convert plan to a YAML string.
    pub fn to_string(&self) -> Result<String, PlanError> {
        serde_norway::to_string(self).map_err(PlanError::PlanSerialize)
    }

    /// Write plan to a file as YAML.
    pub fn to_file(&self, filename: &Path) -> Result<(), PlanError> {
        let plan = serde_norway::to_string(&self).map_err(PlanError::PlanSerialize)?;
        std::fs::write(filename, plan).map_err(|e| PlanError::PlanWrite(filename.into(), e))?;
        Ok(())
    }

    /// Append an action to the plan.
    pub fn push(&mut self, action: RunnableAction) {
        self.steps.push(action);
    }

    /// Append any number of actions to a plan.
    pub fn push_unsafe_actions<'a>(&mut self, actions: impl Iterator<Item = &'a UnsafeAction>) {
        for action in actions {
            self.push(RunnableAction::from_unsafe_action(action));
        }
    }

    /// Iterator over actions in a plan.
    pub fn iter(&self) -> impl Iterator<Item = &RunnableAction> {
        self.steps.iter()
    }

    /// Execute actions in the plan.
    pub fn execute(&self, context: &mut Context) -> Result<(), PlanError> {
        context
            .set_envs_from_plan(self)
            .map_err(PlanError::Context)?;
        for action in self.steps.iter() {
            context.runlog().execute_action(action);
            let result = action.execute(context);
            match &result {
                Ok(()) => context.runlog().action_succeeded(action),
                Err(_) => {
                    context.runlog().action_failed(action);
                    // FIXME: this needs to be passed into the runlog, but that will
                    // FIXME: error to be serde::Deserialize
                    // let mut source = err.source();
                    // while let Some(src) = source {
                    //     error!("caused by: {src}");
                    //     source = src.source();
                    // }
                    result?;
                }
            }
        }

        context.runlog().plan_succeeded();
        Ok(())
    }

    /// Return executor drive, as set in the plan.
    pub fn executor_drive(&self) -> Option<&String> {
        self.executor_drive.as_ref()
    }

    /// Return source drive, as set in the plan.
    pub fn source_drive(&self) -> Option<&String> {
        self.source_drive.as_ref()
    }

    /// Return artifact drive, as set in the plan.
    pub fn run_artifact_drive(&self) -> Option<&String> {
        self.artifact_drive.as_ref()
    }

    /// Return cache drive, as set in the plan.
    pub fn cache_drive(&self) -> Option<&String> {
        self.cache_drive.as_ref()
    }

    /// Return dependencies drive, as set in the plan.
    pub fn deps_drive(&self) -> Option<&String> {
        self.deps_drive.as_ref()
    }

    /// Return root directory of workspace.
    pub fn workspace_dir(&self) -> Option<&String> {
        self.workspace_dir.as_ref()
    }

    /// Return source directory.
    pub fn source_dir(&self) -> Option<&String> {
        self.source_dir.as_ref()
    }

    /// Return dependencies directory.
    pub fn deps_dir(&self) -> Option<&String> {
        self.deps_dir.as_ref()
    }

    /// Return cache directory.
    pub fn cache_dir(&self) -> Option<&String> {
        self.cache_dir.as_ref()
    }

    /// Return artifacts directory.
    pub fn artifacts_dir(&self) -> Option<&String> {
        self.artifacts_dir.as_ref()
    }

    /// Set executor drive.
    pub fn set_executor_drive(&mut self, path: &str) {
        self.executor_drive = Some(path.into());
    }

    /// Set source drive.
    pub fn set_source_drive(&mut self, path: &str) {
        self.source_drive = Some(path.into());
    }

    /// Set artifacts drive.
    pub fn set_artifact_drive(&mut self, path: &str) {
        self.artifact_drive = Some(path.into());
    }

    /// Set cache drive.
    pub fn set_cache_drive(&mut self, path: &str) {
        self.cache_drive = Some(path.into());
    }

    /// Set dependencies drive.
    pub fn set_deps_drive(&mut self, path: &str) {
        self.deps_drive = Some(path.into());
    }

    /// Set root directory of workspace.
    pub fn set_workspace_dir(&mut self, path: &str) {
        self.workspace_dir = Some(path.into());
    }

    /// Set source directory.
    pub fn set_source_dir(&mut self, path: &str) {
        self.source_dir = Some(path.into());
    }

    /// Set depencencies directory.
    pub fn set_deps_dir(&mut self, path: &str) {
        self.deps_dir = Some(path.into());
    }

    /// Set cache directory.
    pub fn set_cache_dir(&mut self, path: &str) {
        self.cache_dir = Some(path.into());
    }

    /// Set artifacts directory.
    pub fn set_artifacts_dir(&mut self, path: &str) {
        self.artifacts_dir = Some(path.into());
    }

    /// Set directories that have not been set yet.
    pub fn set_unset_dirs(&mut self, path: &str) {
        fn set(s: &mut Option<String>, path: &str) {
            if s.is_none() {
                *s = Some(path.to_string());
            }
        }

        set(&mut self.workspace_dir, path);
        set(&mut self.source_dir, path);
        set(&mut self.deps_dir, path);
        set(&mut self.cache_dir, path);
        set(&mut self.artifacts_dir, path);
    }
}

/// Construct runnable plans for pre-plan, plan, and post-plan.
pub fn construct_all_plans(
    config: &Config,
    project_name: &str,
    project: &Project,
    state: &State,
) -> Result<(RunnablePlan, RunnablePlan, RunnablePlan), PlanError> {
    let pre_plan = runnable_plan_from_pre_plan_actions(project, state, project.pre_plan());
    let plan = construct_runnable_plan(project.plan())?;
    let post_plan = runnable_plan_from_post_plan_actions(
        config,
        project_name,
        project,
        state,
        project.post_plan(),
    );
    Ok((pre_plan, plan, post_plan))
}

/// Construct a [`RunnablePlan`] from unsafe actions.
pub fn construct_runnable_plan(actions: &[UnsafeAction]) -> Result<RunnablePlan, PlanError> {
    let prologue = [
        UnsafeAction::mkdir(Path::new(qemu::WORKSPACE_DIR)),
        UnsafeAction::mkdir(Path::new(qemu::ARTIFACTS_DIR)),
        UnsafeAction::tar_extract(Path::new(qemu::SOURCE_DRIVE), Path::new(qemu::SOURCE_DIR)),
        UnsafeAction::tar_extract(Path::new(qemu::DEPS_DRIVE), Path::new(qemu::DEPS_DIR)),
        UnsafeAction::tar_extract(Path::new(qemu::CACHE_DRIVE), Path::new(qemu::CACHE_DIR)),
        UnsafeAction::shell("ln -sf /ci /workspace"),
        UnsafeAction::shell("git config --global user.name 'Ambient CI'"),
        UnsafeAction::shell("git config --global user.email ambient@example.com"),
    ];

    let epilogue = [
        UnsafeAction::tar_create(Path::new(qemu::CACHE_DRIVE), Path::new(qemu::CACHE_DIR)),
        UnsafeAction::tar_create(
            Path::new(qemu::ARTIFACT_DRIVE),
            Path::new(qemu::ARTIFACTS_DIR),
        ),
    ];

    let mut runnable = RunnablePlan::default();
    runnable.set_executor_drive(qemu::EXECUTOR_DRIVE);
    runnable.set_source_drive(qemu::SOURCE_DRIVE);
    runnable.set_artifact_drive(qemu::ARTIFACT_DRIVE);
    runnable.set_cache_drive(qemu::CACHE_DRIVE);
    runnable.set_deps_drive(qemu::DEPS_DRIVE);
    runnable.set_workspace_dir(qemu::WORKSPACE_DIR);
    runnable.set_source_dir(qemu::SOURCE_DIR);
    runnable.set_artifacts_dir(qemu::ARTIFACTS_DIR);
    runnable.set_deps_dir(qemu::DEPS_DIR);
    runnable.set_cache_dir(qemu::CACHE_DIR);

    runnable.push_unsafe_actions(prologue.iter());
    runnable.push_unsafe_actions(actions.iter());
    runnable.push_unsafe_actions(epilogue.iter());

    Ok(runnable)
}

/// Construct a [`RunnablePlan`] from a pre-plan actions.
pub fn runnable_plan_from_pre_plan_actions(
    project: &Project,
    state: &State,
    actions: &[PrePlanAction],
) -> RunnablePlan {
    fn path(path: &Path) -> String {
        path.to_string_lossy().into_owned()
    }

    let mut plan = RunnablePlan::default();
    plan.set_cache_dir(&path(&state.cachedir()));
    plan.set_deps_dir(&path(&state.dependenciesdir()));
    plan.set_artifacts_dir(&path(&state.artifactsdir()));
    plan.set_source_dir(&path(project.source()));
    for action in actions {
        plan.push(RunnableAction::from_pre_plan_action(action));
    }
    plan
}

/// Construct a [`RunnablePlan`] from post-plan actions.
pub fn runnable_plan_from_post_plan_actions(
    config: &Config,
    project_name: &str,
    project: &Project,
    state: &State,
    actions: &[PostPlanAction],
) -> RunnablePlan {
    fn path(path: &Path) -> String {
        path.to_string_lossy().into_owned()
    }

    let mut plan = RunnablePlan::default();
    plan.set_cache_dir(&path(&state.cachedir()));
    plan.set_deps_dir(&path(&state.dependenciesdir()));
    plan.set_artifacts_dir(&path(&state.artifactsdir()));
    plan.set_source_dir(&path(project.source()));
    for action in actions {
        plan.push(RunnableAction::from_post_plan_action(
            action,
            config.rsync_target_for_project(project_name).as_deref(),
            config.dput_target(),
        ));
    }
    plan
}

/// Errors from handling plans.
#[derive(Debug, thiserror::Error)]
pub enum PlanError {
    /// Can't open plan file.
    #[error("failed to read CI plan file: {0}")]
    PlanOpen(PathBuf, #[source] std::io::Error),

    /// Can't parse plan as YAML from a file.
    #[error("failed to parse CI plan file as YAML: {0}")]
    PlanParse(PathBuf, #[source] serde_norway::Error),

    /// Can't parse CI plan as YAML from string.
    #[error("failed to parse CI plan")]
    PlanParseStr(#[source] serde_norway::Error),

    /// Can't serialize a plan as YAML.
    #[error("failed to serialize CI plan as YAML")]
    PlanSerialize(#[source] serde_norway::Error),

    /// Can't write plan to file.
    #[error("failed to write CI plan file: {0}")]
    PlanWrite(PathBuf, #[source] std::io::Error),

    /// Forwarded from action.
    #[error(transparent)]
    Action(#[from] crate::action::ActionError),

    /// The `http-get` filename contains a directory.
    #[error("the filename in a URL/filename pair contains a directory")]
    FilenameIsNotBasename(PathBuf),

    /// Can't create a [`Context`].
    #[error("failed to create a context for executing actions")]
    Context(#[source] crate::action::ActionError),
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn round_trip() -> Result<(), Box<dyn std::error::Error>> {
        let mut plan = RunnablePlan::default();
        plan.set_source_dir("/src");

        let s = plan.to_string()?;
        let des = RunnablePlan::parse_str(&s)?;

        assert_eq!(plan, des);
        Ok(())
    }
}
