use std::net::TcpStream;
use std::sync::mpsc;
use std::sync::mpsc::TryRecvError;
use std::thread;
use std::time::Duration;

use tera::Context as TeraContext;

use crate::build_platform::Image;
use crate::cloud_provider::environment::Environment;
use crate::cloud_provider::kubernetes::Kubernetes;
use crate::cloud_provider::utilities::check_domain_for;
use crate::cloud_provider::DeploymentTarget;
use crate::cmd::helm::Timeout;
use crate::cmd::kubectl::kubectl_exec_delete_secret;
use crate::cmd::structs::LabelsContent;
use crate::error::{cast_simple_error_to_engine_error, StringError};
use crate::error::{EngineError, EngineErrorCause, EngineErrorScope};
use crate::models::ProgressLevel::Info;
use crate::models::{Context, Listen, Listeners, ListenersHelper, ProgressInfo, ProgressLevel, ProgressScope};

pub trait Service {
    fn context(&self) -> &Context;
    fn service_type(&self) -> ServiceType;
    fn id(&self) -> &str;
    fn name(&self) -> &str;
    fn sanitized_name(&self) -> String;
    fn name_with_id(&self) -> String {
        format!("{} ({})", self.name(), self.id())
    }
    fn workspace_directory(&self) -> String {
        let dir_root = match self.service_type() {
            ServiceType::Application => "applications",
            ServiceType::ExternalService => "external-services",
            ServiceType::Database(_) => "databases",
            ServiceType::Router => "routers",
        };

        crate::fs::workspace_directory(
            self.context().workspace_root_dir(),
            self.context().execution_id(),
            format!("{}/{}", dir_root, self.name()),
        )
    }
    fn version(&self) -> &str;
    fn action(&self) -> &Action;
    fn private_port(&self) -> Option<u16>;
    fn start_timeout(&self) -> Timeout<u32>;
    fn total_cpus(&self) -> String;
    fn total_ram_in_mib(&self) -> u32;
    fn total_instances(&self) -> u16;
    fn tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError>;
    // used to retrieve logs by using Kubernetes labels (selector)
    fn selector(&self) -> String;
    fn debug_logs(&self, deployment_target: &DeploymentTarget) -> Vec<String> {
        match deployment_target {
            // TODO retrieve logs from managed service?
            DeploymentTarget::ManagedServices(_, _) => vec![],

            DeploymentTarget::SelfHosted(kubernetes, environment) => {
                match get_stateless_resource_information_for_user(*kubernetes, *environment, self) {
                    Ok(lines) => lines,
                    Err(err) => {
                        error!(
                            "error while retrieving debug logs from {} {}; error: {:?}",
                            self.service_type().name(),
                            self.name_with_id(),
                            err
                        );
                        vec![]
                    }
                }
            }
        }
    }

    fn is_listening(&self, ip: &str) -> bool {
        let private_port = match self.private_port() {
            Some(private_port) => private_port,
            _ => return false,
        };

        TcpStream::connect(format!("{}:{}", ip, private_port)).is_ok()
    }
    fn engine_error_scope(&self) -> EngineErrorScope;
    fn engine_error(&self, cause: EngineErrorCause, message: String) -> EngineError {
        EngineError::new(
            cause,
            self.engine_error_scope(),
            self.context().execution_id(),
            Some(message),
        )
    }
    fn is_valid(&self) -> Result<(), EngineError> {
        let binaries = ["kubectl", "helm", "terraform", "aws-iam-authenticator"];

        for binary in binaries.iter() {
            if !crate::cmd::utilities::does_binary_exist(binary) {
                let err = format!("{} binary not found", binary);

                return Err(EngineError::new(
                    EngineErrorCause::Internal,
                    EngineErrorScope::Engine,
                    self.id(),
                    Some(err),
                ));
            }
        }

        // TODO check lib directories available

        Ok(())
    }

    fn progress_scope(&self) -> ProgressScope {
        let id = self.id().to_string();

        match self.service_type() {
            ServiceType::Application => ProgressScope::Application { id },
            ServiceType::ExternalService => ProgressScope::ExternalService { id },
            ServiceType::Database(_) => ProgressScope::Database { id },
            ServiceType::Router => ProgressScope::Router { id },
        }
    }
}

pub trait StatelessService: Service + Create + Pause + Delete {
    fn exec_action(&self, deployment_target: &DeploymentTarget) -> Result<(), EngineError> {
        match self.action() {
            crate::cloud_provider::service::Action::Create => self.on_create(deployment_target),
            crate::cloud_provider::service::Action::Delete => self.on_delete(deployment_target),
            crate::cloud_provider::service::Action::Pause => self.on_pause(deployment_target),
            crate::cloud_provider::service::Action::Nothing => Ok(()),
        }
    }
}

