package accountimpl

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

	"github.com/sercand/kuberesolver/v5"
	accountsdk "github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/account/idl/pbgen/proto/acl"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	authsdk "github.com/tidbcloud/tiinf/sdk/auth"
	tigrpc "github.com/tidbcloud/tiinf/transport/tigrpc/client"
	"go.uber.org/zap"

	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-portal/domain/adapter/account"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-portal/pkg/config"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	utils "github.com/tidbcloud/tidb-management-service/utilities/grpc"
)

type accountImpl struct {
	accountCli accountsdk.AccountAPIServiceClient
	aclCli     acl.AccessServiceClient
}

func newAccountImpl() account.Client {
	ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
	defer cancel()

	accAddr := config.Global().AccountService.Address
	accountConn, err := tigrpc.NewClientContext(ctx, accAddr, getConnOpts(accAddr, defaultTimeout)...)
	if err != nil {
		panic("dial account-svc: " + err.Error())
	}

	aclAddr := config.Global().AccountService.AclAddress
	aclConn, err := tigrpc.NewClientContext(ctx, aclAddr, getConnOpts(aclAddr, defaultTimeout)...)
	if err != nil {
		panic("dial account-acl-svc: " + err.Error())
	}

	accountCli := accountsdk.NewAccountAPIServiceClient(accountConn)
	// init auth sdk
	authsdk.InitAccountClientByClient(accountCli, defaultTimeout)
	// init auth sdk
	return &accountImpl{
		accountCli: accountCli,
		aclCli:     acl.NewAccessServiceClient(aclConn),
	}
}

func getConnOpts(addr string, timeout time.Duration) []tigrpc.Option {
	opts := []tigrpc.Option{
		tigrpc.WithPrometheus(),
		tigrpc.WithTracing(),
		tigrpc.WithUnaryInterceptors(utils.ClientPayloadLogInterceptor, utils.TimeoutInterceptor(timeout)),
	}
	if strings.HasPrefix(addr, "kubernetes://") {
		kuberesolver.RegisterInCluster()
		opts = append(opts, tigrpc.WithLoadBalance())
	}
	return opts
}

func (a *accountImpl) GetOrCreateSystemUser(ctx context.Context, orgID uint64) (*accountsdk.User, bizerr.BizError) {
	req := &accountsdk.GetOrCreateSystemUserRequest{
		OrgId: orgID,
	}

	resp, err := a.accountCli.GetOrCreateSystemUser(ctx, req)
	if err != nil {
		tilog.WithContextL(ctx).Error("GetOrCreateSystemUser failed", zap.Uint64("OrgID", orgID), zap.Error(err))
		return nil, bizerr.WrapGrpcError(err, "GetOrCreateSystemUser")
	}
	if resp.BaseResp.ErrCode != 0 {
		return resp.User, bizerr.New(bizerr.ErrCode(resp.BaseResp.ErrCode), "%s", resp.BaseResp.ErrMsg)
	}

	return resp.User, nil
}

func (a *accountImpl) GetOpenAPIAccess(
	ctx context.Context,
	publicKey string,
) (*acl.GetOpenAPIAccessResp, bizerr.BizError) {
	req := &acl.GetOpenAPIAccessReq{
		PublicKey: publicKey,
	}

	logger := tilog.WithContextL(ctx)
	resp, err := a.aclCli.GetOpenAPIAccess(ctx, req)
	if err != nil {
		logger.Error("GetOpenAPIAccess failed", zap.String("publickey", publicKey), zap.Error(err))
		return nil, bizerr.WrapGrpcError(err, "GetOpenAPIAccess")
	}

	if resp.Org == nil {
		err := fmt.Errorf("failed to retrieve org: public key=%s", publicKey)
		logger.Error(err.Error())
		return nil, bizerr.WrapGrpcError(err, "GetOpenAPIAccess")
	}
	if len(resp.Projects) == 0 {
		err := fmt.Errorf("failed to retrieve projects: public key=%s", publicKey)
		logger.Error(err.Error())
		return nil, bizerr.WrapGrpcError(err, "GetOpenAPIAccess")
	}

	return resp, nil
}

func (a *accountImpl) GetProjectByID(ctx context.Context, id uint64) (*accountsdk.Project, bizerr.BizError) {
	req := &accountsdk.GetProjectByIDRequest{Id: id}
	resp, err := a.accountCli.GetProjectByID(ctx, req)
	if err != nil {
		return nil, bizerr.WrapGrpcError(err, "GetProjectByID")
	}
	if resp.BaseResp.ErrCode != 0 {
		return nil, bizerr.New(bizerr.ErrCode(resp.BaseResp.ErrCode), "%s", resp.BaseResp.ErrMsg)
	}

	return resp.GetProject(), nil
}

