package hostorg

import (
	"context"
	"fmt"
	"strconv"
	"strings"

	"hyapp-admin-server/internal/appctx"
	"hyapp-admin-server/internal/integration/userclient"
	"hyapp-admin-server/internal/integration/walletclient"
	walletv1 "hyapp.local/api/proto/wallet/v1"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

const (
	coinSellerMerchantAssetType    = "COIN_SELLER_COIN"
	coinSellerStockTypePurchase    = "usdt_purchase"
	coinSellerStockTypeDeduction   = "usdt_deduction"
	coinSellerStockTypeCompensate  = "coin_compensation"
	agencyStatusDeleted            = "deleted"
	membershipStatusActive         = "active"
	paidCurrencyUSDT               = "USDT"
	sortByDiamond                  = "diamond"
	sortByMerchantBalance          = "merchant_balance"
	sortBySalaryWallet             = "salary_wallet"
	sortByTotalRechargeUSDT        = "total_recharge_usdt"
	bdLeaderPositionAliasMaxRunes  = 64
	hostOrgInternalUserIDMinDigits = 15
	managerOperationScopeScene     = "manager_operations"
)

type Service struct {
	userClient   userclient.Client
	walletClient walletclient.Client
	reader       *Reader
}

func NewService(userClient userclient.Client, walletClient walletclient.Client, reader *Reader) *Service {
	return &Service{userClient: userClient, walletClient: walletClient, reader: reader}
}

func (s *Service) ListBDLeaders(ctx context.Context, query listQuery) ([]*userclient.BDProfile, int64, error) {
	query = normalizeListQuery(query)
	return s.reader.ListBDProfiles(ctx, query, "bd_leader")
}

func (s *Service) ListBDs(ctx context.Context, query listQuery) ([]*userclient.BDProfile, int64, error) {
	query = normalizeListQuery(query)
	return s.reader.ListBDProfiles(ctx, query, "bd")
}

func (s *Service) ListAgencies(ctx context.Context, query listQuery) ([]*userclient.Agency, int64, error) {
	query = normalizeListQuery(query)
	return s.reader.ListAgencies(ctx, query)
}

func (s *Service) ListManagers(ctx context.Context, query listQuery) ([]*ManagerListItem, int64, ManagerRoleScopePolicy, error) {
	query = normalizeListQuery(query)
	items, total, err := s.reader.ListManagers(ctx, query)
	if err != nil {
		return nil, 0, ManagerRoleScopePolicy{}, err
	}
	policy, err := s.managerRoleScopePolicy(ctx)
	if err != nil {
		return nil, 0, ManagerRoleScopePolicy{}, err
	}
	decorateManagerScopePolicy(items, policy)
	return items, total, ManagerRoleScopePolicy{
		BaseScope:                   policy.BaseScope,
		ExpandedScope:               policy.ExpandedScope,
		RegionExpansionConfigurable: policy.RegionExpansionConfigurable,
	}, nil
}

func (s *Service) CreateManager(ctx context.Context, actorID int64, requestID string, req createManagerRequest) (*ManagerListItem, error) {
	policy, err := s.managerRoleScopePolicy(ctx)
	if err != nil {
		return nil, err
	}
	req.CanManageCrossRegion = normalizedManagerCrossRegionPermission(firstBool(req.CanManageCrossRegion, req.LegacyBDLeaderInRegion), policy.RegionExpansionConfigurable)
	req.LegacyBDLeaderInRegion = nil
	targetUserID, err := s.resolveDisplayUserID(ctx, requestID, req.TargetUserID.Int64())
	if err != nil {
		return nil, err
	}
	// 经理身份只落 manager_profiles 表；权限开关同时写入 manager_profiles，供 H5 写入口逐项校验。
	item, err := s.reader.CreateManagerProfile(ctx, targetUserID, actorID, req)
	if err != nil {
		return nil, err
	}
	decorateManagerScopePolicy([]*ManagerListItem{item}, policy)
	return item, nil
}

func (s *Service) UpdateManager(ctx context.Context, actorID int64, userID int64, req updateManagerRequest) (*ManagerListItem, error) {
	policy, err := s.managerRoleScopePolicy(ctx)
	if err != nil {
		return nil, err
	}
	req.CanManageCrossRegion = normalizedManagerCrossRegionPermission(firstBool(req.CanManageCrossRegion, req.LegacyBDLeaderInRegion), policy.RegionExpansionConfigurable)
	req.LegacyBDLeaderInRegion = nil
	item, err := s.reader.UpdateManagerProfile(ctx, userID, actorID, req)
	if err != nil {
		return nil, err
	}
	decorateManagerScopePolicy([]*ManagerListItem{item}, policy)
	return item, nil
}

func (s *Service) managerRoleScopePolicy(ctx context.Context) (*userclient.RoleScopePolicy, error) {
	return s.userClient.GetRoleScopePolicy(ctx, userclient.GetRoleScopePolicyRequest{
		Caller: "hyapp-admin-server",
		Scene:  managerOperationScopeScene,
	})
}

// normalizedManagerCrossRegionPermission 让 App 基础策略决定开关是否可配置；global App 即使收到篡改请求也只保存 false。
func normalizedManagerCrossRegionPermission(value *bool, configurable bool) *bool {
	if configurable {
		return value
	}
	disabled := false
	return &disabled
}

func decorateManagerScopePolicy(items []*ManagerListItem, policy *userclient.RoleScopePolicy) {
	if policy == nil {
		return
	}
	for _, item := range items {
		if item == nil {
			continue
		}
		item.LegacyBDLeaderInRegion = item.CanManageCrossRegion
		item.ManagerOperationBaseScope = policy.BaseScope
		item.ManagerCrossRegionConfigurable = policy.RegionExpansionConfigurable
		item.ManagerOperationEffectiveScope = policy.BaseScope
		if policy.RegionExpansionConfigurable && item.CanManageCrossRegion {
			item.ManagerOperationEffectiveScope = policy.ExpandedScope
		}
	}
}

func (s *Service) ListHosts(ctx context.Context, query listQuery) ([]*userclient.HostProfile, int64, error) {
	query = normalizeListQuery(query)
	return s.reader.ListHostProfiles(ctx, query)
}

func (s *Service) ListCoinSellers(ctx context.Context, query listQuery) ([]*CoinSellerListItem, int64, error) {
	query = normalizeListQuery(query)
	return s.reader.ListCoinSellers(ctx, query)
}

func (s *Service) ListCoinSellerSubApplications(ctx context.Context, query listQuery) ([]*CoinSellerSubApplicationListItem, int64, error) {
	query = normalizeListQuery(query)
	return s.reader.ListCoinSellerSubApplications(ctx, query)
}

func (s *Service) ListCoinSellerSubSellers(ctx context.Context, parentUserID int64, query listQuery) ([]*CoinSellerListItem, int64, error) {
	query = normalizeListQuery(query)
	return s.reader.ListCoinSellerSubSellers(ctx, parentUserID, query)
}

func (s *Service) ReviewCoinSellerSubApplication(ctx context.Context, actorID int64, applicationID int64, requestID string, decision string, req coinSellerSubApplicationReviewRequest) (*userclient.CoinSellerSubApplicationReviewResult, error) {
	return s.userClient.ReviewCoinSellerSubApplication(ctx, userclient.ReviewCoinSellerSubApplicationRequest{
		RequestID:     requestID,
		Caller:        "hyapp-admin-server",
		CommandID:     strings.TrimSpace(req.CommandID),
		AdminUserID:   actorID,
		ApplicationID: applicationID,
		Decision:      strings.TrimSpace(decision),
		Reason:        strings.TrimSpace(req.Reason),
	})
}

func (s *Service) GetCoinSellerSalaryRates(ctx context.Context, regionID int64) ([]CoinSellerSalaryRateTier, error) {
	return s.reader.ListCoinSellerSalaryRates(ctx, regionID)
}

func (s *Service) ReplaceCoinSellerSalaryRates(ctx context.Context, actorID int64, regionID int64, req replaceCoinSellerSalaryRatesRequest) ([]CoinSellerSalaryRateTier, error) {
	tiers := make([]CoinSellerSalaryRateTier, 0, len(req.Tiers))
	for index, item := range req.Tiers {
		minUSDMinor, err := salaryRateUSDMinor(item.MinUSDMinor, item.MinUSD)
		if err != nil {
			return nil, fmt.Errorf("min_usd is invalid")
		}
		maxUSDMinor, err := salaryRateUSDMinor(item.MaxUSDMinor, item.MaxUSD)
		if err != nil {
			return nil, fmt.Errorf("max_usd is invalid")
		}
		enabled := item.Status == "active"
		if item.Enabled != nil {
			enabled = *item.Enabled
		}
		status := strings.ToLower(strings.TrimSpace(item.Status))
		if status == "" {
			if enabled {
				status = "active"
			} else {
				status = "disabled"
			}
		}
		if status != "active" && status != "disabled" {
			return nil, fmt.Errorf("status is invalid")
		}
		if minUSDMinor <= 0 || maxUSDMinor < minUSDMinor {
			return nil, fmt.Errorf("usd range is invalid")
		}
		if item.CoinPerUSD <= 0 || item.CoinPerUSD%100 != 0 {
			return nil, fmt.Errorf("coin_per_usd must be a positive multiple of 100")
		}
		sortOrder := item.SortOrder
		if sortOrder == 0 {
			// 大值优先后，表单首行必须获得最高缺省权重，避免保存一次就把兑换档位倒置。
			sortOrder = (len(req.Tiers) - index) * 10
		}
		tiers = append(tiers, CoinSellerSalaryRateTier{
			RegionID:    regionID,
			MinUSDMinor: minUSDMinor,
			MaxUSDMinor: maxUSDMinor,
			MinUSD:      formatUSDMinor(minUSDMinor),
			MaxUSD:      formatUSDMinor(maxUSDMinor),
			CoinPerUSD:  item.CoinPerUSD,
			Status:      status,
			Enabled:     status == "active",
			SortOrder:   sortOrder,
		})
	}
	if err := validateCoinSellerSalaryRateTiers(tiers); err != nil {
		return nil, err
	}
	return s.reader.ReplaceCoinSellerSalaryRates(ctx, regionID, actorID, tiers)
}

func (s *Service) CreateBDLeader(ctx context.Context, actorID int64, requestID string, req createBDLeaderRequest) (*userclient.BDProfile, error) {
	targetUserID, err := s.resolveDisplayUserID(ctx, requestID, req.TargetUserID.Int64())
	if err != nil {
		return nil, err
	}
	positionAlias, err := normalizeBDLeaderPositionAlias(req.PositionAlias)
	if err != nil {
		return nil, err
	}
	// BD Leader 区域由目标用户当前 users.region_id 派生；后台不再传区域，避免创建角色时顺手迁移用户国家/区域。
	profile, err := s.userClient.CreateBDLeader(ctx, userclient.CreateBDLeaderRequest{
		RequestID:    requestID,
		Caller:       "hyapp-admin-server",
		CommandID:    strings.TrimSpace(req.CommandID),
		AdminUserID:  actorID,
		TargetUserID: targetUserID,
		Reason:       strings.TrimSpace(req.Reason),
	})
	if err != nil {
		return nil, err
	}
	// 职位别名只影响 Flutter H5 配置里 admin 入口的展示名，不参与 user-service 的身份创建事务，避免把后台展示字段扩散到 App 身份主协议。
	if err := s.reader.SaveBDLeaderPositionAlias(ctx, targetUserID, actorID, positionAlias); err != nil {
		return nil, err
	}
	profile.PositionAlias = positionAlias
	return profile, nil
}

func (s *Service) UpdateBDLeaderPositionAlias(ctx context.Context, actorID int64, targetUserID int64, req bdLeaderPositionAliasRequest) (*userclient.BDProfile, error) {
	if targetUserID <= 0 {
		return nil, fmt.Errorf("target_user_id is required")
	}
	positionAlias, err := normalizeBDLeaderPositionAlias(req.PositionAlias)
	if err != nil {
		return nil, err
	}
	profile, err := s.reader.GetBDLeader(ctx, targetUserID)
	if err != nil {
		return nil, err
	}
	// 行内编辑只修改后台维护的展示别名；先确认 BD Leader 身份存在，再写覆盖表，避免产生无法在列表里追踪的孤立别名。
	if err := s.reader.SaveBDLeaderPositionAlias(ctx, targetUserID, actorID, positionAlias); err != nil {
		return nil, err
	}
	profile.PositionAlias = positionAlias
	return profile, nil
}

func (s *Service) CreateBD(ctx context.Context, actorID int64, requestID string, req createBDRequest) (*userclient.BDProfile, error) {
	targetUserID, err := s.resolveDisplayUserID(ctx, requestID, req.TargetUserID.Int64())
	if err != nil {
		return nil, err
	}
	parentLeaderUserID := int64(0)
	if req.ParentLeaderUserID.Int64() < 0 {
		return nil, fmt.Errorf("parent_leader_user_id must not be negative")
	}
	if req.ParentLeaderUserID.Int64() > 0 {
		// 父级 Leader 是可选关系；传入短 ID 时才解析成内部 user_id，未传时保留 0 表示独立 BD。
		parentLeaderUserID, err = s.resolveDisplayUserID(ctx, requestID, req.ParentLeaderUserID.Int64())
		if err != nil {
			return nil, err
		}
	}
	return s.userClient.CreateBD(ctx, userclient.CreateBDRequest{
		RequestID:          requestID,
		Caller:             "hyapp-admin-server",
		CommandID:          strings.TrimSpace(req.CommandID),
		AdminUserID:        actorID,
		TargetUserID:       targetUserID,
		ParentLeaderUserID: parentLeaderUserID,
		Reason:             strings.TrimSpace(req.Reason),
	})
}

func (s *Service) SetBDStatus(ctx context.Context, actorID int64, targetUserID int64, requestID string, role string, req bdStatusRequest) (*userclient.BDProfile, error) {
	return s.userClient.SetBDStatus(ctx, userclient.SetBDStatusRequest{
		RequestID:    requestID,
		Caller:       "hyapp-admin-server",
		CommandID:    strings.TrimSpace(req.CommandID),
		AdminUserID:  actorID,
		TargetUserID: targetUserID,
		Role:         role,
		Status:       strings.TrimSpace(req.Status),
		Reason:       strings.TrimSpace(req.Reason),
	})
}

func (s *Service) DeleteBDLeader(ctx context.Context, targetUserID int64) (*userclient.BDProfile, error) {
	if targetUserID <= 0 {
		return nil, fmt.Errorf("target_user_id is required")
	}
	return s.reader.DeleteBDLeader(ctx, targetUserID)
}

func (s *Service) CreateCoinSeller(ctx context.Context, actorID int64, requestID string, req createCoinSellerRequest) (*userclient.CoinSellerProfile, error) {
	targetUserID, err := s.resolveDisplayUserID(ctx, requestID, req.TargetUserID.Int64())
	if err != nil {
		return nil, err
	}
	profile, err := s.userClient.CreateCoinSeller(ctx, userclient.CreateCoinSellerRequest{
		RequestID:               requestID,
		Caller:                  "hyapp-admin-server",
		CommandID:               strings.TrimSpace(req.CommandID),
		AdminUserID:             actorID,
		TargetUserID:            targetUserID,
		Reason:                  strings.TrimSpace(req.Reason),
		CanManageSubCoinSellers: req.CanManageSubCoinSellers,
	})
	if err != nil {
		return nil, err
	}
	contact := firstNonBlank(req.Contact, req.Reason)
	if contact != "" {
		if err := s.reader.UpdateCoinSellerContact(ctx, targetUserID, contact); err != nil {
			return nil, err
		}
	}
	return profile, nil
}

func (s *Service) SetCoinSellerStatus(ctx context.Context, actorID int64, targetUserID int64, requestID string, req coinSellerStatusRequest) (*userclient.CoinSellerProfile, error) {
	profile, err := s.userClient.SetCoinSellerStatus(ctx, userclient.SetCoinSellerStatusRequest{
		RequestID:               requestID,
		Caller:                  "hyapp-admin-server",
		CommandID:               strings.TrimSpace(req.CommandID),
		AdminUserID:             actorID,
		TargetUserID:            targetUserID,
		Status:                  strings.TrimSpace(req.Status),
		Reason:                  strings.TrimSpace(req.Reason),
		CanManageSubCoinSellers: req.CanManageSubCoinSellers,
	})
	if err != nil {
		return nil, err
	}
	if req.Contact != nil {
		if err := s.reader.UpdateCoinSellerContact(ctx, targetUserID, *req.Contact); err != nil {
			return nil, err
		}
	}
	return profile, nil
}

func (s *Service) CreditCoinSellerStock(ctx context.Context, actorID int64, sellerUserID int64, requestID string, req coinSellerStockCreditRequest) (*walletv1.AdminCreditCoinSellerStockResponse, error) {
	if s.walletClient == nil {
		return nil, fmt.Errorf("wallet client is not configured")
	}
	if sellerUserID <= 0 || actorID <= 0 {
		return nil, fmt.Errorf("seller_user_id and operator_user_id are required")
	}
	commandID := strings.TrimSpace(req.CommandID)
	stockType := strings.ToLower(strings.TrimSpace(req.Type))
	evidenceRef := strings.TrimSpace(req.EvidenceRef)
	reason := strings.TrimSpace(req.Reason)
	if commandID == "" || req.CoinAmount <= 0 {
		return nil, fmt.Errorf("command_id and coin_amount are required")
	}

	paidCurrencyCode := ""
	paidAmountMicro := int64(0)
	paymentRef := strings.TrimSpace(req.PaymentRef)
	switch stockType {
	case coinSellerStockTypePurchase:
		paidCurrencyCode = paidCurrencyUSDT
		parsed, err := parseUSDTAmountMicro(req.RechargeAmount)
		if err != nil {
			return nil, err
		}
		paidAmountMicro = parsed
	case coinSellerStockTypeCompensate:
		if strings.TrimSpace(req.RechargeAmount) != "" || paymentRef != "" {
			return nil, fmt.Errorf("coin compensation must not include recharge_amount or payment_ref")
		}
	default:
		return nil, fmt.Errorf("stock type is invalid")
	}

	seller, err := s.coinSellerUser(ctx, sellerUserID, requestID)
	if err != nil {
		return nil, err
	}

	return s.walletClient.AdminCreditCoinSellerStock(ctx, &walletv1.AdminCreditCoinSellerStockRequest{
		CommandId:        commandID,
		SellerUserId:     sellerUserID,
		SellerCountryId:  seller.CountryID,
		SellerRegionId:   seller.RegionID,
		StockType:        stockType,
		CoinAmount:       req.CoinAmount,
		PaidCurrencyCode: paidCurrencyCode,
		PaidAmountMicro:  paidAmountMicro,
		PaymentRef:       paymentRef,
		EvidenceRef:      evidenceRef,
		OperatorUserId:   actorID,
		Reason:           reason,
		AppCode:          appctx.FromContext(ctx),
	})
}

func (s *Service) DebitCoinSellerStock(ctx context.Context, actorID int64, sellerUserID int64, requestID string, req coinSellerStockDebitRequest) (*walletv1.AdminCreditCoinSellerStockResponse, error) {
	if s.walletClient == nil {
		return nil, fmt.Errorf("wallet client is not configured")
	}
	if sellerUserID <= 0 || actorID <= 0 {
		return nil, fmt.Errorf("seller_user_id and operator_user_id are required")
	}
	commandID := strings.TrimSpace(req.CommandID)
	reason := strings.TrimSpace(req.Reason)
	evidenceRef := strings.TrimSpace(req.EvidenceRef)
	if commandID == "" || req.CoinAmount <= 0 {
		return nil, fmt.Errorf("command_id and coin_amount are required")
	}
	if reason == "" {
		return nil, fmt.Errorf("reason is required")
	}
	if len(reason) > 512 || len(commandID) > 128 || len(evidenceRef) > 256 {
		return nil, fmt.Errorf("coin seller debit text fields are too long")
	}
	paidAmountMicro, err := parseUSDTAmountMicro(req.RechargeAmount)
	if err != nil {
		return nil, err
	}
	seller, err := s.coinSellerUser(ctx, sellerUserID, requestID)
	if err != nil {
		return nil, err
	}
	// 币商扣除按 USDT 进货冲回处理：钱包事务会同时写负向库存分录、负向进货记录和统计 outbox，
	// 这样后台列表、币商流水和充值聚合都使用同一条账务事实，避免只扣余额却不冲收入。
	return s.walletClient.AdminCreditCoinSellerStock(ctx, &walletv1.AdminCreditCoinSellerStockRequest{
		CommandId:        commandID,
		SellerUserId:     sellerUserID,
		SellerCountryId:  seller.CountryID,
		SellerRegionId:   seller.RegionID,
		StockType:        coinSellerStockTypeDeduction,
		CoinAmount:       req.CoinAmount,
		PaidCurrencyCode: paidCurrencyUSDT,
		PaidAmountMicro:  paidAmountMicro,
		EvidenceRef:      evidenceRef,
		OperatorUserId:   actorID,
		Reason:           reason,
		AppCode:          appctx.FromContext(ctx),
	})
}

func (s *Service) coinSellerUser(ctx context.Context, sellerUserID int64, requestID string) (*userclient.User, error) {
	if s == nil || s.userClient == nil {
		return nil, fmt.Errorf("user client is not configured")
	}
	profile, err := s.userClient.GetCoinSellerProfile(ctx, userclient.GetCoinSellerProfileRequest{
		RequestID: requestID,
		Caller:    "hyapp-admin-server",
		UserID:    sellerUserID,
	})
	if err != nil {
		return nil, err
	}
	if profile == nil || profile.MerchantAssetType != coinSellerMerchantAssetType {
		return nil, fmt.Errorf("target user is not a coin seller")
	}
	seller, err := s.userClient.GetUser(ctx, userclient.GetUserRequest{
		RequestID: requestID,
		Caller:    "hyapp-admin-server",
		UserID:    sellerUserID,
	})
	if err != nil {
		return nil, err
	}
	if seller == nil || seller.CountryID <= 0 || seller.RegionID <= 0 {
		return nil, fmt.Errorf("coin seller country and region are required")
	}
	return seller, nil
}

func (s *Service) CreateAgency(ctx context.Context, actorID int64, requestID string, req createAgencyRequest) (*userclient.CreateAgencyResult, error) {
	ownerUserID, err := s.resolveDisplayUserID(ctx, requestID, req.OwnerUserID.Int64())
	if err != nil {
		return nil, err
	}
	parentBDUserID := int64(0)
	if req.ParentBDUserID.Int64() < 0 {
		return nil, fmt.Errorf("parent_bd_user_id must not be negative")
	}
	if req.ParentBDUserID.Int64() > 0 {
		// 父级 BD 是可选关系；传入短 ID 时才解析成内部 user_id，未传时保留 0 表示独立 Agency。
		parentBDUserID, err = s.resolveDisplayUserID(ctx, requestID, req.ParentBDUserID.Int64())
		if err != nil {
			return nil, err
		}
	}
	joinEnabled := false
	if req.JoinEnabled != nil {
		joinEnabled = *req.JoinEnabled
	}
	return s.userClient.CreateAgency(ctx, userclient.CreateAgencyRequest{
		RequestID:      requestID,
		Caller:         "hyapp-admin-server",
		CommandID:      strings.TrimSpace(req.CommandID),
		AdminUserID:    actorID,
		OwnerUserID:    ownerUserID,
		ParentBDUserID: parentBDUserID,
		Name:           strings.TrimSpace(req.Name),
		JoinEnabled:    joinEnabled,
		Reason:         strings.TrimSpace(req.Reason),
	})
}

func (s *Service) AdminAddAgencyHost(ctx context.Context, actorID int64, agencyID int64, requestID string, req agencyHostAddRequest) (*userclient.CreateAgencyResult, error) {
	targetUserID, err := s.resolveDisplayUserID(ctx, requestID, req.TargetUserID.Int64())
	if err != nil {
		return nil, err
	}
	// admin-server 只负责短号解析和审计；Agency 状态、区域、唯一有效归属等关系规则必须在 user-service 事务内判断。
	return s.userClient.AdminAddAgencyHost(ctx, userclient.AdminAddAgencyHostRequest{
		RequestID:    requestID,
		Caller:       "hyapp-admin-server",
		CommandID:    strings.TrimSpace(req.CommandID),
		AdminUserID:  actorID,
		AgencyID:     agencyID,
		TargetUserID: targetUserID,
		Reason:       strings.TrimSpace(req.Reason),
	})
}

func (s *Service) CloseAgency(ctx context.Context, actorID int64, agencyID int64, requestID string, req agencyCloseRequest) (*userclient.Agency, error) {
	return s.userClient.CloseAgency(ctx, userclient.CloseAgencyRequest{
		RequestID:   requestID,
		Caller:      "hyapp-admin-server",
		CommandID:   strings.TrimSpace(req.CommandID),
		AdminUserID: actorID,
		AgencyID:    agencyID,
		Reason:      strings.TrimSpace(req.Reason),
	})
}

func (s *Service) SetAgencyStatus(ctx context.Context, actorID int64, agencyID int64, requestID string, req agencyStatusRequest) (*userclient.Agency, error) {
	return s.userClient.SetAgencyStatus(ctx, userclient.SetAgencyStatusRequest{
		RequestID:   requestID,
		Caller:      "hyapp-admin-server",
		CommandID:   strings.TrimSpace(req.CommandID),
		AdminUserID: actorID,
		AgencyID:    agencyID,
		Status:      strings.ToLower(strings.TrimSpace(req.Status)),
		Reason:      strings.TrimSpace(req.Reason),
	})
}

func (s *Service) DeleteAgency(ctx context.Context, actorID int64, agencyID int64, requestID string, req agencyDeleteRequest) (*userclient.Agency, error) {
	return s.userClient.DeleteAgency(ctx, userclient.DeleteAgencyRequest{
		RequestID:   requestID,
		Caller:      "hyapp-admin-server",
		CommandID:   strings.TrimSpace(req.CommandID),
		AdminUserID: actorID,
		AgencyID:    agencyID,
		Reason:      strings.TrimSpace(req.Reason),
	})
}

func (s *Service) resolveDisplayUserID(ctx context.Context, requestID string, displayUserID int64) (int64, error) {
	if displayUserID <= 0 {
		return 0, fmt.Errorf("display_user_id is required")
	}
	rawID := strconv.FormatInt(displayUserID, 10)
	userIDChecked := false
	if len(strings.TrimLeft(rawID, "0")) >= hostOrgInternalUserIDMinDigits {
		// 团队后台新合约的 targetUserId 可能是内部雪花 user_id；长数字先按 user_id 确认，避免把内部 ID 误送到短号解析。
		userIDChecked = true
		found, err := s.userIDExists(ctx, requestID, displayUserID)
		if err != nil {
			return 0, err
		}
		if found {
			return displayUserID, nil
		}
	}
	identity, err := s.userClient.ResolveDisplayUserID(ctx, userclient.ResolveDisplayUserIDRequest{
		RequestID:     requestID,
		Caller:        "hyapp-admin-server",
		DisplayUserID: rawID,
	})
	if err != nil {
		if !isGRPCNotFound(err) {
			return 0, err
		}
	} else if identity != nil && identity.UserID > 0 {
		return identity.UserID, nil
	}

	if !userIDChecked {
		// 当前 active 短号解析失败后，短数字仍可能是本地/旧后台直接填写的真实 user_id 或 held 默认短号；只读 GetUser 做存在性确认，不改变 user-service 的 active 短号协议。
		found, err := s.userIDExists(ctx, requestID, displayUserID)
		if err != nil {
			return 0, err
		}
		if found {
			return displayUserID, nil
		}
	}
	return 0, fmt.Errorf("display_user_id %d not found", displayUserID)
}

func (s *Service) userIDExists(ctx context.Context, requestID string, userID int64) (bool, error) {
	user, err := s.userClient.GetUser(ctx, userclient.GetUserRequest{
		RequestID: requestID,
		Caller:    "hyapp-admin-server",
		UserID:    userID,
	})
	if err != nil {
		if isGRPCNotFound(err) {
			return false, nil
		}
		return false, err
	}
	return user != nil && user.UserID > 0, nil
}

func isGRPCNotFound(err error) bool {
	return status.Code(err) == codes.NotFound
}

func normalizeBDLeaderPositionAlias(value string) (string, error) {
	positionAlias := strings.TrimSpace(value)
	if len([]rune(positionAlias)) > bdLeaderPositionAliasMaxRunes {
		return "", fmt.Errorf("position_alias must not exceed %d characters", bdLeaderPositionAliasMaxRunes)
	}
	return positionAlias, nil
}

func (s *Service) SetAgencyJoinEnabled(ctx context.Context, actorID int64, agencyID int64, requestID string, req agencyJoinEnabledRequest) (*userclient.Agency, error) {
	joinEnabled := false
	if req.JoinEnabled != nil {
		joinEnabled = *req.JoinEnabled
	}
	return s.userClient.SetAgencyJoinEnabled(ctx, userclient.SetAgencyJoinEnabledRequest{
		RequestID:   requestID,
		Caller:      "hyapp-admin-server",
		CommandID:   strings.TrimSpace(req.CommandID),
		AdminUserID: actorID,
		AgencyID:    agencyID,
		JoinEnabled: joinEnabled,
		Reason:      strings.TrimSpace(req.Reason),
	})
}

func parseUSDTAmountMicro(raw string) (int64, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return 0, fmt.Errorf("recharge_amount is required for usdt purchase")
	}
	parts := strings.Split(raw, ".")
	if len(parts) > 2 || parts[0] == "" {
		return 0, fmt.Errorf("recharge_amount is invalid")
	}
	whole := parts[0]
	frac := ""
	if len(parts) == 2 {
		frac = parts[1]
		if frac == "" || len(frac) > 6 {
			return 0, fmt.Errorf("recharge_amount supports up to 6 decimals")
		}
	}
	digits := whole + frac + strings.Repeat("0", 6-len(frac))
	var amount int64
	const maxInt64 = int64(^uint64(0) >> 1)
	for _, char := range digits {
		if char < '0' || char > '9' {
			return 0, fmt.Errorf("recharge_amount is invalid")
		}
		next := int64(char - '0')
		if amount > (maxInt64-next)/10 {
			return 0, fmt.Errorf("recharge_amount is too large")
		}
		amount = amount*10 + next
	}
	if amount <= 0 {
		return 0, fmt.Errorf("recharge_amount must be positive")
	}
	return amount, nil
}

