package main

import (
	"context"
	"fmt"
	"net/http"
	"time"

	"github.com/pingcap/log"
	"github.com/pingcap/tidb-operator/pkg/apis/label"
	httputil "github.com/pingcap/tidb-operator/pkg/util/http"
	"go.uber.org/zap"
	admissionv1 "k8s.io/api/admission/v1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
)

const (
	// AnnDeletePodRequestTime is pod annotation key to indicate the request time for deleting pod
	AnnDeletePodRequestTime = "pingcap.com/delete-pod-req-time"

	DefaultDeletePodWaitTimeout = 40 * time.Second
	DefaultHTTPTimeout          = 5 * time.Second

	// AnnGracefulShutdownTiDB is pod annotation key to indicate the pod will be processed by graceful shutdown tidb webhook
	AnnGracefulShutdownTiDB = "pingcap.com/graceful-shutdown-tidb"
	// AnnDeletePodWaitTimeout is pod annotation key to indicate the wait timeout for deleting pod
	AnnDeletePodWaitTimeout = "pingcap.com/delete-pod-wait-timeout"
	// HealthCheckerContainerName is the name of tidb pod health checker sidecar
	HealthCheckerContainerName = "health-checker"
)

var (
	podResource = metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
)

func admitDeleteTiDBPods(req *admissionv1.AdmissionRequest) error {
	if req.Resource != podResource {
		log.Warn("expect resource to be pod", zap.Stringer("resource", &podResource))
		return nil
	}

	// Parse the Pod object
	raw := req.OldObject.Raw
	pod := corev1.Pod{}
	if _, _, err := universalDeserializer.Decode(raw, nil, &pod); err != nil {
		log.Error("could not deserialize pod object", zap.Error(err))
		return fmt.Errorf("could not deserialize pod object: %v", err)
	}

	name := req.Name
	namespace := req.Namespace
	podPhase := pod.Status.Phase
	log.Info("receive delete pod request", zap.String("namespace", namespace), zap.String("name", name), zap.String("podPhase", string(podPhase)))

	podAnnotations := pod.Annotations
	if podAnnotations == nil {
		podAnnotations = map[string]string{}
	}
	_, exist := podAnnotations[AnnGracefulShutdownTiDB]
	if !exist {
		log.Info("the tidb pod does not contain graceful shutdown tidb annotation, admit to delete", zap.String("namespace", namespace), zap.String("name", name))
		return nil
	}

	if podPhase != corev1.PodRunning {
		log.Info("the tidb pod is not running, admit to delete", zap.String("namespace", namespace), zap.String("name", name), zap.String("podPhase", string(podPhase)))
		return nil
	}
	if !IsHealthCheckerContainerReady(pod) {
		log.Info("the health checker sidecar of tidb pod is not ready, admit to delete", zap.String("namespace", namespace), zap.String("name", name))
		return nil
	}

	if pod.Labels == nil {
		log.Info("the pod does not contain labels, admit to delete", zap.String("namespace", namespace), zap.String("name", name))
		return nil
	}
	l := label.Label(pod.Labels)

	if pod.DeletionTimestamp != nil {
		log.Info("the pod's DeletionTimestamp field is not empty, admit to delete", zap.String("namespace", namespace), zap.String("name", name))
		return nil
	}
	if !l.IsManagedByTiDBOperator() {
		log.Info("the pod is not managed by TiDB Operator, admit to delete", zap.String("namespace", namespace), zap.String("name", name))
		return nil
	}
	if !(l.IsTiDB()) {
		log.Info("the pod is not a TiDB component, admit to delete", zap.String("namespace", namespace), zap.String("name", name))
		return nil
	}

	var tcName string
	if l.IsTidbClusterPod() {
		tcName, exist = pod.Labels[label.InstanceLabelKey]
		if !exist {
			log.Info("the tidb pod does not contain instance label, admit to delete", zap.String("namespace", namespace), zap.String("name", name))
			return nil
		}
		_, err := operatorCli.PingcapV1alpha1().TidbClusters(namespace).Get(context.TODO(), tcName, metav1.GetOptions{})
		if err != nil {
			if errors.IsNotFound(err) {
				log.Info("tc had been deleted, admit to delete", zap.String("namespace", namespace), zap.String("name", name), zap.String("tcName", tcName))
				return nil
			}
			log.Error("failed to get tc", zap.String("namespace", namespace), zap.String("name", name), zap.String("tcName", tcName), zap.Error(err))
			return fmt.Errorf("refuse to delete pod %s/%s, failed to get tc %s/%s", namespace, name, namespace, tcName)
		}
	} else {
		log.Info("the name label of the tidb pod is not tidb-cluster, admit to delete", zap.String("namespace", namespace), zap.String("name", name))
		return nil
	}

	deletePodRequestTimeStr, exist := podAnnotations[AnnDeletePodRequestTime]
	if !exist {
		client := &http.Client{Timeout: DefaultHTTPTimeout}
		apiURL := tidbPodHealthCheckerDownURL(name, tcName, namespace)
		_, err := httputil.GetBodyOK(client, apiURL)
		if err != nil {
			log.Error("failed to call health-checker down API, admit to delete", zap.Error(err))
			return nil
		}
		err = addDeletePodRequestTimeAnnotation(kubeCli, &pod)
		if err != nil {
			return err
		}
		log.Info("set DeletePodRequestTime annotation for pod", zap.String("namespace", namespace), zap.String("name", name))
		return fmt.Errorf("refuse to delete pod %s/%s, wait for deletePodWaitTimeout", namespace, name)
	}
	deletePodRequestTime, err := time.Parse(time.RFC3339, deletePodRequestTimeStr)
	if err != nil {
		log.Error("failed to parse deletePodRequestTime annotation", zap.String("deletePodRequestTime", deletePodRequestTimeStr))
		return err
	}

	deletePodWaitTimeout := deletePodWaitTimeout(podAnnotations)
	if time.Now().After(deletePodRequestTime.Add(deletePodWaitTimeout)) {
		log.Info("deletePodWaitTimeout has passed, admit to delete", zap.String("namespace", namespace), zap.String("name", name))
		return nil
	}
	log.Info("refuse to delete pod, wait for deletePodWaitTimeout", zap.String("namespace", namespace), zap.String("name", name))
	return fmt.Errorf("refuse to delete pod %s/%s, wait for deletePodWaitTimeout", namespace, name)
}

