/*******************************************************************************
 * Copyright (c) 2019, 2019 IBM Corporation and others.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *******************************************************************************/

package com.kruize.environment.kubernetes;

import com.kruize.analysis.AnalysisImpl;
import com.kruize.environment.DeploymentInfo;
import com.kruize.environment.EnvTypeImpl;
import com.kruize.environment.SupportedTypes;
import com.kruize.exceptions.InvalidValueException;
import com.kruize.exceptions.MonitoringAgentMissingException;
import com.kruize.exceptions.MonitoringAgentNotSupportedException;
import com.kruize.metrics.MetricsImpl;
import com.kruize.query.prometheus.PrometheusQuery;
import com.kruize.recommendations.application.ApplicationRecommendationsImpl;
import com.kruize.util.HttpUtil;
import com.kruize.util.MathUtil;
import io.kubernetes.client.ApiClient;
import io.kubernetes.client.ApiException;
import io.kubernetes.client.Configuration;
import io.kubernetes.client.apis.CoreV1Api;
import io.kubernetes.client.custom.Quantity;
import io.kubernetes.client.models.*;
import io.kubernetes.client.util.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Map;

public class KubernetesEnvImpl extends EnvTypeImpl
{
    private static final Logger LOGGER = LoggerFactory.getLogger(KubernetesEnvImpl.class);

