package openapi

import (
	"context"
	"fmt"
	"strings"

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

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

	"github.com/spf13/viper"

	openapiv1veta1 "github.com/tidbcloud/account/idl/pbgen/proto/v1beta1"
	"github.com/tidbcloud/account/internel/controllers"
	"github.com/tidbcloud/account/internel/dao/entity"
	orgsvc "github.com/tidbcloud/account/internel/svc/org"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/sdk/auth"
	"go.uber.org/zap"
)

const (
	closePartnerOrgInviteUserDomainLimitKey = "partner.closePartnerOrgInviteUserDomainLimit"
)

type InvitationService struct {
	openapiv1veta1.UnimplementedInvitationServiceServer
}

func (s *InvitationService) CreateOrgInvitation(ctx context.Context, req *openapiv1veta1.CreateOrgInvitationRequest) (*openapiv1veta1.CreateInvitationResponse, error) {
	tilog.Info(ctx, "CreateOrgInvitation request", zap.Any("req", req))
	identInfo, err := getAuthnIdentityAndCheckOrgId(ctx)
	if err != nil {
		tilog.Error(ctx, "CreateOrgInvitation getAuthnIdentityAndCheckOrgId error", zap.Any("err", err))
		return nil, utils.ConvertErr2GrpcErr(err)
	}
	authOrgId := identInfo.OrgID
	tilog.Info(ctx, "CreateOrgInvitation get auth orgId", zap.Any("orgId", authOrgId))
	orgSvc, _ := orgsvc.GetOrgSvc()
	org, err := orgSvc.GetOrgByID(ctx, authOrgId)
	if err != nil {
		tilog.Error(ctx, "CreateOrgInvitation GetOrgByID error", zap.Any("err", err))
		return nil, utils.ConvertErr2GrpcErr(err)
	}
	partnerOrg := comsvc.IsPartnerOrg(org.OrgTag)

	err = checkInviteesEmailsValid(ctx, req.InviteesEmails, getPartnerAllowEmailDomains(org.ID.Uint64(), org.Email), partnerOrg)
	if err != nil {
		tilog.Info(ctx, "CreateOrgInvitation checkInviteesEmailsValid error", zap.Any("inviteesEmails", req.InviteesEmails), zap.Any("err", err))
		return nil, utils.ConvertErr2GrpcErr(err)
	}

	inviteParam := &oneconsole.InviteUsersParam{
		EmailList: req.InviteesEmails,
		OrgID:     authOrgId,
		ORole:     req.Role,
	}

	operator := getOperator(identInfo)

	oneConsoleUserSvc := oneconsole.GetOneConsoleUserSvc()
	rets, err := oneConsoleUserSvc.InviteUsersToOrg(ctx, inviteParam, operator)
	if err != nil {
		return nil, utils.ConvertErr2GrpcErr(err)
	}

	var invitationRets []*openapiv1veta1.Invitation
	for _, ret := range rets {
		r := &openapiv1veta1.Invitation{
			InviteesEmail: ret.Email,
			OrgId:         fmt.Sprintf("%d", authOrgId),
			Role:          req.Role,
			Result:        openapiv1veta1.Invitation_RESULT_SUCCESS,
			Message:       "SUCCESS",
		}
		if ret.Code != entity.InviteUserSuccess {
			r.Result = openapiv1veta1.Invitation_RESULT_FAIL
			r.Message = ret.Message
		}
		invitationRets = append(invitationRets, r)
	}

	rsp := &openapiv1veta1.CreateInvitationResponse{
		Invitations: invitationRets,
	}
	tilog.Info(ctx, "CreateOrgInvitation response", zap.Any("rsp", rsp))

	return rsp, nil
}

