package rbac

import (
	"context"
	"sync"

	"github.com/tidbcloud/account/commons/database"

	"github.com/duke-git/lancet/v2/maputil"

	"github.com/duke-git/lancet/v2/slice"

	"github.com/tidbcloud/account/internel/dao/rbac"

	"github.com/tidbcloud/account/idl/pbgen/proto/central"
	"github.com/tidbcloud/account/internel/dao"
	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/account/internel/dao/role"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

const (
	DEFAULT_ROLEID = 0
)

type RbacUserSvcIface interface {
	ListUserRoles(ctx context.Context, userId uint64, scopeTypes []entity.ScopeType, scopeIDs []entity.ID) ([]*central.UserRole, error)
	ListUsersByScope(ctx context.Context, scopeType entity.ScopeType, scopeId uint64) ([]RbacUser, error)
	ListUsers(ctx context.Context, filter role.RoleBindingFilter) ([]entity.User, error)
	CheckUserRoleInScope(ctx context.Context, userId uint64, scopeTypes entity.ScopeType, scopeId uint64, roles []string) (bool, error)
	GetUserRolesAndPrivileges(ctx context.Context, userId uint64, systems []string) ([]entity.RoleBinding, map[entity.ID][]entity.RbacPrivilege, error)
}

var (
	rbacUserInitOnce sync.Once
	rbacUserSrv      RbacUserSvcIface
)

func GetRbacUserSvc() (RbacUserSvcIface, error) {
	rbacUserInitOnce.Do(func() {
		rbacUserSrv = NewRbacUserSvc(database.DBConn.DB, dao.GetDaoHub())

	})
	return rbacUserSrv, nil
}

type RbacUserSvc struct {
	db  *gorm.DB
	hub *dao.DaoHub
}

func NewRbacUserSvc(db *gorm.DB, hub *dao.DaoHub) RbacUserSvcIface {
	svc := RbacUserSvc{
		db:  db,
		hub: hub,
	}
	return &svc
}

func (r *RbacUserSvc) ListUserRoles(ctx context.Context, userID uint64, scopeTypes []entity.ScopeType, scopeIDs []entity.ID) ([]*central.UserRole, error) {
	filter := role.RoleBindingFilter{
		TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
		TargetIDs:  []entity.ID{entity.ID(userID)},
		Status:     []string{entity.RoleBindingStatusActive},
	}
	if len(scopeTypes) > 0 {
		filter.ScopeTypes = scopeTypes
	}
	if len(scopeIDs) > 0 {
		filter.ScopeIDs = scopeIDs
	}
	tilog.Info(ctx, "ListUserRoles", zap.Any("RoleBindingFilter", filter))
	// select role_bindings
	roleBindings, err := r.hub.Role.ListRoleBindings(ctx, r.db, filter)

	if err != nil {
		return nil, err
	}
	if len(roleBindings) == 0 {
		return []*central.UserRole{}, nil
	}
	rbacRoleIDs := getValidRbacRoleIDs(roleBindings)
	if len(rbacRoleIDs) == 0 {
		return []*central.UserRole{}, nil
	}
	// select rbac_roles
	rbacRoles, err := r.hub.Rbac.ListRbacRolesByIDs(ctx, r.db, rbacRoleIDs)
	if err != nil {
		return nil, err
	}
	// assemble user_role
	userRoles := convertUserRoles(roleBindings, rbacRoles, userID)

	// add project:owner for org:owner, Temporarily add to the front end and remove it later when ui use rbac
	userRoles = r.setProjectOwnerForOrgOwner(ctx, userRoles, userID)

	return userRoles, nil
}

// Temporarily add to the front end and remove it later  when ui use rbac
func (r *RbacUserSvc) setProjectOwnerForOrgOwner(ctx context.Context, userRoles []*central.UserRole, userID uint64) []*central.UserRole {
	projectOwnerRole, err := r.hub.Rbac.GetRbacRoleByName(ctx, r.db, entity.RoleProjectOwner)
	if err != nil {
		tilog.Error(ctx, "setProjectOwnerForOrgOwner, ListActiveProjectsByOrgID error", zap.Any("err", err))
		return nil
	}

	var projectOwnerUserRoles []*central.UserRole
	for _, userRole := range userRoles {
		if userRole.ScopeType == entity.RoleBindingScopeOrg.String() && userRole.RoleName == entity.RoleOrgOwner {
			projects, err := r.hub.Project.ListActiveProjectsByOrgID(ctx, r.db, userRole.ScopeId)
			if err != nil {
				tilog.Error(ctx, "ListUserRoles add project:owner for owner, ListActiveProjectsByOrgID error", zap.Any("err", err))
				continue
			}
			for _, project := range projects {
				bindings, err := r.hub.Role.ListRoleBindings(ctx, r.db, role.RoleBindingFilter{
					ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeProject},
					ScopeIDs:   []entity.ID{project.ID},
					TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
					TargetIDs:  []entity.ID{entity.ID(userID)},
					Status:     []string{entity.RoleBindingStatusActive},
				})
				if err != nil {
					tilog.Error(ctx, "ListUserRoles add project:owner for owner, ListRoleBindings error", zap.Any("project", project), zap.Any("err", err))
					continue
				}
				if len(bindings) > 0 {
					binding := bindings[0]
					if binding.Role == entity.RoleProjectOwner {
						continue
					}
					for _, tmpUserRole := range userRoles {
						if tmpUserRole.ScopeType == binding.ScopeType.String() && tmpUserRole.ScopeId == binding.ScopeID.Uint64() {
							tmpUserRole.RoleName = projectOwnerRole.Name
							tmpUserRole.RoleDisplayName = projectOwnerRole.DisplayName
							tmpUserRole.RoleDesc = projectOwnerRole.Description
							tmpUserRole.RoleId = projectOwnerRole.ID.Uint64()
							tmpUserRole.RealBinding = false
						}
					}

				} else {
					tmpUserRole := central.UserRole{
						UserId:          userID,
						ScopeType:       entity.RoleBindingScopeProject.String(),
						ScopeId:         project.ID.Uint64(),
						RoleName:        projectOwnerRole.Name,
						RoleDisplayName: projectOwnerRole.DisplayName,
						RoleDesc:        projectOwnerRole.Description,
						RoleId:          projectOwnerRole.ID.Uint64(),
						RealBinding:     false,
					}
					projectOwnerUserRoles = append(projectOwnerUserRoles, &tmpUserRole)
				}
			}
		}
	}
	if len(projectOwnerUserRoles) > 0 {
		userRoles = append(userRoles, projectOwnerUserRoles...)
	}
	return userRoles
}

