package accountcli

import (
	"context"
	"fmt"
	"strconv"

	"braces.dev/errtrace"
	"github.com/samber/lo"
	accountsdk "github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	openapicommonv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/common/v1beta1"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
)

type clientImpl struct {
	account accountsdk.AccountAPIServiceClient
}

const (
	thirdPartyAccountProviderAWS      = "aws_account_provider"
	thirdPartyAccountProviderGCP      = "gcp_account_provider"
	thirdPartyAccountProviderAzure    = "azure_account_provider"
	thirdPartyAccountProviderMSP      = "msp_account_provider"
	thirdPartyAccountProviderAlicloud = "alicloud_account_provider"
	thirdPartyAccountProviderAliyun   = "aliyun_account_provider"
)

func (a *clientImpl) GetTenantThirdPartyAccountProviders(
	ctx context.Context,
	id uint64,
) ([]openapicommonv1beta1.Region_CloudProvider, error) {
	req := &accountsdk.GetOrgByIDRequest{Id: id}
	resp, err := a.account.GetOrgByID(ctx, req)
	if err != nil {
		return nil, errtrace.Wrap(err)
	}
	if resp.BaseResp != nil && resp.BaseResp.ErrCode != 0 {
		return nil, status.Error(codes.Code(resp.BaseResp.ErrCode), resp.BaseResp.ErrMsg)
	}

	providers := make([]openapicommonv1beta1.Region_CloudProvider, 0)

	if resp.Org.ThirdPartyAccountProvider == "" {
		return providers, nil
	}

	switch resp.Org.ThirdPartyAccountProvider {
	case thirdPartyAccountProviderAWS:
		providers = append(providers, openapicommonv1beta1.Region_aws)
	case thirdPartyAccountProviderGCP:
		providers = append(providers, openapicommonv1beta1.Region_gcp)
	case thirdPartyAccountProviderAzure:
		providers = append(providers, openapicommonv1beta1.Region_azure)
	case thirdPartyAccountProviderMSP:
		// msp is msp-customer ThirdPartyAccountProvider
	case thirdPartyAccountProviderAlicloud:
		providers = append(providers, openapicommonv1beta1.Region_alicloud)
	case thirdPartyAccountProviderAliyun:
		providers = append(providers, openapicommonv1beta1.Region_alicloud)
	default:
		// all provider
		// do nothing
		return providers, fmt.Errorf("unsupported third party account provider: %s", resp.Org.ThirdPartyAccountProvider)
	}

	return providers, nil
}

func (a *clientImpl) GetUserById(ctx context.Context, userId uint64) (*accountsdk.User, error) {
	resp, err := a.account.GetUserByID(ctx, &accountsdk.GetUserByIDRequest{UserId: userId})
	if err != nil {
		return nil, errtrace.Wrap(err)
	}
	if resp.BaseResp != nil && resp.BaseResp.ErrCode != 0 {
		return nil, status.Error(codes.Code(resp.BaseResp.ErrCode), resp.BaseResp.ErrMsg)
	}

	return resp.User, nil
}

func (a *clientImpl) GetUserByEmail(ctx context.Context, email string) (*accountsdk.User, error) {
	resp, err := a.account.GetUserByEmail(ctx, &accountsdk.GetUserByEmailRequest{Email: email})
	if err != nil {
		return nil, errtrace.Wrap(err)
	}
	if resp.BaseResp != nil && resp.BaseResp.ErrCode != 0 {
		return nil, status.Error(codes.Code(resp.BaseResp.ErrCode), resp.BaseResp.ErrMsg)
	}

	return resp.User, nil
}

func (a *clientImpl) IsUserInOrg(ctx context.Context, email string, orgId uint64) (bool, error) {
	resp, err := a.account.ListOrgsByUser(ctx, &accountsdk.ListOrgsByUserRequest{Email: email})
	if err != nil {
		return false, errtrace.Wrap(err)
	}
	if resp.GetBaseResp() != nil && resp.GetBaseResp().GetErrCode() != 0 {
		return false, status.Error(codes.Code(resp.GetBaseResp().GetErrCode()), resp.GetBaseResp().GetErrMsg())
	}

	return lo.ContainsBy(resp.GetOrgs(), func(org *accountsdk.Org) bool {
		return org.GetId() == orgId
	}), nil
}

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

	return resp.GetProject(), nil
}

