package tenant

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/tiinf/errs"

	"github.com/duke-git/lancet/v2/slice"
	projectsvc "github.com/tidbcloud/account/internel/svc/project"
	"google.golang.org/protobuf/types/known/timestamppb"

	"github.com/tidbcloud/account/central/utils"
	"github.com/tidbcloud/account/internel/controllers"
	"github.com/tidbcloud/account/internel/svc/org/tenantctx"
	"github.com/tidbcloud/account/thirdparty/jsm"

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

	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/account/commons/config"
	"github.com/tidbcloud/account/idl/pbgen/proto/central"
	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/sdk/auditlog"
	"go.uber.org/zap"
	"google.golang.org/protobuf/types/known/emptypb"
)

func (s *tenantService) ListProjects(ctx context.Context, req *central.ListProjectsReq) (*central.Projects, error) {
	projects, err := s.userContext(ctx).ListProjects(ctx, entity.ID(req.OrgId), req.Types)
	if err != nil {
		return nil, err
	}

	if req.PerPage == 0 {
		return &central.Projects{
			Total: int32(len(projects)),
			Items: projects,
		}, nil
	}

	return &central.Projects{
		Total: int32(len(projects)),
		Items: utils.Pagination(req.Page, req.PerPage, projects).([]*central.Project),
	}, nil
}

func (s *tenantService) ListOrgProjects(ctx context.Context, req *central.ListOrgProjectsReq) (*central.Projects, error) {
	if req.OrgId == 0 {
		return nil, errcode.NewInvalidInputErr(errors.New("org_id is necessary"))
	}
	svc, _ := projectsvc.GetProjectSvc()
	projects, err := svc.ListActiveProjectsByOrgID(ctx, req.OrgId)
	if err != nil {
		tilog.Error(ctx, "ListOrgProjects ListActiveProjectsByOrgID error", zap.Any("req", req), zap.Any("err", err))
		return nil, errcode.NewInternalErr(err)
	}
	var retProjects []*central.Project
	for _, p := range projects {
		if len(req.Types) > 0 && !slice.Contain(req.Types, p.Type) {
			continue
		}
		rp := &central.Project{
			Id:             p.ID.Uint64(),
			Name:           p.Name,
			Type:           p.Type,
			CreatedAt:      timestamppb.New(p.CreatedAt),
			AwsCmekEnabled: p.AwsCmekEnabled,
		}
		retProjects = append(retProjects, rp)
	}
	total := len(retProjects)
	return &central.Projects{
		Total: int32(total),
		Items: retProjects,
	}, nil
}

func (s *tenantService) CreateProject(ctx context.Context, req *central.CreateProjectReq) (*central.ProjectId, error) {
	orgID := entity.ID(req.OrgId)
	userContext := s.userContext(ctx)

	projectName := s.SanitizerPolicy().Sanitize(strings.TrimSpace(req.Name))
	err := controllers.IsValidProjectName(projectName)
	if err != nil {
		return nil, errcode.NewInvalidInputErr(err)
	}
	if req.Type == "" {
		req.Type = entity.ProjectTypeDedicated
	}

	createProjectReq := tenantctx.CreateProjectReq{
		OrgID:                orgID,
		ProjectName:          projectName,
		EnableXAccountDeploy: req.EnableXaccountDeploy,
		AwsCmekEnabled:       req.AwsCmekEnabled,
		Type:                 req.Type,
	}
	project, err := userContext.CreateProject(ctx, createProjectReq)

	details := auditlog.Details{
		auditlog.KeyNewProjectName: projectName,
		auditlog.KeyError:          err,
	}
	e := auditlog.NewEvent(ctx, auditlog.EventCreateProject).SetDetails(details.Build()).SetResult(err == nil).SetOrgID(req.OrgId)
	if err == nil {
		e = e.SetProjectID(project.Id)
	}
	e.Submit() //nolint:errcheck

	if err != nil {
		return project, err
	}

	jsmClient := jsm.GetJsmClientSingleton()
	if jsmClient.IsEnable() {
		notifyCreateProjectErr := jsmClient.NotifyCreateProjectMessage(ctx, project.Id, req.OrgId)
		if notifyCreateProjectErr != nil {
			tilog.Error(ctx, "Sync Jsm failed during new project creation", zap.Any("err", notifyCreateProjectErr))
		}
	}

	return project, err
}

