package oneconsole

import (
	"context"
	"errors"
	"fmt"
	"time"

	daorole "github.com/tidbcloud/account/internel/dao/role"
	"github.com/tidbcloud/tiinf/sdk/auditlog"

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

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

	"github.com/tidbcloud/account/commons/database"
	"github.com/tidbcloud/account/internel/dao"
	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

const (
	ErrFailedInviteEmailTemplate   = "Failed to invite user %s, please try again later or request support."
	ErrRepeatedInviteEmailTemplate = "The user %s is already associated with the current organization."

	defaultInviter = "TiDB Cloud System"
)

type InviteUsersParam struct {
	EmailList []string
	OrgID     uint64
	ORole     string
	PRoles    map[uint64]string
	CRoles    map[uint64]string
}

type InviteUserRet struct {
	UserID  uint64
	Email   string
	Code    int
	Message string
}

type UpdateRolesParam struct {
	UserID uint64
	OrgID  uint64
	ORole  *string
	PRoles map[uint64]string
	CRoles map[uint64]string
}

func (s *OneConsoleUserSvc) InviteUsersToOrg(ctx context.Context, param *InviteUsersParam, operator *comsvc.Operator) ([]InviteUserRet, error) {
	tilog.Info(ctx, "OneConsole InviteUsers start", zap.Any("param", param), zap.Any("operator", operator))
	if param == nil || operator == nil {
		return nil, errs.NewBadRequest("param invalid", nil)
	}
	// invite user must assign org role
	if len(param.EmailList) == 0 || param.OrgID == 0 || param.ORole == "" {
		return nil, errs.NewBadRequest("param invalid", nil)
	}
	// check assign scope valid
	if err := comsvc.CheckAssignScopeAndRoleValid(ctx, param.OrgID, &param.ORole, param.PRoles, param.CRoles); err != nil {
		tilog.Info(ctx, "OneConsole InviteUsers CheckAssignScopeAndRoleValid error", zap.Any("err", err))
		return nil, err
	}
	// get org
	org, err := dao.GetDaoHub().Org.GetOrgByOrgID(ctx, database.DBConn.DB, param.OrgID)
	if err != nil {
		tilog.Info(ctx, "OneConsole InviteUsers GetOrgByOrgID error", zap.Any("err", err))
		return nil, errs.NewInternalUnknown("get org by id internal error", err)
	}

	inviterEmail := getInviterEmail(operator)

	// for each email to assign role and send activation email
	var inviteRets []InviteUserRet
	for _, email := range param.EmailList {
		ret := inviteNewUser(ctx, inviterEmail, email, org, param.ORole, param.PRoles, param.CRoles, operator)
		inviteRets = append(inviteRets, *ret)
	}
	return inviteRets, nil
}

func inviteNewUser(ctx context.Context, inviterEmail, inviteeEmail string, org *entity.Tenant, oRole string, pRoles, cRoles map[uint64]string, operator *comsvc.Operator) *InviteUserRet {
	tilog.Info(ctx, fmt.Sprintf("OneConsole InviteUsers inviteNewUser %s to org %d start", inviteeEmail, org.ID))
	user, oBinding, _, err := comsvc.GetUserAndScopeRoleBinding(ctx, inviteeEmail, org.ID.Uint64(), 0, []string{entity.RoleBindingStatusActive})
	if err != nil {
		tilog.Error(ctx, "OneConsole InviteUsers inviteNewUser getUserAndOrgBinding error", zap.Any("err", err))
		return &InviteUserRet{
			Email:   inviteeEmail,
			Code:    entity.InviteUserInternalErr,
			Message: "internal error: get user and org binding error",
		}
	}
	if oBinding != nil {
		tilog.Info(ctx, "OneConsole InviteUsers inviteNewUser user already has org role", zap.Any("oBinding", oBinding))
		return &InviteUserRet{
			Email:   inviteeEmail,
			Code:    entity.InviteUserRepeated,
			Message: fmt.Sprintf(ErrRepeatedInviteEmailTemplate, inviteeEmail),
		}
	}

	// handle user ent
	now := time.Now()
	if user != nil {
		// if status is active, use active, else use pending
		if user.Status != entity.UserStatusActive {
			user.Status = entity.UserStatusPending
		}
		user.InvitedAt = &now
		user.TenantID = org.ID
		err = dao.GetDaoHub().User.UpdateUserByID(ctx, database.DBConn.DB, user)
	} else {
		tilog.Info(ctx, fmt.Sprintf("email %s relate user is not exist, to create user", inviteeEmail))
		user, err = dao.GetDaoHub().User.CreateUser(ctx, database.DBConn.DB, &entity.User{
			Email:     inviteeEmail,
			FirstName: "",
			LastName:  "",
			Status:    entity.UserStatusPending,
			InvitedAt: &now,
			TenantID:  org.ID,
			Role:      entity.UserRoleMember,
		})
		tilog.Info(ctx, "create user result", zap.Any("user", user), zap.Any("err", err))
	}
	if err != nil {
		tilog.Info(ctx, "OneConsole InviteUsers inviteNewUser handle user ent error", zap.Any("err", err))
		return &InviteUserRet{
			Email:   inviteeEmail,
			Code:    entity.InviteUserInternalErr,
			Message: "internal error: persist user ent error",
		}
	}

	// assign role
	param := &comsvc.AssignIdentRolesParam{
		IdentTye: entity.RoleBindingTargetUser,
		IdentID:  user.ID.Uint64(),
		IdentVal: user.Email,
		OrgID:    org.ID.Uint64(),
		ORole:    &oRole,
		PRoles:   pRoles,
		CRoles:   cRoles,
		Status:   entity.RoleBindingStatusInactive,
	}
	assignRet, err := comsvc.UpdateIdentityRoles(ctx, param)
	if err != nil {
		tilog.Info(ctx, "OneConsole InviteUsers inviteNewUser UpdateIdentityRoles error", zap.Any("err", err))
		return &InviteUserRet{
			UserID:  user.ID.Uint64(),
			Email:   inviteeEmail,
			Code:    entity.InviteUserInternalErr,
			Message: "internal error: assign role error",
		}
	}

	// send activation email
	err = comsvc.InvitationSendActivationEmail(ctx, inviterEmail, inviteeEmail, org.Name, org.ID.Uint64())
	if err != nil {
		tilog.Error(ctx, "OneConsole InviteUsers inviteNewUser InvitationSendActivationEmail error", zap.Any("err", err))
		return &InviteUserRet{
			UserID:  user.ID.Uint64(),
			Email:   inviteeEmail,
			Code:    entity.InviteUserInternalErr,
			Message: "internal error: send activation email error",
		}
	}

	recAuditLog(ctx, org.ID.Uint64(), user.Email, assignRet, err, operator)

	tilog.Info(ctx, fmt.Sprintf("OneConsole InviteUsers inviteNewUser %s to org %d success", inviteeEmail, org.ID))
	return &InviteUserRet{
		UserID:  user.ID.Uint64(),
		Email:   inviteeEmail,
		Code:    entity.InviteUserSuccess,
		Message: "success",
	}

}

func (s *OneConsoleUserSvc) UpdateUserRoles(ctx context.Context, param *UpdateRolesParam, operator *comsvc.Operator) error {
	tilog.Info(ctx, "OneConsole UpdateUserRoles start", zap.Any("param", param), zap.Any("operator", operator))
	if param == nil || operator == nil {
		return errs.NewBadRequest("param invalid", nil)
	}
	if param.UserID == 0 || param.OrgID == 0 {
		return errs.NewBadRequest("param invalid", nil)
	}
	// check assign scope valid
	if err := comsvc.CheckAssignScopeAndRoleValid(ctx, param.OrgID, param.ORole, param.PRoles, param.CRoles); err != nil {
		tilog.Info(ctx, "OneConsole UpdateUserRoles CheckAssignScopeAndRoleValid error", zap.Any("err", err))
		return err
	}
	// check org has at least one org owner
	if param.ORole != nil && *param.ORole != entity.RoleOrgOwner {
		ownerBindings, err := dao.GetDaoHub().Role.ListRoleBindings(ctx, database.DBConn.DB, role.RoleBindingFilter{
			ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeOrg},
			ScopeIDs:   []entity.ID{entity.ID(param.OrgID)},
			TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
			Status:     []string{entity.RoleBindingStatusActive},
			Roles:      []string{entity.RoleOrgOwner},
		})
		if err != nil {
			tilog.Error(ctx, "OneConsole UpdateUserRoles list org owner role bindings error", zap.Any("org_id", param.OrgID), zap.Any("err", err))
			return err
		}
		// if exists only one org owner user and the user is to updated user, can not edit role
		if len(ownerBindings) == 1 && ownerBindings[0].TargetID.Uint64() == param.UserID {
			tilog.Info(ctx, "OneConsole UpdateUserRoles fail, org needs to have at least one org owner", zap.Any("org_id", param.OrgID), zap.Any("err", err))
			return errs.NewBadRequest("The organization needs to have at least one owner.", nil)
		}
	}
	// get user ent
	user, err := dao.GetDaoHub().User.GetUserByID(ctx, database.DBConn.DB, param.UserID)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			tilog.Info(ctx, "OneConsole UpdateUserRoles fail, user not found", zap.Any("user_id", param.UserID))
			return errs.NewBadRequest("user not found", nil)
		} else {
			tilog.Error(ctx, "OneConsole UpdateUserRoles fail, get user by id internal error", zap.Any("user_id", param.UserID), zap.Any("err", err))
			return errs.NewInternalUnknown("get user by id internal error", err)
		}
	}
	// get org role binding
	oBindings, err := dao.GetDaoHub().Role.ListRolesByTargetAndScope(ctx, database.DBConn.DB,
		entity.RoleBindingTargetUser, param.UserID, entity.RoleBindingScopeOrg, param.OrgID, []string{})
	if err != nil {
		tilog.Error(ctx, "OneConsole UpdateUserRoles list org role binding error", zap.Any("err", err))
		return errs.NewInternalUnknown("list org role binding internal error", err)
	}
	if len(oBindings) == 0 || oBindings[0].Status == entity.RoleBindingStatusDelete {
		tilog.Info(ctx, "OneConsole UpdateUserRoles user has not org role")
		return errs.NewBadRequest("user has not org role", nil)
	}

	updRoleP := &comsvc.AssignIdentRolesParam{
		IdentTye: entity.RoleBindingTargetUser,
		IdentID:  user.ID.Uint64(),
		IdentVal: user.Email,
		OrgID:    param.OrgID,
		ORole:    param.ORole,
		PRoles:   param.PRoles,
		CRoles:   param.CRoles,
		Status:   oBindings[0].Status,
	}
	ret, err := comsvc.UpdateIdentityRoles(ctx, updRoleP)

	recAuditLog(ctx, param.OrgID, user.Email, ret, err, operator)

	if err == nil && updRoleP.Status == entity.RoleBindingStatusActive {
		go func() {
			_ = comsvc.SendUserRoleBindingChangeEvent(context.Background(), user.ID.Uint64(), param.OrgID, "OneConsole UpdateUserRoles")
		}()
	}

	tilog.Info(ctx, "OneConsole UpdateUserRoles UpdateIdentityRoles result", zap.Any("param", updRoleP), zap.Any("err", err))

	return err
}