func (a *clientImpl) GetTimeZoneByProjectID(ctx context.Context, projectId uint64) (int32, error) {
	prj, err := a.account.GetProjectByID(ctx, &accountsdk.GetProjectByIDRequest{
		Id: projectId,
	})
	if err != nil {
		return 0, err
	}
	if prj.BaseResp.ErrCode != 0 {
		return 0, bizerr.New(bizerr.ErrCode(prj.BaseResp.ErrCode), "%s", prj.BaseResp.ErrMsg)
	}
	tenant, err := a.account.GetOrgByID(ctx, &accountsdk.GetOrgByIDRequest{Id: prj.GetProject().GetTenantId()})
	if err != nil {
		return 0, err
	}
	if tenant.BaseResp.ErrCode != 0 {
		return 0, bizerr.New(bizerr.ErrCode(tenant.BaseResp.ErrCode), "%s", tenant.BaseResp.ErrMsg)
	}

	tz, err := strconv.ParseInt(tenant.GetOrg().GetTimeZone(), 10, 32)
	if err != nil {
		return 0, err
	}

	return int32(tz), nil
}

func (a *clientImpl) SendEmail(
	ctx context.Context,
	req *accountsdk.SendEmailReq,
) (*accountsdk.SendEmailResp, bizerr.BizError) {
	resp, err := a.account.SendEmail(ctx, req)
	if err != nil {
		return nil, bizerr.WrapGrpcError(err, "failed to send email")
	}
	if resp.BaseResp.ErrCode != 0 {
		return nil, bizerr.New(bizerr.ErrCode(resp.BaseResp.ErrCode), "%s", resp.BaseResp.ErrMsg)
	}

	return resp, nil
}

func (a *clientImpl) ListProjectOwnerUsers(
	ctx context.Context,
	projectId uint64,
) ([]*accountsdk.User, bizerr.BizError) {
	filter := &accountsdk.ListUsersByFilterRequest{
		Filter: &accountsdk.ListUsersFilter{
			ScopeTypes:  []accountsdk.ScopeType{accountsdk.ScopeType_SCOPE_TYPE_PROJECT},
			ScopeIds:    []uint64{projectId},
			TargetTypes: []accountsdk.TargetType{accountsdk.TargetType_TARGET_TYPE_USER},
			TargetIds:   nil,
			Roles:       []string{"project:owner"},
			RoleIds:     nil,
		},
	}
	resp, err := a.account.ListUsersByFilter(ctx, filter)
	if err != nil {
		return nil, bizerr.WrapGrpcError(err, "failed to list project owner users")
	}
	if resp.BaseResp.ErrCode != 0 {
		return nil, bizerr.New(bizerr.ErrCode(resp.BaseResp.ErrCode), "%s", resp.BaseResp.ErrMsg)
	}

	return resp.Users, nil
}

func (a *clientImpl) ListOrgOwnerUsers(ctx context.Context, orgId uint64) ([]*accountsdk.User, bizerr.BizError) {
	filter := &accountsdk.ListUsersByFilterRequest{
		Filter: &accountsdk.ListUsersFilter{
			ScopeTypes:  []accountsdk.ScopeType{accountsdk.ScopeType_SCOPE_TYPE_ORG},
			ScopeIds:    []uint64{orgId},
			TargetTypes: []accountsdk.TargetType{accountsdk.TargetType_TARGET_TYPE_USER},
			TargetIds:   nil,
			Roles:       []string{"org:owner"},
			RoleIds:     nil,
		},
	}
	resp, err := a.account.ListUsersByFilter(ctx, filter)
	if err != nil {
		return nil, bizerr.WrapGrpcError(err, "failed to list org owner users")
	}
	if resp.BaseResp.ErrCode != 0 {
		return nil, bizerr.New(bizerr.ErrCode(resp.BaseResp.ErrCode), "%s", resp.BaseResp.ErrMsg)
	}

	return resp.Users, nil
}

