package scim

import (
	"context"
	"errors"
	"fmt"
	"sort"
	"sync"

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

	"github.com/imulab/go-scim/pkg/v2/spec"

	"github.com/imulab/go-scim/pkg/v2/crud"
	"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"
)

var (
	initScimCommonSvcOnce sync.Once
	scimCommonSvcInstance ScimCommonSvcIface
)

type ScimCommonSvcIface interface {
	GetUserAndRoleBinding(ctx context.Context, orgID, userID uint64) (*entity.User, *entity.RoleBinding, []entity.RoleBinding, error)
	UpdateRoleBindingStatus(ctx context.Context, user *entity.User, orgBinding *entity.RoleBinding, projectBindings []entity.RoleBinding, roleBindingStatus string) error
	UpdateRoleBindingRole(ctx context.Context, binding entity.RoleBinding, rbRole string, email string, orgID uint64) (*entity.RoleBinding, error)
	DeleteRoleBindings(ctx context.Context, user *entity.User, orgBinding *entity.RoleBinding, projectBindings []entity.RoleBinding) error
	PersistUser(ctx context.Context, rbUser *roleBindingUser) (*entity.User, error)
	PersistRoleBindings(ctx context.Context, rbUser *roleBindingUser) error
	GetUser(ctx context.Context, email string, userID uint64) (*entity.User, error)
	GetOrgUsers(ctx context.Context, orgID, userID uint64, pagination *crud.Pagination) (int, []roleBindingUser, error)
	CreateDbAccount4User(ctx context.Context, userID uint64, email string, orgId, projectId uint64)
}

type ScimCommonSvc struct {
}

func NewScimCommonSvc(db *gorm.DB, hub *dao.DaoHub) ScimCommonSvcIface {
	return &ScimCommonSvc{}
}

func GetScimCommonSvc() ScimCommonSvcIface {
	initScimCommonSvcOnce.Do(func() {
		scimCommonSvcInstance = &ScimCommonSvc{}
	})
	return scimCommonSvcInstance
}

func (s *ScimCommonSvc) GetUserAndRoleBinding(ctx context.Context, orgID, userID uint64) (*entity.User, *entity.RoleBinding, []entity.RoleBinding, error) {
	user, err := s.GetUser(ctx, "", userID)
	if err != nil {
		tilog.Error(ctx, "GetUserAndRoleBinding, GetUser error", zap.Any("userId", userID), zap.Any("err", err))
		return nil, nil, nil, err
	}
	if user == nil {
		return nil, nil, nil, fmt.Errorf("%w: user not found", spec.ErrNotFound)
	}
	dbRoleBindings, err := dao.GetDaoHub().Role.ListRoleBindings(ctx, database.DBConn.DB, role.RoleBindingFilter{
		ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeOrg},
		ScopeIDs:   []entity.ID{entity.ID(orgID)},
		TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
		TargetIDs:  []entity.ID{user.ID},
	})
	if err != nil {
		tilog.Error(ctx, "GetUserAndRoleBinding, ListRoleBindings error", zap.Any("userID", userID), zap.Any("orgID", orgID), zap.Any("err", err))
		return nil, nil, nil, err
	}
	if len(dbRoleBindings) == 0 {
		return nil, nil, nil, fmt.Errorf("%w: org user not found", spec.ErrNotFound)
	}
	orgBinding := dbRoleBindings[0]

	projects, err := dao.GetDaoHub().Project.ListActiveProjectsByOrgID(ctx, database.DBConn.DB, orgID)
	if err != nil {
		tilog.Error(ctx, "GetUserAndRoleBinding, ListActiveProjectsByOrgID error", zap.Any("orgID", orgID), zap.Any("err", err))
		return nil, nil, nil, err
	}
	var dbProjectBindings []entity.RoleBinding
	if len(projects) > 0 {
		var projectIDs []entity.ID
		for _, p := range projects {
			projectIDs = append(projectIDs, p.ID)
		}
		dbProjectBindings, err = dao.GetDaoHub().Role.ListRoleBindings(ctx, database.DBConn.DB, role.RoleBindingFilter{
			ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeProject},
			ScopeIDs:   projectIDs,
			TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
			TargetIDs:  []entity.ID{user.ID},
		})
		if err != nil {
			tilog.Error(ctx, "GetUserAndRoleBinding, ListRoleBindings error", zap.Any("userID", userID), zap.Any("projectIDs", projectIDs), zap.Any("err", err))
			return nil, nil, nil, err
		}
	}

	return user, &orgBinding, dbProjectBindings, nil
}

