package user

import (
	"bytes"
	"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 "User"
}

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

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

	return toK8sItem(&dbUser), nil
}

func (s *Storage) List(ctx context.Context, options *metainternalversion.ListOptions, extraOptions runtime.Object) (runtime.Object, error) {
	listOptions := extraOptions.(*apiserver.UserListOptions)
	items, total, err := s.getAllUsers(&query{
		From:     utils.GetStartIndex(listOptions.Page, listOptions.ItemsPerPage),
		Size:     utils.GetSize(listOptions.ItemsPerPage),
		Group:    listOptions.Group,
		Username: listOptions.Username,
	})
	if err != nil {
		return nil, errors.NewInternalError(err)
	}
	return &apiserver.UserList{
		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.User)
	if err := validate(k8sUser); err != nil {
		return nil, err
	}
	return k8sUser, gormutil.DoInTransaction(
		func(tx *gorm.DB) error {

			var dbUser entity.User
			err := tx.Where("username=?", k8sUser.Name).First(&dbUser).Error
			if err == nil {
				return errors.NewBadRequest(fmt.Sprintf("User named %s already exists", k8sUser.Name))
			} else if !gorm.IsRecordNotFoundError(err) {
				return errors.NewInternalError(err)
			}
			encryptedPassword, _ := utils.HashPassword(k8sUser.Password)
			dbUser = entity.User{
				Username: k8sUser.Name,
				Password: encryptedPassword,
			}
			if err := tx.Where("group_name=?", k8sUser.Group).First(&apiserver.UserGroup{}).Error; err == gorm.ErrRecordNotFound {
				return errors.NewBadRequest(fmt.Sprintf("User group named %s is not found", k8sUser.Group))
			}
			dbUser.Group = k8sUser.Group
			if err := tx.Create(&dbUser).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"))
	}
	if err := s.store.DB.Unscoped().Where("username=?", name).Delete(&entity.User{}).Error; err != nil {
		return nil, false, errors.NewInternalError(err)
	}
	utils.AddEventAndLogError(s.store.DB, &utils.Event{
		UserID:         operatorName,
		Payload:        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) {
	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"))
	}
	k8sUser, err := s.Get(ctx, name, &metav1.GetOptions{})
	if err != nil {
		return nil, false, err
	}
	if k8sUser == nil {
		return nil, false, errors.NewBadRequest("k8sUser is not found")
	}
	newObj, err := objInfo.UpdatedObject(ctx, k8sUser)
	if err != nil {
		return nil, false, err
	}
	newUser := newObj.(*apiserver.User)
	if len(strings.TrimSpace(newUser.Group)) == 0 {
		return nil, false, errors.NewBadRequest("invalid group")
	}

	err = gormutil.DoInTransaction(
		func(tx *gorm.DB) error {
			var dbUser entity.User
			if err := tx.Where("username=?", name).First(&dbUser).Error; err != nil {
				if gorm.IsRecordNotFoundError(err) {
					return errors.NewNotFound(s.groupResource, name)
				}
				return errors.NewInternalError(err)
			}
			if newUser.Password != "" {
				//todo: logout k8sUser
				dbUser.Password, _ = utils.HashPassword(newUser.Password)
			}
			if err := tx.Where("group_name=?", newUser.Group).First(&entity.UserGroup{}).Error; err == gorm.ErrRecordNotFound {
				return errors.NewBadRequest(fmt.Sprintf("usergroup %s is not found", newUser.Group))
			}
			dbUser.Group = newUser.Group
			if err := tx.Save(&dbUser).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 newUser, true, err
}

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

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

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

type query struct {
	From     int
	Size     int
	Username string
	Group    string
}

func (s *Storage) getAllUsers(query *query) ([]apiserver.User, int64, error) {
	where, values := buildQuery(query)
	var users []entity.User
	var total int64
	db := s.store.DB.Table("user").Where(where, values...).Count(&total).
		Limit(query.Size).Offset(query.From).
		Find(&users)
	apiItems := make([]apiserver.User, len(users))
	for index, dbUser := range users {
		apiItems[index] = *toK8sItem(&dbUser)
	}
	return apiItems, total, db.Error
}

func buildQuery(query *query) (string, []interface{}) {
	var values []interface{}
	where := bytes.Buffer{}
	where.WriteString(" 1=1  ")
	if query.Username != "" {
		where.WriteString(" and username=? ")
		values = append(values, query.Username)
	}
	if query.Group != "" {
		where.WriteString(" and group=? ")
		values = append(values, query.Group)
	}
	return where.String(), values
}

func toK8sItem(dbUser *entity.User) *apiserver.User {
	return &apiserver.User{
		ObjectMeta: metav1.ObjectMeta{Name: dbUser.Username, UID: types.UID(dbUser.Username), CreationTimestamp: metav1.NewTime(dbUser.CreatedAt)},
		Group:      dbUser.Group,
		CreateTime: strfmt.DateTime(dbUser.CreatedAt).String(),
		UpdateTime: strfmt.DateTime(dbUser.UpdatedAt).String(),
	}
}

func validate(k8sUser *apiserver.User) error {
	if len(strings.TrimSpace(k8sUser.Name)) == 0 {
		return errors.NewBadRequest("invalid username")
	}
	if len(strings.TrimSpace(k8sUser.Password)) == 0 {
		return errors.NewBadRequest("invalid password")
	}
	if len(strings.TrimSpace(k8sUser.Group)) == 0 {
		return errors.NewBadRequest("invalid group")
	}
	return nil
}