func (s *tenantService) UpdateProject(ctx context.Context, req *central.UpdateProjectReq) (*emptypb.Empty, error) {
	orgID := entity.ID(req.OrgId)
	projectID := entity.ID(req.ProjectId)
	userContext := s.userContext(ctx)

	oldProjectName, err := tenantctx.GetProjectName(ctx, orgID, projectID)
	if err != nil {
		return nil, err
	}

	projectName := s.SanitizerPolicy().Sanitize(strings.TrimSpace(req.Name))
	err = controllers.IsValidProjectName(projectName)
	if err != nil {
		return nil, errcode.NewInvalidInputErr(err)
	}

	updateProjectReq := tenantctx.UpdateProjectReq{
		OrgID:       orgID,
		ProjectID:   projectID,
		ProjectName: projectName,
	}
	err = userContext.UpdateProject(ctx, updateProjectReq)

	details := auditlog.Details{
		auditlog.KeyOldProjectName: oldProjectName,
		auditlog.KeyNewProjectName: projectName,
		auditlog.KeyError:          err,
	}
	auditlog.NewEvent(ctx, auditlog.EventEditProject).SetDetails(details.Build()).SetResult(err == nil).SetOrgID(req.OrgId).SetProjectID(req.ProjectId).Submit() //nolint:errcheck

	if err != nil {
		return nil, err
	}
	return &emptypb.Empty{}, nil
}

func (s *tenantService) DeleteProject(ctx context.Context, req *central.DeleteProjectReq) (*emptypb.Empty, error) {
	// Deleting a project in the console is not allowed, for security risk to return not support temporarily
	return nil, errs.NewBadRequest("not support", nil)
	//orgID := entity.ID(req.OrgId)
	//projectID := entity.ID(req.ProjectId)
	//
	//projectName, err := tenantctx.GetProjectName(ctx, orgID, projectID)
	//if err != nil {
	//	return nil, err
	//}
	//err = tenantctx.DeleteProject(ctx, orgID, projectID)
	//
	//details := auditlog.Details{
	//	auditlog.KeyOldProjectName: projectName,
	//	auditlog.KeyError:          err,
	//}
	//auditlog.NewEvent(ctx, auditlog.EventDeleteProject).SetDetails(details.Build()).SetResult(err == nil).SetOrgID(req.OrgId).SetProjectID(req.ProjectId).Submit() //nolint:errcheck
	//return &emptypb.Empty{}, err
}

/*func (s *tenantService) ListProjectUsers(ctx context.Context, req *central.ListProjectUsersReq) (*central.Users, error) {
	orgID := entity.ID(req.OrgId)
	projectID := entity.ID(req.ProjectId)
	userContext := s.userContext(ctx)
	logger := s.Log(ctx)
	err := userContext.CheckOrgIDAndProjectID(ctx, orgID, projectID)
	if err != nil {
		return nil, err
	}

	ListProjectUsersReq := tenant.ListProjectUsersReq{
		OrgID:     orgID,
		ProjectID: projectID,
	}

	userList, err := userContext.ListProjectUsers(ctx, ListProjectUsersReq)
	if err != nil {
		return nil, err
	}
	users := usersOutput(logger, userList)
	getLoginTime(ctx, &users, req.OrgId)
	users = filterAndSortUsers(ctx, users, req.Emails, req.SortBy, req.Order)
	return &central.Users{
		Total: int32(len(users)),
		Items: utils.Pagination(req.Page, req.PerPage, users).([]*central.RoleBindingUser),
	}, nil
}*/

