package facade_imp

import (
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/pingcap/errcode"
	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/account/idl/pbgen/proto/acl"
	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/pkgv2/log"
	"github.com/tidbcloud/serverless-service/internal/domain/constant"
	"github.com/tidbcloud/serverless-service/internal/domain/facade"
	"github.com/tidbcloud/serverless-service/internal/grpc/grpcli"
	"go.buf.build/protocolbuffers/go/linuxgit/tidbcloud/base"
	"go.uber.org/zap"
	"google.golang.org/protobuf/types/known/timestamppb"
)

type accountFacadeImp struct{}

func NewAccountFacade() facade.AccountFacade {
	return &accountFacadeImp{}
}

func (f *accountFacadeImp) GetAuthUserInOrg(
	ctx context.Context,
	email string,
	orgID uint64,
	skipCheckOrg bool,
) (*facade.User, bool, error) {
	req := &account.GetAuthUserRequest{
		UserEmail:    email,
		OrgId:        orgID,
		SkipCheckOrg: skipCheckOrg,
	}
	authUser, err := grpcli.GetAuthUser(ctx, req)
	if err != nil {
		log.WithContextL(ctx).
			Warn("GetAuthUser failed", zap.String("Email", email), zap.Uint64("OrgID", orgID), zap.Error(err))
		if err.RawErrCode() == bizerr.ErrCodeRecordNotFound {
			return nil, false, nil
		}
		if err.RawErrCode() == bizerr.ErrCodePermission {
			return nil, false, errcode.NewForbiddenErr(errors.New(err.ErrMsg()))
		}
		return nil, false, errcode.NewNotAuthenticatedErr(errors.New(err.ErrMsg()))
	}

	user := convertUser(authUser.User)
	user.Role = constant.OrgRole(authUser.OrgRole)
	user.IsImpersonated = authUser.IsImpersonated
	user.Tenant = convertTenant(authUser.Org)
	user.TenantID = user.Tenant.ID
	for _, project := range authUser.AvailableProjects {
		user.Projects = append(user.Projects, *convertProject(project))
	}

	return user, true, nil
}

func (f *accountFacadeImp) GetOrCreateSystemUser(ctx context.Context, orgID uint64) (*facade.User, error) {
	req := &account.GetOrCreateSystemUserRequest{
		OrgId: orgID,
	}

	resp, err := grpcli.GetOrCreateSystemUser(ctx, req)
	if err != nil {
		log.WithContextL(ctx).Error("GetOrCreateSystemUser failed", zap.Uint64("OrgID", orgID), zap.Error(err))
		return nil, err
	}

	user := convertUser(resp.User)
	user.TenantID = orgID
	user.IsImpersonated = false

	return user, nil
}

func (f *accountFacadeImp) GetTenant(ctx context.Context, tenantID uint64) (*facade.Tenant, error) {
	req := &account.GetOrgByIDRequest{
		Id: tenantID,
	}

	resp, err := grpcli.GetOrg(ctx, req)
	if err != nil {
		log.WithContextL(ctx).Error("GetOrg failed", zap.Uint64("tenantID", tenantID), zap.Error(err))
		return nil, err
	}

	return convertTenant(resp.GetOrg()), nil
}

func (f *accountFacadeImp) QueryTenants(
	ctx context.Context,
	filter *facade.TenantFilter,
) (map[uint64]*facade.Tenant, error) {
	arr := make([]string, len(filter.Status))
	for i, value := range filter.Status {
		arr[i] = string(value)
	}
	req := &account.OrgFilter{
		Ids:    filter.TenantIDs,
		Status: arr,
	}

	resp, err := grpcli.ListOrgs(ctx, req)
	if err != nil {
		log.WithContextL(ctx).
			Error("ListOrgs failed", zap.Uint64s("ids", req.Ids), zap.Strings("status", arr), zap.Error(err))
		return nil, err
	}

	tenants := make(map[uint64]*facade.Tenant, len(resp.GetOrgs()))
	for _, org := range resp.GetOrgs() {
		tenants[org.Id] = convertTenant(org)
	}

	return tenants, nil
}

