package dbuser

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

	"github.com/tidbcloud/account/internel/svc/dbuser"
	projectsvc "github.com/tidbcloud/account/internel/svc/project"
	rbac2 "github.com/tidbcloud/account/internel/svc/rbac"
	"github.com/tidbcloud/account/internel/svc/user"

	"github.com/duke-git/lancet/v2/slice"
	"github.com/tidbcloud/account/internel/dao/role"

	"go.opentelemetry.io/otel"

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

	"github.com/tidbcloud/account/thirdparty/infra"

	"go.uber.org/zap"

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

	"github.com/tidbcloud/account/central/utils"
	dbUserDao "github.com/tidbcloud/account/internel/dao/dbuser"
	"github.com/tidbcloud/account/internel/dao/entity"

	"github.com/tidbcloud/core-svc/idl/pbgen/proto/core"
)

// RunUserActionJob
//
//	@Description: parse user command ,such as delete user from org
//	@Author wangletian 2022-12-17 23:09:51
//	@param ctx
//	@param dbUserSrv
//	@return error
func RunUserActionJob(c context.Context, dbUserSvc dbuser.DBUserIface, userSrv user.UserSvcIface) error {
	ctx, span := otel.Tracer(config.TraceComponentNameWorker).Start(context.Background(), "RunUserActionJob")
	defer span.End()
	bm := utils.Begin(ctx, "--RunUserActionJob")
	defer bm.OnExit(ctx)
	// the CreatedAtGte  4 hour
	t := time.Now().Add(time.Hour * -config.CommandIgnoreTime)
	filter := dbUserDao.CommandUserActionFilter{
		Status:       []entity.CommandStatus{entity.CommandStatusWaiting, entity.CommandStatusFail},
		Actions:      []entity.CommandAction{entity.CommandActionCreate, entity.CommandActionDelete, entity.CommandActionCheck},
		CreatedAtGte: &t,
	}
	userActions, err := dbUserSvc.ListCommandUserActions(ctx, filter)
	if err != nil {
		return err
	}

	// the CreatedAtGte  in 30 minuter
	subTime := time.Now().Add(time.Minute * -30)
	checkFilter := dbUserDao.CommandDBUserClusterFilter{
		Status:       []entity.CommandStatus{entity.CommandStatusComplete, entity.CommandStatusWaiting, entity.CommandStatusFail},
		Actions:      []entity.CommandAction{entity.CommandActionCheck},
		CreatedAtGte: &subTime,
	}
	checkCommands, err := dbUserSvc.ListCommandDBUserClusters(ctx, checkFilter)
	if err != nil {
		return err
	}
	for _, userAction := range userActions {
		if !IsIgnore(ctx, userAction.Status, userAction.CreatedAt, userAction) {
			var status entity.CommandStatus
			// Create dbUser
			status, err = createCommandDBUserCluster(ctx, dbUserSvc, userAction, checkCommands)
			if err != nil {
				tilog.Error(ctx, "createCommandDBUserCluster error", zap.Error(err))
			}
			if err := dbUserSvc.UpdateCommandUserAction(ctx, userAction.ID.Uint64(), userAction.Operator.Uint64(), userAction.UserID.Uint64(), userAction.OrgID.Uint64(),
				userAction.ProjectID.Uint64(), status, userAction.Action); err != nil {
				tilog.Error(ctx, "UpdateCommandUserAction error", zap.Error(err))
			}
		}

	}
	return nil

}

