package usergroup

import (
	"context"
	"fmt"
	"github.com/go-openapi/strfmt"
	"github.com/jinzhu/gorm"
	"github.com/pingcap/tep/db/entity"
	"github.com/pingcap/tep/pkg/apis/apiserver"
	"github.com/pingcap/tep/pkg/rbac"
	store "github.com/pingcap/tep/pkg/registry/apiserver"
	"github.com/pingcap/tep/pkg/utils"
	"github.com/pingcap/tep/pkg/utils/gormutil"
	"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/apimachinery/pkg/types"
	"k8s.io/apiserver/pkg/registry/rest"
	"strings"
	"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 "UserGroup"
}

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

func (s *Storage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
	var dbUserGroup entity.UserGroup
	if err := s.store.DB.Where("group_name=?", name).First(&dbUserGroup).Error; err != nil {
		if gorm.IsRecordNotFoundError(err) {
			return nil, errors.NewNotFound(s.groupResource, fmt.Sprintf("user group named %s is not found", name))
		}
		return nil, err
	}

	return toK8sItem(&dbUserGroup), nil
}

func (s *Storage) List(ctx context.Context, options *metainternalversion.ListOptions, extraOptions runtime.Object) (runtime.Object, error) {
	listOptions := extraOptions.(*apiserver.ListOptions)
	items, total, err := s.getAllUsers(utils.GetStartIndex(listOptions.Page, listOptions.ItemsPerPage), utils.GetSize(listOptions.ItemsPerPage))
	if err != nil {
		return nil, errors.NewInternalError(err)
	}
	return &apiserver.UserGroupList{
		Items: items,
		Total: total,
	}, nil
}

