package user

import (
	"context"
	"errors"
	"strings"
	"sync"
	"time"

	accounterr "github.com/tidbcloud/account/commons/errors"

	"github.com/tidbcloud/account/central/utils"
	"github.com/tidbcloud/account/commons/database"
	"github.com/tidbcloud/account/internel/controllers/acl/access/transformer"
	"github.com/tidbcloud/account/internel/dto"

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

	"github.com/form3tech-oss/jwt-go"
	"github.com/tidbcloud/account/commons/config"

	"go.uber.org/zap"

	"github.com/tidbcloud/tiinf/monitor/tilog"

	userDao "github.com/tidbcloud/account/internel/dao/user"

	"github.com/tidbcloud/tiinf/errs"
	"gorm.io/gorm"

	"github.com/tidbcloud/account/idl/pbgen/proto/acl"
	"github.com/tidbcloud/account/internel/dao"
	"github.com/tidbcloud/account/internel/dao/entity"
)

var (
	_ UserSvcIface = (*UserSvc)(nil)
)

type UserSvcIface interface {
	ListRoleBindingUsers(ctx context.Context, filter userDao.RoleBindingUserFilter) ([]userDao.RoleBindingUser, error)
	GetUserByEmail(context.Context, string) (*entity.User, error)
	GetUserByID(context.Context, uint64) (*entity.User, error)
	UpdateUserByID(ctx context.Context, user *entity.User) error
	GetRoleBindingUser(ctx context.Context, scopeType entity.ScopeType, scopeId, userId uint64) (userDao.RoleBindingUser, error)
	ListUsers(ctx context.Context, userIDs []uint64) ([]entity.User, error)
	ListUsersByFilter(ctx context.Context, filter userDao.ListUsersFilter) ([]entity.User, error)
	ListUsersByPage(ctx context.Context, pageParams dto.PageParams) (*utils.Paginator, error)
	GetSystemUserByOrgID(context.Context, uint64) (*entity.User, error)
	GetOrCreateSystemUserByOrgID(ctx context.Context, orgID uint64) (*entity.User, error)
	GetUserOrg(context.Context) (*entity.Tenant, error)
	GetUserImpersonatedOrg(ctx context.Context, userID uint64) (*entity.Tenant, bool, error)
	IsImpersonatedUserInOrg(context.Context, uint64, uint64) (*entity.ImpersonatedUser, bool, error)
	GetAuthUserByEmail(ctx context.Context, email string, orgID uint64, skipCheckOrg bool) (*AuthUserInfo, error)
	GetImpersonatedUser(ctx context.Context, userID uint64) (*entity.ImpersonatedUser, error)
	AddImpersonateUser(ctx context.Context, userID, orgID uint64) error
	DeleteImpersonateUser(ctx context.Context, userID, orgID uint64) error

	RemoveUserFromOrg(ctx context.Context, userId, operatorId, orgId uint64) error
	RemoveUserFromProject(ctx context.Context, userId, dbUserOperatorId, projectId uint64) error
	DeleteUserFromAllOrg(ctx context.Context, userId uint64) error
	AddUserToOrg(ctx context.Context, req AddUserToOrgReq) error
	AddUserToProject(ctx context.Context, req AddUserToProjectReq) error

	// User Landing Actions
	ListUserLandingActions(ctx context.Context, filter userDao.LandingActionFilter) ([]entity.UserLandingAction, error)
	CreateUserLandingAction(ctx context.Context, userLandingAction *entity.UserLandingAction) error
	GetNewUserDefaultCluster(ctx context.Context, userId *uint64, email *string) (*entity.UserLandingAction, error)
	SaveFirstAction(ctx context.Context, queryEntity entity.UserLandingAction, userLandingAction *entity.UserLandingAction) error
	IsNewUser(ctx context.Context, userId *uint64) (bool, error)
	IsExist(ctx context.Context, filter userDao.LandingActionFilter) (*bool, error)

	// InviteToken
	GenUserInviteToken(ctx context.Context, inviteUserToken entity.UserInviteToken, period time.Duration) (*userDao.CreateInviteTokenResp, error)
	NewUserInviteToken(ctx context.Context, inviteUserToken entity.UserInviteToken, period time.Duration) (*userDao.CreateInviteTokenResp, error)
	ValidateUserInviteToken(ctx context.Context, token string, isDelete bool) (*entity.UserInviteToken, error)
	ValidateUserInviteTokenIgnoreEmail(ctx context.Context, token string, isDelete bool) (*entity.UserInviteToken, error)
	DeleteUserInviteToken(ctx context.Context, token string) error

	CreateInviteToken(ctx context.Context, orgId uint64, email string, method string) (*userDao.CreateInviteTokenResp, error)

	GetUserRegistrationLog(ctx context.Context, param userDao.RegistrationLogParam) (*entity.RegistrationLog, error)
}