func (s *tenantService) ListProjectUsers(ctx context.Context, req *central.ListProjectUsersReq) (*central.Users, error) {
	// todo mock offline unused api
	return nil, errs.NewBadRequest("The API has been taken offline", nil)

	//f := userDao.RoleBindingUserFilter{
	//	EmailLike:         req.Email,
	//	RoleBindingStatus: []string{string(entity.Active), entity.RoleBindingStatusInactive},
	//	UserStatus:        []string{string(entity.UserStatusActive), string(entity.UserStatusPending)},
	//	ScopeType:         entity.RoleBindingScopeProject,
	//	ScopeId:           req.ProjectId,
	//	Order:             req.Order,
	//	SortBy:            req.SortBy,
	//}
	//if req.Email != "" {
	//
	//	email, _ := url.QueryUnescape(req.Email)
	//	f.EmailLike = email
	//	req.Email = email
	//}
	//userSvc, _ := user2.GetUserSvc()
	//userList, err := userSvc.ListRoleBindingUsers(ctx, f)
	//
	//if err != nil {
	//	return nil, err
	//}
	////  roleBindingUser convert to central.user and  ignore system user
	//users := usersOutput(userList, []string{})
	//// add login history time
	//loginHistorySvc, _ := historysvc.GetHistorySvc()
	//loginHistorySvc.SetUserLoginTime(ctx, &users)
	//// If it is sorted by login time, it needs to be processed separately
	//userSortByLoginTime(ctx, req.SortBy, req.Order, users)
	//return &central.Users{
	//	Total: int32(len(users)),
	//	Items: utils.Pagination(req.Page, req.PerPage, users).([]*central.User),
	//}, nil
}

func (s *tenantService) InviteUser(ctx context.Context, req *central.InviteUserReq) (*emptypb.Empty, error) {
	// dbUser todo
	orgID := entity.ID(req.OrgId)
	projectID := entity.ID(req.ProjectId)
	userContext := s.userContext(ctx)

	// limitation of send invite-email
	accessor := fmt.Sprint(s.CurrentUser(ctx).ID)
	rateConf := config.Global().RateControlConfig.InviteUser
	now := time.Now()
	rateController := singleton.RateControllerManager.WithScene(rateConf.Scene)
	// target(invitee) is unconcerned. limitation is for inviter.
	if !rateController.AllowAll(ctx, accessor, "", now, rateConf.Rates...) {
		return nil, errcode.NewInvalidInputErr(errors.New("request send invite email too often"))
	}
	rateController.Access(ctx, accessor, req.Email, now)

	emailAddresses := s.SanitizerPolicy().Sanitize(strings.TrimSpace(req.Email))
	emailSlice := strings.Split(emailAddresses, ";")
	var emailList []string
	for _, email := range emailSlice {
		email = strings.ToLower(strings.TrimSpace(email))
		if email != "" {
			emailList = append(emailList, email)
		}
	}

	user := s.CurrentUser(ctx)
	tilog.Info(ctx, "get CurrentUser:", zap.Any("user", user))
	inviteUserReq := tenantctx.InviteUserReq{
		Operator:  user.ID,
		OrgID:     orgID,
		ProjectID: projectID,
		EmailList: emailList,
		Role:      entity.UserRole(req.Role),
		RbacRole:  req.RbacRole,
	}
	err := validateInviteUserReq(inviteUserReq)
	if err != nil {
		return nil, errcode.NewInvalidInputErr(err)
	}
	err = userContext.InviteUser(ctx, inviteUserReq)
	if err != nil {
		return nil, err
	}
	return &emptypb.Empty{}, nil
}

