package account

import (
	"context"
	"fmt"
	"sync"

	"github.com/tidbcloud/account/idl/pbgen/proto/acl"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	tigrpc "github.com/tidbcloud/tiinf/transport/tigrpc/client"
	"go.buf.build/protocolbuffers/go/linuxgit/tidbcloud/base"
	"go.uber.org/zap"
	"google.golang.org/grpc"

	"github.com/tidbcloud/account/idl/pbgen/proto/account"

	"github.com/tidbcloud/auditlog/configs"
)

var (
	accountInitOnce sync.Once
	accountSvc      AccountSvcIface
)

type AccountSvcIface interface {
	GetAccountServiceClient() account.AccountAPIServiceClient
	GetUserByToken(ctx context.Context, token string) (*account.GetUserByTokenResponse, error)
	GetAuthUser(ctx context.Context, email string) (*account.GetAuthUserResponse, error)
	GetOrgByID(ctx context.Context, orgID uint64) (*account.GetOrgByIDResponse, error)
	GetProjectByID(ctx context.Context, projectID uint64) (*account.GetProjectByIDResponse, error)
	GetUserRoleInOrg(ctx context.Context, userEmail string, orgID uint64) (acl.OrgRole, error)
	GetIdentityByAccessToken(ctx context.Context, token string) (*account.GetIdentityByAccessTokenResp, error)
	ListOrgsByUser(ctx context.Context, email string) (*account.ListOrgsByUserResponse, error)
}

func NewAccountSvc(ctx context.Context, cfg *configs.Config) AccountSvcIface {
	accountInitOnce.Do(func() {
		var conn *grpc.ClientConn
		conn, err := tigrpc.NewClientConn(cfg.AccountSvcAddr, tigrpc.WithDefault())
		tilog.Info(ctx, "Initializing account service client", zap.Any("err", err), zap.String("addr", cfg.AccountSvcAddr))
		if err != nil {
			panic(fmt.Sprintf("dial account-svc: %s", err))
		}

		accountClient := account.NewAccountAPIServiceClient(conn)
		accountSvc = &accountSvcImpl{accountClient}
	})

	return accountSvc
}

type accountSvcImpl struct {
	accountClient account.AccountAPIServiceClient
}

func parseAccountResp(ctx context.Context, baseResp *base.BaseResp) error {
	if baseResp == nil {
		return nil
	}

	if baseResp.ErrCode == errs.ErrCodeOK {
		return nil
	}

	if baseResp.ErrCode == errs.ErrCodeNotFound {
		return errs.NewNotFound("not found", nil)
	}

	if baseResp.ErrCode == errs.ErrCodeForbidden {
		return errs.NewForbidden("not allowed", nil)
	}

	if baseResp.ErrCode == errs.ErrCodeConflict {
		return errs.NewConflict("conflict", nil)
	}

	tilog.Info(ctx, "parse account resp", zap.Any("baseResp", baseResp))
	return errs.NewInternalServerError("account server error", nil)
}

func (a *accountSvcImpl) GetAccountServiceClient() account.AccountAPIServiceClient {
	return a.accountClient
}

func (a *accountSvcImpl) ListOrgsByUser(ctx context.Context, email string) (*account.ListOrgsByUserResponse, error) {
	resp, err := a.accountClient.ListOrgsByUser(ctx, &account.ListOrgsByUserRequest{
		Email: email,
	})
	if err != nil {
		tilog.Error(ctx, "failed to list orgs by user", zap.String("email", email), zap.Error(err))
		return resp, errs.NewInternalGRPC("ListOrgsByUser", err)
	}

	return resp, parseAccountResp(ctx, resp.BaseResp)
}

func (a *accountSvcImpl) GetUserRoleInOrg(ctx context.Context, userEmail string, orgID uint64) (acl.OrgRole, error) {
	req := account.GetAuthUserRequest{
		UserEmail:    userEmail,
		OrgId:        orgID,
		SkipCheckOrg: false,
	}

	resp, err := a.accountClient.GetAuthUser(ctx, &req)
	if err != nil {
		tilog.Error(ctx, "failed to get user info", zap.Error(err), zap.Any("req", req))
		return resp.OrgRole, errs.NewInternalGRPC("GetUserRoleInOrg", err)
	}

	return resp.OrgRole, nil
}

func (a *accountSvcImpl) GetUserByToken(ctx context.Context, token string) (*account.GetUserByTokenResponse, error) {
	resp, err := a.accountClient.GetUserByToken(ctx, &account.GetUserByTokenRequest{Token: token})
	if err != nil {
		tilog.Error(ctx, "failed to get user by token", zap.Error(err), zap.String("token", token))
		return resp, errs.NewInternalGRPC("GetUserByToken", err)
	}

	return resp, parseAccountResp(ctx, resp.BaseResp)
}

func (a *accountSvcImpl) GetIdentityByAccessToken(ctx context.Context, token string) (*account.GetIdentityByAccessTokenResp, error) {
	resp, err := a.accountClient.GetIdentityByAccessToken(ctx, &account.GetIdentityByAccessTokenReq{RawAccessToken: token})
	if err != nil {
		tilog.Error(ctx, "failed to get identity by access token", zap.Error(err), zap.String("token", token))
		return resp, errs.NewInternalGRPC("GetIdentityByAccessToken", err)
	}

	return resp, parseAccountResp(ctx, resp.BaseResp)
}

func (a *accountSvcImpl) GetAuthUser(ctx context.Context, email string) (*account.GetAuthUserResponse, error) {
	resp, err := a.accountClient.GetAuthUser(ctx, &account.GetAuthUserRequest{UserEmail: email, SkipCheckOrg: true})
	if err != nil {
		tilog.Error(ctx, "failed to get auth user", zap.Error(err), zap.String("email", email))
		return resp, errs.NewInternalGRPC("GetAuthUser", err)
	}

	return resp, parseAccountResp(ctx, resp.BaseResp)
}

func (a *accountSvcImpl) GetOrgByID(ctx context.Context, orgID uint64) (*account.GetOrgByIDResponse, error) {
	resp, err := a.accountClient.GetOrgByID(ctx, &account.GetOrgByIDRequest{Id: orgID})
	if err != nil {
		tilog.Error(ctx, "failed to get org by id", zap.Error(err), zap.Uint64("orgID", orgID))
		return resp, errs.NewInternalGRPC("GetOrgByID", err)
	}

	return resp, parseAccountResp(ctx, resp.BaseResp)
}

func (a *accountSvcImpl) GetProjectByID(ctx context.Context, projectID uint64) (*account.GetProjectByIDResponse, error) {
	resp, err := a.accountClient.GetProjectByID(ctx, &account.GetProjectByIDRequest{Id: projectID})
	if err != nil {
		tilog.Error(ctx, "failed to get project by id", zap.Error(err), zap.Uint64("projectID", projectID))
		return resp, errs.NewInternalGRPC("GetProjectByID", err)
	}

	return resp, parseAccountResp(ctx, resp.BaseResp)
}
