package orgsso

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

	"github.com/tidbcloud/account/internel/svc/comsvc"

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

	"github.com/tidbcloud/account/central/events/userevent"
	"github.com/tidbcloud/account/thirdparty/jsm"

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

	"github.com/AlekSi/pointer"
	"github.com/tidbcloud/account/internel/dao"
	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"gopkg.in/auth0.v4/management"
	"gorm.io/gorm"
)

type OrgUserRegisterSvcIface interface {
	WhetherHitAutoProvision(ctx context.Context, email string, orgID uint64, methodName string) (bool, error)
	RegisterSsoUser(ctx context.Context, param *RegisterSsoUserParam) error
	AutoAddUserToOrg(ctx context.Context, email string, orgID uint64) error
	WhetherGoAutoRegister(ctx context.Context, email string, orgID uint64, methodName string) (*GoAutoRegisterRet, error)
}

var (
	orgUserRegisterSvcInitOnce sync.Once
	orgUserRegisterSvcInstance OrgUserRegisterSvcIface
)

func GetOrgUserRegisterSvc() (OrgUserRegisterSvcIface, error) {
	orgUserRegisterSvcInitOnce.Do(func() {
		orgUserRegisterSvcInstance = NewOrgUserRegisterSvc()

	})
	return orgUserRegisterSvcInstance, nil
}

type OrgUserRegisterSvc struct {
}

func NewOrgUserRegisterSvc() OrgUserRegisterSvcIface {
	return &OrgUserRegisterSvc{}
}

type RegisterSsoUserParam struct {
	Auth0Token string
	MethodName string
	OrgID      uint64
}

type GoAutoRegisterRet struct {
	AutoProvision   bool
	UserExist       bool
	UserInAccessOrg bool
}

func (s *OrgUserRegisterSvc) WhetherHitAutoProvision(ctx context.Context, email string, orgID uint64, methodName string) (bool, error) {
	authMethod, err := dao.GetDaoHub().OrgAuthMethod.GetOrgAuthMethodByName(ctx, database.DBConn.DB, orgID, methodName)
	if err != nil {
		return false, err
	}
	if authMethod == nil || authMethod.Status != entity.Enable {
		return false, nil
	}
	if !authMethod.AutoProvision {
		return false, nil
	}
	if CheckEmailInAllowDomain(email, authMethod.AllowEmailDomain) {
		tilog.Info(ctx, "WhetherOpenAutoProvision CheckEmailInAllowDomain is true", zap.Any("orgID", orgID), zap.Any("methodName", methodName), zap.Any("email", email))
		return true, nil
	}
	return false, nil
}

func (s *OrgUserRegisterSvc) WhetherGoAutoRegister(ctx context.Context, email string, orgID uint64, methodName string) (*GoAutoRegisterRet, error) {
	bm := utils.Begin(ctx, "WhetherGoAutoRegister", zap.Any("email", email), zap.Any("orgID", orgID), zap.Any("methodName", methodName))
	defer bm.OnExit(ctx)

	autoProvision, err := s.WhetherHitAutoProvision(ctx, email, orgID, methodName)
	if err != nil {
		tilog.Error(ctx, "WhetherGoAutoRegister WhetherOpenAutoProvision check error", zap.Any("orgID", orgID), zap.Any("methodName", methodName))
		return nil, err
	}

	user, err := dao.GetDaoHub().User.GetUserByEmail(ctx, database.DBConn.DB, email)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			user = nil
		} else {
			tilog.Error(ctx, "WhetherGoAutoRegister GetUserByEmail error", zap.Any("orgID", orgID), zap.Any("methodName", methodName), zap.Any("email", email))
			return nil, err
		}
	}
	userExist := false
	if user != nil && user.Status == entity.UserStatusActive {
		userExist = true
	}

	if !userExist {
		ret := GoAutoRegisterRet{
			AutoProvision:   autoProvision,
			UserExist:       userExist,
			UserInAccessOrg: false,
		}
		return &ret, nil
	}

	bindings, err := dao.GetDaoHub().Role.ListRolesByTargetAndScope(ctx, database.DBConn.DB, entity.RoleBindingTargetUser, user.ID.Uint64(), entity.RoleBindingScopeOrg, orgID, []string{entity.RoleBindingStatusActive})
	if err != nil {
		tilog.Error(ctx, "WhetherGoAutoRegister ListRolesByTargetAndScope error", zap.Any("orgID", orgID), zap.Any("methodName", methodName), zap.Any("email", email))
		return nil, err
	}
	if len(bindings) > 0 {
		ret := GoAutoRegisterRet{
			AutoProvision:   autoProvision,
			UserExist:       userExist,
			UserInAccessOrg: true,
		}
		return &ret, nil
	}
	ret := GoAutoRegisterRet{
		AutoProvision:   autoProvision,
		UserExist:       userExist,
		UserInAccessOrg: false,
	}
	return &ret, nil

}

