package com.sequenceiq.cloudbreak.orchestrator.salt.poller;

import static java.util.Collections.singletonMap;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;

import com.sequenceiq.cloudbreak.common.model.recipe.RecipeType;
import com.sequenceiq.cloudbreak.common.orchestration.Node;
import com.sequenceiq.cloudbreak.common.type.RecipeExecutionPhase;
import com.sequenceiq.cloudbreak.orchestrator.OrchestratorBootstrap;
import com.sequenceiq.cloudbreak.orchestrator.exception.CloudbreakOrchestratorFailedException;
import com.sequenceiq.cloudbreak.orchestrator.model.GenericResponse;
import com.sequenceiq.cloudbreak.orchestrator.model.GenericResponses;
import com.sequenceiq.cloudbreak.orchestrator.model.RecipeModel;
import com.sequenceiq.cloudbreak.orchestrator.model.SaltPillarProperties;
import com.sequenceiq.cloudbreak.orchestrator.salt.client.SaltConnector;
import com.sequenceiq.cloudbreak.orchestrator.salt.domain.Pillar;

public class PillarSave implements OrchestratorBootstrap {

    private static final Logger LOGGER = LoggerFactory.getLogger(PillarSave.class);

    private final SaltConnector sc;

    private final Pillar pillar;

    private Set<String> targets;

    private final Set<String> originalTargets;

    private PillarSave(SaltConnector sc, Set<String> targets, Collection<Node> allNodes) {
        this.sc = sc;
        Map<String, Map<String, Object>> fqdn = allNodes
                .stream()
                .filter(node -> node.getHostname() != null && node.getPrivateIp() != null)
                .collect(Collectors.toMap(Node::getPrivateIp, node -> discovery(node.getHostname(), node.getPublicIp(),
                        node.getInstanceId(), node.getInstanceType())));
        pillar = new Pillar("/nodes/hosts.sls", singletonMap("hosts", fqdn), targets);
        this.targets = targets;
        originalTargets = targets;
    }

    private PillarSave(SaltConnector sc, Set<String> targets, Map<String, List<RecipeModel>> recipes, Long recipeExecutionTimeout) {
        this.sc = sc;
        Map<String, Map<String, List<String>>> scripts = new HashMap<>(recipes.size());
        for (Entry<String, List<RecipeModel>> entry : recipes.entrySet()) {
            List<String> preServiceDeployment = entry.getValue().stream().
                    filter(h -> h.getRecipeType() == RecipeType.PRE_CLOUDERA_MANAGER_START || h.getRecipeType() == RecipeType.PRE_SERVICE_DEPLOYMENT)
                    .map(RecipeModel::getName).collect(Collectors.toList());
            List<String> preTermination = entry.getValue().stream().
                    filter(h -> h.getRecipeType() == RecipeType.PRE_TERMINATION).map(RecipeModel::getName).collect(Collectors.toList());
            List<String> postClouderaManagerStart = entry.getValue().stream().
                filter(h -> h.getRecipeType() == RecipeType.POST_CLOUDERA_MANAGER_START).map(RecipeModel::getName).collect(Collectors.toList());
            List<String> postServiceDeployment = entry.getValue().stream().
                    filter(h -> h.getRecipeType() == RecipeType.POST_CLUSTER_INSTALL || h.getRecipeType() == RecipeType.POST_SERVICE_DEPLOYMENT)
                    .map(RecipeModel::getName).collect(Collectors.toList());
            Map<String, List<String>> prePostScripts = new HashMap<>();
            prePostScripts.put(RecipeExecutionPhase.PRE_CLOUDERA_MANAGER_START.value(), preServiceDeployment);
            prePostScripts.put(RecipeExecutionPhase.PRE_SERVICE_DEPLOYMENT.value(), preServiceDeployment);
            prePostScripts.put(RecipeExecutionPhase.PRE_TERMINATION.value(), preTermination);
            prePostScripts.put(RecipeExecutionPhase.POST_CLOUDERA_MANAGER_START.value(), postClouderaManagerStart);
            prePostScripts.put(RecipeExecutionPhase.POST_CLUSTER_INSTALL.value(), postServiceDeployment);
            prePostScripts.put(RecipeExecutionPhase.POST_SERVICE_DEPLOYMENT.value(), postServiceDeployment);
            scripts.put(entry.getKey(), prePostScripts);
        }
        Map<String, Object> pillarConfig = new HashMap<>(scripts);
        pillarConfig.put("timeout", recipeExecutionTimeout);
        pillar = new Pillar("/recipes/init.sls", singletonMap("recipes", pillarConfig), targets);
        this.targets = targets;
        originalTargets = targets;
    }

    private PillarSave(SaltConnector sc, Set<String> targets, SaltPillarProperties pillarProperties) {
        this.sc = sc;
        pillar = new Pillar(pillarProperties.getPath(), pillarProperties.getProperties(), targets);
        this.targets = targets;
        originalTargets = targets;
    }

    public static PillarSave createHostsPillar(SaltConnector sc, Set<String> targets, Collection<Node> allNodes) {
        return new PillarSave(sc, targets, allNodes);
    }

    public static PillarSave createRecipesPillar(SaltConnector sc, Set<String> targets, Map<String, List<RecipeModel>> recipes, Long recipeExecutionTimeout) {
        return new PillarSave(sc, targets, recipes, recipeExecutionTimeout);
    }

    public static PillarSave createCustomPillar(SaltConnector sc, Set<String> targets, SaltPillarProperties pillarProperties) {
        return new PillarSave(sc, targets, pillarProperties);
    }

    private Map<String, Object> discovery(String hostname, String publicAddress,
            String instanceId, String instanceType) {
        Map<String, Object> map = new HashMap<>();
        map.put("fqdn", hostname);
        map.put("hostname", hostname.split("\\.")[0]);
        map.put("domain", hostname.replaceFirst(hostname.split("\\.")[0] + '.', ""));
        // Deprecated: this is just for backward compatibility, it is no longer in use
        map.put("custom_domain", true);
        map.put("public_address", StringUtils.isEmpty(publicAddress) ? Boolean.FALSE : Boolean.TRUE);
        map.put("instance_id", instanceId);
        map.put("instance_type", instanceType);
        return map;
    }

    @Override
    public Boolean call() throws Exception {
        LOGGER.debug("Distribute pillar configs to: {}", targets);
        if (!targets.isEmpty()) {

            GenericResponses responses = sc.pillar(targets, pillar);

            Set<String> failedTargets = new HashSet<>();
            LOGGER.debug("Salt pillar save responses: {}", responses);
            for (GenericResponse genericResponse : responses.getResponses()) {
                if (genericResponse.getStatusCode() != HttpStatus.OK.value()) {
                    LOGGER.info("Failed pillar save attempt to: {}, error: {}", genericResponse.getAddress(), genericResponse.getErrorText());
                    String address = genericResponse.getAddress().split(":")[0];
                    failedTargets.addAll(originalTargets.stream().filter(a -> a.equals(address)).collect(Collectors.toList()));
                }
            }
            targets = failedTargets;

            if (!targets.isEmpty()) {
                LOGGER.info("Missing nodes for pillar save: {}", targets);
                throw new CloudbreakOrchestratorFailedException("There are missing nodes for pillar save: " + targets);
            }
        }

        LOGGER.debug("Pillar save has been completed on nodes: {}", originalTargets);
        return true;
    }

    @Override
    public String toString() {
        return "PillarSave{" +
                "pillar=" + pillar +
                ", targets=" + targets +
                ", originalTargets=" + originalTargets +
                '}';
    }
}