func getValidRbacRoleIDs(roleBindings []entity.RoleBinding) []uint64 {
	// del repeated role_id
	roleIDs := make(map[uint64]struct{})
	for _, roleBinding := range roleBindings {
		if roleBinding.Status != entity.RoleBindingStatusActive {
			continue
		}
		if roleBinding.RoleID == DEFAULT_ROLEID {
			continue
		}
		roleIDs[roleBinding.RoleID.Uint64()] = struct{}{}
	}
	rbacRoleIDs := make([]uint64, 0, len(roleIDs))
	for roleID := range roleIDs {
		rbacRoleIDs = append(rbacRoleIDs, roleID)
	}
	return rbacRoleIDs
}

func convertRbacRoles2Map(rbacRoles []entity.RbacRole) map[uint64]entity.RbacRole {
	rbacRoleMap := make(map[uint64]entity.RbacRole)
	for _, rbacRole := range rbacRoles {
		rbacRoleMap[rbacRole.ID.Uint64()] = rbacRole
	}
	return rbacRoleMap
}

func convertUserRoles(roleBindings []entity.RoleBinding, rbacRoles []entity.RbacRole, userID uint64) []*central.UserRole {
	userRoles := make([]*central.UserRole, 0)
	rbacRolesMap := convertRbacRoles2Map(rbacRoles)
	for _, roleBinding := range roleBindings {
		rbacRole, ok := rbacRolesMap[roleBinding.RoleID.Uint64()]
		if !ok {
			continue
		}
		userRole := &central.UserRole{
			UserId:          userID,
			RoleName:        rbacRole.Name,
			RoleDisplayName: rbacRole.DisplayName,
			ScopeType:       string(roleBinding.ScopeType),
			ScopeId:         roleBinding.ScopeID.Uint64(),
			RoleDesc:        rbacRole.Description,
			RoleId:          rbacRole.ID.Uint64(),
			RealBinding:     true,
		}
		userRoles = append(userRoles, userRole)
	}
	return userRoles
}