var (
	userSvcInitOnce sync.Once
	userSvcInstance UserSvcIface
)

func GetUserSvc() (UserSvcIface, error) {
	userSvcInitOnce.Do(func() {
		userSvcInstance = NewUserSvc()

	})
	return userSvcInstance, nil
}

type UserSvc struct {
}

func NewUserSvc() UserSvcIface {
	return &UserSvc{}
}

func (s *UserSvc) GetUserOrg(ctx context.Context) (*entity.Tenant, error) {
	user, err := GetUserFromContext(ctx)
	if err != nil {
		return nil, err
	}

	orgID := user.TenantID.Uint64()

	return dao.GetDaoHub().Org.GetOrgByOrgID(ctx, database.DBConn.DB.WithContext(ctx), orgID)
}

func (s *UserSvc) GetUserByEmail(ctx context.Context, email string) (*entity.User, error) {
	return dao.GetDaoHub().User.GetUserByEmail(ctx, database.DBConn.DB.WithContext(ctx), email)
}

func (s *UserSvc) GetUserByID(ctx context.Context, userID uint64) (*entity.User, error) {
	return dao.GetDaoHub().User.GetUserByID(ctx, database.DBConn.DB.WithContext(ctx), userID)
}

func (s *UserSvc) UpdateUserByID(ctx context.Context, user *entity.User) error {
	return dao.GetDaoHub().User.UpdateUserByID(ctx, database.DBConn.DB.WithContext(ctx), user)
}

func (s *UserSvc) GetSystemUserByOrgID(ctx context.Context, orgID uint64) (*entity.User, error) {
	return dao.GetDaoHub().User.GetSystemUserByOrgID(ctx, database.DBConn.DB.WithContext(ctx), orgID)
}

func (s *UserSvc) GetOrCreateSystemUserByOrgID(ctx context.Context, orgID uint64) (*entity.User, error) {
	var user *entity.User
	err := database.DBConn.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var e error

		user, e = dao.GetDaoHub().Cross.GetOrCreateSystemUserByOrgID(ctx, tx, orgID)
		if e != nil {
			return e
		}

		_, e = dao.GetDaoHub().Role.AddOrgRoleForUser(ctx, tx, user.ID.Uint64(), orgID, entity.RoleOrgMember)
		if e != nil {
			return e
		}

		return nil
	})

	if err != nil {
		return nil, err
	}

	return user, nil
}

func (s *UserSvc) IsImpersonatedUserInOrg(ctx context.Context, userID uint64, orgID uint64) (*entity.ImpersonatedUser, bool, error) {
	return dao.GetDaoHub().User.IsImpersonatedUserInOrg(ctx, database.DBConn.DB.WithContext(ctx), userID, orgID)
}

func (s *UserSvc) GetImpersonatedUser(ctx context.Context, userID uint64) (*entity.ImpersonatedUser, error) {
	impersonatedUser, _, err := dao.GetDaoHub().User.GetImpersonatedUser(ctx, database.DBConn.DB.WithContext(ctx), userID)
	return impersonatedUser, err
}