func (s *Storage) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
	operatorName := utils.GetAPIUsername(ctx)
	if !rbac.CanOperate(s.store.DB, operatorName, rbac.UserOperation) {
		return nil, errors.NewForbidden(s.groupResource, operatorName, fmt.Errorf("permission denied"))
	}
	startTime := time.Now()
	k8sUser := obj.(*apiserver.UserGroup)
	if k8sUser.Name == "" {
		return nil, errors.NewBadRequest("invalid group name")
	}

	return k8sUser, gormutil.DoInTransaction(
		func(tx *gorm.DB) error {

			var dbUserGroup entity.UserGroup
			err := tx.Where("group_name=?", k8sUser.Name).First(&dbUserGroup).Error
			if err == nil {
				return errors.NewBadRequest(fmt.Sprintf("UserGroup named %s already exists", k8sUser.Name))
			} else if !gorm.IsRecordNotFoundError(err) {
				return errors.NewInternalError(err)
			}
			dbUserGroup = entity.UserGroup{
				GroupName:  k8sUser.Name,
				Privileges: strings.Join(k8sUser.Privileges, ","),
			}
			if err := tx.Create(&dbUserGroup).Error; err != nil {
				return errors.NewInternalError(err)
			}
			utils.AddEventAndLogError(s.store.DB, &utils.Event{
				UserID:         operatorName,
				Payload:        k8sUser.Name,
				OperationType:  entity.CreateOperation,
				ManagementType: entity.UserManagement,
				CostTime:       time.Now().Sub(startTime).Nanoseconds(),
			})
			return nil
		}, s.store.DB)
}

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.UserOperation) {
		return nil, false, errors.NewForbidden(s.groupResource, operatorName, fmt.Errorf("permission denied"))
	}
	err := s.store.DB.Where("group_name=?", name).First(&entity.User{}).Error
	if err == nil {
		return nil, false, errors.NewBadRequest(fmt.Sprintf("group %s is not empty", name))
	} else if !gorm.IsRecordNotFoundError(err) {
		return nil, false, errors.NewInternalError(err)
	}
	if err := s.store.DB.Unscoped().Where("group_name=?", name).Delete(&entity.UserGroup{}).Error; err != nil {
		return nil, false, errors.NewInternalError(err)
	}
	utils.AddEventAndLogError(s.store.DB, &utils.Event{
		UserID:         operatorName,
		Payload:        fmt.Sprintf("group %s", name),
		OperationType:  entity.DeleteOperation,
		ManagementType: entity.UserManagement,
		CostTime:       time.Now().Sub(startTime).Nanoseconds(),
	})
	return nil, true, 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) {
	operatorName := utils.GetAPIUsername(ctx)
	if !rbac.CanOperate(s.store.DB, operatorName, rbac.UserOperation) {
		return nil, false, errors.NewForbidden(s.groupResource, operatorName, fmt.Errorf("permission denied"))
	}
	startTime := time.Now()
	k8sUserGroup, err := s.Get(ctx, name, &metav1.GetOptions{})
	if err != nil {
		return nil, false, err
	}
	if k8sUserGroup == nil {
		return nil, false, errors.NewBadRequest("k8sUserGroup is not found")
	}
	newObj, err := objInfo.UpdatedObject(ctx, k8sUserGroup)
	if err != nil {
		return nil, false, err
	}
	newUserGroup := newObj.(*apiserver.UserGroup)
	if err := validate(newUserGroup); err != nil {
		return nil, false, err
	}

	err = gormutil.DoInTransaction(
		func(tx *gorm.DB) error {
			var dbUserGroup entity.UserGroup
			if err := tx.Where("group_name=?", name).First(&dbUserGroup).Error; err != nil {
				if gorm.IsRecordNotFoundError(err) {
					return errors.NewNotFound(s.groupResource, name)
				}
				return errors.NewInternalError(err)
			}
			dbUserGroup.Privileges = strings.Join(newUserGroup.Privileges, ",")
			if err := tx.Save(&dbUserGroup).Error; err != nil {
				return errors.NewInternalError(err)
			}

			utils.AddEventAndLogError(tx, &utils.Event{
				UserID:         operatorName,
				Payload:        newObj,
				OperationType:  entity.UpdateOperation,
				ManagementType: entity.UserManagement,
				CostTime:       time.Now().Sub(startTime).Nanoseconds(),
			})
			return nil
		},
		s.store.DB)
	if err != nil {
		return nil, false, err
	}
	return newUserGroup, true, err
}

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

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

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

func (s *Storage) getAllUsers(from, size int) ([]apiserver.UserGroup, int64, error) {
	var groups []entity.UserGroup
	var total int64
	db := s.store.DB.Table("user_group").Count(&total).Limit(size).Offset(from).Find(&groups)
	apiItems := make([]apiserver.UserGroup, len(groups))
	for index, dbUserGroup := range groups {
		apiItems[index] = *toK8sItem(&dbUserGroup)
	}
	return apiItems, total, db.Error
}

func toK8sItem(dbUserGroup *entity.UserGroup) *apiserver.UserGroup {
	var privileges = []string{}
	if strings.TrimSpace(dbUserGroup.Privileges) != "" {
		privileges = strings.Split(dbUserGroup.Privileges, ",")
	}
	return &apiserver.UserGroup{
		ObjectMeta: metav1.ObjectMeta{Name: dbUserGroup.GroupName, UID: types.UID(dbUserGroup.GroupName), CreationTimestamp: metav1.NewTime(dbUserGroup.CreatedAt)},
		CreateTime: strfmt.DateTime(dbUserGroup.CreatedAt).String(),
		UpdateTime: strfmt.DateTime(dbUserGroup.UpdatedAt).String(),
		Privileges: privileges,
	}
}

func validate(group *apiserver.UserGroup) error {
	if group.Name == "" {
		return errors.NewBadRequest("invalid group name")
	}
	for _, item := range group.Privileges {
		if !rbac.AllPrivilegeNameSet[item] {
			return errors.NewBadRequest(fmt.Sprintf("invalid privilege %s", item))
		}
	}
	return nil
}