func (a *clientImpl) ClearEmailUniqueID(
	ctx context.Context,
	req *accountsdk.ClearEmailUniqueIDReq,
) (*accountsdk.ClearEmailUniqueIDResp, bizerr.BizError) {
	resp, err := a.account.ClearEmailUniqueID(ctx, req)
	if err != nil {
		return nil, bizerr.WrapGrpcError(err, "failed to clear email unique id")
	}
	if resp.BaseResp.ErrCode != 0 {
		return nil, bizerr.New(bizerr.ErrCode(resp.BaseResp.ErrCode), "%s", resp.BaseResp.ErrMsg)
	}

	return resp, nil
}

func (a *clientImpl) IsHitFeatureFlag(ctx context.Context, req *accountsdk.HitFeatureFlagReq) (bool, error) {
	resp, err := a.account.IsHitFeatureFlag(ctx, req)
	if err != nil {
		return false, errtrace.Wrap(err)
	}
	if resp.BaseResp != nil && resp.BaseResp.ErrCode != 0 {
		return false, status.Error(codes.Code(resp.BaseResp.ErrCode), resp.BaseResp.ErrMsg)
	}

	return resp.IsHit, nil
}

func (a *clientImpl) ListHitFeatureFlagNames(ctx context.Context, req *accountsdk.ListHitFeatureFlagNamesReq) ([]string, error) {
	resp, err := a.account.ListHitFeatureFlagNames(ctx, req)
	if err != nil {
		return nil, errtrace.Wrap(err)
	}
	if resp.BaseResp != nil && resp.BaseResp.ErrCode != 0 {
		return nil, status.Error(codes.Code(resp.BaseResp.ErrCode), resp.BaseResp.ErrMsg)
	}

	return resp.GetFlagNames(), nil
}

func (a *clientImpl) GetOpenAPIAccess(
	ctx context.Context,
	publicKey string,
) (*accountsdk.GetOpenAPIAccessResp, bizerr.BizError) {
	req := &accountsdk.GetOpenAPIAccessReq{
		PublicKey: publicKey,
	}
	resp, err := a.account.GetOpenAPIAccess(ctx, req)
	if err != nil {
		return nil, bizerr.WrapGrpcError(err, "failed to get open api access")
	}

	return resp, nil
}

func (a *clientImpl) GetOrCreateSystemUser(ctx context.Context, orgID uint64) (*accountsdk.User, bizerr.BizError) {
	req := &accountsdk.GetOrCreateSystemUserRequest{
		OrgId: orgID,
	}
	resp, err := a.account.GetOrCreateSystemUser(ctx, req)
	if err != nil {
		return nil, bizerr.WrapGrpcError(err, "GetOrCreateSystemUser")
	}
	if resp.BaseResp.ErrCode != 0 {
		return nil, bizerr.New(bizerr.ErrCode(resp.BaseResp.ErrCode), "%s", resp.BaseResp.ErrMsg)
	}

	return resp.User, nil
}

func (a *clientImpl) GetOrgByID(ctx context.Context, orgId uint64) (*accountsdk.Org, error) {
	resp, err := a.account.GetOrgByID(ctx, &accountsdk.GetOrgByIDRequest{Id: orgId})
	if err != nil {
		return nil, errtrace.Wrap(err)
	}
	if resp.BaseResp != nil && resp.BaseResp.ErrCode != 0 {
		return nil, status.Error(codes.Code(resp.BaseResp.ErrCode), resp.BaseResp.ErrMsg)
	}

	return resp.GetOrg(), nil
}