func (s *InvitationService) CreateProjectInvitation(ctx context.Context, req *openapiv1veta1.CreateProjectInvitationRequest) (*openapiv1veta1.CreateInvitationResponse, error) {
	tilog.Info(ctx, "CreateProjectInvitation request", zap.Any("req", req))
	identInfo, err := getAuthnIdentityAndCheckOrgId(ctx)
	if err != nil {
		tilog.Error(ctx, "CreateProjectInvitation getAuthnIdentityAndCheckOrgId error", zap.Any("err", err))
		return nil, utils.ConvertErr2GrpcErr(err)
	}
	authOrgId := identInfo.OrgID
	tilog.Info(ctx, "CreateProjectInvitation get auth orgId", zap.Any("orgId", authOrgId))
	orgSvc, _ := orgsvc.GetOrgSvc()
	org, err := orgSvc.GetOrgByID(ctx, authOrgId)
	if err != nil {
		tilog.Error(ctx, "CreateProjectInvitation GetOrgByID error", zap.Any("err", err))
		return nil, utils.ConvertErr2GrpcErr(err)
	}
	err = checkInviteesEmailsValid(ctx, req.InviteesEmails, getPartnerAllowEmailDomains(org.ID.Uint64(), org.Email), comsvc.IsPartnerOrg(org.OrgTag))
	if err != nil {
		tilog.Info(ctx, "CreateProjectInvitation checkInviteesEmailsValid error", zap.Any("inviteesEmails", req.InviteesEmails), zap.Any("err", err))
		return nil, utils.ConvertErr2GrpcErr(err)
	}

	oneConsoleUserSvc := oneconsole.GetOneConsoleUserSvc()
	operator := getOperator(identInfo)

	var invitationRets []*openapiv1veta1.Invitation

	for _, email := range req.InviteesEmails {
		user, oBinding, pBinding, err := comsvc.GetUserAndScopeRoleBinding(ctx, email, authOrgId, req.ProjectId, []string{entity.RoleBindingStatusActive})
		if err != nil {
			tilog.Error(ctx, "CreateProjectInvitation GetUserAndScopeRoleBinding error", zap.Any("email", email), zap.Any("err", err))
			ret := &openapiv1veta1.Invitation{
				InviteesEmail: email,
				OrgId:         fmt.Sprintf("%d", authOrgId),
				ProjectId:     fmt.Sprintf("%d", req.ProjectId),
				Role:          req.Role,
				Result:        openapiv1veta1.Invitation_RESULT_FAIL,
				Message:       err.Error(),
			}
			invitationRets = append(invitationRets, ret)
			continue
		}
		if pBinding != nil {
			ret := &openapiv1veta1.Invitation{
				InviteesEmail: email,
				OrgId:         fmt.Sprintf("%d", authOrgId),
				ProjectId:     fmt.Sprintf("%d", req.ProjectId),
				Role:          req.Role,
				Result:        openapiv1veta1.Invitation_RESULT_FAIL,
				Message:       "The email is already in the project",
			}
			invitationRets = append(invitationRets, ret)
			continue
		}
		if oBinding != nil {
			_, err := comsvc.UpdateIdentitySpecRoles(ctx, &comsvc.AssignIdentRolesParam{
				IdentTye: entity.RoleBindingTargetUser,
				IdentID:  user.ID.Uint64(),
				IdentVal: email,
				OrgID:    authOrgId,
				PRoles: map[uint64]string{
					req.ProjectId: req.Role,
				},
				Status: oBinding.Status,
			})
			if err != nil {
				ret := &openapiv1veta1.Invitation{
					InviteesEmail: email,
					OrgId:         fmt.Sprintf("%d", authOrgId),
					ProjectId:     fmt.Sprintf("%d", req.ProjectId),
					Role:          req.Role,
					Result:        openapiv1veta1.Invitation_RESULT_FAIL,
					Message:       err.Error(),
				}
				invitationRets = append(invitationRets, ret)
			} else {
				ret := &openapiv1veta1.Invitation{
					InviteesEmail: email,
					OrgId:         fmt.Sprintf("%d", authOrgId),
					Role:          req.Role,
					Result:        openapiv1veta1.Invitation_RESULT_SUCCESS,
					Message:       "SUCCESS",
				}
				invitationRets = append(invitationRets, ret)
			}
		} else {
			ret, err := oneConsoleUserSvc.InviteUsersToOrg(ctx, &oneconsole.InviteUsersParam{
				EmailList: []string{email},
				OrgID:     authOrgId,
				ORole:     entity.RoleOrgMember,
				PRoles: map[uint64]string{
					req.ProjectId: req.Role,
				},
			}, operator)
			if err != nil {
				r := &openapiv1veta1.Invitation{
					InviteesEmail: email,
					OrgId:         fmt.Sprintf("%d", authOrgId),
					ProjectId:     fmt.Sprintf("%d", req.ProjectId),
					Role:          req.Role,
					Result:        openapiv1veta1.Invitation_RESULT_FAIL,
					Message:       err.Error(),
				}
				invitationRets = append(invitationRets, r)
			} else {
				retCode := ret[0].Code
				if retCode == entity.InviteUserSuccess {
					r := &openapiv1veta1.Invitation{
						InviteesEmail: email,
						OrgId:         fmt.Sprintf("%d", authOrgId),
						Role:          req.Role,
						Result:        openapiv1veta1.Invitation_RESULT_SUCCESS,
						Message:       "SUCCESS",
					}
					invitationRets = append(invitationRets, r)
				} else {
					r := &openapiv1veta1.Invitation{
						InviteesEmail: email,
						OrgId:         fmt.Sprintf("%d", authOrgId),
						ProjectId:     fmt.Sprintf("%d", req.ProjectId),
						Role:          req.Role,
						Result:        openapiv1veta1.Invitation_RESULT_FAIL,
						Message:       ret[0].Message,
					}
					invitationRets = append(invitationRets, r)
				}
			}
		}
	}

	rsp := &openapiv1veta1.CreateInvitationResponse{
		Invitations: invitationRets,
	}
	tilog.Info(ctx, "CreateProjectInvitation response", zap.Any("rsp", rsp))

	return rsp, nil
}

