use super::service::{LocalDynamoDB, LocalQueue, LocalSqlDB, Service};
use crate::process::Process;
use crate::{error::Error, writer::Writer};
use eyre::Context;
use serde_json::{Map, Value};
use std::{path::Path, path::PathBuf, process, process::Stdio};

/// Manage docker containers
///
/// Docker is mostly used to provision services (e.g. a database, or a queue)
/// when a function gets invoked locally.
pub struct Docker<'a> {
    /// Path to .kinetics dir
    build_path: PathBuf,

    /// A flag indicating the instance was started
    is_started: bool,

    /// List of services to start
    services: Vec<Service<'a>>,
}

impl<'a> Docker<'a> {
    pub fn new(build_path: &Path) -> Self {
        Self {
            build_path: build_path.to_owned(),
            is_started: false,
            services: vec![],
        }
    }

    /// Start docker containers
    pub fn start(&mut self, writer: &Writer) -> eyre::Result<()> {
        // There is nothing to start if there are no services
        if self.services.is_empty() {
            return Ok(());
        }

        let docker_compose_file = self.docker_compose_string()?;
        let dest = self.docker_compose_path();

        std::fs::write(&dest, docker_compose_file)
            .inspect_err(|e| {
                log::error!("Failed to write DOCKER_COMPOSE_FILE to {:?}: {}", dest, e)
            })
            .wrap_err(Error::new(
                "Failed to set up Docker",
                Some(&format!("Make sure you can write to {dest:?}")),
            ))?;

        // Config file functionality must ensure that the root dirs are all valid
        let file_path = dest.to_string_lossy();

        let child = process::Command::new("docker-compose")
            .args(["-f", &file_path, "up", "-d"])
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()
            .wrap_err("Failed to execute docker-compose")?;

        let mut process = Process::new(child, writer);
        let status = process.log()?;

        if !status.success() {
            process.print_error()?;

            return Err(Error::new(
                "Failed to start Docker containers",
                Some("Make sure the docker is installed and running."),
            )
            .into());
        }

        self.is_started = true;

        Ok(())
    }

    /// Stop DynamoDB container
    pub fn stop(&self) -> eyre::Result<()> {
        if !self.is_started {
            // self.start was not called
            return Ok(());
        }

        let status = process::Command::new("docker-compose")
            .args(["-f", &self.docker_compose_path().to_string_lossy(), "down"])
            .stderr(Stdio::null())
            .stdout(Stdio::null())
            .status()
            .inspect_err(|e| log::error!("Error: {}", e))
            .wrap_err("Failed to execute docker-compose")?;

        if !status.success() {
            return Err(eyre::eyre!(
                "docker-compose command failed with exit code: {}",
                status
            ));
        }

        Ok(())
    }

    /// Provision services
    ///
    /// This will provision required resources withing a service. E.g. create a database schema.
    pub async fn provision(&self) -> eyre::Result<()> {
        // Collect futures (only for services that actually need provisioning)
        let tasks = self
            .services
            .iter()
            .map(|service| async move {
                match service {
                    Service::DynamoDB(db) => db
                        .provision()
                        .await
                        .wrap_err("Failed to provision local DynamoDB"),

                    Service::Queue(queue) => queue
                        .provision()
                        .await
                        .wrap_err("Failed to provision local SQS queue"),

                    Service::SqlDB(db) => db
                        .provision()
                        .await
                        .wrap_err("Failed to provision local SQL DB"),
                }
            })
            .collect::<Vec<_>>();

        // Run all futures in parallel
        futures::future::try_join_all(tasks)
            .await
            .wrap_err("Failed to provision services")?;

        Ok(())
    }

    pub fn with_dynamodb(&mut self, dynamodb: LocalDynamoDB) {
        self.services.push(Service::DynamoDB(dynamodb));
    }

    pub fn with_sqldb(&mut self, sqldb: LocalSqlDB<'a>) {
        self.services.push(Service::SqlDB(sqldb));
    }

    pub fn with_queue(&mut self, queue: LocalQueue) {
        self.services.push(Service::Queue(queue));
    }

    /// Creates docker-compose.yml string with all docker services
    fn docker_compose_string(&self) -> eyre::Result<String> {
        // Contains all services for docker-compose.yml file
        let mut services = Map::new();

        for service in &self.services {
            // Prepare service YAML snippets for each service
            let service_snippet = match service {
                Service::DynamoDB(service) => service.docker_compose_snippet(),
                Service::Queue(service) => service.docker_compose_snippet(),
                Service::SqlDB(service) => service.docker_compose_snippet(),
            };

            let mapping: Map<String, Value> = serde_saphyr::from_str(&service_snippet)
                .wrap_err("failed to parse service YAML snippet")?;

            for (k, v) in mapping {
                services.insert(k.clone(), v.clone());
            }
        }

        // Create root YAML for docker-compose.yml file and insert services
        let root = Map::from_iter([("services".to_string(), Value::from(services))]);

        let docker_compose =
            serde_saphyr::to_string(&root).wrap_err("failed to serialize docker-compose YAML")?;

        Ok(docker_compose)
    }

    /// Path to docker-compose.yml file
    fn docker_compose_path(&self) -> PathBuf {
        self.build_path.join("docker-compose.yml")
    }
}

impl Drop for Docker<'_> {
    fn drop(&mut self) {
        self.stop().unwrap();
    }
}
