package account

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

	"github.com/sercand/kuberesolver/v5"
	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	tigrpc "github.com/tidbcloud/tiinf/transport/tigrpc/client"
	"go.uber.org/zap"
	"google.golang.org/grpc"

	"github.com/tidbcloud/tidb-management-service/tempo-wrapper/internal/config"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	grpcutil "github.com/tidbcloud/tidb-management-service/utilities/grpc"
)

var (
	accountProviderSvcOnce sync.Once
	accountProviderSvcCli  account.AccountAPIServiceClient
)

func getAccountProviderSvcClient() account.AccountAPIServiceClient {
	accountProviderSvcOnce.Do(func() {
		timeout := time.Duration(config.Config().Grpc.Timeout) * time.Second
		ctx, cancel := context.WithTimeout(context.Background(), timeout)
		defer cancel()

		addr := config.Config().Grpc.AccountProviderSvcAddr
		interceptors := []grpc.UnaryClientInterceptor{
			grpcutil.TimeoutInterceptor(timeout),
			grpcutil.ClientPayloadLogInterceptor,
		}
		opts := []tigrpc.Option{
			tigrpc.WithDefault(),
			tigrpc.WithUnaryInterceptors(interceptors...),
		}
		if strings.HasPrefix(addr, "kubernetes://") {
			kuberesolver.RegisterInCluster()
			opts = append(opts, tigrpc.WithLoadBalance())
		}
		conn, err := tigrpc.NewClientContext(ctx, addr, opts...)
		if err != nil {
			panic(fmt.Sprintf("dial account-provider-svc: %s", err))
		}

		accountProviderSvcCli = account.NewAccountAPIServiceClient(conn)
	})

	return accountProviderSvcCli
}

func GetAccountProviderSvcCli() account.AccountAPIServiceClient {
	return getAccountProviderSvcClient()
}

func GetAuthUser(ctx context.Context, req *account.GetAuthUserRequest) (*account.GetAuthUserResponse, bizerr.BizError) {
	client := getAccountProviderSvcClient()

	resp, err := client.GetAuthUser(ctx, req)
	if err != nil {
		return resp, bizerr.WrapGrpcError(err, "GetAuthUser")
	}
	if resp.BaseResp.ErrCode != 0 {
		return resp, bizerr.New(bizerr.ErrCode(resp.BaseResp.ErrCode), "%s", resp.BaseResp.ErrMsg)
	}
	return resp, nil
}

func GetOrCreateSystemUser(
	ctx context.Context,
	req *account.GetOrCreateSystemUserRequest,
) (*account.GetOrCreateSystemUserResponse, bizerr.BizError) {
	client := getAccountProviderSvcClient()

	resp, err := client.GetOrCreateSystemUser(ctx, req)
	if err != nil {
		return resp, bizerr.WrapGrpcError(err, "GetOrCreateSystemUser")
	}
	if resp.BaseResp.ErrCode != 0 {
		return resp, bizerr.New(bizerr.ErrCode(resp.BaseResp.ErrCode), "%s", resp.BaseResp.ErrMsg)
	}
	return resp, nil
}

func GetUserByID(ctx context.Context, id uint64) (*account.User, bizerr.BizError) {
	getUserRequest := &account.GetUserByIDRequest{
		UserId: id,
	}
	user, err := _getUserByID(ctx, getUserRequest)
	if err != nil {
		tilog.Error(ctx, "fail to call account-svc GetUserByID API", zap.String("id", fmt.Sprint(id)), zap.Error(err))
		return nil, err
	}

	return user.User, nil
}

func _getUserByID(
	ctx context.Context,
	req *account.GetUserByIDRequest,
) (*account.GetUserByIDResponse, bizerr.BizError) {
	client := getAccountProviderSvcClient()

	resp, err := client.GetUserByID(ctx, req)
	if err != nil {
		return resp, bizerr.WrapGrpcError(err, "GetUserByID")
	}
	if resp.BaseResp.ErrCode != 0 {
		return resp, bizerr.New(bizerr.ErrCode(resp.BaseResp.ErrCode), "%s", resp.BaseResp.ErrMsg)
	}
	return resp, nil
}

func ListProjects(ctx context.Context, ids []uint64) ([]*account.Project, bizerr.BizError) {
	listProjectsRequest := &account.ProjectFilter{
		Ids: ids,
	}
	projects, err := _listProjects(ctx, listProjectsRequest)
	if err != nil {
		tilog.Error(ctx, "fail to call account-svc ListProjects API", zap.Any("ids", ids), zap.Error(err))
		return nil, err
	}
	return projects.Projects, nil
}

func _listProjects(ctx context.Context, req *account.ProjectFilter) (*account.ListProjectsResponse, bizerr.BizError) {
	client := getAccountProviderSvcClient()

	resp, err := client.ListProjects(ctx, req)
	if err != nil {
		return resp, bizerr.WrapGrpcError(err, "ListProjects")
	}
	if resp.BaseResp.ErrCode != 0 {
		return resp, bizerr.New(bizerr.ErrCode(resp.BaseResp.ErrCode), "%s", resp.BaseResp.ErrMsg)
	}
	return resp, nil
}