func checkInviteesEmailsValid(ctx context.Context, inviteesEmails []string, domains []string, isPartnerOrg bool) error {
	if len(inviteesEmails) == 0 {
		return errs.NewBadRequest("invitees_emails is empty", nil)
	}

	var checkDomain = true
	closeDomainLimit := viper.GetString(closePartnerOrgInviteUserDomainLimitKey)
	tilog.Info(ctx, fmt.Sprintf("viper get closePartnerOrgInviteUserDomainLimit result: %s", closeDomainLimit))
	if closeDomainLimit == "1" {
		checkDomain = false
	}

	var emailSuffix []string
	for _, domain := range domains {
		suffix := fmt.Sprintf("@%s", domain)
		emailSuffix = append(emailSuffix, suffix)
	}
	for _, email := range inviteesEmails {
		if !controllers.IsValidEmail(email) {
			return errs.NewBadRequest(fmt.Sprintf("email %s is invalid", email), nil)
		}
		if isPartnerOrg && checkDomain {
			var matched = false
			for _, suffix := range emailSuffix {
				if strings.HasSuffix(email, suffix) {
					matched = true
					break
				}
			}
			if !matched {
				return errs.NewBadRequest(fmt.Sprintf("the domain of email %s is invalid", email), nil)
			}
		}
	}
	return nil
}

func getOperator(identInfo *auth.AuthnIdentityInfo) *comsvc.Operator {
	if identInfo.IdentityType == auth.AuthnIdentityTypeApiKeyAK {
		return &comsvc.Operator{
			OpTye: entity.RoleBindingTargetAPIKey,
			OpID:  identInfo.IdentityID,
			OpVal: identInfo.IdentityVal,
		}
	}
	if identInfo.IdentityType == auth.AuthnIdentityTypeAccessToken {
		return &comsvc.Operator{
			OpTye: entity.RoleBindingTargetUser,
			OpID:  identInfo.AccessTokenRsp.AccessTokenIdentity.UserId,
			OpVal: identInfo.AccessTokenRsp.AccessTokenIdentity.UserEmail,
		}
	}
	return &comsvc.Operator{
		OpTye: entity.RoleBindingTargetUser,
		OpID:  identInfo.UserTokenRsp.User.Id,
		OpVal: identInfo.UserTokenRsp.User.Email,
	}
}
