package controller

import (
	"errors"
	"fmt"
	"time"

	"github.com/pingcap/log"
	"github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"
	"go.uber.org/zap"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	errorutils "k8s.io/apimachinery/pkg/util/errors"
)

var (
	tikvFailoverPeriod = 5 * time.Minute
	ProbeSQL           = "SELECT count(*) FROM tidb"
)

type ComponentState string

const (
	FailedState    ComponentState = "failed"
	UnhealthyState ComponentState = "unhealthy"
	UpgradingState ComponentState = "upgrading"
	RunningState   ComponentState = "running"
	ScalingState   ComponentState = "scaling"

	InstanceStatusStatusCreating   string = "creating"
	InstanceStatusStatusUpgrading  string = "upgrading"
	InstanceStatusStatusScaling    string = "scaling"
	InstanceStatusStatusRunning    string = "running"
	InstanceStatusStatusFailed     string = "failed"
	InstanceStatusStatusUnhealthy  string = "unhealthy"
	InstanceStatusStatusRecovering string = "recovering"
)

type RecoveryState string

const (
	RecoveryFailedState    RecoveryState = "failed"
	RecoveryRunningState   RecoveryState = "running"
	RecoverySucceededState RecoveryState = "succeeded"
	RecoveryUnknownState   RecoveryState = "unknown"
)

var (
	ErrPDFailed             = errors.New("PD failure")
	ErrTiDBFailed           = errors.New("TiDB failure")
	ErrTiKVFailed           = errors.New("TiKV failure")
	ErrQueryFailed          = errors.New("SQL query failure")
	ErrBackupRecoveryFailed = errors.New("Backup recovery failure")
)

func PDMembersStatus(tc *v1alpha1.TidbCluster) (ComponentState, error) {
	lowerLimit := int(tc.Spec.PD.Replicas/2 + 1)
	var healthyNum, unhealthyNum int

	for _, member := range tc.Status.PD.Members {
		if member.Health {
			healthyNum++
		} else {
			unhealthyNum++
		}
	}

	if healthyNum < lowerLimit {
		return FailedState, ErrPDFailed
	}

	if tc.PDUpgrading() {
		return UpgradingState, nil
	}

	if len(tc.Status.PD.FailureMembers) > 0 || unhealthyNum > 0 {
		return UnhealthyState, nil
	}

	if len(tc.Status.PD.Members) == int(tc.Spec.PD.Replicas) && unhealthyNum == 0 {
		return RunningState, nil
	}

	return ScalingState, nil
}

func TiDBMembersStatus(tc *v1alpha1.TidbCluster) (ComponentState, error) {
	var healthyNum, unhealthyNum int

	for _, member := range tc.Status.TiDB.Members {
		if member.Health {
			healthyNum++
		} else {
			unhealthyNum++
		}
	}

	if healthyNum < 1 {
		return FailedState, ErrTiDBFailed
	}

	if tc.TiDBUpgrading() {
		return UpgradingState, nil
	}

	if len(tc.Status.TiDB.FailureMembers) > 0 || unhealthyNum > 0 {
		return UnhealthyState, nil
	}

	if len(tc.Status.TiDB.Members) == int(tc.Spec.TiDB.Replicas) && unhealthyNum == 0 {
		return RunningState, nil
	}

	return ScalingState, nil
}

func TiKVStoresStatus(tc *v1alpha1.TidbCluster) (ComponentState, error) {
	var healthyNum, unhealthyNum int
	for _, store := range tc.Status.TiKV.Stores {
		if store.State == v1alpha1.TiKVStateUp {
			healthyNum++
		}

		if store.State == v1alpha1.TiKVStateDown && !store.LastTransitionTime.IsZero() {
			deadline := store.LastTransitionTime.Add(tikvFailoverPeriod)
			if time.Now().After(deadline) {
				unhealthyNum++
			}
		}
	}

	if healthyNum < 2 {
		return FailedState, ErrTiKVFailed
	}

	if tc.TiKVUpgrading() {
		return UpgradingState, nil
	}

	if unhealthyNum > 0 {
		return UnhealthyState, nil
	}

	// If there's a tikv failover, and the original tikv pod recovers, tidb operator will not remove tikv pod
	if healthyNum >= int(tc.Spec.TiKV.Replicas) {
		return RunningState, nil
	}

	return ScalingState, nil
}