// createCommandDBUserCluster
//
//	@Description: parse user_action command, call cm to create/delete dbUser
//	@Author wangletian 2022-12-25 23:04:40
//	@param ctx
//	@param dbUserSrv
//	@param userSrv
//	@param userAction
//	@return entity.CommandStatus
//	@return error
func createCommandDBUserCluster(ctx context.Context, dbUserSrv dbuser.DBUserIface, userAction entity.CommandUserAction, checkCommands []entity.CommandDBUserCluster) (entity.CommandStatus, error) {

	var clusters []*core.Cluster
	var err error

	if userAction.TargetType == entity.RoleBindingTargetAPIKey {
		if userAction.ProjectID != 0 {
			tilog.Info(ctx, "query api key ref project active cluster ", zap.Any("userAction.ProjectID ", userAction.ProjectID))
			clusters, err = infra.GetHandler().GetNormalStatusClusters(ctx, 0, []uint64{userAction.ProjectID.Uint64()})
		} else {
			tilog.Info(ctx, "query api key ref org active cluster ", zap.Any("userAction.OrgId ", userAction.OrgID))
			clusters, err = infra.GetHandler().GetNormalStatusClusters(ctx, userAction.OrgID.Uint64(), []uint64{})
		}
		if err != nil {
			return entity.CommandStatusFail, nil
		}
		return batchInsertClusters(ctx, dbUserSrv, clusters, userAction, entity.DbRoleAdmin, checkCommands)
	} else if userAction.TargetType == entity.RoleBindingTargetUser {
		// invite to project
		if userAction.ProjectID != 0 {
			tilog.Info(ctx, "query project active cluster ", zap.Any("userAction.ProjectID ", userAction.ProjectID))
			clusters, err = infra.GetHandler().GetNormalStatusClusters(ctx, 0, []uint64{userAction.ProjectID.Uint64()})
			if err != nil {
				return entity.CommandStatusFail, nil
			}
			var dbRole string
			if userAction.Action == entity.CommandActionDelete {
				dbRole = entity.DbRoleAdmin
			} else {
				dbRole, err = getDbRole(ctx, userAction.UserID, entity.RoleBindingTargetUser, userAction.ProjectID, entity.RoleBindingScopeProject)
				if err != nil {
					tilog.Info(ctx, "get db role error, ignore", zap.Any("userAction ", userAction), zap.Any("err", err))
					return entity.CommandStatusIgnore, nil
				}
			}
			return batchInsertClusters(ctx, dbUserSrv, clusters, userAction, dbRole, checkCommands)
			// invite to org
		} else if userAction.OrgID != 0 {
			tilog.Info(ctx, "query org active cluster ", zap.Any("userAction ", userAction))
			if userAction.Action == entity.CommandActionDelete {
				tilog.Info(ctx, "user action delete org db user", zap.Any("userAction", userAction))
				// delete org clusters db account
				clusters, err = infra.GetHandler().GetNormalStatusClusters(ctx, userAction.OrgID.Uint64(), []uint64{})
				if err != nil {
					tilog.Info(ctx, "GetOrgClusterSupportDbUser fail", zap.Error(err))
					return entity.CommandStatusFail, nil
				}
				_, err = batchInsertClusters(ctx, dbUserSrv, clusters, userAction, entity.DbRoleAdmin, checkCommands)
				if err != nil {
					tilog.Info(ctx, "batchInsertClusters fail", zap.Error(err))
					return entity.CommandStatusFail, nil
				}

				tilog.Info(ctx, "user action delete org db user, crate project db user", zap.Any("userAction", userAction))
				// create project clusters db account
				projectSvc, _ := projectsvc.GetProjectSvc()
				projects, err := projectSvc.ListUserVisibleProjects(ctx, userAction.UserID.Uint64(), []uint64{userAction.OrgID.Uint64()})
				if err != nil {
					tilog.Info(ctx, "ListUserVisibleProjects fail", zap.Error(err))
					return entity.CommandStatusFail, nil
				}
				userAction.Action = entity.CommandActionCreate
				for _, project := range projects {
					clusters, err = infra.GetHandler().GetNormalStatusClusters(ctx, 0, []uint64{project.ID.Uint64()})
					if err != nil {
						tilog.Info(ctx, "user action delete org db user, crate project db user, get project cluster err", zap.Any("userAction", userAction), zap.Any("projectID", project.ID))
						return entity.CommandStatusFail, nil
					}
					dbRole, err := getDbRole(ctx, userAction.UserID, entity.RoleBindingTargetUser, project.ID, entity.RoleBindingScopeProject)
					if err != nil {
						tilog.Info(ctx, "get db role error, ignore", zap.Any("userAction ", userAction), zap.Any("projectID ", project.ID), zap.Any("err", err))
						continue
					}
					_, err = batchInsertClusters(ctx, dbUserSrv, clusters, userAction, dbRole, checkCommands)
					if err != nil {
						tilog.Info(ctx, "batchInsertClusters fail", zap.Error(err))
						return entity.CommandStatusFail, nil
					}
				}

			} else if userAction.Action == entity.CommandActionCreate || userAction.Action == entity.CommandActionCheck {

				rbacSvc, _ := rbac2.GetRbacUserSvc()
				userRoles, _ := rbacSvc.ListUserRoles(ctx, userAction.UserID.Uint64(), []entity.ScopeType{entity.RoleBindingScopeOrg}, []entity.ID{userAction.OrgID})
				if len(userRoles) > 0 {
					role := userRoles[0]
					if role.RoleName == entity.RoleOrgOwner {
						tilog.Info(ctx, "this  user should be to create(delete/check) dbuser, query projects cluster ", zap.Any("userId", userAction.UserID), zap.Any("orgId", userAction.OrgID), zap.Any("user.role", role.RoleName))

						clusters, err = infra.GetHandler().GetNormalStatusClusters(ctx, userAction.OrgID.Uint64(), []uint64{})
						if err != nil {
							return entity.CommandStatusFail, nil
						}
						_, err = batchInsertClusters(ctx, dbUserSrv, clusters, userAction, entity.DbRoleAdmin, checkCommands)
						if err != nil {
							return entity.CommandStatusFail, nil
						}

					} else {
						tilog.Info(ctx, "this  user should be to create(delete/check) dbuser, query projects cluster ", zap.Any("userId", userAction.UserID), zap.Any("orgId", userAction.OrgID), zap.Any("user.role", role.RoleName))

						projectSvc, _ := projectsvc.GetProjectSvc()

						projects, err := projectSvc.ListUserVisibleProjects(ctx, userAction.UserID.Uint64(), []uint64{userAction.OrgID.Uint64()})

						tilog.Info(ctx, "get user projects ", zap.Any("userId", userAction.UserID), zap.Any("orgId", userAction.OrgID.Uint64()))

						if err != nil {
							tilog.Info(ctx, "get user projects err ", zap.Error(err))

							tilog.Info(ctx, "user maybe not belong to any project ", zap.Any("userId", userAction.UserID), zap.Any("orgId", userAction.OrgID), zap.Error(err))
							return entity.CommandStatusIgnore, nil
						}
						for _, project := range projects {
							clusters, err = infra.GetHandler().GetNormalStatusClusters(ctx, 0, []uint64{project.ID.Uint64()})
							if err != nil {
								return entity.CommandStatusFail, nil
							}
							dbRole, err := getDbRole(ctx, userAction.UserID, entity.RoleBindingTargetUser, project.ID, entity.RoleBindingScopeProject)
							if err != nil {
								tilog.Info(ctx, "get db role error, ignore", zap.Any("userAction ", userAction), zap.Any("projectID ", project.ID), zap.Any("err", err))
								continue
							}
							_, err = batchInsertClusters(ctx, dbUserSrv, clusters, userAction, dbRole, checkCommands)
							if err != nil {
								return entity.CommandStatusFail, nil
							}

						}
					}
				}
			}
		}
	}

	return entity.CommandStatusComplete, nil

}