func deletePodWaitTimeout(podAnnotations map[string]string) time.Duration {
	deletePodWaitTimeoutStr, exist := podAnnotations[AnnDeletePodWaitTimeout]
	if exist {
		deletePodWaitTimeout, err := time.ParseDuration(deletePodWaitTimeoutStr)
		if err == nil {
			return deletePodWaitTimeout
		}
		log.Error("failed to parse deletePodWaitTimeout annotation", zap.String("deletePodWaitTimeout", deletePodWaitTimeoutStr))
	}
	return DefaultDeletePodWaitTimeout
}

//copy from https://github.com/pingcap/tidb-operator/blob/release-1.3/pkg/controller/controller_utils.go
// TiDBPeerMemberName returns tidb peer service name
func TiDBPeerMemberName(clusterName string) string {
	return fmt.Sprintf("%s-tidb-peer", clusterName)
}

func tidbPodHealthCheckerDownURL(podName, tcName, namespace string) string {
	svcName := TiDBPeerMemberName(tcName)
	return fmt.Sprintf("http://%s.%s.%s.svc:8080/down", podName, svcName, namespace)
}

func addDeletePodRequestTimeAnnotation(kubeCli kubernetes.Interface, pod *corev1.Pod) error {
	namespace := pod.Namespace

	if pod.Annotations == nil {
		pod.Annotations = map[string]string{}
	}
	now := time.Now().Format(time.RFC3339)
	pod.Annotations[AnnDeletePodRequestTime] = now
	_, err := kubeCli.CoreV1().Pods(namespace).Update(context.TODO(), pod, metav1.UpdateOptions{})
	if err != nil {
		return err
	}
	return nil
}

func IsHealthCheckerContainerReady(pod corev1.Pod) bool {
	healthCheckerContainerReady := false
	for _, c := range pod.Status.ContainerStatuses {
		if c.Name == HealthCheckerContainerName && c.Ready {
			healthCheckerContainerReady = true
		}
	}
	return healthCheckerContainerReady
}
