package user

import (
	"context"
	"fmt"
	"time"

	"github.com/tidbcloud/account/central/utils"

	"go.uber.org/zap"

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

	"github.com/pingcap/errors"
	"github.com/tidbcloud/account/commons/config"
	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/account/internel/dto"
	"github.com/tidbcloud/tiinf/errs"
	"gorm.io/gorm"
)

var _ UserDaoIface = (*UserDao)(nil)

const (
	SortByEmail      = "email"
	SortByInviteTime = "invite_time"
	sortByRole       = "role"
	sortByStatus     = "status"
)

type ListUsersFilter struct {
	ScopeTypes   []entity.ScopeType
	ScopeIDs     []uint64
	TargetType   []entity.TargetType
	TargetIDs    []uint64
	UserStatuses []entity.UserStatus
	Roles        []string
	RoleIDs      []uint64
}

// query from rolebindings left join  user
type RoleBindingUserFilter struct {
	EmailLike         string
	ScopeType         entity.ScopeType
	ScopeId           uint64
	UserId            uint64
	RoleBindingStatus []string
	UserStatus        []string
	Order             string
	SortBy            string
	Roles             []string
}

type UserDaoIface interface {
	// User
	GetUserByEmail(ctx context.Context, tx *gorm.DB, email string) (*entity.User, error)
	GetUserByEmailIn(ctx context.Context, tx *gorm.DB, emails []string) ([]entity.User, error)
	GetUserByID(ctx context.Context, tx *gorm.DB, id uint64) (*entity.User, error)
	UpdateUserByID(ctx context.Context, tx *gorm.DB, user *entity.User) error
	CreateUser(ctx context.Context, tx *gorm.DB, user *entity.User) (*entity.User, error)
	ListUsers(ctx context.Context, tx *gorm.DB, userIDs []uint64, statuses []entity.UserStatus) ([]entity.User, error)
	ListUsersFromRoleBinding(ctx context.Context, tx *gorm.DB, filter ListUsersFilter) ([]uint64, error)
	ListUsersByPage(ctx context.Context, tx *gorm.DB, pageParams dto.PageParams) (*utils.Paginator, error)
	//
	// ListRoleBindingUsers
	//  @Description: select * from  rolebindings left join users  where  xxxxxx
	//  @param ctx
	//  @param tx
	//  @param filter
	//  @return []RoleBindingUser
	//  @return error
	//
	ListRoleBindingUsers(ctx context.Context, tx *gorm.DB, filter RoleBindingUserFilter) ([]RoleBindingUser, error)

	// System user
	GetSystemUserByOrgID(ctx context.Context, tx *gorm.DB, orgID uint64) (*entity.User, error)

	// Impersonated user
	IsImpersonatedUserInOrg(ctx context.Context, tx *gorm.DB, userID uint64, orgID uint64) (*entity.ImpersonatedUser, bool, error)
	GetImpersonatedUser(ctx context.Context, tx *gorm.DB, userID uint64) (*entity.ImpersonatedUser, bool, error)
	AddImpersonateUser(ctx context.Context, tx *gorm.DB, userID, orgID uint64) error
	DeleteImpersonateUser(ctx context.Context, tx *gorm.DB, userID, orgID uint64) error

	// User Landing Actions
	ListUserLandingActions(ctx context.Context, tx *gorm.DB, filter LandingActionFilter) ([]entity.UserLandingAction, error)
	CreateUserLandingAction(ctx context.Context, tx *gorm.DB, userLandingAction *entity.UserLandingAction) error

	////InviteTokens
	//FindInviteTokens(ctx context.Context, tx *gorm.DB, filter InviteTokenFilter) ([]entity.UserInviteToken, error)
	//SaveOrUpdateInviteToken(ctx context.Context, tx *gorm.DB, userInviteToken entity.UserInviteToken) (entity.UserInviteToken, error)
	//
	// GenUserInviteToken
	//  @Description: if token exist ,the update old UserInviteToken
	//  @param ctx
	//  @param tx
	//  @param inviteUserToken
	//  @param period
	//  @return *CreateInviteTokenResp
	//  @return error
	//
	GenUserInviteToken(ctx context.Context, tx *gorm.DB, inviteUserToken entity.UserInviteToken, period time.Duration) (*CreateInviteTokenResp, error)
	//
	// NewUserInviteToken
	//  @Description: create new token
	//  @param ctx
	//  @param tx
	//  @param inviteUserToken
	//  @param period
	//  @return *CreateInviteTokenResp
	//  @return error
	//
	NewUserInviteToken(ctx context.Context, tx *gorm.DB, inviteUserToken entity.UserInviteToken, period time.Duration) (*CreateInviteTokenResp, error)
	ValidateUserInviteToken(ctx context.Context, tx *gorm.DB, token string, isDelete bool) (*entity.UserInviteToken, error)
	ValidateUserInviteTokenIgnoreEmail(ctx context.Context, tx *gorm.DB, token string, isDelete bool) (*entity.UserInviteToken, error)
	DeleteUserInviteToken(ctx context.Context, tx *gorm.DB, token string) error

	// RegistrationLog
	GetUserRegistrationLog(ctx context.Context, tx *gorm.DB, param RegistrationLogParam) (*entity.RegistrationLog, error)
}

