use std::path::PathBuf;

use clap::Parser;
use serde::Serialize;

use ambient_ci::{
    plan::{
        construct_all_plans, construct_runnable_plan, runnable_plan_from_post_plan_actions,
        runnable_plan_from_pre_plan_actions, RunnablePlan,
    },
    project::{ProjectError, Projects},
    runlog::RunLog,
};

use super::{AmbientError, Config, Leaf};

/// Prepare image.
#[derive(Debug, Parser)]
pub struct Plan {
    /// Project specification file. May contain any number of projects.
    #[clap(long)]
    projects: Option<PathBuf>,

    /// Which project to show plan for?
    project: String,

    /// rsync target for publishing artifacts with rsync program.
    #[clap(long, aliases = ["rsync", "target"])]
    rsync_target: Option<String>,

    /// dput target for publishing .deb package.
    #[clap(long, alias = "dput")]
    dput_target: Option<String>,

    /// Output as YAML. Default is JSON.
    #[clap(long)]
    yaml: bool,

    /// Output runnable plan for `ambient-exeucte-plan`.
    #[clap(long)]
    runnable: bool,
}

impl Leaf for Plan {
    fn run(&self, config: &Config, _runlog: &mut RunLog) -> Result<(), AmbientError> {
        let projects = if let Some(filename) = &self.projects {
            filename.to_path_buf()
        } else {
            config.projects().into()
        };

        let projects =
            Projects::from_file(&projects).map_err(|err| PlanError::FromFile(projects, err))?;
        let project = projects
            .get(&self.project)
            .ok_or(PlanError::NoSuchProject(self.project.clone()))?;

        config.lint(&projects)?;

        if self.runnable {
            let runnable = runnable_plan_from_pre_plan_actions(project.pre_plan())
                .map_err(PlanError::Runnable)?;
            let text = if self.yaml {
                serde_norway::to_string(&runnable).map_err(PlanError::ToYaml)?
            } else {
                serde_json::to_string_pretty(&runnable).map_err(PlanError::ToJson)?
            };
            println!("{text}");

            let runnable = construct_runnable_plan(project.plan()).map_err(PlanError::Runnable)?;
            let text = if self.yaml {
                serde_norway::to_string(&runnable).map_err(PlanError::ToYaml)?
            } else {
                serde_json::to_string_pretty(&runnable).map_err(PlanError::ToJson)?
            };
            println!("{text}");

            let runnable =
                runnable_plan_from_post_plan_actions(config, &self.project, project.post_plan())
                    .map_err(PlanError::Runnable)?;
            let text = if self.yaml {
                serde_norway::to_string(&runnable).map_err(PlanError::ToYaml)?
            } else {
                serde_json::to_string_pretty(&runnable).map_err(PlanError::ToJson)?
            };
            println!("{text}");
        } else {
            let mut config = config.clone();
            if let Some(s) = &self.dput_target {
                config.set_dput_target(s);
            };
            if let Some(s) = &self.rsync_target {
                config.set_rsync_target(s);
            };

            let (pre_plan, plan, post_plan) = construct_all_plans(&config, &self.project, project)?;
            let plans = Plans {
                pre_plan,
                plan,
                post_plan,
            };

            let text = if self.yaml {
                serde_norway::to_string(&plans).map_err(PlanError::ToYaml)?
            } else {
                serde_json::to_string_pretty(&plans).map_err(PlanError::ToJson)?
            };
            println!("{text}");
        }

        Ok(())
    }
}

#[derive(Serialize)]
struct Plans {
    pre_plan: RunnablePlan,
    plan: RunnablePlan,
    post_plan: RunnablePlan,
}

#[derive(Debug, thiserror::Error)]
pub enum PlanError {
    #[error("failed to load project list from file {0}")]
    FromFile(PathBuf, #[source] ProjectError),

    #[error("failed to serialize plans to JSON")]
    ToJson(#[source] serde_json::Error),

    #[error("failed to serialize plans to YAML")]
    ToYaml(#[source] serde_norway::Error),

    #[error("can't find project {0}")]
    NoSuchProject(String),

    #[error("can't construct runnable plan")]
    Runnable(#[source] ambient_ci::plan::PlanError),
}
