package machinestatus

import (
	"context"
	"fmt"
	"github.com/pingcap/tep/pkg/i18n"
	"github.com/pingcap/tep/pkg/rbac"
	"time"

	"github.com/jinzhu/gorm"
	"github.com/pingcap/log"
	"github.com/pingcap/tep/db/entity"
	"github.com/pingcap/tep/pkg/apis/apiserver"
	"github.com/pingcap/tep/pkg/operator"
	store "github.com/pingcap/tep/pkg/registry/apiserver"
	"github.com/pingcap/tep/pkg/utils"
	"go.uber.org/zap"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apiserver/pkg/endpoints/request"
	"k8s.io/apiserver/pkg/registry/rest"
	"k8s.io/client-go/util/retry"
)

type Storage struct {
	store         *store.Store
	groupResource schema.GroupResource
}

func NewStorage(store *store.Store, groupResource schema.GroupResource) *Storage {
	return &Storage{
		store:         store,
		groupResource: groupResource,
	}
}

func (s *Storage) getMachine(name string) (*entity.Machine, error) {
	db := s.store.DB
	machine := entity.Machine{}

	if err := db.First(&machine, "id = ?", name).Error; err != nil {
		return nil, err
	}

	return &machine, nil
}

func (s *Storage) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
	startTime := time.Now()
	operatorName := utils.GetAPIUsername(ctx)
	if !rbac.CanOperate(s.store.DB, operatorName, rbac.DeleteAndOfflineMachine) {
		return nil, false, apierrors.NewForbidden(s.groupResource, operatorName, fmt.Errorf("permission denied"))
	}
	var err, msg error
	var oldMachine, newMachine *apiserver.Machine
	var oldMachineEntity *entity.Machine
	var newObj runtime.Object
	var node *corev1.Node
	var az *entity.Az
	var userID string

	db := s.store.DB
	if oldMachineEntity, err = s.getMachine(name); err != nil {
		if gorm.IsRecordNotFoundError(err) {
			msg = fmt.Errorf("machine %s is not found in the table err: %s", name, err.Error())
			log.Error("", zap.Error(msg))
			return nil, false, apierrors.NewNotFound(s.groupResource, msg.Error())
		}
		msg = fmt.Errorf("failed to get machine %s err: %s", name, err.Error())
		log.Error("", zap.Error(msg))
		return nil, false, apierrors.NewInternalError(msg)
	}

	oldMachine = s.store.Controller().AssembleMachine(oldMachineEntity)

	if az, err = s.store.Controller().GetAzByID(oldMachineEntity.AzID, db); err != nil {
		msg = fmt.Errorf("failed to get az by id %u err: %s", oldMachineEntity.AzID, err.Error())
		log.Error("", zap.Error(msg))
		return nil, false, apierrors.NewInternalError(msg)
	}

	if user, found := request.UserFrom(ctx); found {
		userID = user.GetUID()
	}

	newObj, err = objInfo.UpdatedObject(ctx, oldMachine)
	if err != nil {
		msg = fmt.Errorf("failed to update machine object %s err: %s", name, err.Error())
		log.Error("", zap.Error(msg))
		return nil, false, apierrors.NewInternalError(msg)
	}
	newMachine = newObj.(*apiserver.Machine)
	//	if newMachine.Status == entity.MachineOffline.String() {
	// TODO: leave for machine offline logic
	if newMachine.Status == entity.MachineOnline.String() {
		// Nodes update their heartbeat timestamp every ~10 seconds,
		// which makes it likely a get/update will hit a conflict error.
		// See：https://github.com/kubernetes/client-go/issues/414
		err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
			var retryErr error

			if node, retryErr = s.store.Controller().GetNode(newMachine.Name); retryErr != nil {
				return retryErr
			}

			// find TEP Available label and check the value
			if val, ok := node.Labels[operator.TEPManagedLabel]; !ok {
				return apierrors.NewBadRequest(fmt.Sprintf("machine %s is unavailable", oldMachine.Name))
			} else if val == entity.MachineOnline.String() {
				return nil
			}

			// Give the node an online label
			node.Labels[operator.TEPSchedulableLabel] = entity.MachineOnline.String()
			if node, retryErr = s.store.Controller().UpdateNode(node); retryErr != nil {
				return retryErr
			}
			return nil
		})

		if err != nil {
			msg = fmt.Errorf("failed to update node %s err: %s", name, err.Error())
			log.Error("", zap.Error(msg))
			return nil, false, apierrors.NewInternalError(msg)
		}

		if oldMachine.Status == entity.MachineOnline.String() {
			oldMachine.AZ = az.Name
			return oldMachine, true, nil
		}

		oldMachineEntity.Status = entity.MachineOnline.String()

		if err = db.Save(oldMachineEntity).Error; err != nil {
			msg = fmt.Errorf("failed to update machine table %s err: %s", name, err.Error())
			log.Error("", zap.Error(msg))
			return nil, false, apierrors.NewInternalError(msg)
		}
		i18n.PlatformMessage.AddPlatformMessageAndLogError(s.store.DB, i18n.OperateMessageType, i18n.MachineOnline, name)
	} else {
		msg = fmt.Errorf("machine %s status %s setting error", name, newMachine.Status)
		log.Error("", zap.Error(msg))
		return nil, false, apierrors.NewInternalError(msg)
	}

	if err := s.store.AddEvent(&utils.Event{
		UserID:         userID,
		Payload:        fmt.Sprintf("%s", oldMachine.Name),
		OperationType:  entity.UpdateOperation,
		ManagementType: entity.MachineManagement,
		CostTime:       time.Now().Sub(startTime).Nanoseconds(),
	}); err != nil {
		msg = fmt.Errorf("failed to add update machine_status event err: %s", err.Error())
		log.Error("", zap.Error(msg))
		return nil, false, apierrors.NewInternalError(msg)
	}

	oldMachine.AZ = az.Name

	return oldMachine, true, nil
}

func (s *Storage) Kind() string {
	return "MachineStatus"
}

func (s *Storage) New() runtime.Object {
	return &apiserver.Machine{}
}

func (s *Storage) NamespaceScoped() bool {
	return false
}