func (s *tenantService) DeleteProjectUser(ctx context.Context, req *central.DeleteProjectUserReq) (*emptypb.Empty, error) {
	// todo mock offline unused api
	return nil, errs.NewBadRequest("The API has been taken offline", nil)

	//orgID := entity.ID(req.OrgId)
	//projectID := entity.ID(req.ProjectId)
	//userID := entity.ID(req.UserId)
	//userContext := s.userContext(ctx)
	//
	//deletedUserEmail, deletedUserRole, userOrgRoleBinding, err := tenantctx.GetUserEmailAndRole(ctx, orgID, userID)
	//if err != nil {
	//	return nil, err
	//}
	//
	//err = userContext.DeleteProjectUser(ctx, orgID, projectID, userID)
	//
	//details := auditlog.Details{
	//	auditlog.KeyUserEmail: deletedUserEmail,
	//	auditlog.KeyRole:      deletedUserRole,
	//	auditlog.KeyError:     err,
	//}
	//eventType := auditlog.EventDeleteInvitationToProject
	//if userOrgRoleBinding.Status == entity.RoleBindingStatusActive {
	//	eventType = auditlog.EventDeleteInvitationToProject
	//}
	//auditlog.NewEvent(ctx, eventType).SetDetails(details.Build()).SetResult(err == nil).SetOrgID(req.OrgId).SetProjectID(req.ProjectId).Submit() //nolint:errcheck
	//if err == nil {
	//	// wangletian update
	//	//insert user_action table ,when delete user from Project
	//	operator, _ := user2.GetUserFromContext(ctx)
	//	userSvc, _ := user2.GetUserSvc()
	//	user, _ := userSvc.GetUserByID(ctx, userID.Uint64())
	//	dbUserSvc, _ := dbuser.GetDBUserSvc()
	//	ignore, err := dbUserSvc.IsIgnoreProjectDbUser(ctx, user.ID.Uint64(), req.ProjectId, orgID.Uint64())
	//	// if err, just to delete
	//	if err != nil || !ignore {
	//		if err = dbUserSvc.CreateCommandUserAction(ctx, operator.ID.Uint64(), user.ID.Uint64(), deletedUserEmail, orgID.Uint64(), req.ProjectId, entity.CommandStatusWaiting, entity.CommandActionDelete, entity.RoleBindingTargetUser, entity.RoleBindingTargetUser); err != nil {
	//			tilog.Error(ctx, "CreateCommandUserAction fail", zap.Error(err))
	//		}
	//	}
	//}
	//
	//// NOTE(bing.wang): Notify user is removed from project
	//jsmClient := jsm.GetJsmClientSingleton()
	//if jsmClient.IsEnable() {
	//	notifyRemoveUserFromOrgErr := jsmClient.NotifyRemoveUserFromProjectMessage(ctx, req.ProjectId, uint64(userID))
	//	if notifyRemoveUserFromOrgErr != nil {
	//		tilog.Error(ctx, "Sync Jsm failed when removing User from org",
	//			zap.Uint64("user", uint64(userID)), zap.Uint64("org", uint64(orgID)), zap.Uint64("project", req.ProjectId))
	//	}
	//}
	//
	//return &emptypb.Empty{}, err
}

func (s *tenantService) UpdateProjectUserRole(ctx context.Context, req *central.UpdateProjectUserRoleReq) (*emptypb.Empty, error) {
	// todo mock offline unused api
	return nil, errs.NewBadRequest("The API has been taken offline", nil)

	//orgID := entity.ID(req.OrgId)
	//projectID := entity.ID(req.ProjectId)
	//userID := entity.ID(req.UserId)
	//role := req.RbacRole
	//
	//userContext := s.userContext(ctx)
	//
	//err := IsValidUserProjectRbacRole(req.RbacRole)
	//if err != nil {
	//	return nil, errcode.NewInvalidInputErr(err)
	//}
	//
	//err = userContext.UpdateProjectUserRole(ctx, orgID, projectID, userID, role)
	//return &emptypb.Empty{}, err
}