func (a *accountImpl) GetOrgByID(ctx context.Context, id uint64) (*accountsdk.Org, bizerr.BizError) {
	req := &accountsdk.GetOrgByIDRequest{Id: id}
	resp, err := a.accountCli.GetOrgByID(ctx, req)
	if err != nil {
		return nil, bizerr.WrapGrpcError(err, "GetOrgByID")
	}
	if resp.BaseResp.ErrCode != 0 {
		return nil, bizerr.New(bizerr.ErrCode(resp.BaseResp.ErrCode), "%s", resp.BaseResp.ErrMsg)
	}

	return resp.GetOrg(), nil
}

func (a *accountImpl) ListOrgsByUser(ctx context.Context, userID uint64) ([]*accountsdk.Org, bizerr.BizError) {
	req := &accountsdk.ListOrgsByUserRequest{UserId: userID}
	resp, err := a.accountCli.ListOrgsByUser(ctx, req)
	if err != nil {
		return nil, bizerr.WrapGrpcError(err, "ListOrgsByUser")
	}
	if resp.BaseResp.ErrCode != 0 {
		return nil, bizerr.New(bizerr.ErrCode(resp.BaseResp.ErrCode), "%s", resp.BaseResp.ErrMsg)
	}

	return resp.GetOrgs(), nil
}

func (a *accountImpl) ListProjectsByOrgsInUser(
	ctx context.Context,
	userID, orgID uint64,
) ([]*accountsdk.Project, bizerr.BizError) {
	req := &accountsdk.ListProjectsByOrgsInUserRequest{UserId: userID, OrgIds: []uint64{orgID}}
	resp, err := a.accountCli.ListProjectsByOrgsInUser(ctx, req)
	if err != nil {
		return nil, bizerr.WrapGrpcError(err, "ListProjectsByOrgsInUser")
	}
	if resp.BaseResp.ErrCode != 0 {
		return nil, bizerr.New(bizerr.ErrCode(resp.BaseResp.ErrCode), "%s", resp.BaseResp.ErrMsg)
	}

	for _, orgProject := range resp.Projects {
		if orgProject.OrgId == orgID {
			return orgProject.Projects, nil
		}
	}

	return []*accountsdk.Project{}, bizerr.New(bizerr.ErrCodeRecordNotFound, "no projects found")
}

func (a *accountImpl) ListProjects(ctx context.Context, ids []uint64) ([]*accountsdk.Project, bizerr.BizError) {
	req := &accountsdk.ProjectFilter{Ids: ids}
	resp, err := a.accountCli.ListProjects(ctx, req)
	if err != nil {
		return nil, bizerr.WrapGrpcError(err, "ListProjects")
	}
	if resp.BaseResp.ErrCode != 0 {
		return nil, bizerr.New(bizerr.ErrCode(resp.BaseResp.ErrCode), "%s", resp.BaseResp.ErrMsg)
	}

	return resp.Projects, nil
}

func (a *accountImpl) GetApiKeyByAccessKey(
	ctx context.Context,
	accessKey string,
) (*accountsdk.GetApiKeyByAccessKeyRsp, bizerr.BizError) {
	req := &accountsdk.GetApiKeyByAccessKeyReq{AccessKey: accessKey}
	resp, err := a.accountCli.GetApiKeyByAccessKey(ctx, req)
	if err != nil {
		return nil, bizerr.WrapGrpcError(err, "GetApiKeyByAccessKey")
	}
	if resp.BaseResp.ErrCode != 0 {
		return nil, bizerr.New(bizerr.ErrCode(resp.BaseResp.ErrCode), "%s", resp.BaseResp.ErrMsg)
	}

	return resp, nil
}

func (a *accountImpl) GetUserByEmail(ctx context.Context, email string) (*accountsdk.User, bizerr.BizError) {
	req := &accountsdk.GetUserByEmailRequest{Email: email}
	resp, err := a.accountCli.GetUserByEmail(ctx, req)
	if err != nil {
		return nil, bizerr.WrapGrpcError(err, "GetUserByEmail")
	}
	if resp.BaseResp.ErrCode != 0 {
		return nil, bizerr.New(bizerr.ErrCode(resp.BaseResp.ErrCode), "%s", resp.BaseResp.ErrMsg)
	}

	return resp.User, nil
}

func (a *accountImpl) GetAccountClient() accountsdk.AccountAPIServiceClient {
	return a.accountCli
}