func batchInsertClusters(ctx context.Context, dbUserSrv dbuser.DBUserIface, clusters []*core.Cluster, userAction entity.CommandUserAction, dbRole string, checkCommands []entity.CommandDBUserCluster) (entity.CommandStatus, error) {
	if len(clusters) > 0 {
		for i, c := range clusters {
			if userAction.Action == entity.CommandActionCheck && isIgnoreCheck(ctx, userAction, checkCommands, c.Id) {
				continue
			}
			flag := !c.IsDevTier
			var createDBUser *string
			if userAction.TargetType == entity.RoleBindingTargetAPIKey {
				req := utils.GetDBUserReq{
					Email:           userAction.Email,
					ClusterId:       &c.Id,
					IsDedicated:     &flag,
					ServerlessToken: c.ServerlessToken,
					TargetType:      entity.RoleBindingTargetAPIKey.String(),
				}
				dbUser, err := utils.GetDBUser(ctx, req)
				if err != nil {
					return entity.CommandStatusFail, err
				}
				createDBUser = dbUser
			} else {
				req := utils.GetDBUserReq{Email: userAction.Email,
					ClusterId:       &c.Id,
					IsDedicated:     &flag,
					ServerlessToken: c.ServerlessToken}

				dbUser, err := utils.GetDBUser(ctx, req)
				if err != nil {
					return entity.CommandStatusFail, err
				}
				createDBUser = dbUser
			}

			var createDbRole = dbRole
			if c.IsDevTier && dbRole != entity.DbRoleAdmin {
				createDbRole = *c.ServerlessToken + "." + dbRole
			}

			var dbUserType, operatorType = entity.ClusterUserTargetUser, entity.ClusterUserTargetUser
			if userAction.TargetType == entity.RoleBindingTargetAPIKey {
				dbUserType = entity.ClusterUserTargetApiKey
			}
			if userAction.OperatorType == entity.RoleBindingTargetAPIKey {
				operatorType = entity.ClusterUserTargetApiKey
			}

			err := dbUserSrv.CreateCommandDBUserCluster(ctx, c.Id, c.TenantId, userAction.Operator.Uint64(), userAction.UserID.Uint64(), *createDBUser, entity.CommandStatusWaiting, userAction.Action, dbUserType, createDbRole, operatorType)
			if err != nil {
				tilog.Error(ctx, "CreateCommandDBUserCluster err",
					zap.Any("clusterId", c.Id),
					zap.Any("c.TenantId", c.TenantId),
					zap.Any("operator", userAction.Operator.Uint64()),
					zap.Any("uerID", userAction.UserID.Uint64()),
					zap.Any("entity.CommandStatus", entity.CommandStatusWaiting),
					zap.Any("action", userAction.Action),
					zap.Error(err),
				)
				return entity.CommandStatusFail, err
			}
			tilog.Info(ctx, "CreateCommandDBUserCluster ",
				zap.Any("i", i),
				zap.Any("clusterId", c.Id),
				zap.Any("c.TenantId", c.TenantId),
				zap.Any("operator", userAction.Operator.Uint64()),
				zap.Any("uerID", userAction.UserID.Uint64()),
				zap.Any("entity.CommandStatus", entity.CommandStatusWaiting),
				zap.Any("action", userAction.Action))

		}
		return entity.CommandStatusComplete, nil
	}

	tilog.Info(ctx, "GetClusters is null, ignore this command, ",
		zap.Any("orgID", userAction.OrgID),
		zap.Any("operator", userAction.Operator.Uint64()),
		zap.Any("uerID", userAction.UserID.Uint64()),
		zap.Any("action", userAction.Action))
	return entity.CommandStatusIgnore, nil
}