type UserDao struct {
}

func NewDao() UserDaoIface {
	return &UserDao{}
}

func (d *UserDao) GetUserByEmail(ctx context.Context, tx *gorm.DB, email string) (*entity.User, error) {
	var user entity.User
	err := entity.NewUserQuerySet(tx).EmailEq(email).One(&user)

	if err != nil {
		return nil, err
	}

	return &user, nil
}

func (d *UserDao) GetUserByEmailIn(ctx context.Context, tx *gorm.DB, emails []string) ([]entity.User, error) {
	var users []entity.User
	err := entity.NewUserQuerySet(tx).EmailIn(emails...).All(&users)

	if err != nil {
		return nil, err
	}

	return users, nil
}

func (d *UserDao) CreateUser(ctx context.Context, tx *gorm.DB, user *entity.User) (*entity.User, error) {
	err := tx.Create(user).Error
	if err != nil {
		return nil, err
	}

	return user, nil
}

func (d *UserDao) ListUsers(ctx context.Context, tx *gorm.DB, userIDs []uint64, statuses []entity.UserStatus) ([]entity.User, error) {
	var users []entity.User

	qs := entity.NewUserQuerySet(tx)

	if len(userIDs) > 0 {
		userIDList := make([]entity.ID, len(userIDs))
		for i, userID := range userIDs {
			userIDList[i] = entity.ID(userID)
		}

		qs = qs.IDIn(userIDList...)
	}

	if len(statuses) > 0 {
		qs = qs.StatusIn(statuses...)
	}

	err := qs.All(&users)

	return users, err
}
func (d *UserDao) ListUsersByPage(ctx context.Context, tx *gorm.DB, pageParams dto.PageParams) (*utils.Paginator, error) {
	tilog.Info(ctx, "ListUsersByPage", zap.Any("queryParams", pageParams))

	pagination, err := utils.Paginate(tx, pageParams, &entity.User{})

	if err != nil {
		tilog.Error(ctx, "failed to list users", zap.Error(err))
		return nil, errs.WrapGORMError(err, "failed to list users")
	}

	return pagination, nil
}

func buildRoleBindingGetter(tx *gorm.DB) *gorm.DB {
	return tx.Model(&entity.RoleBinding{}).Where("status in (?)", []string{entity.RoleBindingStatusActive})
}