    @Override
    public void setupMonitoringAgent()
    {
        try {

            if (DeploymentInfo.getMonitoringAgentEndpoint() == null
                    || DeploymentInfo.getMonitoringAgentEndpoint().equals("")) {
                if (DeploymentInfo.getMonitoringAgentService() != null) {
                    getMonitoringEndpointFromService();
                } else {
                    throw new MonitoringAgentMissingException("ERROR: No service or endpoint specified");
                }
            }
            if (!checkMonitoringAgentSupported()) {
                throw new MonitoringAgentNotSupportedException();
            }
            if (!checkMonitoringAgentRunning()) {
                throw new MonitoringAgentMissingException(DeploymentInfo.getMonitoringAgent() + " not running");
            }
            setMonitoringLabels();
        } catch (MonitoringAgentNotSupportedException |
                MonitoringAgentMissingException |
                IOException |
                ApiException e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    private boolean checkMonitoringAgentRunning() throws IOException, ApiException
    {
        ApiClient client = Config.defaultClient();
        Configuration.setDefaultApiClient(client);
        CoreV1Api api = new CoreV1Api();

        V1ServiceList serviceList = api.listServiceForAllNamespaces(
                null,
                null,
                null,
                null,
                null,
                null,
                null,
                null,
                null
        );

        for (V1Service service : serviceList.getItems()) {
            String serviceName = service.getMetadata().getName();
            if (serviceName.toUpperCase().contains(DeploymentInfo.getMonitoringAgent()))
                return true;
        }

        return false;
    }

    private boolean checkMonitoringAgentSupported()
    {
        String monitoring_agent = DeploymentInfo.getMonitoringAgent();
        return SupportedTypes.MONITORING_AGENTS_SUPPORTED.contains(monitoring_agent);
    }

    @Override
    public void setupApplicationRecommendations()
    {
        this.applicationRecommendations = ApplicationRecommendationsImpl.getInstance();
    }

    @Override
    public void setupAnalysis()
    {
        this.analysis = AnalysisImpl.getInstance();
    }

    @Override
    public void setupQuery()
    {
        this.query = PrometheusQuery.getInstance();
    }

    @Override
    public void getAllApps()
    {
        ArrayList<String> monitoredInstances = new ArrayList<>();
        ApiClient apiClient = null;
        try {
            apiClient = Config.defaultClient();
        } catch (IOException e) {
            e.printStackTrace();
        }
        Configuration.setDefaultApiClient(apiClient);
        CoreV1Api api = new CoreV1Api();

        //Get all the pods in the cluster
        V1PodList podList = null;
        try {
            podList = api.listPodForAllNamespaces(
                    null,
                    null,
                    null,
                    null,
                    null,
                    null,
                    null,
                    null,
                    null
            );
        } catch (ApiException e) {
            e.printStackTrace();
        }

        if (podList != null) {
            for (V1Pod pod : podList.getItems()) {
                try {
                    final String label = "app.kubernetes.io/name";
                    boolean containsLabel = pod.getMetadata().getLabels().containsKey(label);
                    boolean isAppsodyApplication = pod.getKind() != null && pod.getKind().equals("AppsodyApplication");

                    if (containsLabel || isAppsodyApplication) {
                        insertMetrics(pod);
                        monitoredInstances.add(pod.getMetadata().getName());
                    }
                } catch (NullPointerException ignored) {
                }
            }
        } else {
            LOGGER.debug("Insufficient RBAC permissions (list, get, watch) for pods and services.");
            System.exit(1);
        }

        updateStatus(monitoredInstances);
    }

    /**
     * Check if the application being monitored has been terminated,
     * and update status if so
     * @param monitoredInstances Arraylist of all pods currently running that Kruize is monitoring
     */
    private void updateStatus(ArrayList<String> monitoredInstances)
    {
        for (String application : applicationRecommendations.applicationMap.keySet())
        {
            for (MetricsImpl instance : applicationRecommendations.applicationMap.get(application))
            {
                if (!monitoredInstances.contains(instance.getName()))
                {
                    try {
                        instance.setStatus("terminated");
                    } catch (InvalidValueException ignored) { }
                }
            }
        }
    }

    private void insertMetrics(V1Pod pod)
    {
        MetricsImpl metricsImpl = null;
        try {
            metricsImpl = getPodMetrics(pod);
        } catch (InvalidValueException e) {
            e.printStackTrace();
        }

        assert metricsImpl != null;
        String applicationName = metricsImpl.getApplicationName();

        if (applicationRecommendations.applicationMap.containsKey(applicationName)) {
            applicationRecommendations.addMetricToApplication(applicationName, metricsImpl);
        } else {
            ArrayList<MetricsImpl> podMetricsArrayList = new ArrayList<>();
            podMetricsArrayList.add(metricsImpl);
            applicationRecommendations.applicationMap.put(applicationName, podMetricsArrayList);
        }
    }

    private static MetricsImpl getPodMetrics(V1Pod pod) throws InvalidValueException
    {
        MetricsImpl metrics = new MetricsImpl();
        metrics.setName(pod.getMetadata().getName());
        metrics.setNamespace(pod.getMetadata().getNamespace());
        metrics.setStatus(pod.getStatus().getPhase().toString().toLowerCase());

        String podTemplateHash;

        try {
            String podHashLabel = "pod-template-hash";
            podTemplateHash = pod.getMetadata()
                    .getLabels().get(podHashLabel);

        } catch (NullPointerException e) {
            podTemplateHash = null;
        }

        String applicationName = parseApplicationName(pod.getMetadata().getName(),
                podTemplateHash);

        metrics.setApplicationName(applicationName);

        V1ResourceRequirements resources = pod.getSpec().getContainers().get(0).getResources();

        Map podRequests = resources.getRequests();
        Map podLimits = resources.getLimits();

        if (podRequests != null) {
            if (podRequests.containsKey("memory")) {
                Quantity memoryRequests = (Quantity) podRequests.get("memory");
                double memoryRequestsValue = memoryRequests.getNumber().doubleValue();
                LOGGER.debug("Original memory requests for {}: {} MB", applicationName,
                        MathUtil.bytesToMB(memoryRequestsValue));

                metrics.setOriginalMemoryRequests(memoryRequests.getNumber().doubleValue());
            }

            if (podRequests.containsKey("cpu")) {
                Quantity cpuRequests = (Quantity) podRequests.get("cpu");
                double cpuRequestsValue = cpuRequests.getNumber().doubleValue();
                LOGGER.debug("Original CPU requests for {}: {}", applicationName,
                        cpuRequestsValue);

                metrics.setOriginalCpuRequests(cpuRequests.getNumber().doubleValue());
            }
        }

        if (podLimits != null) {
            if (podLimits.containsKey("memory")) {
                Quantity memoryLimit = (Quantity) podLimits.get("memory");
                double memoryLimitValue = memoryLimit.getNumber().doubleValue();
                LOGGER.debug("Original memory limit for {}: {} MB", applicationName,
                        MathUtil.bytesToMB(memoryLimitValue));

                metrics.setOriginalMemoryLimit(memoryLimit.getNumber().doubleValue());
            }

            if (podLimits.containsKey("cpu")) {
                Quantity cpuLimit = (Quantity) podLimits.get("cpu");
                double cpuLimitValue = cpuLimit.getNumber().doubleValue();
                LOGGER.debug("Original CPU limit for {}: {}", applicationName,
                        cpuLimitValue);

                metrics.setOriginalCpuLimit(cpuLimit.getNumber().doubleValue());
            }
        }

        return metrics;
    }

    private static String parseApplicationName(String podName, String hash)
    {
        if (hash != null) {
            int index = podName.indexOf(hash);
            if (index > 0)
                return podName.substring(0, index - 1);
        }
        return parseApplicationNameFromInstanceName(podName);
    }

    private void getMonitoringEndpointFromService() throws IOException, ApiException
    {
        ApiClient client = Config.defaultClient();
        Configuration.setDefaultApiClient(client);
        CoreV1Api api = new CoreV1Api();

        V1ServiceList serviceList = api.listServiceForAllNamespaces(
                null,
                null,
                null,
                null,
                null,
                null,
                null,
                null,
                null
        );

        for (V1Service service : serviceList.getItems()) {
            String serviceName = service.getMetadata().getName();
            if (serviceName.toUpperCase().equals(DeploymentInfo.getMonitoringAgentService())) {
                String clusterIP = service.getSpec().getClusterIP();
                int port = service.getSpec().getPorts().get(0).getPort();
                DeploymentInfo.setMonitoringAgentEndpoint("http://" + clusterIP + ":" + port);
            }
        }
    }

    private void setMonitoringLabels()
    {
        PrometheusQuery prometheusQuery = PrometheusQuery.getInstance();

        try {
            URL labelURL = new URL(DeploymentInfo.getMonitoringAgentEndpoint() + "/api/v1/labels");
            String result = HttpUtil.getDataFromURL(labelURL);

            if (result.contains("\"pod\"")) {
                prometheusQuery.setPodLabel("pod");
                prometheusQuery.setContainerLabel("container");
            }
        }
        /* Use the default labels */
        catch (MalformedURLException | NullPointerException ignored) {
            LOGGER.info("Using default labels");
        }
    }
}