func setAuditLogOperator(eBuilder *auditlog.EventBuilder, operator *comsvc.Operator) *auditlog.EventBuilder {
	if operator != nil {
		if operator.OpIP != "" {
			eBuilder.SetOperatorIP(operator.OpIP)
		}
		if operator.OpTye == entity.RoleBindingTargetAPIKey {
			eBuilder.SetOperatorType(auditlog.OperatorTypeAPIKey).SetAPIPubKey(operator.OpVal)
		} else {
			eBuilder.SetOperatorType(auditlog.OperatorTypeUser).SetOperatorID(operator.OpID).SetOperatorEmail(operator.OpVal)
		}
	}
	return eBuilder
}

func recAuditLog(ctx context.Context, orgId uint64, email string, r *comsvc.AssignIdentRolesRet, err error, operator *comsvc.Operator) {
	// org level audit log
	if r.InsOBinding != nil {
		b := r.InsOBinding
		detail := auditlog.Details{
			auditlog.KeyUserEmail: email,
			auditlog.KeyRole:      entity.RoleBindingDecode(b.ToRole),
			auditlog.KeyError:     err,
		}
		eBuilder := auditlog.NewEvent(ctx, auditlog.EventInviteUserToOrg).SetDetails(detail.Build()).SetResult(err == nil).SetOrgID(b.ScopeId)
		setAuditLogOperator(eBuilder, operator)
		e := eBuilder.Submit()
		tilog.Info(ctx, "EditConsoleUserRole rec create org role binding audit log result", zap.Any("org_id", b.ScopeId), zap.Any("detail", detail), zap.Any("err", e))
	}
	if r.UpdOBinding != nil {
		b := r.UpdOBinding
		detail := auditlog.Details{
			auditlog.KeyUserEmail: email,
			auditlog.KeyOldRole:   entity.RoleBindingDecode(b.FromRole),
			auditlog.KeyNewRole:   entity.RoleBindingDecode(b.ToRole),
			auditlog.KeyError:     err,
		}
		eBuilder := auditlog.NewEvent(ctx, auditlog.EventUpdateUserRole).SetOrgID(b.ScopeId).SetDetails(detail.Build()).SetResult(err == nil)
		setAuditLogOperator(eBuilder, operator)
		e := eBuilder.Submit()
		tilog.Info(ctx, "EditConsoleUserRole rec update org role binding audit log result", zap.Any("org_id", b.ScopeId), zap.Any("detail", detail), zap.Any("err", e))
	}
	if r.DelOBinding != nil {
		b := r.DelOBinding
		et := auditlog.EventDeleteInvitationToOrg
		if b.FromStatus == entity.RoleBindingStatusActive {
			et = auditlog.EventDeleteUserFromOrg
		}
		detail := auditlog.Details{
			auditlog.KeyUserEmail: email,
			auditlog.KeyRole:      entity.RoleBindingDecode(b.FromRole),
			auditlog.KeyError:     err,
		}
		eBuilder := auditlog.NewEvent(ctx, et).SetOrgID(b.ScopeId).SetDetails(detail.Build()).SetResult(err == nil)
		setAuditLogOperator(eBuilder, operator)
		e := eBuilder.Submit()
		tilog.Info(ctx, "EditConsoleUserRole rec delete org role binding audit log result", zap.Any("org_id", b.ScopeId), zap.Any("detail", detail), zap.Any("err", e))
	}
	// project level audit log
	for _, b := range r.InsPBindings {
		detail := auditlog.Details{
			auditlog.KeyUserEmail: email,
			auditlog.KeyRole:      b.ToRole,
			auditlog.KeyError:     err,
		}
		eBuilder := auditlog.NewEvent(ctx, auditlog.EventInviteUserToProject).SetOrgID(orgId).SetProjectID(b.ScopeId).SetDetails(detail.Build()).SetResult(err == nil)
		setAuditLogOperator(eBuilder, operator)
		e := eBuilder.Submit()
		tilog.Info(ctx, "EditConsoleUserRole rec create project role binding audit log result", zap.Any("org_id", orgId), zap.Any("project_id", b.ScopeId), zap.Any("detail", detail), zap.Any("err", e))
	}
	for _, b := range r.UpdPBindings {
		detail := auditlog.Details{
			auditlog.KeyUserEmail: email,
			auditlog.KeyOldRole:   entity.RoleBindingDecode(b.FromRole),
			auditlog.KeyNewRole:   entity.RoleBindingDecode(b.ToRole),
			auditlog.KeyError:     err,
		}
		eBuilder := auditlog.NewEvent(ctx, auditlog.EventUpdateUserRoleInProject).SetOrgID(orgId).SetProjectID(b.ScopeId).SetDetails(detail.Build()).SetResult(err == nil)
		setAuditLogOperator(eBuilder, operator)
		e := eBuilder.Submit()
		tilog.Info(ctx, "EditConsoleUserRole rec update project role binding audit log result", zap.Any("org_id", orgId), zap.Any("project_id", b.ScopeId), zap.Any("detail", detail), zap.Any("err", e))
	}
	for _, b := range r.DelPBindings {
		et := auditlog.EventDeleteInvitationToProject
		if b.FromStatus == entity.RoleBindingStatusActive {
			et = auditlog.EventDeleteUserFromProject
		}
		detail := auditlog.Details{
			auditlog.KeyUserEmail: email,
			auditlog.KeyRole:      entity.RoleBindingDecode(b.FromRole),
			auditlog.KeyError:     err,
		}
		eBuilder := auditlog.NewEvent(ctx, et).SetOrgID(orgId).SetProjectID(b.ScopeId).SetDetails(detail.Build()).SetResult(err == nil)
		setAuditLogOperator(eBuilder, operator)
		e := eBuilder.Submit()
		tilog.Info(ctx, "EditConsoleUserRole rec delete project role binding audit log result", zap.Any("org_id", orgId), zap.Any("project_id", b.ScopeId), zap.Any("detail", detail), zap.Any("err", e))
	}
	for _, b := range r.InsCBindings {
		detail := auditlog.Details{
			auditlog.KeyUserEmail: email,
			auditlog.KeyRole:      entity.RoleBindingDecode(b.ToRole),
			auditlog.KeyError:     err,
		}
		eBuilder := auditlog.NewEvent(ctx, auditlog.EventInviteUserToTiDB).SetOrgID(orgId).SetClusterID(b.ScopeId).SetDetails(detail.Build()).SetResult(err == nil)
		setAuditLogOperator(eBuilder, operator)
		e := eBuilder.Submit()
		tilog.Info(ctx, "EditConsoleUserRole rec create cluster role binding audit log result", zap.Any("org_id", orgId), zap.Any("cluster_id", b.ScopeId), zap.Any("detail", detail), zap.Any("err", e))

	}
	for _, b := range r.UpdCBindings {
		detail := auditlog.Details{
			auditlog.KeyUserEmail: email,
			auditlog.KeyOldRole:   entity.RoleBindingDecode(b.FromRole),
			auditlog.KeyNewRole:   entity.RoleBindingDecode(b.ToRole),
			auditlog.KeyError:     err,
		}
		eBuilder := auditlog.NewEvent(ctx, auditlog.EventUpdateUserRoleInTiDB).SetOrgID(orgId).SetClusterID(b.ScopeId).SetDetails(detail.Build()).SetResult(err == nil)
		setAuditLogOperator(eBuilder, operator)
		e := eBuilder.Submit()
		tilog.Info(ctx, "EditConsoleUserRole rec update cluster role binding audit log result", zap.Any("org_id", orgId), zap.Any("cluster_id", b.ScopeId), zap.Any("detail", detail), zap.Any("err", e))

	}
	for _, b := range r.DelCBindings {
		et := auditlog.EventDeleteInvitationToTiDB
		if b.FromStatus == entity.RoleBindingStatusActive {
			et = auditlog.EventDeleteUserFromTiDB
		}
		detail := auditlog.Details{
			auditlog.KeyUserEmail: email,
			auditlog.KeyRole:      entity.RoleBindingDecode(b.FromRole),
			auditlog.KeyError:     err,
		}
		eBuilder := auditlog.NewEvent(ctx, et).SetOrgID(orgId).SetClusterID(b.ScopeId).SetDetails(detail.Build()).SetResult(err == nil)
		setAuditLogOperator(eBuilder, operator)
		e := eBuilder.Submit()
		tilog.Info(ctx, "EditConsoleUserRole rec delete cluster role binding audit log result", zap.Any("org_id", orgId), zap.Any("cluster_id", b.ScopeId), zap.Any("detail", detail), zap.Any("err", e))
	}
}