func buildRoleBindingFilter(tx *gorm.DB, filter ListUsersFilter) (*gorm.DB, error) {
	if len(filter.ScopeTypes) != 0 {
		scopeTypes := make([]string, 0, len(filter.ScopeTypes))
		for _, each := range filter.ScopeTypes {
			scopeTypes = append(scopeTypes, each.String())
		}
		tx = tx.Where("scope_type in (?)", scopeTypes)
	}
	if len(filter.ScopeIDs) != 0 {
		tx = tx.Where("scope_id in (?)", filter.ScopeIDs)
	}
	if len(filter.TargetType) != 0 {
		targetTypes := make([]string, 0, len(filter.TargetType))
		for _, each := range filter.TargetType {
			targetTypes = append(targetTypes, each.String())
		}
		tx = tx.Where("target_type in (?)", targetTypes)
	}
	if len(filter.TargetIDs) != 0 {
		tx = tx.Where("target_id in (?)", filter.TargetIDs)
	}
	if len(filter.Roles) != 0 {
		tx = tx.Where("role in (?)", filter.Roles)
	}
	if len(filter.RoleIDs) != 0 {
		tx = tx.Where("role_id in (?)", filter.RoleIDs)
	}

	return tx, nil
}

func (d *UserDao) ListUsersFromRoleBinding(ctx context.Context, tx *gorm.DB, filter ListUsersFilter) ([]uint64, error) {
	var roleBindings []*entity.RoleBinding
	tx, err := buildRoleBindingFilter(buildRoleBindingGetter(tx), filter)
	if err != nil {
		return nil, err
	}
	if err := tx.Find(&roleBindings).Error; err != nil {
		return nil, errs.WrapGORMError(err, "query role bindings")
	}
	if len(roleBindings) == 0 {
		return nil, nil
	}

	userIDs := make([]uint64, 0, len(roleBindings))
	for _, roleBinding := range roleBindings {
		userIDs = append(userIDs, uint64(roleBinding.TargetID))
	}
	return userIDs, nil
}

func (d *UserDao) GetUserByID(ctx context.Context, tx *gorm.DB, userID uint64) (*entity.User, error) {
	var user entity.User
	err := entity.NewUserQuerySet(tx).IDEq(entity.ID(userID)).One(&user)
	tilog.Info(ctx, "dao GetUserByID", zap.Any("success", err == nil), zap.Any("err", err))
	if err != nil {
		if err != gorm.ErrRecordNotFound {
			tilog.Error(ctx, "UserDao GetUserByID err", zap.Any("userId", userID), zap.Error(err))
		}
		return nil, err
	}

	return &user, nil
}

func (d *UserDao) UpdateUserByID(ctx context.Context, tx *gorm.DB, user *entity.User) error {
	updater := entity.NewUserQuerySet(tx).IDEq(user.ID).GetUpdater()
	if user.FirstName != "" {
		updater = updater.SetFirstName(user.FirstName)
	}
	if user.LastName != "" {
		updater = updater.SetLastName(user.LastName)
	}
	if user.Job != "" {
		updater = updater.SetJob(user.Job)
	}
	if user.PhoneNumber != "" {
		updater = updater.SetPhoneNumber(user.PhoneNumber)
	}
	if user.Status != "" {
		updater = updater.SetStatus(user.Status)
	}
	if user.InvitedAt != nil {
		updater = updater.SetInvitedAt(user.InvitedAt)
	}
	if user.TenantID != 0 {
		updater = updater.SetTenantID(user.TenantID)
	}
	return updater.Update()
}

func MakeSystemEmail(orgID uint64) string {
	return fmt.Sprintf("%d-%s", orgID, config.SystemEmail)
}

func (d *UserDao) GetSystemUserByOrgID(ctx context.Context, tx *gorm.DB, orgID uint64) (*entity.User, error) {
	systemUserEmail := MakeSystemEmail(orgID)
	return d.GetUserByEmail(ctx, tx, systemUserEmail)
}

func (d *UserDao) IsImpersonatedUserInOrg(ctx context.Context, tx *gorm.DB, userID uint64, orgID uint64) (*entity.ImpersonatedUser, bool, error) {
	var user entity.ImpersonatedUser

	err := tx.Where(
		`user_id = ? AND tenant_id = ?`,
		userID, orgID,
	).First(&user).Error

	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil, false, nil
		}
		e := errors.Annotate(err, "failed to get impersonated user")
		return nil, false, e
	}

	result := user.UserID.Uint64() == userID && user.TenantID.Uint64() == orgID
	return &user, result, nil
}