func (r *RbacUserSvc) ListUsersByScope(ctx context.Context, scopeType entity.ScopeType, scopeID uint64) ([]RbacUser, error) {
	roleBindings, err := r.hub.Role.ListRolesByScope(ctx, r.db, scopeType, scopeID, []string{})
	if err != nil {
		return nil, err
	}
	var targetId2RoleBindingMap = map[uint64]entity.RoleBinding{}
	for _, binding := range roleBindings {
		if binding.TargetType != entity.RoleBindingTargetUser {
			continue
		}
		targetId2RoleBindingMap[binding.TargetID.Uint64()] = binding
	}
	userIds := getValidUserIds(roleBindings)
	if len(userIds) == 0 {
		return []RbacUser{}, nil
	}
	users, err := r.hub.User.ListUsers(ctx, r.db, userIds, nil)
	if err != nil {
		return nil, err
	}
	retUsers := make([]RbacUser, 0)
	for _, user := range users {
		if user.Status != entity.UserStatusActive {
			continue
		}
		binding := targetId2RoleBindingMap[user.ID.Uint64()]
		rbacUser := RbacUser{
			User:              user,
			RbacRoleID:        binding.RoleID.Uint64(),
			RbacRole:          binding.Role,
			RoleBindingStatus: binding.Status,
		}
		retUsers = append(retUsers, rbacUser)
	}
	return retUsers, nil
}

func getValidUserIds(roleBindings []entity.RoleBinding) []uint64 {
	userIdsMap := make(map[uint64]struct{})
	for _, roleBinding := range roleBindings {
		if roleBinding.TargetType != entity.RoleBindingTargetUser {
			continue
		}
		if roleBinding.Status != entity.RoleBindingStatusActive {
			continue
		}
		userIdsMap[roleBinding.TargetID.Uint64()] = struct{}{}
	}
	userIds := make([]uint64, 0)
	for userId := range userIdsMap {
		userIds = append(userIds, userId)
	}
	return userIds
}

func (r *RbacUserSvc) ListUsers(ctx context.Context, filter role.RoleBindingFilter) ([]entity.User, error) {
	tilog.Info(ctx, "ListUsers", zap.Any("RoleBindingFilter", filter))
	roleBindings, err := r.hub.Role.ListRoleBindings(ctx, r.db, filter)
	if err != nil {
		return nil, err
	}
	userIds := getUserIds(roleBindings)
	if len(userIds) == 0 {
		return []entity.User{}, nil
	}
	users, err := r.hub.User.ListUsers(ctx, r.db, userIds, nil)
	if err != nil {
		tilog.Error(ctx, "ListUsers error", zap.Error(err))
		return nil, err
	}
	retUsers := make([]entity.User, 0)
	for _, user := range users {
		if user.Status != entity.UserStatusActive {
			continue
		}
		retUsers = append(retUsers, user)
	}
	tilog.Info(ctx, "ListUsers result", zap.Any("retUsers", retUsers), zap.Any("RoleBindingFilter", filter))
	return retUsers, nil
}
func getUserIds(roleBindings []entity.RoleBinding) []uint64 {
	userIdsMap := make(map[uint64]struct{})
	for _, roleBinding := range roleBindings {
		userIdsMap[roleBinding.TargetID.Uint64()] = struct{}{}
	}
	userIds := make([]uint64, 0)
	for userId := range userIdsMap {
		userIds = append(userIds, userId)
	}
	return userIds
}