func (s *OneConsoleUserSvc) CheckEmailAlreadyHasOrgRole(ctx context.Context, emails []string, orgId uint64) ([]string, error) {
	tilog.Info(ctx, "CheckEmailAlreadyHasOrgRole req", zap.Any("emails", emails), zap.Any("orgId", orgId))
	if len(emails) == 0 || orgId == 0 {
		return nil, errs.NewBadRequest("param invalid", nil)
	}
	users, err := dao.GetDaoHub().User.GetUserByEmailIn(ctx, database.DBConn.DB, emails)
	if err != nil {
		tilog.Error(ctx, "CheckEmailAlreadyHasOrgRole list users by email error", zap.Any("err", err))
		return nil, errs.NewInternalUnknown("get user by email error", err)
	}
	var email2uidMap = map[string]uint64{}
	var uids []entity.ID
	for _, u := range users {
		uids = append(uids, u.ID)
		email2uidMap[u.Email] = u.ID.Uint64()
	}
	bindings, err := dao.GetDaoHub().Role.ListRoleBindings(ctx, database.DBConn.DB, daorole.RoleBindingFilter{
		TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
		TargetIDs:  uids,
		ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeOrg},
		ScopeIDs:   []entity.ID{entity.ID(orgId)},
		Status:     []string{entity.RoleBindingStatusActive, entity.RoleBindingStatusInactive},
	})
	if err != nil {
		tilog.Error(ctx, "CheckEmailAlreadyHasOrgRole list org bindings error", zap.Any("err", err))
		return nil, errs.NewInternalUnknown("list org bindings error", err)
	}
	var uid2bindingMap = map[uint64]entity.RoleBinding{}
	for _, b := range bindings {
		uid2bindingMap[b.TargetID.Uint64()] = b
	}

	var hasRoleEmails []string
	for _, e := range emails {
		if uid, ok := email2uidMap[e]; ok {
			if _, ok := uid2bindingMap[uid]; ok {
				hasRoleEmails = append(hasRoleEmails, e)
			}
		}
	}

	return hasRoleEmails, nil
}