func (s *UserSvc) GetUserImpersonatedOrg(ctx context.Context, userID uint64) (*entity.Tenant, bool, error) {
	impersonatedUser, _, err := dao.GetDaoHub().User.GetImpersonatedUser(ctx, database.DBConn.DB.WithContext(ctx), userID)
	var org *entity.Tenant
	if impersonatedUser != nil && impersonatedUser.TenantID != 0 {
		tilog.Info(ctx, "user has impersonate org", zap.Any("impersonatedOrg", impersonatedUser.TenantID))
		org, err = dao.GetDaoHub().Org.GetOrgByOrgID(ctx, database.DBConn.DB.WithContext(ctx), impersonatedUser.TenantID.Uint64())
		if err != nil {
			return nil, true, err
		}
	}
	return org, false, err
}

func (s *UserSvc) AddImpersonateUser(ctx context.Context, userID, orgID uint64) error {
	return dao.GetDaoHub().User.AddImpersonateUser(ctx, database.DBConn.DB.WithContext(ctx), userID, orgID)
}

func (s *UserSvc) DeleteImpersonateUser(ctx context.Context, userID, orgID uint64) error {
	return dao.GetDaoHub().User.DeleteImpersonateUser(ctx, database.DBConn.DB.WithContext(ctx), userID, orgID)
}

type AuthUserInfo struct {
	User              *entity.User
	Org               *entity.Tenant
	IsImpersonated    bool
	AvailableProjects []entity.Project
	OrgRole           acl.OrgRole
}

func IsSystemUser(ctx context.Context, email string) bool {
	return strings.HasSuffix(email, "-system@pingcap.com")
}

func (s *UserSvc) GetAuthUserByEmail(ctx context.Context, email string, orgID uint64, skipCheckOrg bool) (*AuthUserInfo, error) {
	user, err := dao.GetDaoHub().User.GetUserByEmail(ctx, database.DBConn.DB.WithContext(ctx), email)
	if err != nil {
		return nil, errs.NewNotFound("failed to get user by email", err)
	}
	impersonatedUser, isImpersonated, err := dao.GetDaoHub().User.GetImpersonatedUser(ctx, database.DBConn.DB.WithContext(ctx), user.ID.Uint64())
	if err != nil {
		return nil, errs.NewInternalDB("failed to get impersonated user", err)
	}
	var orgRole acl.OrgRole
	if isImpersonated {
		orgID = impersonatedUser.TenantID.Uint64()
		orgRole = acl.OrgRole_ORG_ROLE_OWNER
	} else {
		if skipCheckOrg {
			_, roleBinding, err := s.selectUserRelateOrg(ctx, user.ID.Uint64())
			if err != nil {
				return nil, err
			}
			orgID = roleBinding.ScopeID.Uint64()
			orgRole = transformer.OrgRoleMap[entity.RoleBindingDecode(roleBinding.Role)]
		} else {
			roleBindings, e := dao.GetDaoHub().Role.ListRolesByTargetAndScope(ctx, database.DBConn.DB.WithContext(ctx), entity.RoleBindingTargetUser, user.ID.Uint64(), entity.RoleBindingScopeOrg, orgID, []string{entity.RoleBindingStatusActive})
			if e != nil {
				return nil, errs.NewInternalDB("failed to check whether user belong to org", e)
			}
			if len(roleBindings) == 0 {
				return nil, accounterr.NewErrUserNotInOrg("user not belong to org", nil)
			}
			rbacRole := roleBindings[0].Role
			userRole := entity.RoleBindingDecode(rbacRole)
			orgRole = transformer.OrgRoleMap[userRole]
		}
	}

	org, err := dao.GetDaoHub().Org.GetOrgByOrgID(ctx, database.DBConn.DB.WithContext(ctx), orgID)
	if err != nil {
		return nil, errs.NewInternalDB("failed to get org by org_id", err)
	}

	listAllProjects := false
	if IsSystemUser(ctx, user.Email) || orgRole == acl.OrgRole_ORG_ROLE_OWNER {
		listAllProjects = true
	}
	availableProjects, err := dao.GetDaoHub().Cross.ListActiveProjects(ctx, database.DBConn.DB.WithContext(ctx), user.ID.Uint64(), orgID, listAllProjects)
	if err != nil {
		return nil, errs.NewInternalDB("failed to list active projects", err)
	}

	result := &AuthUserInfo{
		User:              user,
		Org:               org,
		IsImpersonated:    isImpersonated,
		AvailableProjects: availableProjects,
		OrgRole:           orgRole,
	}

	return result, nil
}

