package openapi

import (
	"context"
	"fmt"
	"net/url"

	"github.com/tidbcloud/account/internel/dao/history"
	historysvc "github.com/tidbcloud/account/internel/svc/history"

	"github.com/tidbcloud/account/central/utils"
	"github.com/tidbcloud/account/internel/dao/role"
	userDao "github.com/tidbcloud/account/internel/dao/user"
	"github.com/tidbcloud/tiinf/sdk/auth"

	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/account/internel/svc/rbac"
	usersvc "github.com/tidbcloud/account/internel/svc/user"

	openapiv1veta1 "github.com/tidbcloud/account/idl/pbgen/proto/v1beta1"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/protobuf/types/known/emptypb"
)

type UserService struct {
	openapiv1veta1.UnimplementedUserServiceServer
}

func (s *UserService) GetCurrentUser(ctx context.Context, req *emptypb.Empty) (*openapiv1veta1.User, error) {
	tilog.Info(ctx, "GetCurrentUser request", zap.Any("req", req))
	identInfo, err := auth.GetIdentityInfoFromGrpcContext(ctx)
	if err != nil {
		return nil, errs.NewInternalUnknown(err.Error(), err)
	}
	var userId uint64
	if identInfo.IdentityType == auth.AuthnIdentityTypeUserToken {
		userId = identInfo.UserTokenRsp.User.Id
	} else if identInfo.IdentityType == auth.AuthnIdentityTypeAccessToken {
		userId = identInfo.AccessTokenRsp.AccessTokenIdentity.UserId
	} else {
		return nil, errs.NewBadRequest("auth type not support", nil)
	}
	svc, _ := usersvc.GetUserSvc()
	user, err := svc.GetUserByID(ctx, userId)
	if err != nil {
		tilog.Error(ctx, "GetCurrentUser GetUserByID error", zap.Any("userId", userId), zap.Any("err", err))
		return nil, err
	}
	if user.Status != entity.UserStatusActive {
		tilog.Info(ctx, "GetCurrentUser user is not active", zap.Any("userId", userId))
		return nil, errs.NewBadRequest("user is not active", nil)
	}
	retUser := convEntity2OpenApiUser(user)
	tilog.Info(ctx, "GetCurrentUser response", zap.Any("rsp", retUser))
	return retUser, nil
}

