use crate::commands::build::pipeline::Pipeline;
use crate::commands::deploy::DeployCommand;
use crate::error::Error;
use crate::function::Function;
use crate::project::Project;
use crate::runner::Runner;
use crate::writer::Writer;
use eyre::Context;
use kinetics_api::stack;
use reqwest::StatusCode;
use serde_json::json;
use std::collections::HashMap;

pub(crate) struct DeployRunner<'a> {
    pub(crate) command: DeployCommand,
    pub(crate) writer: &'a Writer,
}

impl Runner for DeployRunner<'_> {
    /// Invoke the function either locally or remotely
    async fn run(&mut self) -> Result<(), Error> {
        let mut project = self.project(&self.command.project).await?;

        if self.command.org.is_some() {
            project = project.with_org(self.command.org.as_deref());
        }

        // DataDog API key only needed during deployment, to send it to the backend
        if project
            .observability
            .as_ref()
            .filter(|o| o.dd_api_key.is_empty())
            .is_some()
        {
            return Err(Error::new(
                "DataDog API key is missing",
                Some("Failed to read from specified env var. Check kinetics.toml:[observability]"),
            ));
        }

        if self.command.envs {
            self.deploy_envs(project).await?;
        } else {
            self.deploy_all(project).await?;
        }

        Project::clear_cache().map_err(|e| {
            self.error(
                Some("Failed to clear project cache"),
                Some(&e.to_string()),
                Some(e.into()),
            )
        })?;

        self.writer.json(json!({"success": true}))?;
        Ok(())
    }
}

impl DeployRunner<'_> {
    /// Deploy only environment variables for functions
    async fn deploy_envs(&mut self, project: Project) -> eyre::Result<()> {
        self.writer.text(&format!(
            "{}...\n",
            console::style("Provisioning envs").green().bold()
        ))?;

        let client = self.api_client().await?;

        let functions: Vec<Function> = project
            .parse(&self.command.functions)?
            .iter()
            .filter(|f| f.is_deploying)
            .cloned()
            .collect();

        if functions.is_empty() {
            self.writer.text(&format!(
                "{}\n",
                console::style("No functions found").yellow().bold()
            ))?;

            return Ok(());
        }

        // Collect environment variables from all functions
        // {"<Function name>": {"<Env>": "<Value>"}}
        let mut envs = HashMap::new();

        for function in &functions {
            let function_envs = function.environment();

            let envs_string = function_envs
                .keys()
                .map(|k| k.as_str())
                .collect::<Vec<_>>()
                .join(", ");

            self.writer.text(&format!(
                "{} {}\n",
                console::style(function.name.clone()).bold(),
                if envs_string.is_empty() {
                    console::style("None").dim().yellow()
                } else {
                    console::style(envs_string.as_str()).dim()
                }
            ))?;

            envs.insert(function.name.clone(), function_envs.clone());
        }

        let result = client
            .post("/stack/deploy/envs")
            .json(&stack::deploy::envs::Request {
                project: (&project).into(),
                functions: envs,
            })
            .send()
            .await
            .wrap_err("Request to update envs failed")?;

        let status = result.status();
        let response_text = result.text().await?;
        log::debug!("Got status from /stack/deploy/envs: {}", status);
        log::debug!("Got response from /stack/deploy/envs: {}", response_text);

        let response_json: stack::deploy::envs::Response = serde_json::from_str(&response_text)
            .wrap_err("Failed to parse response from the backend as JSON")?;

        if StatusCode::OK != status {
            log::error!("Got error response: {}", response_text);
            return Err(eyre::eyre!("Failed to deploy envs"));
        }

        let fails = response_json.fails;

        if !fails.is_empty() {
            return Err(eyre::eyre!(
                "Failed to provision envs for: {}",
                fails
                    .iter()
                    .map(|v| v.as_str())
                    .collect::<Vec<&str>>()
                    .join(", "),
            ));
        }

        self.writer
            .text(&format!("{}\n", console::style("Done").green().bold()))?;

        Ok(())
    }

    /// Do full deployment of requested functions
    async fn deploy_all(&self, project: Project) -> eyre::Result<()> {
        Pipeline::builder(self.writer)
            .set_max_concurrent(self.command.max_concurrency)
            .with_deploy_enabled(true)
            .with_hotswap(self.command.hotswap)
            .with_version_message(self.command.message.clone())
            .set_project(project)
            .build()
            .wrap_err("Failed to build pipeline")?
            .run(&self.command.functions)
            .await
    }
}