func (s *UserSvc) selectUserRelateOrg(ctx context.Context, userID uint64) (*entity.Tenant, *entity.RoleBinding, error) {
	roleBindings, err := dao.GetDaoHub().Role.ListRolesByTargetOnScopeList(ctx, database.DBConn.DB.WithContext(ctx), entity.RoleBindingTargetUser, userID, entity.RoleBindingScopeOrg, []uint64{}, []string{entity.RoleBindingStatusActive})
	if err != nil {
		return nil, nil, errs.NewInternalDB("failed to get user orgs", err)
	}
	var orgIDs []uint64
	for _, rb := range roleBindings {
		orgIDs = append(orgIDs, rb.ScopeID.Uint64())
	}
	if len(orgIDs) == 0 {
		return nil, nil, accounterr.NewErrUserNotInOrg("user does not belong to any org", nil)
	}
	resp, err := dao.GetDaoHub().Org.ListOrgs(ctx, database.DBConn.DB.WithContext(ctx), org.ListOrgsParam{
		Filter: org.ListOrgsFilter{
			ID: orgIDs,
		},
	})
	tilog.Info(ctx, "selectUserRelateOrg ListOrgs result ", zap.Any("resp", resp))
	if err != nil {
		return nil, nil, errs.NewInternalDB("failed to get user orgs", err)
	}
	if len(resp.List) == 0 {
		return nil, nil, accounterr.NewErrUserNotInOrg("user does not belong to any org", nil)
	}
	org := resp.List[0]
	// Return active org if possible
	for _, o := range resp.List {
		if o.Status == entity.TenantStatusActive {
			org = o
			break
		}
	}

	var roleBinding entity.RoleBinding
	for _, rb := range roleBindings {
		if rb.ScopeID == org.ID {
			roleBinding = rb
			break
		}
	}

	return &org, &roleBinding, nil
}

func (s *UserSvc) ListUsers(ctx context.Context, userIDs []uint64) ([]entity.User, error) {
	users, err := dao.GetDaoHub().User.ListUsers(ctx, database.DBConn.DB.WithContext(ctx), userIDs, nil)
	if err != nil {
		return nil, errs.WrapGORMError(err, "failed to list users")
	}

	return users, nil
}
func (s *UserSvc) ListUsersByPage(ctx context.Context, pageParams dto.PageParams) (*utils.Paginator, error) {

	return dao.GetDaoHub().User.ListUsersByPage(ctx, database.DBConn.DB.WithContext(ctx), pageParams)
}

func (s *UserSvc) ListUsersByFilter(ctx context.Context, filter userDao.ListUsersFilter) ([]entity.User, error) {
	userIDs, err := dao.GetDaoHub().User.ListUsersFromRoleBinding(ctx, database.DBConn.DB.WithContext(ctx), filter)
	if err != nil {
		return nil, errs.WrapGORMError(err, "failed to list users ids")
	}
	if len(userIDs) == 0 {
		return nil, nil
	}

	return dao.GetDaoHub().User.ListUsers(ctx, database.DBConn.DB.WithContext(ctx), userIDs, filter.UserStatuses)
}