func (s *OrgUserRegisterSvc) AutoAddUserToOrg(ctx context.Context, email string, orgID uint64) error {
	tilog.Info(ctx, "RegisterSsoUser AutoAddUserToOrg start", zap.Any("email", email), zap.Any("orgID", orgID))
	hub := dao.GetDaoHub()
	db := database.DBConn.DB
	dbUser, err := hub.User.GetUserByEmail(ctx, db, email)
	if err != nil {
		tilog.Error(ctx, "RegisterSsoUser GetUserByEmail error", zap.Any("err", err))
		return err
	}
	oRoleBinding, err := hub.Role.ListRolesByTargetAndScope(ctx, db, entity.RoleBindingTargetUser, dbUser.ID.Uint64(), entity.RoleBindingScopeOrg, orgID, []string{entity.RoleBindingStatusActive})
	if err != nil {
		tilog.Error(ctx, "RegisterSsoUser ListRolesByTargetAndScope error", zap.Any("err", err))
		return err
	}
	if len(oRoleBinding) > 0 {
		tilog.Info(ctx, "RegisterSsoUser user has active org role, ignore to add", zap.Any("userID", dbUser.ID.Uint64()), zap.Any("orgID", orgID))
		return nil
	}

	var oRole = entity.RoleOrgMember
	param := &comsvc.AssignIdentRolesParam{
		IdentTye: entity.RoleBindingTargetUser,
		IdentID:  dbUser.ID.Uint64(),
		IdentVal: dbUser.Email,
		OrgID:    orgID,
		ORole:    &oRole,
		PRoles:   map[uint64]string{},
		CRoles:   map[uint64]string{},
		Status:   entity.RoleBindingStatusActive,
	}
	_, err = comsvc.UpdateIdentityRoles(ctx, param)
	if err != nil {
		tilog.Error(ctx, "RegisterSsoUser add user to org error", zap.Any("err", err))
		return err
	}

	tilog.Info(ctx, "RegisterSsoUser AddUserToOrg success", zap.Any("userID", dbUser.ID.Uint64()), zap.Any("orgID", orgID))

	go func() {
		_ = comsvc.SendUserRoleBindingChangeEvent(context.Background(), dbUser.ID.Uint64(), orgID, "AutoProvision")
	}()

	return nil
}

func (s *OrgUserRegisterSvc) RegisterSsoUser(ctx context.Context, param *RegisterSsoUserParam) error {
	bm := utils.Begin(ctx, "RegisterSsoUser")
	defer bm.OnExit(ctx)
	tilog.Info(ctx, "RegisterSsoUser start", zap.Any("param", param))

	email, err := Auth0TokenValidator.GetEmailFromAuth0Token(param.Auth0Token)
	if err != nil {
		tilog.Error(ctx, "RegisterSsoUser GetEmailFromAuth0Token error", zap.Any("err", err))
		return err
	}
	createUser, err := s.PersistUser(ctx, email, param)
	if err != nil {
		tilog.Error(ctx, "RegisterSsoUser persistUser error", zap.Any("err", err))
		return err
	}
	tilog.Info(ctx, "RegisterSsoUser PersistUser result", zap.Any("createUser", createUser))

	err = s.AutoAddUserToOrg(ctx, email, param.OrgID)
	if err != nil {
		return err
	}
	go func() {
		jsm.GetJsmClientSingleton().ProcessSSOUserRegistration(ctx, createUser, param.OrgID)
		_ = userevent.PublishAddUserToOrgEvent(ctx, param.OrgID, createUser.ID.Uint64(), entity.RoleOrgMember)
	}()
	return nil
}