func (r *RbacUserSvc) CheckUserRoleInScope(ctx context.Context, userId uint64, scopeType entity.ScopeType, scopeId uint64, roles []string) (bool, error) {
	tilog.Info(ctx, "CheckUserRoleInScope", zap.Any("userID", userId), zap.Any("scopeType", scopeType), zap.Any("scopeId", scopeId))
	// select role_bindings
	filter := role.RoleBindingFilter{
		TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
		TargetIDs:  []entity.ID{entity.ID(userId)},
		ScopeTypes: []entity.ScopeType{scopeType},
		ScopeIDs:   []entity.ID{entity.ID(scopeId)},
		Status:     []string{entity.RoleBindingStatusActive},
	}
	roleBindings, err := r.hub.Role.ListRoleBindings(ctx, r.db, filter)
	if err != nil {
		return false, err
	}

	var inScope = false
	if len(roleBindings) > 0 {
		inScope = true
	}

	tilog.Info(ctx, "CheckUserRoleInScope result", zap.Any("userID", userId), zap.Any("scopeType", scopeType), zap.Any("scopeID", scopeId), zap.Any("inScope", inScope))
	return inScope, nil
}

func (r *RbacUserSvc) GetUserRolesAndPrivileges(ctx context.Context, userId uint64, systems []string) ([]entity.RoleBinding, map[entity.ID][]entity.RbacPrivilege, error) {

	var roleBindings []entity.RoleBinding
	var privileges []entity.RbacPrivilege
	var roelPidsMap = map[entity.ID][]entity.ID{}
	var rolePrivilegeMap = map[entity.ID][]entity.RbacPrivilege{}
	var err error
	filter := role.RoleBindingFilter{
		TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
		TargetIDs:  []entity.ID{entity.ID(userId)},
		ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeOrg, entity.RoleBindingScopeProject},
		Status:     []string{string(entity.UserStatusActive)},
	}
	roleBindings, err = r.hub.Role.ListRoleBindings(ctx, r.db, filter)
	tilog.Info(ctx, "query user RoleBindings", zap.Any("userID", userId), zap.Any("roleBindings size", len(roleBindings)))
	if err != nil || len(roleBindings) == 0 {
		return roleBindings, rolePrivilegeMap, err
	}
	roleIDs := map[entity.ID]entity.ID{}
	for _, e := range roleBindings {
		roleIDs[e.RoleID] = e.RoleID
	}
	f := rbac.RolePrivilegeFilter{
		RoleIDs: maputil.Keys(roleIDs),
	}
	//query  role->privilege_id
	rolePrivileges, err := r.hub.Rbac.ListRbacRolePrivileges(ctx, r.db, f)
	if err != nil {
		return roleBindings, rolePrivilegeMap, err
	}
	//var pids []entity.ID
	var pids []uint64
	for _, p := range rolePrivileges {
		if arr, ok := roelPidsMap[p.RbacRoleID]; ok {
			// exist
			arr = append(arr, p.RbacPrivilegeID)
			roelPidsMap[p.RbacRoleID] = arr
		} else {
			// not exist
			roelPidsMap[p.RbacRoleID] = []entity.ID{p.RbacPrivilegeID}
		}
		if !slice.Contain(pids, p.RbacPrivilegeID.Uint64()) {
			pids = append(pids, p.RbacPrivilegeID.Uint64())
		}
	}
	if len(rolePrivileges) > 0 {

		ft := rbac.PrivilegesFilter{ID: pids, System: systems}
		queryFilter := rbac.ListPrivilegeParam{Filter: ft, Offset: -1, Limit: -1}
		result, err := r.hub.Rbac.ListRbacPrivileges(ctx, r.db, queryFilter)
		if err != nil {
			return roleBindings, rolePrivilegeMap, err
		}
		privileges = result.List
	}

	if len(privileges) > 0 {

		for roleID, arr := range roelPidsMap {
			isEven := func(i int, e entity.RbacPrivilege) bool {
				return slice.Contain(arr, e.ID)
			}

			ps := slice.Filter(privileges, isEven)
			rolePrivilegeMap[roleID] = ps
		}
	}

	tilog.Info(ctx, "GetUserRolesAndPrivileges result", zap.Any("userID", userId), zap.Any("roleBindings size", len(roleBindings)), zap.Any("rolePrivilegeMap", len(rolePrivilegeMap)))
	return roleBindings, rolePrivilegeMap, err

}