func (s *UserSvc) Update(ctx context.Context, filter userDao.ListUsersFilter) ([]entity.User, error) {

	return dao.GetDaoHub().User.ListUsers(ctx, database.DBConn.DB.WithContext(ctx), []uint64{2}, filter.UserStatuses)
}
func (s *UserSvc) ListRoleBindingUsers(ctx context.Context, filter userDao.RoleBindingUserFilter) ([]userDao.RoleBindingUser, error) {
	tilog.Info(ctx, "ListRoleBindingUsers filter ", zap.Any("filter", filter))

	return dao.GetDaoHub().User.ListRoleBindingUsers(ctx, database.DBConn.DB.WithContext(ctx), filter)

}

/*
func (s *UserSvc) AutoInvite(ctx context.Context, req *central.AutoInviteReq) error {

		email, err := s.SessionTokenVerify(ctx, req.SessionToken)
		if config.IsLocalEnv() {
			email = "wlt2016@gmail.com"
		}

		if err != nil {
			return errs.NewBadRequest("the invitation url is invalid", nil)
		}
		inviteToken, err := singleton.GetSession().ValidateUserInviteToken(ctx, req.InviteToken, false)

		if inviteToken == nil || inviteToken.Email != email {
			return errs.NewBadRequest("the invitation url is invalid", nil)
		}
		user, _ := s.hub.User.GetUserByEmail(ctx, s.db, email)

		binding := entity.RoleBinding{
			TargetType: entity.RoleBindingTargetUser,
			TargetID:   user.ID,
			ScopeType:  entity.RoleBindingScopeOrg,
			ScopeID:    inviteToken.TenantID,
			Status:     entity.RoleBindingStatusActive,
		}

		// delete dbuser
		if user.Status == entity.UserStatusActive {
			if err = s.hub.Role.UpdateRoleOrStatus(ctx, s.db, binding); err != nil {
				return err
			}
			if err = s.hub.DBUser.CreateCommandUserAction(ctx, s.db, inviteToken.Operator.Uint64(), user.ID.Uint64(), user.Email, inviteToken.TenantID.Uint64(), 0, entity.CommandStatusWaiting, entity.CommandActionCreate, entity.RoleBindingTargetUser); err != nil {
				tilog.Error(ctx, "create delete dbuser command  fail ", zap.Any("operator", inviteToken.TenantID), zap.Any("userId", user.ID), zap.Any("email", user.Email), zap.Error(err))

			}
		}

		return err
	}
*/
func (s *UserSvc) SessionTokenVerify(ctx context.Context, sessionToken string) (string, error) {
	claims, err := ParseFromActionJwt(ctx, sessionToken)
	if err != nil {
		tilog.Info(ctx, "SessionTokenVerify ParseFromActionJwt error", zap.Any("err", err))
		return "", err
	}
	tilog.Info(ctx, "SessionTokenVerify ParseFromActionJwt get claims", zap.Any("claims", claims))

	return claims.Email, nil
}
func ParseFromActionJwt(ctx context.Context, token string) (claim *FromActionClaims, err error) {
	key := config.Global().SSOConfig.ActionKey
	tokenClaims, _ := jwt.ParseWithClaims(token, &FromActionClaims{}, func(token *jwt.Token) (interface{}, error) {
		return []byte(key), nil
	})
	if tokenClaims == nil {
		return nil, errors.New("action_claims_is_nil")
	}
	if !tokenClaims.Valid {
		tilog.Info(ctx, "ParseFromActionJwt parse claims result: invalid", zap.Any("token", token))
	} else {
		tilog.Info(ctx, "ParseFromActionJwt parse claims result: valid", zap.Any("token", token))
	}
	claims, ok := tokenClaims.Claims.(*FromActionClaims)
	if !ok {
		return nil, errors.New("invalid_action_claims_type")
	}
	return claims, nil
}

type FromActionClaims struct {
	jwt.StandardClaims
	Email string `json:"email"`
}