func (f *accountFacadeImp) GetProject(ctx context.Context, projectID uint64) (*facade.Project, error) {
	req := &account.GetProjectByIDRequest{
		Id: projectID,
	}

	resp, err := grpcli.GetProject(ctx, req)
	if err != nil {
		log.WithContextL(ctx).Error("GetProject failed", zap.Uint64("projectID", projectID), zap.Error(err))
		return nil, err
	}

	return convertProject(resp.GetProject()), nil
}

func (f *accountFacadeImp) GetOpenAPIAccess(ctx context.Context, publicKey string) (*facade.OpenAPIAccess, error) {
	req := &acl.GetOpenAPIAccessReq{
		PublicKey: publicKey,
	}

	logger := log.WithContextL(ctx)
	resp, err := grpcli.GetOpenAPIAccess(ctx, req)
	if err != nil {
		logger.Error("GetOpenAPIAccess failed", zap.String("publickey", publicKey), zap.Error(err))
		return nil, err
	}

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

	return convertOpenAPIAccess(resp), nil
}

func (f *accountFacadeImp) GetJwtByUser(
	ctx context.Context,
	email string,
	userID, clusterID uint64,
) (*facade.DBUserJwt, error) {
	req := &acl.GetJwtByUserReq{
		Email:     email,
		UserId:    userID,
		ClusterId: clusterID,
	}

	resp, err := grpcli.GetJwtByUser(ctx, req)
	if err != nil {
		log.WithContextL(ctx).
			Error("GetJwtByUser failed", zap.String("email", email), zap.Uint64("clusterID", clusterID), zap.Error(err))
		return nil, err
	}

	return &facade.DBUserJwt{UserName: resp.DbUser, Jwt: resp.Jwt}, nil
}

func (f *accountFacadeImp) GetBranchJwtByApiKey(
	ctx context.Context,
	publicKey string,
	branchID string,
) (*facade.DBUserJwt, error) {
	req := &acl.GetJwtByApiKeyReq{
		ClusterId: 1,
		AccessKey: publicKey,
	}

	resp, err := grpcli.GetJwtByApiKey(ctx, req)
	if err != nil {
		log.WithContextL(ctx).
			Error("GetBranchJwtByApiKey failed", zap.String("publicKey", publicKey), zap.String("branchID", branchID), zap.Error(err))
		return nil, err
	}

	return &facade.DBUserJwt{UserName: resp.DbUser, Jwt: resp.Jwt}, nil
}

func (f *accountFacadeImp) GetJwtByApiKey(
	ctx context.Context,
	publicKey string,
	clusterID uint64,
) (*facade.DBUserJwt, error) {
	req := &acl.GetJwtByApiKeyReq{
		ClusterId: clusterID,
		AccessKey: publicKey,
	}

	resp, err := grpcli.GetJwtByApiKey(ctx, req)
	if err != nil {
		log.WithContextL(ctx).
			Error("GetJwtByApiKey failed", zap.String("publicKey", publicKey), zap.Uint64("clusterID", clusterID), zap.Error(err))
		return nil, err
	}

	return &facade.DBUserJwt{UserName: resp.DbUser, Jwt: resp.Jwt}, nil
}

func (f *accountFacadeImp) GetDBUsers(
	ctx context.Context,
	orgID, projectID uint64,
	token string,
) ([]*facade.DBUser, error) {
	resp, err := grpcli.GetDBUsers(ctx, &account.GetDBUsersReq{
		OrgId:           orgID,
		ProjectId:       projectID,
		IsServerless:    true,
		ServerlessToken: token,
	})
	if err != nil {
		log.WithContextL(ctx).
			Error("GetDBUsers failed", zap.Uint64("orgID", orgID), zap.Uint64("projectID", projectID), zap.Error(err))
		return nil, err
	}

	out := make([]*facade.DBUser, 0, len(resp.DbUsers))
	for _, each := range resp.DbUsers {
		if each.BuiltinRole != account.BuiltinRole_BUILTIN_ROLE_USER_ADMIN {
			log.WithContextL(ctx).Info("no builtin role", zap.Any("dbuser", each))
			continue
		}
		builtinRole := convertBuiltinRole(each.BuiltinRole)
		out = append(out, &facade.DBUser{
			TenantId:    each.OrgId,
			Username:    each.Username,
			Email:       each.Email,
			BuiltinRole: builtinRole,
		})
	}
	return out, nil
}