pub trait StatefulService: Service + Create + Pause + Delete + Backup + Clone + Upgrade + Downgrade {
    fn exec_action(&self, deployment_target: &DeploymentTarget) -> Result<(), EngineError> {
        match self.action() {
            crate::cloud_provider::service::Action::Create => self.on_create(deployment_target),
            crate::cloud_provider::service::Action::Delete => self.on_delete(deployment_target),
            crate::cloud_provider::service::Action::Pause => self.on_pause(deployment_target),
            crate::cloud_provider::service::Action::Nothing => Ok(()),
        }
    }
}

pub trait Application: StatelessService {
    fn image(&self) -> &Image;
    fn set_image(&mut self, image: Image);
}

pub trait ExternalService: StatelessService {}

pub trait Router: StatelessService + Listen {
    fn domains(&self) -> Vec<&str>;
    fn check_domains(&self) -> Result<(), EngineError> {
        check_domain_for(
            ListenersHelper::new(self.listeners()),
            self.domains(),
            self.id(),
            self.context().execution_id(),
        )?;
        Ok(())
    }
}

pub trait Database: StatefulService {
    fn check_domains(&self, listeners: Listeners, domains: Vec<&str>) -> Result<(), EngineError> {
        check_domain_for(
            ListenersHelper::new(&listeners),
            domains,
            self.id(),
            self.context().execution_id(),
        )?;
        Ok(())
    }
}

pub trait Create {
    fn on_create(&self, target: &DeploymentTarget) -> Result<(), EngineError>;
    fn on_create_check(&self) -> Result<(), EngineError>;
    fn on_create_error(&self, target: &DeploymentTarget) -> Result<(), EngineError>;
}

pub trait Pause {
    fn on_pause(&self, target: &DeploymentTarget) -> Result<(), EngineError>;
    fn on_pause_check(&self) -> Result<(), EngineError>;
    fn on_pause_error(&self, target: &DeploymentTarget) -> Result<(), EngineError>;
}

pub trait Delete {
    fn on_delete(&self, target: &DeploymentTarget) -> Result<(), EngineError>;
    fn on_delete_check(&self) -> Result<(), EngineError>;
    fn on_delete_error(&self, target: &DeploymentTarget) -> Result<(), EngineError>;
}

pub trait Backup {
    fn on_backup(&self, target: &DeploymentTarget) -> Result<(), EngineError>;
    fn on_backup_check(&self) -> Result<(), EngineError>;
    fn on_backup_error(&self, target: &DeploymentTarget) -> Result<(), EngineError>;
    fn on_restore(&self, target: &DeploymentTarget) -> Result<(), EngineError>;
    fn on_restore_check(&self) -> Result<(), EngineError>;
    fn on_restore_error(&self, target: &DeploymentTarget) -> Result<(), EngineError>;
}

pub trait Clone {
    fn on_clone(&self, target: &DeploymentTarget) -> Result<(), EngineError>;
    fn on_clone_check(&self) -> Result<(), EngineError>;
    fn on_clone_error(&self, target: &DeploymentTarget) -> Result<(), EngineError>;
}

pub trait Upgrade {
    fn on_upgrade(&self, target: &DeploymentTarget) -> Result<(), EngineError>;
    fn on_upgrade_check(&self) -> Result<(), EngineError>;
    fn on_upgrade_error(&self, target: &DeploymentTarget) -> Result<(), EngineError>;
}

pub trait Downgrade {
    fn on_downgrade(&self, target: &DeploymentTarget) -> Result<(), EngineError>;
    fn on_downgrade_check(&self) -> Result<(), EngineError>;
    fn on_downgrade_error(&self, target: &DeploymentTarget) -> Result<(), EngineError>;
}

pub trait Terraform {
    fn terraform_common_resource_dir_path(&self) -> String;
    fn terraform_resource_dir_path(&self) -> String;
}

pub trait Helm {
    fn helm_release_name(&self) -> String;
    fn helm_chart_dir(&self) -> String;
    fn helm_chart_values_dir(&self) -> String;
    fn helm_chart_external_name_service_dir(&self) -> String;
}

#[derive(Clone, Eq, PartialEq, Hash)]
pub enum Action {
    Create,
    Pause,
    Delete,
    Nothing,
}

#[derive(Eq, PartialEq)]
pub struct DatabaseOptions {
    pub login: String,
    pub password: String,
    pub host: String,
    pub port: u16,
    pub disk_size_in_gib: u32,
    pub database_disk_type: String,
}