func (d *UserDao) GetImpersonatedUser(ctx context.Context, tx *gorm.DB, userID uint64) (*entity.ImpersonatedUser, bool, error) {
	var impersonatedUser entity.ImpersonatedUser
	err := entity.NewImpersonatedUserQuerySet(tx).UserIDEq(entity.ID(userID)).One(&impersonatedUser)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil, false, nil
		}
		return nil, false, err
	}

	return &impersonatedUser, true, nil
}

func (d *UserDao) AddImpersonateUser(ctx context.Context, tx *gorm.DB, userID, orgID uint64) error {
	var impersonatedUser = &entity.ImpersonatedUser{
		UserID:   entity.ID(userID),
		TenantID: entity.ID(orgID),
	}
	return tx.Create(impersonatedUser).Error
}

func (d *UserDao) DeleteImpersonateUser(ctx context.Context, tx *gorm.DB, userID, orgID uint64) error {
	return entity.NewImpersonatedUserQuerySet(tx).UserIDEq(entity.ID(userID)).TenantIDEq(entity.ID(orgID)).Delete()
}

func (d *UserDao) ListRoleBindingUsers(ctx context.Context, tx *gorm.DB, filter RoleBindingUserFilter) ([]RoleBindingUser, error) {

	var users []RoleBindingUser
	//  SELECT t1.status, t1.role,t1.role_id, t1.created_at as invite_time, u.id as user_id, u.email, u.first_name,u.last_name
	//  FROM role_bindings t1 left join users u on t1.target_id=u.id
	//  WHERE t1.target_type='USER' AND t1.status in ('active','inactive')
	//  AND t1.scope_type='ORG' AND t1.scope_id=1 ORDER BY email asc
	tx = tx.Table("role_bindings t1").Select("t1.status, t1.role,t1.role_id, t1.created_at as invite_time, u.id as user_id, u.email, u.first_name,u.last_name").
		Joins("left join users u on t1.target_id=u.id")
	tx.Where("t1.target_type=?", entity.RoleBindingTargetUser)
	if len(filter.RoleBindingStatus) != 0 {
		tx.Where("t1.status in ?", filter.RoleBindingStatus)
	}
	if filter.UserId != 0 {
		tx.Where("t1.target_id=?", filter.UserId)
	}
	if filter.ScopeType != "" {
		tx.Where("t1.scope_type=?", filter.ScopeType)
	}
	if filter.ScopeId != 0 {
		tx.Where("t1.scope_id=?", filter.ScopeId)
	}
	if filter.EmailLike != "" {
		tx.Where("u.email like ?", "%"+filter.EmailLike+"%")
	}
	if len(filter.UserStatus) != 0 {
		tx.Where("u.status in ?", filter.UserStatus)
	}
	if len(filter.Roles) > 0 {
		tx.Where("t1.role in ?", filter.Roles)
	}

	if filter.SortBy == SortByEmail {
		tx = tx.Order("email " + filter.Order)
	} else if filter.SortBy == SortByInviteTime {
		tx = tx.Order("t1.created_at " + filter.Order)
	} else if filter.SortBy == sortByRole {
		tx = tx.Order("t1.role " + filter.Order)
	} else if filter.SortBy == sortByStatus {
		tx = tx.Order("t1.status " + filter.Order)
	} else {
		tx = tx.Order("email asc")
	}

	tx.Scan(&users)

	tilog.Info(ctx, "ListRoleBindingUsers", zap.Any("len", len(users)))
	return users, nil

}

type RoleBindingUser struct {
	UserId     uint64
	Email      string
	FirstName  string
	LastName   string
	Status     string
	InviteTime time.Time
	Role       string
	RoleId     uint64
}

func (s *RoleBindingUser) MarshalJSON() ([]byte, error) {
	return []byte(fmt.Sprintf("%q", s.InviteTime.Format(time.RFC3339))), nil
}

type UserFilter struct {
	UserId     uint64
	Email      string
	FirstName  string
	LastName   string
	Status     string
	InviteTime time.Time
	Page       uint64
	PageSize   uint64
}