func (s *ScimCommonSvc) GetUser(ctx context.Context, email string, userID uint64) (*entity.User, error) {
	if email == "" && userID == 0 {
		return nil, nil
	}
	var user *entity.User
	var err error
	if email != "" {
		user, err = dao.GetDaoHub().User.GetUserByEmail(ctx, database.DBConn.DB, email)
	} else {
		user, err = dao.GetDaoHub().User.GetUserByID(ctx, database.DBConn.DB, userID)
	}
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, nil
		}
		return nil, err
	}
	return user, nil
}

func (s *ScimCommonSvc) GetOrgUsers(ctx context.Context, orgID, userID uint64, pagination *crud.Pagination) (int, []roleBindingUser, error) {
	var rbUsers []roleBindingUser
	var total = 0
	roleBindingFilter := role.RoleBindingFilter{
		ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeOrg},
		ScopeIDs:   []entity.ID{entity.ID(orgID)},
		TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
	}
	if userID != 0 {
		roleBindingFilter.TargetIDs = []entity.ID{entity.ID(userID)}
	}
	dbRoleBindings, err := dao.GetDaoHub().Role.ListRoleBindings(ctx, database.DBConn.DB, roleBindingFilter)
	if err != nil {
		return total, rbUsers, err
	}
	if len(dbRoleBindings) == 0 {
		return total, rbUsers, nil
	}
	total = len(dbRoleBindings)
	// sort by created_at, id asc
	sort.SliceStable(dbRoleBindings, func(i, j int) bool {
		ri := dbRoleBindings[i]
		rj := dbRoleBindings[j]

		if ri.CreatedAt.Before(rj.CreatedAt) {
			return true
		} else if ri.CreatedAt.After(rj.CreatedAt) {
			return false
		} else {
			return dbRoleBindings[i].ID.Uint64() > dbRoleBindings[j].ID.Uint64()
		}
	})
	pagedRoleBindings := dbRoleBindings
	if pagination != nil {
		lb := pagination.StartIndex - 1
		if lb < 0 {
			lb = 0
		}
		ub := lb + pagination.Count
		if ub > len(dbRoleBindings) {
			ub = len(dbRoleBindings)
		}
		if lb > ub {
			lb = ub
		}
		pagedRoleBindings = dbRoleBindings[lb:ub]
	}
	if len(pagedRoleBindings) == 0 {
		return total, rbUsers, nil
	}

	var userIds []uint64
	for _, rb := range pagedRoleBindings {
		userIds = append(userIds, rb.TargetID.Uint64())
	}
	users, err := dao.GetDaoHub().User.ListUsers(ctx, database.DBConn.DB, userIds, []entity.UserStatus{})
	if err != nil {
		return total, rbUsers, err
	}

	userCache := map[uint64]entity.User{}
	for _, u := range users {
		userCache[u.ID.Uint64()] = u
	}

	for _, rb := range pagedRoleBindings {
		u, ok := userCache[rb.TargetID.Uint64()]
		if !ok {
			continue
		}
		rbu := genRoleBindingUser(&u, &rb)
		rbUsers = append(rbUsers, *rbu)
	}

	return total, rbUsers, nil
}