func salaryRateUSDMinor(minor int64, raw string) (int64, error) {
	if minor > 0 {
		return minor, nil
	}
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return 0, fmt.Errorf("usd amount is required")
	}
	parts := strings.Split(raw, ".")
	if len(parts) > 2 || parts[0] == "" {
		return 0, fmt.Errorf("usd amount is invalid")
	}
	digits := parts[0]
	fraction := ""
	if len(parts) == 2 {
		fraction = parts[1]
		if len(fraction) > 2 {
			return 0, fmt.Errorf("usd amount supports up to 2 decimals")
		}
	}
	digits += fraction + strings.Repeat("0", 2-len(fraction))
	var amount int64
	const maxInt64 = int64(^uint64(0) >> 1)
	for _, char := range digits {
		if char < '0' || char > '9' {
			return 0, fmt.Errorf("usd amount is invalid")
		}
		next := int64(char - '0')
		if amount > (maxInt64-next)/10 {
			return 0, fmt.Errorf("usd amount is too large")
		}
		amount = amount*10 + next
	}
	return amount, nil
}

func validateCoinSellerSalaryRateTiers(tiers []CoinSellerSalaryRateTier) error {
	active := make([]CoinSellerSalaryRateTier, 0, len(tiers))
	for _, tier := range tiers {
		if tier.Status == "active" {
			active = append(active, tier)
		}
	}
	for i := 0; i < len(active); i++ {
		for j := i + 1; j < len(active); j++ {
			if active[i].MinUSDMinor <= active[j].MaxUSDMinor && active[j].MinUSDMinor <= active[i].MaxUSDMinor {
				return fmt.Errorf("active usd ranges must not overlap")
			}
		}
	}
	return nil
}