func (s *OrgUserRegisterSvc) PersistUser(ctx context.Context, email string, param *RegisterSsoUserParam) (*entity.User, error) {
	toCreateUser, err := genUserEntity(ctx, email, param)
	if err != nil {
		tilog.Error(ctx, "RegisterSsoUser genUserEntity error", zap.Any("err", err))
		return nil, err
	}
	dbUser, err := dao.GetDaoHub().User.GetUserByEmail(ctx, database.DBConn.DB, email)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			dbUser = nil
		} else {
			tilog.Error(ctx, "RegisterSsoUser GetUserByEmail error", zap.Any("err", err))
			return nil, err
		}
	}
	if dbUser == nil {
		err = toCreateUser.Create(database.DBConn.DB)
		if err != nil {
			tilog.Error(ctx, "RegisterSsoUser Create User error", zap.Any("err", err))
			return nil, err
		}
	} else {
		if dbUser.Status != entity.UserStatusActive {
			toCreateUser.ID = dbUser.ID
			err = toCreateUser.Update(database.DBConn.DB,
				entity.UserDBSchema.FirstName,
				entity.UserDBSchema.LastName,
				entity.UserDBSchema.Status,
				entity.UserDBSchema.InvitedAt,
			)
		} else {
			tilog.Info(ctx, "RegisterSsoUser user is active, ignore to persist user", zap.Any("email", email))
		}
	}
	return toCreateUser, err
}

func SetUserEmailVerified(email string) error {
	users, err := Auth0UserManagement.ListByEmail(email)
	if err != nil {
		return err
	}
	var auth0User *management.User
	for _, u := range users {
		for _, identity := range u.Identities {
			if *identity.Connection == config.Global().Auth0Config.Auth0DatabaseConnectionName {
				auth0User = u
			}
		}
	}
	if auth0User == nil {
		return errors.New("failed to query the user of custom-email connection by email in auth0")
	}
	updateUser := &management.User{
		EmailVerified: pointer.ToBool(true),
	}
	err = Auth0UserManagement.Update(*auth0User.ID, updateUser)
	if err != nil {
		return err
	}
	return nil
}

func genUserEntity(ctx context.Context, email string, param *RegisterSsoUserParam) (*entity.User, error) {
	users, err := Auth0UserManagement.ListByEmail(email)
	if err != nil {
		tilog.Error(ctx, "RegisterSsoUser ListAuth0UsersByEmail error", zap.Any("err", err))
		return nil, err
	}
	firstName, lastName, found := getUserFirstNameAndLastName(users, param.MethodName)
	if !found {
		tilog.Error(ctx, "RegisterSsoUser not found user firstname lastname")
		return nil, errors.New("not found user firstname lastname")
	}

	now := time.Now()
	user := entity.User{
		Email:     email,
		FirstName: firstName,
		LastName:  lastName,
		Status:    entity.UserStatusActive,
		InvitedAt: &now,
		TenantID:  entity.ID(param.OrgID),
		Role:      entity.UserRoleMember,
	}
	return &user, nil
}

func getUserFirstNameAndLastName(users []*management.User, methodName string) (string, string, bool) {
	for _, u := range users {
		email := *u.Email
		for _, userIdentity := range u.Identities {
			if pointer.GetString(userIdentity.Connection) == methodName {
				name := pointer.GetString(u.Name)
				nickName := pointer.GetString(u.Nickname)
				// login from okta is nearly the same with GitHub, including retrieve first name and last name
				firstName, lastName := parseFirstNameAndLastName(name, nickName, email)
				// fallback to email
				if firstName == "" {
					firstName = *u.Email
				}
				return firstName, lastName, true
			}
		}
	}
	return "", "", false
}

func parseFirstNameAndLastName(name, nickName, email string) (firstName, lastName string) {
	if name == "" {
		return nickName, ""
	}
	// If GitHub profile name is empty, auth0 will use email as name.
	if name == email {
		return nickName, ""
	}

	names := strings.Split(name, " ")
	if len(names) == 1 {
		return names[0], ""
	}
	return strings.Join(names[:len(names)-1], " "), names[len(names)-1]
}
