package io.jenkins.plugins.remotingkafka;

import com.cloudbees.plugins.credentials.CredentialsMatchers;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.common.StandardCertificateCredentials;
import com.cloudbees.plugins.credentials.common.StandardCredentials;
import com.cloudbees.plugins.credentials.common.StandardListBoxModel;
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder;
import hudson.Extension;
import hudson.Util;
import hudson.model.Computer;
import hudson.model.Descriptor;
import hudson.model.Label;
import hudson.model.Node;
import hudson.model.labels.LabelAtom;
import hudson.security.ACL;
import hudson.slaves.Cloud;
import hudson.slaves.NodeProperty;
import hudson.slaves.NodeProvisioner.PlannedNode;
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClientException;
import jenkins.model.Jenkins;
import org.apache.commons.lang.StringUtils;
import org.jenkinsci.plugins.kubernetes.credentials.TokenProducer;
import org.jenkinsci.plugins.plaincredentials.FileCredentials;
import org.jenkinsci.plugins.plaincredentials.StringCredentials;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.interceptor.RequirePOST;

import javax.annotation.CheckForNull;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

public class KafkaKubernetesCloud extends Cloud {
    private static final Logger LOGGER = Logger.getLogger(KafkaKubernetesCloud.class.getName());
    public static final int AGENT_NUM_EXECUTORS = 1;

    private String serverUrl;
    @CheckForNull
    private String serverCertificate;
    private String credentialsId;
    private boolean skipTlsVerify;
    private String namespace;

    private String jenkinsUrl;
    private String containerImage;
    private String idleMinutes;
    private String label;
    private Node.Mode nodeUsageMode;
    private String description;
    private String workingDir;
    private List<? extends NodeProperty<?>> nodeProperties;

    private String kafkaUsername;
    private String sslTruststoreLocation;
    private String sslKeystoreLocation;
    private boolean enableSSL = false;

    @DataBoundConstructor
    public KafkaKubernetesCloud(String name) {
        super(name);
    }

    public String getServerUrl() {
        return serverUrl;
    }

    @DataBoundSetter
    public void setServerUrl(String serverUrl) {
        this.serverUrl = serverUrl;
    }

    public String getServerCertificate() {
        return serverCertificate;
    }

    @DataBoundSetter
    public void setServerCertificate(String serverCertificate) {
        this.serverCertificate = serverCertificate;
    }

    public String getCredentialsId() {
        return credentialsId;
    }

    @DataBoundSetter
    public void setCredentialsId(String credentialsId) {
        this.credentialsId = credentialsId;
    }

    public boolean isSkipTlsVerify() {
        return skipTlsVerify;
    }

    @DataBoundSetter
    public void setSkipTlsVerify(boolean skipTlsVerify) {
        this.skipTlsVerify = skipTlsVerify;
    }

    public String getNamespace() {
        return StringUtils.defaultIfBlank(namespace, "default");
    }

    @DataBoundSetter
    public void setNamespace(String namespace) {
        this.namespace = namespace;
    }

    public String getJenkinsUrl() {
        return jenkinsUrl;
    }

    @DataBoundSetter
    public void setJenkinsUrl(String jenkinsUrl) {
        this.jenkinsUrl = jenkinsUrl;
    }

    public String getContainerImage() {
        return containerImage;
    }

    @DataBoundSetter
    public void setContainerImage(String containerImage) {
        this.containerImage = containerImage;
    }

    public String getIdleMinutes() {
        return StringUtils.defaultIfBlank(idleMinutes, "0");
    }

    @DataBoundSetter
    public void setIdleMinutes(String idleMinutes) {
        this.idleMinutes = idleMinutes;
    }

    public String getLabel() {
        return label;
    }

    @DataBoundSetter
    public void setLabel(String label) {
        this.label = label;
    }

    public Node.Mode getNodeUsageMode() {
        return nodeUsageMode;
    }

    @DataBoundSetter
    public void setNodeUsageMode(Node.Mode nodeUsageMode) {
        this.nodeUsageMode = nodeUsageMode;
    }

    public String getDescription() {
        return description;
    }

    @DataBoundSetter
    public void setDescription(String description) {
        this.description = description;
    }

    public String getWorkingDir() {
        return workingDir;
    }

    @DataBoundSetter
    public void setWorkingDir(String workingDir) {
        this.workingDir = workingDir;
    }

    public List<? extends NodeProperty<?>> getNodeProperties() {
        return nodeProperties;
    }

    @DataBoundSetter
    public void setNodeProperties(List<? extends NodeProperty<?>> nodeProperties) {
        this.nodeProperties = nodeProperties;
    }

    public String getKafkaUsername() {
        return kafkaUsername;
    }

    @DataBoundSetter
    public void setKafkaUsername(String kafkaUsername) {
        this.kafkaUsername = kafkaUsername;
    }

    public String getSslTruststoreLocation() {
        return sslTruststoreLocation;
    }