func formatUSDMinor(value int64) string {
	sign := ""
	if value < 0 {
		sign = "-"
		value = -value
	}
	return fmt.Sprintf("%s%d.%02d", sign, value/100, value%100)
}

func normalizeListQuery(query listQuery) listQuery {
	if query.Page < 1 {
		query.Page = 1
	}
	if query.PageSize < 1 {
		query.PageSize = 20
	}
	if query.PageSize > 100 {
		query.PageSize = 100
	}
	query.Keyword = strings.TrimSpace(query.Keyword)
	query.UserFilter.UserID = strings.TrimSpace(query.UserFilter.UserID)
	query.UserFilter.DisplayUserID = strings.TrimSpace(query.UserFilter.DisplayUserID)
	query.UserFilter.Username = strings.TrimSpace(query.UserFilter.Username)
	query.OwnerFilter.UserID = strings.TrimSpace(query.OwnerFilter.UserID)
	query.OwnerFilter.DisplayUserID = strings.TrimSpace(query.OwnerFilter.DisplayUserID)
	query.OwnerFilter.Username = strings.TrimSpace(query.OwnerFilter.Username)
	query.ParentLeaderFilter.UserID = strings.TrimSpace(query.ParentLeaderFilter.UserID)
	query.ParentLeaderFilter.DisplayUserID = strings.TrimSpace(query.ParentLeaderFilter.DisplayUserID)
	query.ParentLeaderFilter.Username = strings.TrimSpace(query.ParentLeaderFilter.Username)
	query.ParentBDFilter.UserID = strings.TrimSpace(query.ParentBDFilter.UserID)
	query.ParentBDFilter.DisplayUserID = strings.TrimSpace(query.ParentBDFilter.DisplayUserID)
	query.ParentBDFilter.Username = strings.TrimSpace(query.ParentBDFilter.Username)
	query.Status = strings.ToLower(strings.TrimSpace(query.Status))
	query.SortBy = strings.ToLower(strings.TrimSpace(query.SortBy))
	switch query.SortBy {
	case sortByDiamond, sortByMerchantBalance, sortBySalaryWallet, sortByTotalRechargeUSDT:
	default:
		query.SortBy = ""
		query.SortDirection = ""
		return query
	}
	query.SortDirection = strings.ToLower(strings.TrimSpace(query.SortDirection))
	if query.SortDirection != "asc" {
		query.SortDirection = "desc"
	}
	return query
}

func firstNonBlank(values ...string) string {
	for _, value := range values {
		if trimmed := strings.TrimSpace(value); trimmed != "" {
			return trimmed
		}
	}
	return ""
}
