package org.csanchez.jenkins.plugins.kubernetes;

import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import edu.umd.cs.findbugs.annotations.NonNull;
import net.jcip.annotations.GuardedBy;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

import hudson.Extension;
import hudson.ExtensionList;
import hudson.init.InitMilestone;
import hudson.init.Initializer;
import hudson.model.Node;
import hudson.model.Queue;
import jenkins.metrics.api.Metrics;
import jenkins.model.Jenkins;
import jenkins.model.NodeListener;

/**
 * Implements provisioning limits for clouds and pod templates
 */
@Extension
public final class KubernetesProvisioningLimits {
    private static final Logger LOGGER = Logger.getLogger(KubernetesProvisioningLimits.class.getName());

    @GuardedBy("this")
    private boolean init;

    /**
     * Tracks current number of kubernetes agents per pod template
     */
    private final Map<String, Integer> podTemplateCounts = new HashMap<>();

    /**
     * Tracks current number of kubernetes agents per kubernetes cloud
     */
    private final Map<String, Integer> cloudCounts = new HashMap<>();

    public static void init() {
        get().initInstance();
    }

    /**
     * Initialize limits counter
     * @return whether the instance was already initialized
     */
    private synchronized boolean initInstance() {
        if (init) {
            return true;
        }
        Queue.withLock(() -> {
            Jenkins.get().getNodes()
                    .stream()
                    .filter(KubernetesSlave.class::isInstance)
                    .map(KubernetesSlave.class::cast)
                    .forEach(node -> {
                        cloudCounts.put(node.getCloudName(), getGlobalCount(node.getCloudName()) + node.getNumExecutors());
                        podTemplateCounts.put(node.getTemplateId(), getPodTemplateCount(node.getTemplateId()) + node.getNumExecutors());
                    });
        });
        init = true;
        return false;
    }

    /**
     * @return the singleton instance
     */
    public static KubernetesProvisioningLimits get() {
        return ExtensionList.lookupSingleton(KubernetesProvisioningLimits.class);
    }

    /**
     * Register executors
     * @param cloud the kubernetes cloud the executors will be on
     * @param podTemplate the pod template used to schedule the agent
     * @param numExecutors the number of executors (pretty much always 1)
     */
    public synchronized boolean register(@NonNull KubernetesCloud cloud, @NonNull PodTemplate podTemplate, int numExecutors) {
        initInstance();
        int newGlobalCount = getGlobalCount(cloud.name) + numExecutors;
        if (newGlobalCount <= cloud.getContainerCap()) {
            int newPodTemplateCount = getPodTemplateCount(podTemplate.getId()) + numExecutors;
            if (newPodTemplateCount <= podTemplate.getInstanceCap()) {
                cloudCounts.put(cloud.name, newGlobalCount);
                LOGGER.log(Level.FINEST, () -> cloud.name + " global limit: " + newGlobalCount + "/" + cloud.getContainerCap());

                podTemplateCounts.put(podTemplate.getId(), newPodTemplateCount);
                LOGGER.log(Level.FINEST, () -> podTemplate.getName() + " template limit: " + newPodTemplateCount + "/" + podTemplate.getInstanceCap());
                return true;
            } else {
                LOGGER.log(Level.FINEST, () -> podTemplate.getName() + " template limit reached: " + getPodTemplateCount(podTemplate.getId()) + "/" + podTemplate.getInstanceCap() + ". Cannot add " + numExecutors + " more!");
                Metrics.metricRegistry().counter(MetricNames.REACHED_POD_CAP).inc();
            }
        } else {
            LOGGER.log(Level.FINEST, () -> cloud.name + " global limit reached: " + getGlobalCount(cloud.name) + "/" + cloud.getContainerCap() + ". Cannot add " + numExecutors + " more!");
            Metrics.metricRegistry().counter(MetricNames.REACHED_GLOBAL_CAP).inc();
        }
        return false;
    }