func GetOrgByID(ctx context.Context, id uint64) (*account.Org, bizerr.BizError) {
	tilog.Info(ctx, "request to account-svc GetOrgByID API", zap.String("id", fmt.Sprint(id)))
	getOrgByIDRequest := &account.GetOrgByIDRequest{
		Id: id,
	}
	org, err := _getOrgByID(ctx, getOrgByIDRequest)
	if err != nil {
		tilog.Error(ctx, "fail to call account-svc GetOrgByID API", zap.String("id", fmt.Sprint(id)), zap.Error(err))
		return nil, err
	}
	return org.Org, nil
}

func _getOrgByID(ctx context.Context, req *account.GetOrgByIDRequest) (*account.GetOrgByIDResponse, bizerr.BizError) {
	client := getAccountProviderSvcClient()
	resp, err := client.GetOrgByID(ctx, req)
	if err != nil {
		return resp, bizerr.WrapGrpcError(err, "GetOrgByID")
	}
	if resp.BaseResp.ErrCode != 0 {
		return resp, bizerr.New(bizerr.ErrCode(resp.BaseResp.ErrCode), "%s", resp.BaseResp.ErrMsg)
	}
	return resp, nil
}

func ListOrgs(ctx context.Context, req *account.OrgFilter) (*account.ListOrgsResponse, bizerr.BizError) {
	client := getAccountProviderSvcClient()

	resp, err := client.ListOrgs(ctx, req)
	if err != nil {
		return resp, bizerr.WrapGrpcError(err, "ListOrgs")
	}
	if resp.BaseResp.ErrCode != 0 {
		return resp, bizerr.New(bizerr.ErrCode(resp.BaseResp.ErrCode), "%s", resp.BaseResp.ErrMsg)
	}
	return resp, nil
}

func GetUserByEmail(ctx context.Context, email string) (*account.User, bizerr.BizError) {
	tilog.Info(ctx, "request to account-svc GetUserByEmail API", zap.String("email", email))
	getUserByEmailRequest := &account.GetUserByEmailRequest{
		Email: email,
	}
	user, err := _getUserByEmail(ctx, getUserByEmailRequest)
	if err != nil {
		tilog.Error(ctx, "fail to call account-svc GetUserByEmail API", zap.String("email", email), zap.Error(err))
		return nil, err
	}
	return user.User, nil
}

func _getUserByEmail(
	ctx context.Context,
	req *account.GetUserByEmailRequest,
) (*account.GetUserByEmailResponse, bizerr.BizError) {
	client := getAccountProviderSvcClient()

	resp, err := client.GetUserByEmail(ctx, req)
	if err != nil {
		return resp, bizerr.WrapGrpcError(err, "GetUserByEmail")
	}
	if resp.BaseResp.ErrCode != 0 {
		return resp, bizerr.New(bizerr.ErrCode(resp.BaseResp.ErrCode), "%s", resp.BaseResp.ErrMsg)
	}
	return resp, nil
}

func GetProjectByID(ctx context.Context, id uint64) (*account.Project, bizerr.BizError) {
	req := &account.GetProjectByIDRequest{Id: id}
	resp, err := _getProjectByID(ctx, req)
	if err != nil {
		return nil, err
	}

	return resp.GetProject(), nil
}

func _getProjectByID(
	ctx context.Context,
	req *account.GetProjectByIDRequest,
) (*account.GetProjectByIDResponse, bizerr.BizError) {
	client := getAccountProviderSvcClient()
	resp, err := client.GetProjectByID(ctx, req)
	if err != nil {
		return resp, bizerr.WrapGrpcError(err, "GetProjectByID")
	}
	if resp.BaseResp.ErrCode != 0 {
		return resp, bizerr.New(bizerr.ErrCode(resp.BaseResp.ErrCode), "%s", resp.BaseResp.ErrMsg)
	}
	return resp, nil
}

func ListProjectsByOrgsInUser(ctx context.Context, userId, orgId uint64) ([]*account.Project, bizerr.BizError) {
	req := &account.ListProjectsByOrgsInUserRequest{
		UserId: userId,
		OrgIds: []uint64{orgId},
	}
	resp, err := _listProjectsByOrgsInUser(ctx, req)
	if err != nil {
		return nil, err
	}
	for _, p := range resp.Projects {
		if p.OrgId == orgId {
			return p.Projects, nil
		}
	}

	// TODO: should return error
	// no access project in this org
	return []*account.Project{}, nil
}

func _listProjectsByOrgsInUser(
	ctx context.Context,
	req *account.ListProjectsByOrgsInUserRequest,
) (*account.ListProjectsByOrgsInUserResponse, bizerr.BizError) {
	client := getAccountProviderSvcClient()
	resp, err := client.ListProjectsByOrgsInUser(ctx, req)
	if err != nil {
		return resp, bizerr.WrapGrpcError(err, "ListProjectsByOrgsInUser")
	}
	if resp.BaseResp.ErrCode != 0 {
		return resp, bizerr.New(bizerr.ErrCode(resp.BaseResp.ErrCode), "%s", resp.BaseResp.ErrMsg)
	}
	return resp, nil
}

func GetApiKeyByAccessKey(ctx context.Context, accessKey string) (*account.GetApiKeyByAccessKeyRsp, bizerr.BizError) {
	client := getAccountProviderSvcClient()
	req := &account.GetApiKeyByAccessKeyReq{AccessKey: accessKey}
	resp, err := client.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
}
