package machine

import (
	"context"
	"database/sql"
	"fmt"
	"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/i18n"
	"github.com/pingcap/tep/pkg/operator"
	"github.com/pingcap/tep/pkg/rbac"
	store "github.com/pingcap/tep/pkg/registry/apiserver"
	"github.com/pingcap/tep/pkg/utils"
	"go.uber.org/zap"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
	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"
	"time"
)

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) Kind() string {
	return "Machine"
}

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

func (s *Storage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
	var msg, err error
	var machine *apiserver.Machine
	var az *entity.Az
	db := s.store.DB
	machineEntity := &entity.Machine{}

	if err = db.First(machineEntity, "ID = ?", name).Error; err != nil {
		if gorm.IsRecordNotFoundError(err) {
			msg = fmt.Errorf("machine[%s] is not found, err: %s", name, err.Error())
			log.Error("", zap.Error(msg))
			return nil, errors.NewNotFound(s.groupResource, msg.Error())
		}
		msg = fmt.Errorf("failed to find machine[%s], err: %s", name, err.Error())
		log.Error("", zap.Error(msg))
		return nil, errors.NewInternalError(msg)
	}
	machine = s.store.Controller().AssembleMachine(machineEntity)

	if az, err = s.store.Controller().GetAzByID(machineEntity.AzID, db.New()); err != nil {
		msg = fmt.Errorf("failed to get az by id[%d], err: %s", machineEntity.AzID, err.Error())
		log.Error("", zap.Error(msg))
		return nil, errors.NewInternalError(msg)
	}

	machine.AZ = az.Name

	return machine, nil
}

func (s *Storage) List(ctx context.Context, options *metainternalversion.ListOptions, extraOptions runtime.Object) (runtime.Object, error) {
	var err, msg error
	extraOption := extraOptions.(*apiserver.MachineListOptions)
	db := s.store.DB
	var total int64
	var az *entity.Az
	var machine *apiserver.Machine
	machineList := []entity.Machine{}

	if extraOption.IP != "" {
		db = db.Where("ip = ?", extraOption.IP)
	}
	if extraOption.Status != "" {
		db = db.Where("status = ?", extraOption.Status)
	}
	if extraOption.Purpose != "" {
		db = db.Where("purpose like ?", fmt.Sprintf("%%%s%%", extraOption.Purpose))
	}

	db.Table("machine").Count(&total)

	if extraOption.SortBy != "" {
		db = db.Order(extraOption.SortBy)
	}
	if extraOption.ItemsPerPage > 0 {
		db = db.Offset((extraOption.Page - 1) * extraOption.ItemsPerPage).Limit(extraOption.ItemsPerPage)
	} else {
		db = db.Offset((extraOption.Page - 1) * 10).Limit(10)
	}

	lists := &apiserver.MachineList{Items: []apiserver.Machine{}}
	if err = db.Find(&machineList).Error; err != nil {
		msg = fmt.Errorf("failed to query machine err: %s", err.Error())
		log.Error("", zap.Error(msg))
		return nil, errors.NewInternalError(msg)
	}

	for _, machineEntity := range machineList {
		if az, err = s.store.Controller().GetAzByID(machineEntity.AzID, db.New()); err != nil {
			msg = fmt.Errorf("failed to get az by id[%d], err: %s", machineEntity.AzID, err.Error())
			log.Error("", zap.Error(msg))
			return nil, errors.NewInternalError(msg)
		}
		machine = s.store.Controller().AssembleMachine(&machineEntity)
		machine.AZ = az.Name
		lists.Items = append(lists.Items, *machine)
	}
	lists.Total = total

	return lists, nil
}

func (s *Storage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
	startTime := time.Now()
	operatorName := utils.GetAPIUsername(ctx)
	if !rbac.CanOperate(s.store.DB, operatorName, rbac.DeleteAndOfflineMachine) {
		return nil, false, errors.NewForbidden(s.groupResource, operatorName, fmt.Errorf("permission denied"))
	}
	var err, msg error
	db := s.store.DB
	machine := entity.Machine{}
	userID := ""
	if user, found := request.UserFrom(ctx); found {
		userID = user.GetUID()
	}

	// 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
		var node *corev1.Node

		if node, retryErr = s.store.Controller().GetNode(name); retryErr != nil {
			return retryErr
		}
		// the machine in the online state cannot be deleted.
		if _, ok := node.Labels[operator.TEPSchedulableLabel]; ok {
			return errors.NewInternalError(fmt.Errorf("online machine %s can't be deleted", name))
		}

		if _, ok := node.Labels[operator.TEPManagedLabel]; !ok {
			return nil
		}

		// delete TEP label for node
		delete(node.Labels, operator.TEPManagedLabel)
		if _, ok := node.Labels[operator.TEPStorageNodeLabel]; ok {
			delete(node.Labels, operator.TEPStorageNodeLabel)
		}
		if _, ok := node.Labels[operator.TEPComputeNodeLabel]; ok {
			delete(node.Labels, operator.TEPComputeNodeLabel)
		}
		if node, retryErr = s.store.Controller().UpdateNode(node); retryErr != nil {
			return retryErr
		}
		return nil
	})

	if err != nil {
		msg = fmt.Errorf("delete machine label 'TEP' failed err: %s", err.Error())
		log.Error("", zap.Error(msg))
		return nil, false, errors.NewInternalError(msg)
	}

	if err = db.Where("ID= ? ", name).Delete(&machine).Error; err != nil {
		if err == sql.ErrNoRows {
			msg = fmt.Errorf("backup %s is not found err: %s", name, err.Error())
			log.Error("", zap.Error(msg))
			return nil, false, errors.NewNotFound(s.groupResource, msg.Error())
		}
		msg = fmt.Errorf("delete machine %s failed err: %s", name, err.Error())
		log.Error("", zap.Error(msg))
		return nil, false, errors.NewInternalError(msg)
	}

	utils.AddEventAndLogError(s.store.DB, &utils.Event{
		UserID:         userID,
		Payload:        name,
		OperationType:  entity.DeleteOperation,
		ManagementType: entity.MachineManagement,
		CostTime:       time.Now().Sub(startTime).Nanoseconds(),
	})
	i18n.PlatformMessage.AddPlatformMessageAndLogError(s.store.DB, i18n.OperateMessageType, i18n.DeleteMachine, name)
	return nil, true, nil
}

func (s *Storage) NewList() runtime.Object {
	return &apiserver.MachineList{}
}

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

func (s *Storage) NewExtraListOptions() runtime.Object {
	return &apiserver.MachineListOptions{}
}
