package admin

import (
	"fmt"

	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/central-sdk/db"
	"github.com/tidbcloud/central-sdk/db/dbutil"
	"github.com/tidbcloud/central-sdk/db/entity"
	"gorm.io/gorm"
)

type Role struct {
	Id         uint64
	Name       string
	Privileges []Privilege
}

type Privilege struct {
	Resource  string
	Operation string
}

func (m UserContext) UpsertRole(role *Role) (*Role, error) {
	if err := validateRole(role); err != nil {
		return nil, err
	}
	if m.tenantAndUser.ID == 0 {
		return nil, errors.New("not a normal user")
	}
	dbRole := buildRole(role)
	err := db.Transaction(func(tx *gorm.DB) error {
		if dbRole.ID > 0 {
			if err := dbRole.Update(tx, entity.RoleDBSchema.Privileges, entity.RoleDBSchema.Name); err != nil {
				return errors.Annotate(err, "update role")
			}
			return dbutil.AddAccessLogNow(tx, m.tenantAndUser.TenantID, m.tenantAndUser.Email, fmt.Sprintf("update role %s with privileges %v", role.Name, role.Privileges))
		}
		if err := tx.Create(dbRole).Error; err != nil {
			return errors.Annotate(err, "create role")
		}
		return dbutil.AddAccessLogNow(tx, m.tenantAndUser.TenantID, m.tenantAndUser.Email, fmt.Sprintf("add new role %s with privileges %v", role.Name, role.Privileges))

	})
	if err != nil {
		return nil, err
	}
	role.Id = dbRole.ID.Uint64()
	return role, nil
}

func (m UserContext) DeleteRole(id uint64) error {
	if m.tenantAndUser.ID == 0 {
		return errors.New("not a normal user")
	}

	err := db.Transaction(func(tx *gorm.DB) error {
		var role entity.Role
		err := entity.NewRoleQuerySet(tx).TenantIDEq(entity.PingCAPTenantID).IDEq(entity.ID(id)).One(&role)
		if err != nil {
			return errors.Annotate(err, "fetch role failed")
		}

		var users []entity.User
		err = entity.NewUserQuerySet(tx).TenantIDEq(entity.PingCAPTenantID).All(&users)
		if err != nil {
			return err
		}
		for _, user := range users {
			for _, item := range user.Roles {
				if item.ID.Uint64() == id {
					return errors.New("this role is still referenced by some roles")
				}
			}
		}

		err = role.Delete(tx)
		if err != nil {
			return err
		}
		return dbutil.AddAccessLogNow(tx, m.tenantAndUser.TenantID, m.tenantAndUser.Email, fmt.Sprintf("delete role %s", role.Name))
	})
	return err
}

func (m UserContext) GetRoleByID(id uint64) (*Role, error) {

	if m.tenantAndUser.ID == 0 {
		return nil, errors.New("not a normal user")
	}
	var role entity.Role
	err := db.Transaction(func(tx *gorm.DB) error {
		return entity.NewRoleQuerySet(tx).IDEq(entity.ID(id)).TenantIDEq(entity.PingCAPTenantID).One(&role)
	})
	if err != nil {
		return nil, err
	}
	return buildInternalRole(role), nil
}

func (m UserContext) ListRoles() ([]*Role, error) {

	if m.tenantAndUser.ID == 0 {
		return nil, errors.New("not a normal user")
	}
	var roles []entity.Role
	err := db.Transaction(func(tx *gorm.DB) error {
		return entity.NewRoleQuerySet(tx).TenantIDEq(entity.PingCAPTenantID).All(&roles)
	})
	if err != nil {
		return nil, err
	}
	var internalUsers []*Role
	for _, item := range roles {
		internalUsers = append(internalUsers, buildInternalRole(item))
	}
	return internalUsers, nil
}

func (m UserContext) GetRolesCount() (int64, error) {
	var count int64

	err := db.Transaction(func(tx *gorm.DB) error {
		var err error
		count, err = entity.NewRoleQuerySet(tx).TenantIDEq(entity.PingCAPTenantID).Count()

		return err
	})

	return count, err
}

func buildInternalRole(item entity.Role) *Role {
	var privileges []Privilege
	for _, item := range item.Privileges.Privileges {
		privileges = append(privileges, Privilege{
			Resource:  item.Resource,
			Operation: item.Operation,
		})
	}
	return &Role{
		Id:         item.ID.Uint64(),
		Name:       item.Name,
		Privileges: privileges,
	}
}

func validateRole(role *Role) error {
	if role.Name == "" {
		return errcode.NewInvalidInputErr(errors.New("invalid role name"))
	}
	for _, item := range role.Privileges {
		if item.Resource == "" || item.Operation == "" {
			return errcode.NewInvalidInputErr(errors.New("invalid role privilege"))
		}
	}
	return nil
}

func buildRole(role *Role) *entity.Role {
	dbRoles := &entity.Role{
		TenantID: entity.PingCAPTenantID,
		Name:     role.Name,
	}
	dbRoles.ID = entity.ID(role.Id)
	for _, item := range role.Privileges {
		dbRoles.Privileges.Privileges = append(dbRoles.Privileges.Privileges, entity.Privilege{
			Resource:  item.Resource,
			Operation: item.Operation,
		})
	}
	return dbRoles
}