    @DataBoundSetter
    public void setSslTruststoreLocation(String sslTruststoreLocation) {
        this.sslTruststoreLocation = sslTruststoreLocation;
    }

    public String getSslKeystoreLocation() {
        return sslKeystoreLocation;
    }

    @DataBoundSetter
    public void setSslKeystoreLocation(String sslKeystoreLocation) {
        this.sslKeystoreLocation = sslKeystoreLocation;
    }

    public boolean isEnableSSL() {
        return enableSSL;
    }

    @DataBoundSetter
    public void setEnableSSL(boolean enableSSL) {
        this.enableSSL = enableSSL;
    }

    public Set<LabelAtom> getLabelSet() {
        return Label.parse(label);
    }

    public KubernetesClient connect() {
        try (KubernetesClient client = new KubernetesFactoryAdapter(serverUrl, namespace,
                Util.fixEmpty(serverCertificate), Util.fixEmpty(credentialsId), skipTlsVerify
        ).createClient()) {
            return client;
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            LOGGER.warning("Error connecting to Kubernetes client from Cloud " + name);
            return null;
        }
    }

    @Override
    public Collection<PlannedNode> provision(Label label, int excessWorkload) {
        Set<String> allInProvisioning = getNodesInProvisioning(label);
        LOGGER.info("In provisioning : " + allInProvisioning);
        int toBeProvisioned = Math.max(0, excessWorkload - allInProvisioning.size());
        LOGGER.info("Excess workload after pending Kubernetes agents: " + toBeProvisioned);

        List<PlannedNode> provisionNodes = new ArrayList<>();
        for (int i = 0; i < toBeProvisioned; i++) {
            PlannedNode node = new PlannedNode(name,
                    Computer.threadPoolForRemoting.submit(() -> new KafkaCloudSlave(this)),
                    AGENT_NUM_EXECUTORS);
            provisionNodes.add(node);
        }
        return provisionNodes;
    }

    public Set<String> getNodesInProvisioning(@CheckForNull Label label) {
        if (label == null) return Collections.emptySet();
        return label.getNodes().stream()
                .filter(KafkaCloudSlave.class::isInstance)
                .filter(node -> {
                    Computer computer = node.toComputer();
                    return computer != null && !computer.isOnline();
                })
                .map(Node::getNodeName)
                .collect(Collectors.toSet());
    }

    @Override
    public boolean canProvision(@CheckForNull Label label) {
        if (label == null) return false;
        return label.matches(getLabelSet());
    }

    @Extension
    public static class DescriptorImpl extends Descriptor<Cloud> {
        @Override
        public String getDisplayName() {
            return "Kafka Kubernetes";
        }

        @RequirePOST
        public FormValidation doTestConnection(
                @QueryParameter("serverUrl") String serverUrl,
                @QueryParameter("credentialsId") String credentialsId,
                @QueryParameter("serverCertificate") String serverCertificate,
                @QueryParameter("skipTlsVerify") boolean skipTlsVerify,
                @QueryParameter("namespace") String namespace
        ) {
            Jenkins.get().checkPermission(Jenkins.ADMINISTER);

            try {
                KubernetesClient client = new KubernetesFactoryAdapter(serverUrl, namespace,
                        Util.fixEmpty(serverCertificate), Util.fixEmpty(credentialsId), skipTlsVerify
                ).createClient();
                // Call Pod list API to ensure functionality
                client.pods().list();
                return FormValidation.ok("Success");
            } catch (KubernetesClientException e) {
                LOGGER.log(Level.FINE, "Error testing Kubernetes connection", e);
                return FormValidation.error("Error: %s", e.getCause() == null
                        ? e.getMessage()
                        : String.format("%s: %s", e.getCause().getClass().getName(), e.getCause().getMessage()));
            } catch (Exception e) {
                LOGGER.log(Level.FINE, "Error testing Kubernetes connection", e);
                return FormValidation.error("Error: %s", e.getMessage());
            }
        }

        @RequirePOST
        public ListBoxModel doFillCredentialsIdItems(@QueryParameter String serverUrl) {
            Jenkins.get().checkPermission(Jenkins.ADMINISTER);
            return new StandardListBoxModel().withEmptySelection()
                    .withMatching(
                            CredentialsMatchers.anyOf(
                                    CredentialsMatchers.instanceOf(StandardUsernamePasswordCredentials.class),
                                    CredentialsMatchers.instanceOf(FileCredentials.class),
                                    CredentialsMatchers.instanceOf(TokenProducer.class),
                                    CredentialsMatchers.instanceOf(StandardCertificateCredentials.class),
                                    CredentialsMatchers.instanceOf(StringCredentials.class)),
                            CredentialsProvider.lookupCredentials(StandardCredentials.class,
                                    Jenkins.get(),
                                    ACL.SYSTEM,
                                    serverUrl != null ? URIRequirementBuilder.fromUri(serverUrl).build()
                                            : Collections.EMPTY_LIST
                            ));
        }
    }

}