func (s *ScimCommonSvc) PersistUser(ctx context.Context, rbUser *roleBindingUser) (*entity.User, error) {
	dbUser, err := s.GetUser(ctx, rbUser.Email, rbUser.UserID)
	if err != nil {
		tilog.Error(ctx, "get dbUser error", zap.Any("rbUser", rbUser), zap.Any("err", err))
		return nil, err
	}
	if dbUser != nil {
		if dbUser.Status == entity.UserStatusActive {
			tilog.Info(ctx, "dbUser already exist and status is active, ignore to persist user", zap.Any("user", rbUser.Email))
		} else {
			tilog.Info(ctx, "dbUser already exist, to update dbUser status to active", zap.Any("user", rbUser.Email))
			dbUser.Status = entity.UserStatusActive
			err := dbUser.Update(database.DBConn.DB,
				entity.UserDBSchema.Status,
			)
			if err != nil {
				return nil, err
			}
		}
		return dbUser, nil
	}

	tilog.Info(ctx, "dbUser not exist, to create dbUser", zap.Any("user", rbUser.Email))
	toInsertUser := &entity.User{
		Email:     rbUser.Email,
		FirstName: rbUser.FirstName,
		LastName:  rbUser.LastName,
		Status:    entity.UserStatusActive,
		TenantID:  entity.ID(rbUser.OrgBinding.ScopeID),
		Role:      entity.UserRoleMember, // no use, just set
	}
	err = (toInsertUser).Create(database.DBConn.DB)
	if err != nil {
		return nil, err
	}
	return toInsertUser, nil
}