func (a *clientImpl) AddClusterRelations(ctx context.Context, orgId, clusterId uint64) error {
	req := &accountsdk.AddClusterRelationsReq{
		ClusterId: clusterId,
		OrgId:     orgId,
	}

	reqMeta, err := metadata.ExtractRequestMeta(ctx)
	if err != nil {
		return err
	}

	if reqMeta.ApiPublicKeyId > 0 {
		req.OperatorId = reqMeta.ApiPublicKeyId
		req.OperatorType = accountsdk.OperatorType_OPERATOR_TYPE_APIKEY
	} else if reqMeta.UserID != "" {
		userId, err := strconv.ParseUint(reqMeta.UserID, 10, 64)
		if err != nil {
			tilog.Warn(ctx, "parse uint failed", zap.String("reqMeta.UserID", reqMeta.UserID), zap.Error(err))
		}
		req.OperatorId = userId
		req.OperatorType = accountsdk.OperatorType_OPERATOR_TYPE_USER
	}

	resp, err := a.account.AddClusterRelations(ctx, req)
	if err != nil {
		return errtrace.Wrap(err)
	}
	if resp.BaseResp != nil && resp.BaseResp.ErrCode != 0 {
		return status.Error(codes.Code(resp.BaseResp.ErrCode), resp.BaseResp.ErrMsg)
	}

	return nil
}

func (a *clientImpl) DeleteClusterRelations(ctx context.Context, clusterId uint64) error {
	resp, err := a.account.DeleteClusterRelations(ctx, &accountsdk.DeleteClusterRelationsReq{
		ClusterId: clusterId,
	})
	if err != nil {
		return errtrace.Wrap(err)
	}
	if resp.BaseResp != nil && resp.BaseResp.ErrCode != 0 {
		return status.Error(codes.Code(resp.BaseResp.ErrCode), resp.BaseResp.ErrMsg)
	}

	return nil
}

func (a *clientImpl) UpsertClusterRelations(ctx context.Context, clusterId, targetOrgId, targetProjectId uint64) error {
	req := &accountsdk.AddClusterRelationsReq{
		ClusterId: clusterId,
		OrgId:     targetOrgId,
		ProjectId: targetProjectId,
	}

	reqMeta, err := metadata.ExtractRequestMeta(ctx)
	if err != nil {
		return err
	}

	if reqMeta.ApiPublicKeyId > 0 {
		req.OperatorId = reqMeta.ApiPublicKeyId
		req.OperatorType = accountsdk.OperatorType_OPERATOR_TYPE_APIKEY
	} else if reqMeta.UserID != "" {
		userId, err := strconv.ParseUint(reqMeta.UserID, 10, 64)
		if err != nil {
			tilog.Warn(ctx, "parse uint failed", zap.String("reqMeta.UserID", reqMeta.UserID), zap.Error(err))
		}
		req.OperatorId = userId
		req.OperatorType = accountsdk.OperatorType_OPERATOR_TYPE_USER
	}

	resp, err := a.account.AddClusterRelations(ctx, req)
	if err != nil {
		return errtrace.Wrap(err)
	}
	if resp.BaseResp != nil && resp.BaseResp.ErrCode != 0 {
		return status.Error(codes.Code(resp.BaseResp.ErrCode), resp.BaseResp.ErrMsg)
	}

	return nil
}

var clusterViewRoles = []string{
	"cluster:admin",
	"cluster:viewer",
}

func (a *clientImpl) CheckOperatorHasClusterRole(ctx context.Context, orgId uint64, clusterIds []uint64) (map[uint64]bool, error) {
	checkReq := &accountsdk.CheckOperatorHasClusterRoleReq{}
	reqMeta, err := metadata.ExtractRequestMeta(ctx)
	if err != nil {
		return nil, err
	}

	if reqMeta.ApiPublicKeyId > 0 {
		checkReq.OperatorId = reqMeta.ApiPublicKeyId
		checkReq.OperatorType = accountsdk.OperatorType_OPERATOR_TYPE_APIKEY
	} else if reqMeta.UserID != "" {
		userId, err := strconv.ParseUint(reqMeta.UserID, 10, 64)
		if err != nil {
			tilog.Warn(ctx, "parse uint failed", zap.String("reqMeta.UserID", reqMeta.UserID), zap.Error(err))
		}

		checkReq.OperatorId = userId
		checkReq.OperatorType = accountsdk.OperatorType_OPERATOR_TYPE_USER
	}

	checkReq.ClusterIds = clusterIds
	checkReq.Roles = clusterViewRoles

	resp, err := a.account.CheckOperatorHasClusterRole(ctx, checkReq)

	if err != nil {
		return nil, errtrace.Wrap(err)
	}

	return resp.Ret, nil
}