func (s *UserService) GetUser(ctx context.Context, req *openapiv1veta1.GetUserRequest) (*openapiv1veta1.User, error) {
	tilog.Info(ctx, "GetUser request", zap.Any("req", req))
	identInfo, err := getAuthnIdentityAndCheckOrgId(ctx)
	if err != nil {
		tilog.Error(ctx, "GetUser getAuthnIdentityAndCheckOrgId error", zap.Any("err", err))
		return nil, err
	}
	authOrgId := identInfo.OrgID
	userId := req.UserId
	belong, err := checkAccessUserBelongToAuthOrg(ctx, userId, authOrgId)
	tilog.Info(ctx, "GetUser checkAccessUserBelongToAuthOrg result", zap.Any("userId", userId), zap.Any("authOrgId", authOrgId), zap.Any("belong", belong), zap.Any("err", err))
	if err != nil {
		return nil, err
	}
	if !belong {
		return nil, errs.NewForbidden("to get user not belong to the auth org", nil)
	}
	svc, _ := usersvc.GetUserSvc()
	user, err := svc.GetUserByID(ctx, userId)
	if err != nil {
		tilog.Error(ctx, "GetUser GetUserByID error", zap.Any("userId", userId), zap.Any("err", err))
		return nil, err
	}
	retUser := convEntity2OpenApiUser(user)
	tilog.Info(ctx, "GetUser response", zap.Any("rsp", retUser))
	return retUser, nil
}
func (s *UserService) UpdateCurrentUser(ctx context.Context, req *openapiv1veta1.UpdateUserRequest) (*openapiv1veta1.User, error) {
	tilog.Info(ctx, "UpdateCurrentUser request", zap.Any("req", req))
	identInfo, err := getAuthnIdentityAndCheckOrgId(ctx)
	if err != nil {
		tilog.Error(ctx, "UpdateCurrentUser getAuthnIdentityAndCheckOrgId error", zap.Any("err", err))
		return nil, err
	}
	var userId uint64
	if identInfo.IdentityType == auth.AuthnIdentityTypeUserToken {
		userId = identInfo.UserTokenRsp.User.Id
	} else if identInfo.IdentityType == auth.AuthnIdentityTypeAccessToken {
		userId = identInfo.AccessTokenRsp.AccessTokenIdentity.UserId
	} else {
		return nil, errs.NewBadRequest("auth type not support", nil)
	}
	authOrgId := identInfo.OrgID
	belong, err := checkAccessUserBelongToAuthOrg(ctx, userId, authOrgId)
	tilog.Info(ctx, "UpdateCurrentUser checkAccessUserBelongToAuthOrg result", zap.Any("userId", userId), zap.Any("authOrgId", authOrgId), zap.Any("belong", belong), zap.Any("err", err))
	if err != nil {
		return nil, err
	}
	if !belong {
		return nil, errs.NewForbidden("to update user not belong to the auth org", nil)
	}
	svc, _ := usersvc.GetUserSvc()
	user, err := svc.GetUserByID(ctx, userId)
	if err != nil {
		tilog.Error(ctx, "UpdateCurrentUser GetUserByID error", zap.Any("userId", userId), zap.Any("err", err))
		return nil, err
	}
	user.FirstName = req.User.FirstName
	user.LastName = req.User.LastName
	err = svc.UpdateUserByID(ctx, user)
	if err != nil {
		tilog.Error(ctx, "UpdateCurrentUser UpdateUserByID error", zap.Any("toUpdateUser", user), zap.Any("err", err))
		return nil, err
	}
	retUser := convEntity2OpenApiUser(user)
	tilog.Info(ctx, "UpdateCurrentUser response", zap.Any("rsp", retUser))
	return retUser, nil
}
func (s *UserService) DeleteOrgUser(ctx context.Context, req *openapiv1veta1.DeleteOrgUserRequest) (*emptypb.Empty, error) {
	tilog.Info(ctx, "DeleteOrgUser request", zap.Any("req", req))
	identInfo, err := getAuthnIdentityAndCheckOrgId(ctx)
	if err != nil {
		tilog.Error(ctx, "UpdateUser getAuthnIdentityAndCheckOrgId error", zap.Any("err", err))
		return nil, err
	}
	authOrgId := identInfo.OrgID
	tilog.Info(ctx, "DeleteOrgUser get authOrgId", zap.Any("authOrgId", authOrgId))

	userId := req.UserId
	belong, err := checkAccessUserBelongToAuthOrg(ctx, userId, authOrgId)
	tilog.Info(ctx, "DeleteOrgUser checkAccessUserBelongToAuthOrg result", zap.Any("userId", userId), zap.Any("authOrgId", authOrgId), zap.Any("belong", belong), zap.Any("err", err))
	if err != nil {
		return nil, err
	}
	if !belong {
		return nil, errs.NewForbidden("to update user not belong to the auth org", nil)
	}
	operatorId, _ := getOperatorIdType(ctx, identInfo)

	svc, _ := usersvc.GetUserSvc()
	err = svc.RemoveUserFromOrg(ctx, userId, operatorId, authOrgId)
	if err != nil {
		tilog.Error(ctx, "DeleteOrgUser RemoveUserFromOrg error", zap.Any("userId", userId), zap.Any("orgId", authOrgId), zap.Any("err", err))
		return nil, err
	}
	tilog.Info(ctx, "DeleteOrgUser success", zap.Any("userId", userId), zap.Any("orgId", authOrgId))
	return &emptypb.Empty{}, nil
}
func (s *UserService) DeleteProjectUser(ctx context.Context, req *openapiv1veta1.DeleteProjectUserRequest) (*emptypb.Empty, error) {
	tilog.Info(ctx, "DeleteProjectUser request", zap.Any("req", req))
	identInfo, err := getAuthnIdentityAndCheckOrgId(ctx)
	if err != nil {
		tilog.Error(ctx, "DeleteProjectUser getAuthnIdentityAndCheckOrgId error", zap.Any("err", err))
		return nil, err
	}
	authOrgId := identInfo.OrgID
	tilog.Info(ctx, "DeleteProjectUser get authOrgId", zap.Any("authOrgId", authOrgId))

	projectId := req.ProjectId
	userId := req.UserId
	belong, err := checkAccessUserBelongToAuthOrg(ctx, userId, authOrgId)
	tilog.Info(ctx, "DeleteProjectUser checkAccessUserBelongToAuthOrg result", zap.Any("userId", userId), zap.Any("authOrgId", authOrgId), zap.Any("belong", belong), zap.Any("err", err))
	if err != nil {
		return nil, err
	}
	if !belong {
		return nil, errs.NewForbidden("to delete user not belong to the org", nil)
	}
	operatorId, _ := getOperatorIdType(ctx, identInfo)

	svc, _ := usersvc.GetUserSvc()
	err = svc.RemoveUserFromProject(ctx, userId, operatorId, projectId)
	if err != nil {
		tilog.Error(ctx, "DeleteProjectUser RemoveUserFromProject error", zap.Any("userId", userId), zap.Any("projectId", projectId), zap.Any("orgId", authOrgId), zap.Any("err", err))
		return nil, err
	}
	tilog.Info(ctx, "DeleteProjectUser success", zap.Any("userId", userId), zap.Any("projectId", projectId), zap.Any("orgId", authOrgId))
	return &emptypb.Empty{}, nil
}
func (s *UserService) ListUsers(ctx context.Context, req *openapiv1veta1.ListUsersRequest) (*openapiv1veta1.ListUsersResponse, error) {
	tilog.Info(ctx, "ListUsers request", zap.Any("req", req))
	identInfo, err := getAuthnIdentityAndCheckOrgId(ctx)
	if err != nil {
		tilog.Error(ctx, "ListUsers getAuthnIdentityAndCheckOrgId error", zap.Any("err", err))
		return nil, err
	}
	authOrgId := identInfo.OrgID
	tilog.Info(ctx, "ListUsers get authOrgId", zap.Any("authOrgId", authOrgId))

	f := userDao.RoleBindingUserFilter{
		RoleBindingStatus: []string{string(entity.Active), entity.RoleBindingStatusInactive},
		UserStatus:        []string{string(entity.UserStatusActive), string(entity.UserStatusPending)},
		ScopeType:         entity.RoleBindingScopeOrg,
		ScopeId:           authOrgId,
	}
	if req.ProjectId != 0 {
		f.ScopeType = entity.RoleBindingScopeProject
		f.ScopeId = uint64(req.ProjectId)
	}
	if req.Email != "" {
		email, _ := url.QueryUnescape(req.Email)
		f.EmailLike = email
		req.Email = email
	}
	userSvc, _ := usersvc.GetUserSvc()
	userList, err := userSvc.ListRoleBindingUsers(ctx, f)
	if err != nil {
		tilog.Error(ctx, "ListUsers ListRoleBindingUsers error", zap.Any("filter", f), zap.Any("err", err))
		return nil, err
	}
	var noSystemUsers []userDao.RoleBindingUser
	for _, ru := range userList {
		if ru.FirstName == "system" && ru.LastName == "user" {
			continue
		}
		noSystemUsers = append(noSystemUsers, ru)
	}

	totalSize, pageUsers, nextPageToken, err := getPageUsers(noSystemUsers, req.PageToken, req.Skip, req.PageSize)
	if err != nil {
		tilog.Error(ctx, "ListUsers getPageUsers error", zap.Any("users", noSystemUsers), zap.Any("err", err))
		return nil, err
	}

	var userIDs []entity.ID
	for _, u := range pageUsers {
		userIDs = append(userIDs, entity.ID(u.UserId))
	}
	historyMap := make(map[uint64]*entity.EventHistoryLast)
	if len(userIDs) > 0 {
		loginHistorySvc, _ := historysvc.GetHistorySvc()

		historyFilter := history.EventHistoryFilter{
			IdentityIDs:   userIDs,
			IdentityTypes: []entity.HistoryIdentityType{entity.IdentityTypeUser},
			Events:        []entity.HistoryEvent{entity.HistoryEventLogin}}

		histories, err := loginHistorySvc.ListEventHistoryLast(ctx, historyFilter)
		if err != nil {
			tilog.Error(ctx, "ListUsers ListEventHistoryLast error", zap.Any("historyFilter", historyFilter), zap.Any("err", err))
			return nil, err
		}
		for i, h := range histories {
			historyMap[h.IdentityID.Uint64()] = &histories[i]
		}
	}

	var returnUsers []*openapiv1veta1.User
	for _, k := range pageUsers {
		loginTime := ""
		h := historyMap[k.UserId]
		if k.Status == string(entity.Active) && h != nil && h.UpdatedAt != nil && !h.UpdatedAt.IsZero() {
			loginTime = formatTime(*h.UpdatedAt)
		}
		rk := &openapiv1veta1.User{
			FirstName:         k.FirstName,
			LastName:          k.LastName,
			Email:             k.Email,
			UserId:            fmt.Sprintf("%d", k.UserId),
			Name:              fmt.Sprintf("users/%d", k.UserId),
			RoleBindingStatus: k.Status,
			RoleBindingRole:   k.Role,
			InviteTime:        formatTime(k.InviteTime),
			LastLoginTime:     loginTime,
		}
		returnUsers = append(returnUsers, rk)
	}

	rsp := &openapiv1veta1.ListUsersResponse{
		Users:         returnUsers,
		NextPageToken: nextPageToken,
		TotalSize:     totalSize,
	}
	tilog.Info(ctx, "ListUsers response", zap.Any("rsp", rsp))
	return rsp, nil
}