func (s *ScimCommonSvc) PersistRoleBindings(ctx context.Context, rbUser *roleBindingUser) error {
	// persist org binding
	if rbUser.OrgBinding.ScopeID != 0 {
		dbOrgBindings, err := dao.GetDaoHub().Role.ListRoleBindings(ctx, database.DBConn.DB, role.RoleBindingFilter{
			TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
			TargetIDs:  []entity.ID{entity.ID(rbUser.UserID)},
			ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeOrg},
			ScopeIDs:   []entity.ID{entity.ID(rbUser.OrgBinding.ScopeID)},
		})
		if err != nil {
			tilog.Error(ctx, "PersistRoleBindings get dbOrgBinding error", zap.Any("userID", rbUser.UserID), zap.Any("orgID", rbUser.OrgBinding.ScopeID), zap.Any("err", err))
			return err
		}
		if len(dbOrgBindings) > 0 {
			tilog.Info(ctx, "PersistRoleBindings dbOrgBinding already exist, to update dbOrgBindings status to active", zap.Any("userID", rbUser.UserID), zap.Any("orgID", rbUser.OrgBinding.ScopeID))
			orgBinding := dbOrgBindings[0]
			if rbUser.OrgBinding.Role != "" {
				updateBinding, err := s.UpdateRoleBindingRole(ctx, orgBinding, rbUser.OrgBinding.Role, rbUser.Email, rbUser.OrgBinding.ScopeID)
				if err != nil {
					tilog.Error(ctx, "PersistRoleBindings UpdateRoleBindingRole error", zap.Any("userID", rbUser.UserID), zap.Any("orgID", rbUser.OrgBinding.ScopeID), zap.Any("err", err))
					return err
				}
				orgBinding = *updateBinding
			}
			if rbUser.OrgBinding.RoleBindingStatus != "" {
				orgBinding.Status = rbUser.OrgBinding.RoleBindingStatus
				err = dao.GetDaoHub().Role.UpdateRoleOrStatus(ctx, database.DBConn.DB, orgBinding)
				if err != nil {
					return err
				}
				if rbUser.OrgBinding.RoleBindingStatus == entity.RoleBindingStatusActive && orgBinding.Role == entity.RoleOrgOwner {
					s.CreateDbAccount4User(ctx, rbUser.UserID, rbUser.Email, rbUser.OrgBinding.ScopeID, 0)
				}
			}

		} else {
			tilog.Info(ctx, "PersistRoleBindings dbOrgBinding not exist, to create dbOrgBinding", zap.Any("userID", rbUser.UserID), zap.Any("orgID", rbUser.OrgBinding.ScopeID))
			assignOrgRole := entity.RoleOrgMember
			if rbUser.OrgBinding.Role != "" {
				assignOrgRole = rbUser.OrgBinding.Role
			}
			assignStatus := entity.RoleBindingStatusActive
			if rbUser.OrgBinding.RoleBindingStatus != "" {
				assignStatus = rbUser.OrgBinding.RoleBindingStatus
			}
			_, err = dao.GetDaoHub().Role.CreateRole(ctx, database.DBConn.DB,
				entity.RoleBindingTargetUser, rbUser.UserID, entity.RoleBindingScopeOrg, rbUser.OrgBinding.ScopeID, assignOrgRole, assignStatus)
			if err != nil {
				return err
			}
			if assignOrgRole == entity.RoleOrgOwner {
				s.CreateDbAccount4User(ctx, rbUser.UserID, rbUser.Email, rbUser.OrgBinding.ScopeID, 0)
			}
		}
	}

	// persist project binding
	if len(rbUser.ProjectBindings) > 0 {
		projects, err := dao.GetDaoHub().Project.ListActiveProjectsByOrgID(ctx, database.DBConn.DB, rbUser.OrgID)
		if err != nil {
			tilog.Error(ctx, "PersistRoleBindings ListActiveProjectsByOrgID error", zap.Any("orgID", rbUser.OrgID), zap.Any("err", err))
			return err
		}
		validOrgProjectIds := map[uint64]struct{}{}
		for _, p := range projects {
			validOrgProjectIds[p.ID.Uint64()] = struct{}{}
		}

		var projectIds []entity.ID
		for _, pb := range rbUser.ProjectBindings {
			if _, ok := validOrgProjectIds[pb.ScopeID]; !ok {
				tilog.Warn(ctx, "PersistRoleBindings project id not belong to org, ignore", zap.Any("projectId", pb.ScopeID), zap.Any("orgId", rbUser.OrgID))
				continue
			}
			projectIds = append(projectIds, entity.ID(pb.ScopeID))
		}
		dbProjectBindings, err := dao.GetDaoHub().Role.ListRoleBindings(ctx, database.DBConn.DB, role.RoleBindingFilter{
			TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
			TargetIDs:  []entity.ID{entity.ID(rbUser.UserID)},
			ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeProject},
			ScopeIDs:   projectIds,
		})
		if err != nil {
			tilog.Error(ctx, "PersistRoleBindings get dbProjectBindings error", zap.Any("userID", rbUser.UserID), zap.Any("projectIds", projectIds), zap.Any("err", err))
			return err
		}
		var dbProjectBindingMap = map[uint64]entity.RoleBinding{}
		for _, binding := range dbProjectBindings {
			dbProjectBindingMap[binding.ScopeID.Uint64()] = binding
		}
		for _, pBinding := range rbUser.ProjectBindings {
			// check if project id valid
			if _, ok := validOrgProjectIds[pBinding.ScopeID]; !ok {
				tilog.Warn(ctx, "PersistRoleBindings project id not belong to org, ignore", zap.Any("projectId", pBinding.ScopeID), zap.Any("orgId", rbUser.OrgBinding.ScopeID))
				continue
			}
			dbBinding, ok := dbProjectBindingMap[pBinding.ScopeID]
			if !ok {
				tilog.Info(ctx, "PersistRoleBindings dbProjectBinding not exist, to create dbProjectBinding", zap.Any("userID", rbUser.UserID), zap.Any("projectID", pBinding.ScopeID))
				assignRole := entity.RoleProjectReadOnly
				if pBinding.Role != "" {
					assignRole = pBinding.Role
				}
				assignStatus := entity.RoleBindingStatusActive
				if pBinding.RoleBindingStatus != "" {
					assignStatus = pBinding.RoleBindingStatus
				}
				_, err = dao.GetDaoHub().Role.CreateRole(ctx, database.DBConn.DB,
					entity.RoleBindingTargetUser, rbUser.UserID, entity.RoleBindingScopeProject, pBinding.ScopeID, assignRole, assignStatus)
				if err != nil {
					return err
				}
				s.CreateDbAccount4User(ctx, rbUser.UserID, rbUser.Email, rbUser.OrgID, pBinding.ScopeID)
			} else {
				if pBinding.Role != "" {
					updateBinding, err := s.UpdateRoleBindingRole(ctx, dbBinding, pBinding.Role, rbUser.Email, rbUser.OrgID)
					if err != nil {
						tilog.Error(ctx, "PersistRoleBindings UpdateRoleBindingRole error", zap.Any("userID", rbUser.UserID), zap.Any("orgID", rbUser.OrgID), zap.Any("err", err))
						return err
					}
					dbBinding = *updateBinding
				}
				tilog.Info(ctx, "PersistRoleBindings dbProjectBinding already exist, to update dbProjectBinding to active", zap.Any("userID", rbUser.UserID), zap.Any("projectID", pBinding.ScopeID))
				if pBinding.RoleBindingStatus != "" {
					dbBinding.Status = pBinding.RoleBindingStatus
					err = dao.GetDaoHub().Role.UpdateRoleOrStatus(ctx, database.DBConn.DB, dbBinding)
					if err != nil {
						return err
					}
					if pBinding.RoleBindingStatus == entity.RoleBindingStatusActive {
						s.CreateDbAccount4User(ctx, rbUser.UserID, rbUser.Email, rbUser.OrgID, pBinding.ScopeID)
					}
				}
			}
		}
	}

	return nil
}