func getDbRole(ctx context.Context, targetID entity.ID, targetType entity.TargetType, scopeID entity.ID, scopeType entity.ScopeType) (string, error) {
	svc, _ := rbac2.GetRbacRoleBindingSvc()
	filter := role.RoleBindingFilter{
		TargetType: []entity.TargetType{targetType},
		TargetIDs:  []entity.ID{targetID},
		ScopeTypes: []entity.ScopeType{scopeType},
		ScopeIDs:   []entity.ID{scopeID},
	}
	roleBindings, err := svc.ListRoleBindings(ctx, filter)
	if err != nil {
		return "", err
	}
	var binding entity.RoleBinding
	if len(roleBindings) > 0 {
		binding = roleBindings[0]
	} else {
		if scopeType == entity.RoleBindingScopeProject {
			projectSvc, _ := projectsvc.GetProjectSvc()
			project, err := projectSvc.GetProjectByID(ctx, scopeID.Uint64())
			if err != nil {
				return "", err
			}
			orgFilter := role.RoleBindingFilter{
				TargetType: []entity.TargetType{targetType},
				TargetIDs:  []entity.ID{targetID},
				ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeOrg},
				ScopeIDs:   []entity.ID{project.TenantID},
			}
			orgRoleBindings, err := svc.ListRoleBindings(ctx, orgFilter)
			if err != nil {
				return "", err
			}
			if len(orgRoleBindings) == 0 {
				return "", errors.New("not found role_bindings")
			}
			binding = orgRoleBindings[0]

		} else {
			return "", errors.New("not found role_bindings")
		}
	}
	dbRole, ok := entity.RoleBindingRole2DbRoleMap[binding.Role]
	if !ok {
		return "", errors.New("not match db role")
	}
	return dbRole, nil
}
func isIgnoreCheck(ctx context.Context, userAction entity.CommandUserAction, checkCommands []entity.CommandDBUserCluster, clusterId uint64) bool {
	isEven := func(command entity.CommandDBUserCluster) bool {
		return userAction.UserID == command.UserID && clusterId == command.ClusterID.Uint64()
	}
	isContain := slice.ContainBy(checkCommands, isEven)
	tilog.Info(ctx, "30 minute checkCommands contain this checkUser result= "+fmt.Sprintf("%t, user_id=%d,culster_id=%d", isContain, userAction.UserID, clusterId))
	return isContain

}