func (s *OneConsoleUserSvc) SendActivationEmail(ctx context.Context, userId, orgId uint64, operator *comsvc.Operator) error {
	tilog.Info(ctx, "OneConsole ResendActivationEmail start", zap.Any("userId", userId), zap.Any("orgId", orgId))
	if userId == 0 || orgId == 0 || operator == nil {
		return errs.NewBadRequest("param invalid", nil)
	}
	oBindings, err := dao.GetDaoHub().Role.ListRolesByTargetAndScope(ctx, database.DBConn.DB, entity.RoleBindingTargetUser, userId, entity.RoleBindingScopeOrg, orgId, []string{})
	if err != nil {
		tilog.Error(ctx, "OneConsole ResendActivationEmail list org role binding error", zap.Any("err", err))
		return errs.NewInternalUnknown("get user failed", err)
	}
	if len(oBindings) == 0 {
		tilog.Info(ctx, "OneConsole ResendActivationEmail fail: not existing org role binding")
		return errs.NewBadRequest("not existing org role binding", nil)
	}
	if oBindings[0].Status != entity.RoleBindingStatusInactive {
		tilog.Info(ctx, "OneConsole ResendActivationEmail fail: existing org role binding status is not inactive")
		return errs.NewBadRequest("org role binding status is not inactive", nil)
	}

	u, err := dao.GetDaoHub().User.GetUserByID(ctx, database.DBConn.DB, userId)
	if err != nil {
		tilog.Error(ctx, "OneConsole ResendActivationEmail GetUserByID error", zap.Any("err", err))
		return errs.NewInternalUnknown("get user failed", err)
	}

	org, err := dao.GetDaoHub().Org.GetOrgByOrgID(ctx, database.DBConn.DB, orgId)
	if err != nil {
		tilog.Error(ctx, "OneConsole ResendActivationEmail GetOrgByOrgID error", zap.Any("err", err))
		return errs.NewInternalUnknown("get org failed", err)
	}

	err = comsvc.InvitationSendActivationEmail(ctx, getInviterEmail(operator), u.Email, org.Name, org.ID.Uint64())
	if err != nil {
		tilog.Error(ctx, "OneConsole ResendActivationEmail InvitationSendActivationEmail error", zap.Any("err", err))
		return errs.NewInternalUnknown("resend invitation failed", err)
	}
	return nil
}

func getInviterEmail(operator *comsvc.Operator) string {
	if operator == nil {
		return defaultInviter
	}
	if operator.OpTye == entity.RoleBindingTargetUser {
		return operator.OpVal
	}
	return defaultInviter
}