func (s *ScimCommonSvc) DeleteRoleBindings(ctx context.Context, user *entity.User, orgBinding *entity.RoleBinding, projectBindings []entity.RoleBinding) error {
	if orgBinding != nil {
		_, err := dao.GetDaoHub().Role.DeleteRoleByTargetAndScope(ctx, database.DBConn.DB, entity.RoleBindingTargetUser, user.ID.Uint64(), entity.RoleBindingScopeOrg, []uint64{orgBinding.ScopeID.Uint64()})
		if err != nil {
			tilog.Error(ctx, "DeleteOrgRoleBindings error", zap.Any("orgBinding", orgBinding), zap.Any("err", err))
			return err
		}
	}
	if len(projectBindings) > 0 {
		var projectIds []uint64
		for _, pb := range projectBindings {
			projectIds = append(projectIds, pb.ScopeID.Uint64())
		}
		_, err := dao.GetDaoHub().Role.DeleteRoleByTargetAndScope(ctx, database.DBConn.DB, entity.RoleBindingTargetUser, user.ID.Uint64(), entity.RoleBindingScopeProject, projectIds)
		if err != nil {
			tilog.Error(ctx, "DeleteProjectRoleBindings error", zap.Any("userID", user.ID.Uint64()), zap.Any("projectIds", projectIds), zap.Any("err", err))
			return err
		}
	}

	return nil
}

func (s *ScimCommonSvc) UpdateRoleBindingStatus(ctx context.Context, user *entity.User, orgBinding *entity.RoleBinding, projectBindings []entity.RoleBinding, roleBindingStatus string) error {
	if orgBinding != nil {
		orgBinding.Status = roleBindingStatus
		err := dao.GetDaoHub().Role.UpdateRoleOrStatus(ctx, database.DBConn.DB, *orgBinding)
		if err != nil {
			tilog.Error(ctx, "updateRoleBindingStatus error", zap.Any("orgBinding", orgBinding), zap.Any("err", err))
			return err
		}
		if roleBindingStatus == entity.RoleBindingStatusActive && orgBinding.Role == entity.RoleOrgOwner {
			s.CreateDbAccount4User(ctx, user.ID.Uint64(), user.Email, orgBinding.ScopeID.Uint64(), 0)
		}
		tilog.Info(ctx, "updateOrgRoleBindingStatus success", zap.Any("orgBinding", orgBinding))
	}

	if len(projectBindings) > 0 {
		var projectIDs []entity.ID
		for _, pb := range projectBindings {
			projectIDs = append(projectIDs, pb.ScopeID)
		}
		projectBindingFilter := role.RoleBindingFilter{
			TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
			TargetIDs:  []entity.ID{user.ID},
			ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeProject},
			ScopeIDs:   projectIDs,
		}
		updateParamMap := map[string]interface{}{
			"status": roleBindingStatus,
		}
		err := dao.GetDaoHub().Role.BatchUpdateRoleOrStatus(ctx, database.DBConn.DB, projectBindingFilter, updateParamMap)
		if err != nil {
			tilog.Error(ctx, "updateProjectRoleBindingStatus error", zap.Any("userID", user.ID), zap.Any("projectIDs", projectIDs), zap.Any("err", err))
			return err
		}
		if roleBindingStatus == entity.RoleBindingStatusActive {
			for _, pid := range projectIDs {
				s.CreateDbAccount4User(ctx, user.ID.Uint64(), user.Email, orgBinding.ScopeID.Uint64(), pid.Uint64())
			}
		}
		tilog.Info(ctx, "updateProjectRoleBindingStatus success", zap.Any("userID", user.ID), zap.Any("projectIDs", projectIDs))
	}

	return nil
}

