// Copyright 2017 PingCAP, Inc.
//
// 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,
// See the License for the specific language governing permissions and
// limitations under the License.package spec

package controller

import (
	"time"

	"github.com/pingcap/tidb-operator/pkg/apis/pingcap.com/v1"
	"github.com/pingcap/tidb-operator/pkg/util"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/util/integer"
	podutil "k8s.io/kubernetes/pkg/api/v1/pod"
)

const (
	// TaintNodeUnreachableAlpha configures the alpha taint key of node unreachable,it will be used before k8s v1.9
	TaintNodeUnreachableAlpha = "node.alpha.kubernetes.io/unreachable"
	// TaintNodeUnreachable configures the taint key of node unreachable,it will be used begin k8s v1.9
	TaintNodeUnreachable = "node.kubernetes.io/unreachable"
	// TolerationTime configures toleration time when node become unready
	TolerationTime = 300
)

// GetImageByPodSpec get container image based on a specific container name and k8s podSpec
func GetImageByPodSpec(ctype v1.ContainerType, ps *corev1.PodSpec) string {
	var image string
	for _, c := range ps.Containers {
		if string(ctype) == c.Name {
			image = c.Image
			break
		}
	}
	return image
}

// SetImageByPodSpec set specific image for a container based on a specific container name and k8s podSpec
func SetImageByPodSpec(ctype v1.ContainerType, ps *corev1.PodSpec, image string) {
	for i, c := range ps.Containers {
		if string(ctype) == c.Name {
			ps.Containers[i].Image = image
			break
		}
	}
}

// ActivePods type allows custom sorting of pods so a controller can pick the best ones to delete.
// Those code snippet copied from kubernetes, have slight changed.
type ActivePods []corev1.Pod

func (s ActivePods) Len() int      { return len(s) }
func (s ActivePods) Swap(i, j int) { s[i], s[j] = s[j], s[i] }

func (s ActivePods) Less(i, j int) bool {
	// 1. Unassigned < assigned
	// If only one of the pods is unassigned, the unassigned one is smaller
	if s[i].Spec.NodeName != s[j].Spec.NodeName && (len(s[i].Spec.NodeName) == 0 || len(s[j].Spec.NodeName) == 0) {
		return len(s[i].Spec.NodeName) == 0
	}
	// 2. PodPending < PodUnknown < PodRunning
	m := map[corev1.PodPhase]int{corev1.PodPending: 0, corev1.PodUnknown: 1, corev1.PodRunning: 2}
	if m[s[i].Status.Phase] != m[s[j].Status.Phase] {
		return m[s[i].Status.Phase] < m[s[j].Status.Phase]
	}
	// 3. Not ready < ready
	// If only one of the pods is not ready, the not ready one is smaller
	if podutil.IsPodReady(&s[i]) != podutil.IsPodReady(&s[j]) {
		return !podutil.IsPodReady(&s[i])
	}
	// 4. Been ready for empty time < less time < more time
	// If both pods are ready, the latest ready one is smaller
	if podutil.IsPodReady(&s[i]) && podutil.IsPodReady(&s[j]) && !podReadyTime(&s[i]).Equal(podReadyTime(&s[j])) {
		return afterOrZero(podReadyTime(&s[i]), podReadyTime(&s[j]))
	}
	// 5. Pods with containers with higher restart counts < lower restart counts
	if maxContainerRestarts(&s[i]) != maxContainerRestarts(&s[j]) {
		return maxContainerRestarts(&s[i]) > maxContainerRestarts(&s[j])
	}
	// 6. Empty creation time pods < newer pods < older pods
	if !s[i].CreationTimestamp.Equal(&s[j].CreationTimestamp) {
		return afterOrZero(&s[i].CreationTimestamp, &s[j].CreationTimestamp)
	}
	return false
}

// afterOrZero checks if time t1 is after time t2; if one of them
// is zero, the zero time is seen as after non-zero time.
func afterOrZero(t1, t2 *metav1.Time) bool {
	if t1.Time.IsZero() || t2.Time.IsZero() {
		return t1.Time.IsZero()
	}
	return t1.After(t2.Time)
}

func maxContainerRestarts(pod *corev1.Pod) int {
	maxRestarts := 0
	for _, c := range pod.Status.ContainerStatuses {
		maxRestarts = integer.IntMax(maxRestarts, int(c.RestartCount))
	}
	return maxRestarts
}

func podReadyTime(pod *corev1.Pod) *metav1.Time {
	if podutil.IsPodReady(pod) {
		for _, c := range pod.Status.Conditions {
			// we only care about pod ready conditions
			if c.Type == corev1.PodReady && c.Status == corev1.ConditionTrue {
				return &c.LastTransitionTime
			}
		}
	}
	return &metav1.Time{}
}

// PodInUnreachableNode determine if the pod is on an unreachable node
func PodInUnreachableNode(pod *corev1.Pod, nodes []corev1.Node) bool {
	if pod.Status.Reason == util.NodeUnreachablePodReason {
		return true
	}
	for _, node := range nodes {
		if node.Name == pod.Spec.NodeName {
			return nodeUnReachable(node)
		}
	}
	return false
}

func nodeUnReachable(node corev1.Node) bool {
	for _, taint := range node.Spec.Taints {
		if taint.Key == TaintNodeUnreachable || taint.Key == TaintNodeUnreachableAlpha {
			dur := time.Since(taint.TimeAdded.Time)
			if dur.Seconds() > TolerationTime {
				return true
			}
		}
	}
	return false
}