func (c *Controller) TidbClusterQueryFailed(tc *v1alpha1.TidbCluster) (bool, error) {
	dbConn, err := c.GetDBConn(tc)
	if err != nil {
		log.Error("failed to get db connection", zap.String("TidbCluster", tc.GetNamespace()), zap.Error(err))
		return true, ErrQueryFailed
	}

	err = dbConn.ProbeQuery()
	if err != nil {
		log.Error("failed to probe query", zap.String("TidbCluster", tc.GetNamespace()), zap.Error(err))
		return true, ErrQueryFailed
	}
	return false, nil
}

func restoreJobStatus(restorePod *corev1.Pod) (RecoveryState, error) {
	if restorePod == nil {
		return RecoveryUnknownState, nil
	}

	switch restorePod.Status.Phase {
	case corev1.PodFailed:
		return RecoveryFailedState, ErrBackupRecoveryFailed
	case corev1.PodSucceeded:
		return RecoverySucceededState, nil
	default:
		return RecoveryRunningState, nil
	}
}

func (c *Controller) getRecentRestoreCR(namespace string) *v1alpha1.Restore {
	var ret *v1alpha1.Restore
	restoreCRList, err := c.ListRestores(namespace, metav1.ListOptions{})
	if err != nil {
		log.Error("failed to get restore List", zap.String("namespace", namespace), zap.Error(err))
		return ret
	}
	for _, restoreCR := range restoreCRList.Items {
		if ret == nil || restoreCR.CreationTimestamp.After(ret.CreationTimestamp.Time) {
			ret = &restoreCR
		}
	}
	return ret

}

// GetClusterStatus returns tidb cluster status and reason of failure
func (c *Controller) GetClusterStatus(tc *v1alpha1.TidbCluster) (string, string) {

	initializerJob, err := c.kubeCli.BatchV1().Jobs(tc.GetNamespace()).Get(fmt.Sprintf("%s-tidb-initializer", tc.GetName()), metav1.GetOptions{})

	if err != nil {
		log.Error("failed to get initializer job", zap.String("TidbCluster", tc.GetNamespace()), zap.Error(err))
		return InstanceStatusStatusCreating, ""
	}

	if initializerJob.Status.Succeeded == 0 {
		return InstanceStatusStatusCreating, ""
	}

	queryFailed, queryErr := c.TidbClusterQueryFailed(tc)
	pdState, pdErr := PDMembersStatus(tc)
	tikvState, tikvErr := TiKVStoresStatus(tc)
	tidbState, tidbErr := TiDBMembersStatus(tc)

	var errs []error
	errs = append(errs, queryErr, pdErr, tikvErr, tidbErr)

	if queryFailed || pdState == FailedState || tidbState == FailedState || tikvState == FailedState {
		return InstanceStatusStatusFailed, errorutils.NewAggregate(errs).Error()
	}

	restoreCR := c.getRecentRestoreCR(tc.GetNamespace())
	if restoreCR != nil {
		restoreStatue := c.GetRestoreStatus(restoreCR)
		switch restoreStatue {
		case RecoveryFailedState:
			return InstanceStatusStatusFailed, ErrBackupRecoveryFailed.Error()
		case RecoverySucceededState:
			break
		default:
			return InstanceStatusStatusRecovering, ""
		}
	}

	if pdState == UpgradingState || tidbState == UpgradingState || tikvState == UpgradingState {
		return InstanceStatusStatusUpgrading, ""
	}

	if pdState == RunningState && tidbState == RunningState && tikvState == RunningState {
		return InstanceStatusStatusRunning, ""
	}

	if pdState == UnhealthyState || tidbState == UnhealthyState || tikvState == UnhealthyState {
		return InstanceStatusStatusUnhealthy, ""
	}

	return InstanceStatusStatusScaling, ""
}