func (s *ScimCommonSvc) CreateDbAccount4User(ctx context.Context, userID uint64, email string, orgId, projectId uint64) {
	err := dao.GetDaoHub().DBUser.CreateCommandUserAction(ctx, database.DBConn.DB, userID, userID, email, orgId, projectId, entity.CommandStatusWaiting, entity.CommandActionCreate, entity.RoleBindingTargetUser, entity.RoleBindingTargetUser)
	if err != nil {
		tilog.Error(ctx, "create scim user CreateCommandUserAction error", zap.Any("user", email), zap.Any("orgId", orgId), zap.Any("projectId", projectId), zap.Any("err", err))
		return
	}
	tilog.Info(ctx, "create scim user CreateCommandUserAction success", zap.Any("user", email), zap.Any("orgId", orgId), zap.Any("projectId", projectId))

}

func (s *ScimCommonSvc) UpdateRoleBindingRole(ctx context.Context, binding entity.RoleBinding, rbRole string, email string, orgID uint64) (*entity.RoleBinding, error) {
	rbacRole, err := dao.GetDaoHub().Rbac.GetRbacRoleByName(ctx, database.DBConn.DB, rbRole)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			rbacRole = nil
		} else {
			return nil, err
		}
	}
	if rbacRole == nil {
		return nil, errors.New("rbRole unknown")
	}
	binding.Role = rbacRole.Name
	binding.RoleID = rbacRole.ID
	err = dao.GetDaoHub().Role.UpdateRoleOrStatus(ctx, database.DBConn.DB, binding)
	if err != nil {
		tilog.Error(ctx, "UpdateRoleBindingRole error", zap.Any("binding", binding), zap.Any("rbRole", rbRole), zap.Any("err", err))
		return nil, err
	}
	if binding.Status == entity.RoleBindingStatusActive {
		if binding.ScopeType == entity.RoleBindingScopeProject {
			_ = dao.GetDaoHub().DBUser.CreateCommandUserAction(ctx, database.DBConn.DB, binding.TargetID.Uint64(), binding.TargetID.Uint64(), email, orgID, binding.ScopeID.Uint64(), entity.CommandStatusWaiting, entity.CommandActionDelete, entity.RoleBindingTargetUser, entity.RoleBindingTargetUser)
			_ = dao.GetDaoHub().DBUser.CreateCommandUserAction(ctx, database.DBConn.DB, binding.TargetID.Uint64(), binding.TargetID.Uint64(), email, orgID, binding.ScopeID.Uint64(), entity.CommandStatusWaiting, entity.CommandActionCreate, entity.RoleBindingTargetUser, entity.RoleBindingTargetUser)

		} else if binding.ScopeType == entity.RoleBindingScopeOrg {
			if rbRole == entity.RoleOrgOwner {
				_ = dao.GetDaoHub().DBUser.CreateCommandUserAction(ctx, database.DBConn.DB, binding.TargetID.Uint64(), binding.TargetID.Uint64(), email, binding.ScopeID.Uint64(), 0, entity.CommandStatusWaiting, entity.CommandActionCreate, entity.RoleBindingTargetUser, entity.RoleBindingTargetUser)
			}
		}
	}
	return &binding, nil
}