    /**
     * Unregisters executors, when an agent is terminated
     * @param cloud the kubernetes cloud the executors were on
     * @param podTemplate the pod template used to schedule the agent
     * @param numExecutors the number of executors (pretty much always 1)
     */
    public synchronized void unregister(@NonNull KubernetesCloud cloud, @NonNull PodTemplate podTemplate, int numExecutors) {
        if (initInstance()) {
            int newGlobalCount = getGlobalCount(cloud.name) - numExecutors;
            if (newGlobalCount < 0) {
                LOGGER.log(Level.WARNING, "Global count for " + cloud.name + " went below zero. There is likely a bug in kubernetes-plugin");
            }
            cloudCounts.put(cloud.name, Math.max(0, newGlobalCount));
            LOGGER.log(Level.FINEST, () -> cloud.name + " global limit: " + Math.max(0, newGlobalCount) + "/" + cloud.getContainerCap());

            int newPodTemplateCount = getPodTemplateCount(podTemplate.getId()) - numExecutors;
            if (newPodTemplateCount < 0) {
                LOGGER.log(Level.WARNING, "Pod template count for " + podTemplate.getName() + " went below zero. There is likely a bug in kubernetes-plugin");
            }
            podTemplateCounts.put(podTemplate.getId(), Math.max(0, newPodTemplateCount));
            LOGGER.log(Level.FINEST, () -> podTemplate.getName() + " template limit: " + Math.max(0, newPodTemplateCount) + "/" + podTemplate.getInstanceCap());
        }
    }

    @NonNull
    @Restricted(NoExternalUse.class)
    int getGlobalCount(String cloudName) {
        return cloudCounts.getOrDefault(cloudName, 0);
    }

    @NonNull
    @Restricted(NoExternalUse.class)
    int getPodTemplateCount(String podTemplate) {
        return podTemplateCounts.getOrDefault(podTemplate, 0);
    }

    @Extension
    public static class NodeListenerImpl extends NodeListener {
        @Override
        protected void onDeleted(@NonNull Node node) {
            if (node instanceof KubernetesSlave) {
                KubernetesProvisioningLimits instance = KubernetesProvisioningLimits.get();
                KubernetesSlave kubernetesNode = (KubernetesSlave) node;
                PodTemplate template = kubernetesNode.getTemplateOrNull();
                if (template != null) {
                    instance.unregister(kubernetesNode.getKubernetesCloud(), template, node.getNumExecutors());
                }
            }
        }
    }

}

@@ -1222,77 +1222,8 @@
 );%0A%0A
-    public static void init() %7B%0A        get().initInstance();%0A    %7D%0A%0A
     
@@ -1318,16 +1318,34 @@
 tialized
+ before this call.
 %0A     */
@@ -1407,55 +1407,62 @@
     
-if (init) %7B%0A            return true;%0A        %7D%0A
+boolean previousInit = init;%0A        if (!init) %7B%0A    
     
@@ -1500,16 +1500,20 @@
         
+ 
+   
 Jenkins.
@@ -1529,16 +1529,20 @@
 Nodes()%0A
+    
         
@@ -1583,16 +1583,20 @@
         
+    
 .filter(
@@ -1650,16 +1650,20 @@
         
+  
+  
 .map(Kub
@@ -1708,16 +1708,20 @@
         
+    
 .forEach
@@ -1727,24 +1727,28 @@
 h(node -%3E %7B%0A
+    
             
@@ -1875,32 +1875,36 @@
                 
+   
+ 
 podTemplateCount
@@ -2012,32 +2012,36 @@
                 
+    
 %7D);%0A        %7D);%0A
@@ -2028,36 +2028,44 @@
     %7D);%0A        
+  
+  
 %7D);%0A
+    
         init = t
@@ -2073,36 +2073,53 @@
 ue;%0A        
-return false
+%7D%0A        return previousInit
 ;%0A    %7D%0A%0A   

Clean up