func (f *accountFacadeImp) GetCloudAdminPasswd(ctx context.Context, clusterId uint64) (string, bizerr.BizError) {
	resp, err := grpcli.GetCloudAdminCredential(ctx, &account.GetCloudAdminCredentialReq{
		ClusterId: clusterId,
	})
	if err != nil {
		log.WithContextL(ctx).
			Error("GetCloudAdminCredential failed", zap.Uint64("clusterID", clusterId), zap.Error(err))
		return "", err
	}

	if resp.BaseResp != nil && resp.BaseResp.ErrCode != 0 {
		log.WithContextL(ctx).Error("GetCloudAdminCredential failed", zap.Uint64("clusterID", clusterId),
			zap.Int64("errcode", resp.BaseResp.ErrCode), zap.String("errMsg", resp.BaseResp.ErrMsg))
		return "", bizerr.New(bizerr.ErrCodeUnknown, "%s", resp.BaseResp.ErrMsg)
	}

	return resp.EncryptedCloudAdminPasswd, nil
}

func (f *accountFacadeImp) ListOrgMapByIDs(
	ctx context.Context,
	ids []uint64,
) (map[uint64]*account.Org, bizerr.BizError) {
	if len(ids) == 0 {
		return nil, nil
	}

	request := &account.OrgFilter{
		Ids: ids,
	}

	response, err := grpcli.ListOrgs(ctx, request)
	if err != nil {
		return nil, err
	}

	result := make(map[uint64]*account.Org)
	for _, org := range response.Orgs {
		result[org.Id] = org
	}

	return result, nil
}

func (f *accountFacadeImp) SendTenantOwnerEmailOnce(
	ctx context.Context,
	tenantID uint64,
	key string,
	templateID string,
	vars map[string]string,
) error {
	logger := log.WithContextL(ctx).With(zap.Uint64("tenantID", tenantID))
	request := &account.SendEmailByLabelsReq{
		OrgId:        tenantID,
		TemplateId:   templateID,
		TemplateVars: vars,
		Scene:        "serverless-svc",
		Identifier:   key,
		ToLabel:      account.MailLabels_MAIL_LABELS_ORG_OWNER,
	}
	resp, err := grpcli.SendEmailByLabels(ctx, request)
	if err != nil {
		logger.Error("failed call account send email by labels", zap.Error(err))
		return err
	}

	if resp.BaseResp.ErrCode != 0 {
		// magic number for tiinf error ErrCodeConflict, if conflict means mail aleady sent, just ignore
		// we dont want to import tiinf error in this repo. it will create two error system in this repo.
		if resp.BaseResp.ErrCode == 49900409 {
			logger.Info("mail already sent before", zap.String("key", key))
			return nil
		}

		if resp.BaseResp.ErrCode == 49900001 {
			// just record error if send mail failed in invalid params
			// this caused by account manager is null in tenants.
			logger.Info("mail send failed", zap.String("key", key), zap.String("message", resp.BaseResp.ErrMsg))
			return nil
		}
		logger.Error("failed call account send email by labels", zap.Error(err), zap.Any("base_resp", resp.BaseResp))
		return err
	}

	logger.Info("success call account send email by labels")
	return nil
}

func (f *accountFacadeImp) ListActiveOrgs(ctx context.Context) ([]*account.Org, bizerr.BizError) {
	request := &account.ListOrgsParam{
		Filter: &account.OrgFilter{
			Status: []string{"active"},
		},
		Pager: &base.Pager{
			Page:     1,
			PageSize: 1000,
		},
	}

	response, err := grpcli.ListOrgsAdvance(ctx, request)
	if err != nil {
		return nil, err
	}
	result := make([]*account.Org, 0, response.Total)
	result = append(result, response.Orgs...)
	for len(result) < int(response.Total) && len(response.Orgs) > 0 {
		request.Pager.Page++
		response, err = grpcli.ListOrgsAdvance(ctx, request)
		if err != nil {
			return nil, err
		}
		result = append(result, response.Orgs...)
	}
	return result, nil
}