func getPageUsers(filterUsers []userDao.RoleBindingUser, pageToken string, skip int32, pageSize int32) (int32, []userDao.RoleBindingUser, string, error) {
	totalSize, pageItems, nextPageToken, err := utils.GetPageItems(filterUsers, pageToken, int(skip), int(pageSize))
	if err != nil {
		return 0, nil, "", err
	}
	return int32(totalSize), pageItems.([]userDao.RoleBindingUser), nextPageToken, nil
}

func checkAccessUserBelongToAuthOrg(ctx context.Context, accessUserId, authOrgId uint64) (bool, error) {
	svc, err := rbac.GetRbacRoleBindingSvc()
	if err != nil {
		return false, err
	}
	roleBindings, err := svc.ListRoleBindings(ctx, role.RoleBindingFilter{
		ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeOrg},
		ScopeIDs:   []entity.ID{entity.ID(authOrgId)},
		TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
		TargetIDs:  []entity.ID{entity.ID(accessUserId)},
	})
	if err != nil {
		return false, err
	}
	if len(roleBindings) > 0 {
		return true, nil
	}
	return false, nil
}

func convEntity2OpenApiUser(user *entity.User) *openapiv1veta1.User {
	return &openapiv1veta1.User{
		FirstName: user.FirstName,
		LastName:  user.LastName,
		Email:     user.Email,
		UserId:    fmt.Sprintf("%d", user.ID.Uint64()),
		Name:      fmt.Sprintf("users/%d", user.ID.Uint64()),
	}
}

func getOperatorIdType(ctx context.Context, identInfo *auth.AuthnIdentityInfo) (operatorId uint64, operatorType int32) {
	if identInfo.IdentityType == auth.AuthnIdentityTypeApiKeyAK {
		return identInfo.ApiKeyRsp.ApiKey.Id, entity.OperatorTypeApiKey
	}
	if identInfo.IdentityType == auth.AuthnIdentityTypeAccessToken {
		return identInfo.AccessTokenRsp.AccessTokenIdentity.UserId, 0
	}
	return identInfo.UserTokenRsp.User.Id, 0
}