#[derive(Eq, PartialEq)]
pub enum DatabaseType<'a> {
    PostgreSQL(&'a DatabaseOptions),
    MongoDB(&'a DatabaseOptions),
    MySQL(&'a DatabaseOptions),
    Redis(&'a DatabaseOptions),
}

#[derive(Eq, PartialEq)]
pub enum ServiceType<'a> {
    Application,
    ExternalService,
    Database(DatabaseType<'a>),
    Router,
}

impl<'a> ServiceType<'a> {
    pub fn name(&self) -> &str {
        match self {
            ServiceType::Application => "Application",
            ServiceType::ExternalService => "ExternalService",
            ServiceType::Database(db_type) => match db_type {
                DatabaseType::PostgreSQL(_) => "PostgreSQL database",
                DatabaseType::MongoDB(_) => "MongoDB database",
                DatabaseType::MySQL(_) => "MySQL database",
                DatabaseType::Redis(_) => "Redis database",
            },
            ServiceType::Router => "Router",
        }
    }
}

pub fn default_tera_context(
    service: &dyn Service,
    kubernetes: &dyn Kubernetes,
    environment: &Environment,
) -> TeraContext {
    let mut context = TeraContext::new();

    context.insert("id", service.id());
    context.insert("owner_id", environment.owner_id.as_str());
    context.insert("project_id", environment.project_id.as_str());
    context.insert("organization_id", environment.organization_id.as_str());
    context.insert("environment_id", environment.id.as_str());
    context.insert("region", kubernetes.region());
    context.insert("name", service.name());
    context.insert("sanitized_name", &service.sanitized_name());
    context.insert("namespace", environment.namespace());
    context.insert("cluster_name", kubernetes.name());
    context.insert("total_cpus", &service.total_cpus());
    context.insert("total_ram_in_mib", &service.total_ram_in_mib());
    context.insert("total_instances", &service.total_instances());

    context.insert("is_private_port", &service.private_port().is_some());
    if service.private_port().is_some() {
        context.insert("private_port", &service.private_port().unwrap());
    }

    context.insert("version", service.version());

    context
}

/// deploy a stateless service created by the user (E.g: App or External Service)
/// the difference with `deploy_service(..)` is that this function provides the thrown error in case of failure
pub fn deploy_user_stateless_service<T>(target: &DeploymentTarget, service: &T) -> Result<(), EngineError>
where
    T: Service + Helm,
{
    deploy_stateless_service(
        target,
        service,
        service.engine_error(
            EngineErrorCause::User(
                "Your application didn't start for some reason. \
                Are you sure your application is correctly running? You can give a try by running \
                locally `qovery run`. You can also check the application log from the web \
                interface or the CLI with `qovery log`",
            ),
            format!(
                "{} {} has failed to start ⤬",
                service.service_type().name(),
                service.name_with_id()
            ),
        ),
    )
}

/// deploy a stateless service (app, router, database...) on Kubernetes
pub fn deploy_stateless_service<T>(
    target: &DeploymentTarget,
    service: &T,
    thrown_error: EngineError,
) -> Result<(), EngineError>
where
    T: Service + Helm,
{
    let (kubernetes, environment) = match target {
        DeploymentTarget::ManagedServices(k, env) => (*k, *env),
        DeploymentTarget::SelfHosted(k, env) => (*k, *env),
    };

    let workspace_dir = service.workspace_directory();
    let tera_context = service.tera_context(target)?;

    let _ = cast_simple_error_to_engine_error(
        service.engine_error_scope(),
        service.context().execution_id(),
        crate::template::generate_and_copy_all_files_into_dir(
            service.helm_chart_dir(),
            workspace_dir.as_str(),
            &tera_context,
        ),
    )?;

    let helm_release_name = service.helm_release_name();
    let kubernetes_config_file_path = kubernetes.config_file_path()?;

    // define labels to add to namespace
    let namespace_labels = match service.context().resource_expiration_in_seconds() {
        Some(_) => Some(vec![
            (LabelsContent {
                name: "ttl".to_string(),
                value: format! {"{}", service.context().resource_expiration_in_seconds().unwrap()},
            }),
        ]),
        None => None,
    };

    // create a namespace with labels if do not exists
    let _ = cast_simple_error_to_engine_error(
        service.engine_error_scope(),
        service.context().execution_id(),
        crate::cmd::kubectl::kubectl_exec_create_namespace(
            kubernetes_config_file_path.as_str(),
            environment.namespace(),
            namespace_labels,
            kubernetes.cloud_provider().credentials_environment_variables(),
        ),
    )?;

    // do exec helm upgrade and return the last deployment status
    let helm_history_row = cast_simple_error_to_engine_error(
        service.engine_error_scope(),
        service.context().execution_id(),
        crate::cmd::helm::helm_exec_with_upgrade_history(
            kubernetes_config_file_path.as_str(),
            environment.namespace(),
            helm_release_name.as_str(),
            workspace_dir.as_str(),
            service.start_timeout(),
            kubernetes.cloud_provider().credentials_environment_variables(),
        ),
    )?;

    // check deployment status
    if helm_history_row.is_none() || !helm_history_row.unwrap().is_successfully_deployed() {
        return Err(thrown_error);
    }

    let _ = cast_simple_error_to_engine_error(
        service.engine_error_scope(),
        service.context().execution_id(),
        crate::cmd::kubectl::kubectl_exec_is_pod_ready_with_retry(
            kubernetes_config_file_path.as_str(),
            environment.namespace(),
            service.selector().as_str(),
            kubernetes.cloud_provider().credentials_environment_variables(),
        ),
    )?;

    Ok(())
}

/// do specific operations on a stateless service deployment error
pub fn deploy_stateless_service_error<T>(target: &DeploymentTarget, service: &T) -> Result<(), EngineError>
where
    T: Service + Helm,
{
    let (kubernetes, environment) = match target {
        DeploymentTarget::ManagedServices(k, env) => (*k, *env),
        DeploymentTarget::SelfHosted(k, env) => (*k, *env),
    };

    let kubernetes_config_file_path = kubernetes.config_file_path()?;
    let helm_release_name = service.helm_release_name();

    let history_rows = cast_simple_error_to_engine_error(
        service.engine_error_scope(),
        service.context().execution_id(),
        crate::cmd::helm::helm_exec_history(
            kubernetes_config_file_path.as_str(),
            environment.namespace(),
            helm_release_name.as_str(),
            kubernetes.cloud_provider().credentials_environment_variables(),
        ),
    )?;

    if history_rows.len() == 1 {
        cast_simple_error_to_engine_error(
            service.engine_error_scope(),
            service.context().execution_id(),
            crate::cmd::helm::helm_exec_uninstall(
                kubernetes_config_file_path.as_str(),
                environment.namespace(),
                helm_release_name.as_str(),
                kubernetes.cloud_provider().credentials_environment_variables(),
            ),
        )?;
    }

    Ok(())
}

pub fn delete_stateless_service<T>(target: &DeploymentTarget, service: &T, is_error: bool) -> Result<(), EngineError>
where
    T: Service + Helm,
{
    let (kubernetes, environment) = match target {
        DeploymentTarget::ManagedServices(k, env) => (*k, *env),
        DeploymentTarget::SelfHosted(k, env) => (*k, *env),
    };

    let helm_release_name = service.helm_release_name();

    if is_error {
        let _ = get_stateless_resource_information(kubernetes, environment, service.selector().as_str())?;
    }

    // clean the resource
    let _ = do_stateless_service_cleanup(kubernetes, environment, helm_release_name.as_str())?;

    Ok(())
}

pub fn deploy_stateful_service<T>(target: &DeploymentTarget, service: &T) -> Result<(), EngineError>
where
    T: StatefulService + Helm + Terraform,
{
    let workspace_dir = service.workspace_directory();

    match target {
        DeploymentTarget::ManagedServices(kubernetes, _) => {
            // use terraform
            info!(
                "deploy {} with name {} on {}",
                service.service_type().name(),
                service.name_with_id(),
                kubernetes.cloud_provider().kind().name()
            );

            let context = service.tera_context(target)?;

            let _ = cast_simple_error_to_engine_error(
                service.engine_error_scope(),
                service.context().execution_id(),
                crate::template::generate_and_copy_all_files_into_dir(
                    service.terraform_common_resource_dir_path(),
                    &workspace_dir,
                    &context,
                ),
            )?;

            let _ = cast_simple_error_to_engine_error(
                service.engine_error_scope(),
                service.context().execution_id(),
                crate::template::generate_and_copy_all_files_into_dir(
                    service.terraform_resource_dir_path(),
                    workspace_dir.as_str(),
                    &context,
                ),
            )?;

            let _ = cast_simple_error_to_engine_error(
                service.engine_error_scope(),
                service.context().execution_id(),
                crate::template::generate_and_copy_all_files_into_dir(
                    service.helm_chart_external_name_service_dir(),
                    format!("{}/{}", workspace_dir, "external-name-svc"),
                    &context,
                ),
            )?;

            let _ = cast_simple_error_to_engine_error(
                service.engine_error_scope(),
                service.context().execution_id(),
                crate::cmd::terraform::terraform_exec_with_init_validate_plan_apply(
                    workspace_dir.as_str(),
                    service.context().is_dry_run_deploy(),
                ),
            )?;
        }
        DeploymentTarget::SelfHosted(kubernetes, environment) => {
            // use helm
            info!(
                "deploy {} with name {} on {:?} Kubernetes cluster id {}",
                service.service_type().name(),
                service.name_with_id(),
                kubernetes.cloud_provider().kind().name(),
                kubernetes.id()
            );

            let context = service.tera_context(target)?;
            let kubernetes_config_file_path = kubernetes.config_file_path()?;

            // default chart
            let _ = cast_simple_error_to_engine_error(
                service.engine_error_scope(),
                service.context().execution_id(),
                crate::template::generate_and_copy_all_files_into_dir(
                    service.helm_chart_dir(),
                    workspace_dir.as_str(),
                    &context,
                ),
            )?;

            // overwrite with our chart values
            let _ = cast_simple_error_to_engine_error(
                service.engine_error_scope(),
                service.context().execution_id(),
                crate::template::generate_and_copy_all_files_into_dir(
                    service.helm_chart_values_dir(),
                    workspace_dir.as_str(),
                    &context,
                ),
            )?;

            // define labels to add to namespace
            let namespace_labels = match service.context().resource_expiration_in_seconds() {
                Some(_) => Some(vec![
                    (LabelsContent {
                        name: "ttl".into(),
                        value: format!("{}", service.context().resource_expiration_in_seconds().unwrap()),
                    }),
                ]),
                None => None,
            };

            // create a namespace with labels if it does not exist
            let _ = cast_simple_error_to_engine_error(
                service.engine_error_scope(),
                service.context().execution_id(),
                crate::cmd::kubectl::kubectl_exec_create_namespace(
                    kubernetes_config_file_path.as_str(),
                    environment.namespace(),
                    namespace_labels,
                    kubernetes.cloud_provider().credentials_environment_variables(),
                ),
            )?;

            // do exec helm upgrade and return the last deployment status
            let helm_history_row = cast_simple_error_to_engine_error(
                service.engine_error_scope(),
                service.context().execution_id(),
                crate::cmd::helm::helm_exec_with_upgrade_history(
                    kubernetes_config_file_path.as_str(),
                    environment.namespace(),
                    service.helm_release_name().as_str(),
                    workspace_dir.as_str(),
                    service.start_timeout(),
                    kubernetes.cloud_provider().credentials_environment_variables(),
                ),
            )?;

            // check deployment status
            if helm_history_row.is_none() || !helm_history_row.unwrap().is_successfully_deployed() {
                return Err(service.engine_error(
                    EngineErrorCause::Internal,
                    format!(
                        "{} service fails to be deployed (before start)",
                        service.service_type().name()
                    ),
                ));
            }

            // check app status
            match crate::cmd::kubectl::kubectl_exec_is_pod_ready_with_retry(
                kubernetes_config_file_path.as_str(),
                environment.namespace(),
                service.selector().as_str(),
                kubernetes.cloud_provider().credentials_environment_variables(),
            ) {
                Ok(Some(true)) => {}
                _ => {
                    return Err(service.engine_error(
                        EngineErrorCause::Internal,
                        format!(
                            "{} database {} failed to start after several retries",
                            service.service_type().name(),
                            service.name_with_id()
                        ),
                    ));
                }
            }
        }
    }

    Ok(())
}

pub fn delete_stateful_service<T>(target: &DeploymentTarget, service: &T) -> Result<(), EngineError>
where
    T: StatefulService + Helm + Terraform,
{
    match target {
        DeploymentTarget::ManagedServices(kubernetes, _) => {
            let workspace_dir = service.workspace_directory();
            let tera_context = service.tera_context(target)?;

            let _ = cast_simple_error_to_engine_error(
                service.engine_error_scope(),
                service.context().execution_id(),
                crate::template::generate_and_copy_all_files_into_dir(
                    service.terraform_common_resource_dir_path(),
                    workspace_dir.as_str(),
                    &tera_context,
                ),
            )?;

            let _ = cast_simple_error_to_engine_error(
                service.engine_error_scope(),
                service.context().execution_id(),
                crate::template::generate_and_copy_all_files_into_dir(
                    service.terraform_resource_dir_path(),
                    workspace_dir.as_str(),
                    &tera_context,
                ),
            )?;

            let _ = cast_simple_error_to_engine_error(
                service.engine_error_scope(),
                service.context().execution_id(),
                crate::template::generate_and_copy_all_files_into_dir(
                    service.helm_chart_external_name_service_dir(),
                    format!("{}/{}", workspace_dir, "external-name-svc"),
                    &tera_context,
                ),
            )?;

            let _ = cast_simple_error_to_engine_error(
                service.engine_error_scope(),
                service.context().execution_id(),
                crate::template::generate_and_copy_all_files_into_dir(
                    service.helm_chart_external_name_service_dir(),
                    workspace_dir.as_str(),
                    &tera_context,
                ),
            )?;

            match crate::cmd::terraform::terraform_exec_destroy(workspace_dir.as_str(), true) {
                Ok(_) => {
                    info!("let's delete secrets containing tfstates");
                    let _ = delete_terraform_tfstate_secret(*kubernetes, &get_tfstate_name(service));
                }
                Err(e) => {
                    let message = format!("{:?}", e);
                    error!("{}", message);

                    return Err(service.engine_error(EngineErrorCause::Internal, message));
                }
            }
        }
        DeploymentTarget::SelfHosted(kubernetes, environment) => {
            let helm_release_name = service.helm_release_name();

            // clean the resource
            let _ = do_stateless_service_cleanup(*kubernetes, *environment, helm_release_name.as_str())?;
        }
    }

    Ok(())
}

pub fn check_service_version<T>(result: Result<String, StringError>, service: &T) -> Result<String, EngineError>
where
    T: Service + Listen,
{
    let listeners_helper = ListenersHelper::new(service.listeners());

    match result {
        Ok(version) => {
            if service.version() != version.as_str() {
                let message = format!(
                    "{} version {} has been requested by the user; but matching version is {}",
                    service.service_type().name(),
                    service.version(),
                    version.as_str()
                );

                info!("{}", message.as_str());

                let progress_info = ProgressInfo::new(
                    service.progress_scope(),
                    ProgressLevel::Info,
                    Some(message),
                    service.context().execution_id(),
                );

                listeners_helper.deployment_in_progress(progress_info);
            }

            Ok(version)
        }
        Err(err) => {
            let message = format!(
                "{} version {} is not supported!",
                service.service_type().name(),
                service.version(),
            );

            error!("{}", message.as_str());

            let progress_info = ProgressInfo::new(
                service.progress_scope(),
                ProgressLevel::Error,
                Some(message),
                service.context().execution_id(),
            );

            listeners_helper.deployment_error(progress_info);

            error!("{}", err);

            Err(service.engine_error(
                EngineErrorCause::User(
                    "The provided database version is not supported, please refer to the \
                documentation https://docs.qovery.com",
                ),
                err,
            ))
        }
    }
}

fn delete_terraform_tfstate_secret(kubernetes: &dyn Kubernetes, secret_name: &str) -> Result<(), EngineError> {
    let config_file_path = kubernetes.config_file_path()?;

    //create the namespace to insert the tfstate in secrets
    let _ = kubectl_exec_delete_secret(
        config_file_path,
        secret_name,
        kubernetes.cloud_provider().credentials_environment_variables(),
    );

    Ok(())
}

pub enum CheckAction {
    Deploy,
    Pause,
    Delete,
}

pub fn check_kubernetes_service_error<T>(
    result: Result<(), EngineError>,
    kubernetes: &dyn Kubernetes,
    service: &Box<T>,
    deployment_target: &DeploymentTarget,
    listeners_helper: &ListenersHelper,
    action_verb: &str,
    action: CheckAction,
) -> Result<(), EngineError>
where
    T: Service + ?Sized,
{
    let progress_info = ProgressInfo::new(
        service.progress_scope(),
        ProgressLevel::Info,
        Some(format!(
            "{} {} {}",
            action_verb,
            service.service_type().name().to_lowercase(),
            service.name()
        )),
        kubernetes.context().execution_id(),
    );

    match action {
        CheckAction::Deploy => listeners_helper.deployment_in_progress(progress_info),
        CheckAction::Pause => listeners_helper.pause_in_progress(progress_info),
        CheckAction::Delete => listeners_helper.delete_in_progress(progress_info),
    }

    match result {
        Err(err) => {
            error!(
                "{} error with {} {} , id: {} => {:?}",
                action_verb,
                service.service_type().name(),
                service.name(),
                service.id(),
                err
            );

            let progress_info = ProgressInfo::new(
                service.progress_scope(),
                ProgressLevel::Error,
                Some(format!(
                    "{} error {} {} : error => {:?}",
                    action_verb,
                    service.service_type().name().to_lowercase(),
                    service.name(),
                    err
                )),
                kubernetes.context().execution_id(),
            );

            match action {
                CheckAction::Deploy => listeners_helper.deployment_error(progress_info),
                CheckAction::Pause => listeners_helper.pause_error(progress_info),
                CheckAction::Delete => listeners_helper.delete_error(progress_info),
            }

            let debug_logs = service.debug_logs(deployment_target);
            let debug_logs_string = if !debug_logs.is_empty() {
                debug_logs.join("\n")
            } else {
                String::from("<no debug logs>")
            };

            info!("{}", debug_logs_string);

            let progress_info = ProgressInfo::new(
                service.progress_scope(),
                ProgressLevel::Debug,
                Some(debug_logs_string),
                kubernetes.context().execution_id(),
            );

            match action {
                CheckAction::Deploy => listeners_helper.deployment_error(progress_info),
                CheckAction::Pause => listeners_helper.pause_error(progress_info),
                CheckAction::Delete => listeners_helper.delete_error(progress_info),
            }

            return Err(err);
        }
        _ => {
            let progress_info = ProgressInfo::new(
                service.progress_scope(),
                ProgressLevel::Info,
                Some(format!(
                    "{} succeeded for {} {}",
                    action_verb,
                    service.service_type().name().to_lowercase(),
                    service.name()
                )),
                kubernetes.context().execution_id(),
            );

            match action {
                CheckAction::Deploy => listeners_helper.deployment_in_progress(progress_info),
                CheckAction::Pause => listeners_helper.pause_in_progress(progress_info),
                CheckAction::Delete => listeners_helper.delete_in_progress(progress_info),
            }

            Ok(())
        }
    }
}

pub type Logs = String;
pub type Describe = String;

/// return debug information line by line to help the user to understand what's going on,
/// and why its app does not start
pub fn get_stateless_resource_information_for_user<T>(
    kubernetes: &dyn Kubernetes,
    environment: &Environment,
    service: &T,
) -> Result<Vec<String>, EngineError>
where
    T: Service + ?Sized,
{
    let selector = service.selector();
    let kubernetes_config_file_path = kubernetes.config_file_path()?;
    let mut result = Vec::with_capacity(50);

    // get logs
    let logs = cast_simple_error_to_engine_error(
        kubernetes.engine_error_scope(),
        kubernetes.context().execution_id(),
        crate::cmd::kubectl::kubectl_exec_logs(
            kubernetes_config_file_path.as_str(),
            environment.namespace(),
            selector.as_str(),
            kubernetes.cloud_provider().credentials_environment_variables(),
        ),
    )?;

    let _ = result.extend(logs);

    // get pod state
    let pods = cast_simple_error_to_engine_error(
        kubernetes.engine_error_scope(),
        kubernetes.context().execution_id(),
        crate::cmd::kubectl::kubectl_exec_get_pod(
            kubernetes_config_file_path.as_str(),
            environment.namespace(),
            selector.as_str(),
            kubernetes.cloud_provider().credentials_environment_variables(),
        ),
    )?;

    for pod in pods.items {
        for container_condition in pod.status.conditions {
            if container_condition.status.to_ascii_lowercase() == "false" {
                result.push(format!(
                    "Condition not met to start the container: {} -> {}: {}",
                    container_condition.typee,
                    container_condition.reason.unwrap_or_default(),
                    container_condition.message.unwrap_or_default()
                ))
            }
        }
        for container_status in pod.status.container_statuses.unwrap_or_default() {
            if let Some(last_state) = container_status.last_state {
                if let Some(terminated) = last_state.terminated {
                    if let Some(message) = terminated.message {
                        result.push(format!("terminated state message: {}", message));
                    }

                    result.push(format!("terminated state exit code: {}", terminated.exit_code));
                }

                if let Some(waiting) = last_state.waiting {
                    if let Some(message) = waiting.message {
                        result.push(format!("waiting state message: {}", message));
                    }
                }
            }
        }
    }

    // get pod events
    let events = cast_simple_error_to_engine_error(
        kubernetes.engine_error_scope(),
        kubernetes.context().execution_id(),
        crate::cmd::kubectl::kubectl_exec_get_event(
            kubernetes_config_file_path.as_str(),
            environment.namespace(),
            kubernetes.cloud_provider().credentials_environment_variables(),
        ),
    )?;

    let pod_name_start = format!("{}-", service.name());
    for event in events.items {
        if event.type_.to_lowercase() != "normal" && event.involved_object.name.starts_with(&pod_name_start) {
            if let Some(message) = event.message {
                result.push(format!("{}: {}", event.type_, message));
            }
        }
    }

    Ok(result)
}

/// show different output (kubectl describe, log..) for debug purpose
pub fn get_stateless_resource_information(
    kubernetes: &dyn Kubernetes,
    environment: &Environment,
    selector: &str,
) -> Result<(Describe, Logs), EngineError> {
    let kubernetes_config_file_path = kubernetes.config_file_path()?;

    // exec describe pod...
    let describe = match cast_simple_error_to_engine_error(
        kubernetes.engine_error_scope(),
        kubernetes.context().execution_id(),
        crate::cmd::kubectl::kubectl_exec_describe_pod(
            kubernetes_config_file_path.as_str(),
            environment.namespace(),
            selector,
            kubernetes.cloud_provider().credentials_environment_variables(),
        ),
    ) {
        Ok(output) => {
            info!("{}", output);
            output
        }
        Err(err) => {
            error!("{:?}", err);
            return Err(err);
        }
    };

    // exec logs...
    let logs = match cast_simple_error_to_engine_error(
        kubernetes.engine_error_scope(),
        kubernetes.context().execution_id(),
        crate::cmd::kubectl::kubectl_exec_logs(
            kubernetes_config_file_path.as_str(),
            environment.namespace(),
            selector,
            kubernetes.cloud_provider().credentials_environment_variables(),
        ),
    ) {
        Ok(output) => {
            info!("{:?}", output);
            output.join("\n")
        }
        Err(err) => {
            error!("{:?}", err);
            return Err(err);
        }
    };

    Ok((describe, logs))
}

pub fn do_stateless_service_cleanup(
    kubernetes: &dyn Kubernetes,
    environment: &Environment,
    helm_release_name: &str,
) -> Result<(), EngineError> {
    let kubernetes_config_file_path = kubernetes.config_file_path()?;

    let history_rows = cast_simple_error_to_engine_error(
        kubernetes.engine_error_scope(),
        kubernetes.context().execution_id(),
        crate::cmd::helm::helm_exec_history(
            kubernetes_config_file_path.as_str(),
            environment.namespace(),
            helm_release_name,
            kubernetes.cloud_provider().credentials_environment_variables(),
        ),
    )?;

    // if there is no valid history - then delete the helm chart
    let first_valid_history_row = history_rows.iter().find(|x| x.is_successfully_deployed());

    if first_valid_history_row.is_some() {
        cast_simple_error_to_engine_error(
            kubernetes.engine_error_scope(),
            kubernetes.context().execution_id(),
            crate::cmd::helm::helm_exec_uninstall(
                kubernetes_config_file_path.as_str(),
                environment.namespace(),
                helm_release_name,
                kubernetes.cloud_provider().credentials_environment_variables(),
            ),
        )?;
    }

    Ok(())
}

/// This function call (start|pause|delete)_in_progress function every 10 seconds when a
/// long blocking task is running.
pub fn send_progress_on_long_task<S, R, F>(service: &S, action: Action, long_task: F) -> R
where
    S: Service + Listen,
    F: Fn() -> R,
{
    let waiting_message = match action {
        Action::Create => Some(format!(
            "{} '{}' deployment is in progress...",
            service.service_type().name(),
            service.name_with_id()
        )),
        Action::Pause => Some(format!(
            "{} '{}' pause is in progress...",
            service.service_type().name(),
            service.name_with_id()
        )),
        Action::Delete => Some(format!(
            "{} '{}' deletion is in progress...",
            service.service_type().name(),
            service.name_with_id()
        )),
        Action::Nothing => None,
    };

    send_progress_on_long_task_with_message(service, waiting_message, action, long_task)
}

/// This function call (start|pause|delete)_in_progress function every 10 seconds when a
/// long blocking task is running.
pub fn send_progress_on_long_task_with_message<S, M, R, F>(
    service: &S,
    waiting_message: Option<M>,
    action: Action,
    long_task: F,
) -> R
where
    S: Service + Listen,
    M: Into<String>,
    F: Fn() -> R,
{
    let listeners = std::clone::Clone::clone(service.listeners());

    let progress_info = ProgressInfo::new(
        service.progress_scope(),
        Info,
        match waiting_message {
            Some(message) => Some(message.into()),
            _ => None,
        },
        service.context().execution_id(),
    );

    let (tx, rx) = mpsc::channel();

    // monitor thread to notify user while the blocking task is executed
    let _ = std::thread::Builder::new()
        .name("task-monitor".to_string())
        .spawn(move || {
            // stop the thread when the blocking task is done
            let listeners_helper = ListenersHelper::new(&listeners);
            let action = action;
            let progress_info = progress_info;

            loop {
                // do notify users here
                let progress_info = std::clone::Clone::clone(&progress_info);

                match action {
                    Action::Create => listeners_helper.deployment_in_progress(progress_info),
                    Action::Pause => listeners_helper.pause_in_progress(progress_info),
                    Action::Delete => listeners_helper.delete_in_progress(progress_info),
                    Action::Nothing => {} // should not happens
                };

                thread::sleep(Duration::from_secs(10));

                // watch for thread termination
                match rx.try_recv() {
                    Ok(_) | Err(TryRecvError::Disconnected) => break,
                    Err(TryRecvError::Empty) => {}
                }
            }
        });

    let blocking_task_result = long_task();
    let _ = tx.send(());

    blocking_task_result
}

pub fn get_tfstate_suffix(service: &dyn Service) -> String {
    format!("{}", service.id())
}

// Name generated from TF secret suffix
// https://www.terraform.io/docs/backends/types/kubernetes.html#secret_suffix
// As mention the doc: Secrets will be named in the format: tfstate-{workspace}-{secret_suffix}.
pub fn get_tfstate_name(service: &dyn Service) -> String {
    format!("tfstate-default-{}", service.id())
}