func (f *accountFacadeImp) ListProjectsMapByOrg(
	ctx context.Context,
	orgId uint64,
) (map[uint64]*account.Project, bizerr.BizError) {
	request := &account.ProjectFilter{
		OrgIds: []uint64{orgId},
	}

	response, err := grpcli.ListProjects(ctx, request)
	if err != nil {
		return nil, err
	}

	result := make(map[uint64]*account.Project)
	for _, project := range response.Projects {
		result[project.Id] = project
	}
	return result, nil
}

func fromTimestamp(t any) *time.Time {
	var tm time.Time
	switch v := t.(type) {
	case timestamppb.Timestamp:
		tm = v.AsTime()
	case *timestamppb.Timestamp:
		if v == nil {
			return nil
		}
		tm = v.AsTime()
	default:
		return nil
	}

	return &tm
}

func convertUser(user *account.User) *facade.User {
	return &facade.User{
		ID:          user.Id,
		FirstName:   user.FirstName,
		LastName:    user.LastName,
		Email:       user.Email,
		Status:      constant.UserStatus(user.Status),
		InvitedAt:   fromTimestamp(user.InvitedAt),
		Job:         user.Job,
		PhoneNumber: user.PhoneNumber,
	}
}

func convertTenant(org *account.Org) *facade.Tenant {
	return &facade.Tenant{
		ID:                        org.Id,
		Name:                      org.Name,
		Alias:                     org.Alias,
		Email:                     org.Email,
		Country:                   org.Country,
		TimeZone:                  org.TimeZone,
		Status:                    constant.TenantStatus(org.Status),
		Plan:                      constant.TenantPlan(org.Plan),
		POCBindedPackageID:        org.PocBindedPackageId,
		OnDemandBindedPackageID:   org.OnDemandBindedPackageId,
		DevTierBindedPackageID:    org.DevTierBindedPackageId,
		Kind:                      constant.TenantKind(org.Kind),
		AcceptService:             org.AcceptService,
		ZendeskID:                 org.ZendeskId,
		ThirdPartyAccountProvider: &org.ThirdPartyAccountProvider,
		ThirdPartyAccountCode:     &org.ThirdPartyAccountCode,
		Projects:                  []*facade.Project{}, // WARN: Empty value, do not use
		BillingEmail:              org.BillingEmail,
		AccountManager:            org.AccountManager,
	}
}

func convertProject(project *account.Project) *facade.Project {
	return &facade.Project{
		ID:             project.Id,
		Name:           project.Name,
		TenantID:       project.TenantId,
		Tenant:         &facade.Tenant{}, // WARN: Empty value, do not use
		Status:         constant.ProjectStatus(project.Status),
		DeletedTime:    fromTimestamp(project.DeletedAt),
		IsCrossAccount: project.IsCrossAccount,
		AwsCmekEnabled: project.AwsCmekEnabled,
	}
}

func convertOpenAPIAccess(resp *acl.GetOpenAPIAccessResp) *facade.OpenAPIAccess {
	projects := make([]uint64, 0, len(resp.Projects))
	for _, p := range resp.Projects {
		projects = append(projects, p.GetId())
	}
	return &facade.OpenAPIAccess{
		OrgID:    resp.Org.GetId(),
		Projects: projects,
		Roles:    resp.Org.OrgRoles,
	}
}

func convertBuiltinRole(role account.BuiltinRole) constant.BuiltinRole {
	switch role {
	case account.BuiltinRole_BUILTIN_ROLE_USER_ADMIN:
		return constant.BuiltinRoleAdmin
	case account.BuiltinRole_BUILTIN_ROLE_USER_MEMBER:
		return constant.BuiltinRoleMember
	case account.BuiltinRole_BUILTIN_ROLE_USER_OBSERVER:
		return constant.BuiltinRoleObserver
	default:
		return constant.BuiltinRoleUnknown
	}
}