// GetInstancePermissionScopeOfUser gets the instance permission scope of a user.
// It returns three values:
// 1. isAllInstance: whether the user has access to all instances.
// 2. allowedProjectIDs: the list of project IDs that the user has access to.
// 3. allowedInstanceIDs: the list of instance IDs that the user has access to.
// 4. error: any error encountered during the operation.
func (a *clientImpl) GetInstancePermissionScopeOfUser(
	ctx context.Context,
	orgId uint64,
	userId string) (all bool, projectIds, instanceIds []uint64, err error) {
	resp, reqMeta, err := a.listIdentityRoleBindings(ctx, orgId, userId)
	if err != nil {
		return false, nil, nil, err
	}

	if resp.OrgRole == nil {
		tilog.Error(ctx, "identity has no role in org",
			zap.String("userId", userId),
			zap.Uint64("apiPublicKeyId", reqMeta.ApiPublicKeyId),
			zap.Uint64("orgId", orgId))
		return false, nil, nil, bizerr.New(bizerr.ErrCodeAuth, "identity has no role in org")
	}

	if lo.Contains(orgRoleWithAllInstancePermission, resp.OrgRole.Role) {
		all = true
	}

	// get all unique project ids from resp.ProjectRoles
	projectIds = lo.UniqMap(resp.ProjectRoles, func(item *accountsdk.ScopeRoleBinding, index int) uint64 {
		return item.ScopeId
	})

	// get all unique instance ids from resp.ClusterRoles
	instanceIds = lo.UniqMap(resp.ClusterRoles, func(item *accountsdk.ScopeRoleBinding, index int) uint64 {
		return item.ScopeId
	})
	return all, projectIds, instanceIds, nil
}

var orgRoleWithAllInstancePermission = []string{"org:owner"}

// var projectRoleWithInstancePermission = []string{"project:owner", "project:viewer"}
// var instanceRoleWithInstancePermission = []string{"instance:admin", "instance:viewer"}

func (a *clientImpl) CheckBackupMutationPermission(
	ctx context.Context,
	orgId, projectId, clusterId uint64,
	userId string,
) (bool, error) {
	resp, reqMeta, err := a.listIdentityRoleBindings(ctx, orgId, userId)
	if err != nil {
		return false, err
	}
	if resp.OrgRole == nil {
		tilog.Error(ctx, "identity has no role in org",
			zap.String("userId", userId),
			zap.Uint64("apiPublicKeyId", reqMeta.ApiPublicKeyId),
			zap.Uint64("orgId", orgId))
		return false, bizerr.New(bizerr.ErrCodeAuth, "identity has no role in org")
	}

	if lo.Contains(backupMutationOrgRoles, resp.OrgRole.Role) {
		return true, nil
	}
	if lo.ContainsBy(resp.ProjectRoles, func(role *accountsdk.ScopeRoleBinding) bool {
		return role.GetScopeId() == projectId && lo.Contains(backupMutationProjectRoles, role.GetRole())
	}) {
		return true, nil
	}
	if lo.ContainsBy(resp.ClusterRoles, func(role *accountsdk.ScopeRoleBinding) bool {
		return role.GetScopeId() == clusterId && lo.Contains(backupMutationClusterRoles, role.GetRole())
	}) {
		return true, nil
	}

	return false, nil
}

func (a *clientImpl) listIdentityRoleBindings(
	ctx context.Context,
	orgId uint64,
	userId string,
) (*accountsdk.ListIdentityRoleBindingsRsp, *metadata.RequestMeta, error) {
	reqMeta, err := metadata.ExtractRequestMeta(ctx)
	if err != nil {
		tilog.Error(ctx, "ExtractRequestMeta failed", zap.String("userId", userId), zap.Error(err))
		return nil, nil, err
	}

	req := &accountsdk.ListIdentityRoleBindingsReq{
		OrgId: orgId,
	}
	switch {
	case reqMeta.ApiPublicKeyId > 0:
		req.IdentType = accountsdk.OperatorType_OPERATOR_TYPE_APIKEY
		req.IdentId = reqMeta.ApiPublicKeyId
	default:
		parsedUserId, parseErr := strconv.ParseUint(userId, 10, 64)
		if parseErr != nil {
			tilog.Error(ctx, "parse uint failed", zap.String("userId", userId), zap.Error(parseErr))
			return nil, nil, parseErr
		}
		req.IdentType = accountsdk.OperatorType_OPERATOR_TYPE_USER
		req.IdentId = parsedUserId
	}

	resp, err := a.account.ListIdentityRoleBindings(ctx, req)
	if err != nil {
		tilog.Error(ctx, "ListIdentityRoleBindings failed",
			zap.String("userId", userId),
			zap.Uint64("apiPublicKeyId", reqMeta.ApiPublicKeyId),
			zap.Error(err))
		return nil, nil, err
	}
	if resp == nil {
		tilog.Error(ctx, "ListIdentityRoleBindings returned nil response",
			zap.String("userId", userId),
			zap.Uint64("apiPublicKeyId", reqMeta.ApiPublicKeyId))
		return nil, nil, bizerr.New(bizerr.GeneralInternalErrorCode, "nil identity role bindings response")
	}

	return resp, reqMeta, nil
}

var backupMutationOrgRoles = []string{"org:owner"}
var backupMutationProjectRoles = []string{"project:owner"}
var backupMutationClusterRoles = []string{"cluster:admin"}

func (a *clientImpl) CopyXProject(ctx context.Context, orgID uint64, name string, sourceProjectId uint64) (uint64, error) {
	req := &accountsdk.CreateProjectReq{
		TenantId:        orgID,
		Name:            name,
		SourceProjectId: sourceProjectId,
		Type:            accountsdk.ProjectType_PROJECT_TYPE_TIDBX,
	}
	reqMeta, err := metadata.ExtractRequestMeta(ctx)
	if err != nil {
		return 0, errtrace.Wrap(err)
	}

	if reqMeta.ApiPublicKeyId > 0 {
		req.IdentId = reqMeta.ApiPublicKeyId
		req.IdentType = accountsdk.OperatorType_OPERATOR_TYPE_APIKEY
	} else if reqMeta.UserID != "" {
		userId, err := strconv.ParseUint(reqMeta.UserID, 10, 64)
		if err != nil {
			tilog.Warn(ctx, "parse uint failed", zap.String("reqMeta.UserID", reqMeta.UserID), zap.Error(err))
		}
		req.IdentId = userId
		req.IdentType = accountsdk.OperatorType_OPERATOR_TYPE_USER
	}

	resp, err := a.account.CreateProject(ctx, req)

	if err != nil {
		return 0, errtrace.Wrap(err)
	}
	if resp.BaseResp != nil && resp.BaseResp.ErrCode != 0 {
		return 0, bizerr.New(bizerr.ErrCode(resp.BaseResp.ErrCode), "%s", resp.BaseResp.ErrMsg)
	}
	return resp.GetProject().GetId(), nil
}

// GetTidbxVirtualProjectId returns the ID of the virtual project
// (PROJECT_TYPE_TIDBXVIRTUAL) for the given organization. This virtual project
// is used as the default project for Starter/Essential/Premium/BYOC clusters when no explicit
// project_id is provided during cluster creation.
func (a *clientImpl) GetTidbxVirtualProjectId(ctx context.Context, orgId uint64) (uint64, error) {
	req := &accountsdk.ProjectFilter{
		OrgIds: []uint64{orgId},
	}
	resp, err := a.account.ListProjects(ctx, req)
	if err != nil {
		return 0, fmt.Errorf("ListProjects for tidbx virtual project: %w", err)
	}
	if resp.BaseResp != nil && resp.BaseResp.ErrCode != 0 {
		return 0, fmt.Errorf("ListProjects error %d: %s", resp.BaseResp.ErrCode, resp.BaseResp.ErrMsg)
	}
	for _, project := range resp.GetProjects() {
		if project.GetType() == accountsdk.ProjectType_PROJECT_TYPE_TIDBXVIRTUAL {
			return project.GetId(), nil
		}
	}
	return 0, fmt.Errorf("no tidbx virtual project found for org %d", orgId)
}
