package host

import (
	"context"
	"database/sql"
	"fmt"
	"strconv"
	"strings"

	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	hostdomain "hyapp/services/user-service/internal/domain/host"
	hostservice "hyapp/services/user-service/internal/service/host"
)

// SearchAgencies 返回当前 App 内可加入的有效 Agency；owner 策略为非全域时继续按用户当前国家收敛候选集。
func (r *Repository) SearchAgencies(ctx context.Context, command hostservice.SearchAgenciesCommand) ([]hostdomain.Agency, error) {
	regionMatchRequired, err := r.roleRegionMatchRequired(ctx)
	if err != nil {
		return nil, err
	}
	country, err := r.userCountry(ctx, r.db, command.UserID, "")
	if err != nil {
		return nil, err
	}
	if regionMatchRequired && country == "" {
		// 国家是 App 推荐列表的硬边界，缺失时不能退化成跨国或全局列表。
		return nil, xerr.New(xerr.InvalidArgument, "user country is required")
	}

	query := `
		SELECT
		  a.agency_id,
		  a.owner_user_id,
		  COALESCE(owner.region_id, 0),
		  a.parent_bd_user_id,
		  a.name,
		  a.avatar,
		  a.status,
		  a.join_enabled,
		  a.max_hosts,
		  a.created_by_user_id,
		  a.created_at_ms,
		  a.updated_at_ms,
		  COUNT(active_member.membership_id)
		FROM agencies a
		INNER JOIN users owner
		  ON owner.app_code = a.app_code AND owner.user_id = a.owner_user_id
		LEFT JOIN agency_memberships active_member
		  ON active_member.app_code = a.app_code
		 AND active_member.agency_id = a.agency_id
		 AND active_member.status = ?
		WHERE a.app_code = ?
		  AND owner.status = 'active'
		  AND a.status = ?
		  AND a.join_enabled = TRUE
	`
	args := []any{hostdomain.MembershipStatusActive, appcode.FromContext(ctx), hostdomain.AgencyStatusActive}
	if regionMatchRequired {
		// 非全域策略保留历史国家边界；条件由服务端用户资料派生，客户端不能伪造国家扩大结果集。
		query += ` AND owner.country = ?`
		args = append(args, country)
	}
	if strings.TrimSpace(command.Keyword) != "" {
		keyword := strings.TrimSpace(command.Keyword)
		query += ` AND (a.name LIKE ? OR a.agency_id = ? OR owner.current_display_user_id = ?)`
		args = append(args, "%"+keyword+"%", int64FromKeyword(keyword), keyword)
	}
	query += `
		GROUP BY
		  a.agency_id,
		  a.owner_user_id,
		  owner.region_id,
		  a.parent_bd_user_id,
		  a.name,
		  a.avatar,
		  a.status,
		  a.join_enabled,
		  a.max_hosts,
		  a.created_by_user_id,
		  a.created_at_ms,
		  a.updated_at_ms
		ORDER BY a.created_at_ms DESC, a.agency_id DESC
		LIMIT ?
	`
	args = append(args, command.PageSize)

	rows, err := r.db.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	agencies := make([]hostdomain.Agency, 0, command.PageSize)
	for rows.Next() {
		agency, err := scanSearchAgency(rows)
		if err != nil {
			return nil, err
		}
		agencies = append(agencies, agency)
	}

	return agencies, rows.Err()
}

// GetHostProfile 读取 Host 身份事实。
func (r *Repository) GetHostProfile(ctx context.Context, userID int64) (hostdomain.HostProfile, error) {
	return queryHostProfile(ctx, r.db, "WHERE user_id = ?", userID)
}

// BatchGetHostProfiles 批量读取 Host 身份事实，缺失 user_id 不填充空 profile。
func (r *Repository) BatchGetHostProfiles(ctx context.Context, userIDs []int64) (map[int64]hostdomain.HostProfile, error) {
	if len(userIDs) == 0 {
		return map[int64]hostdomain.HostProfile{}, nil
	}
	placeholders := make([]string, 0, len(userIDs))
	args := make([]any, 0, len(userIDs)+1)
	args = append(args, appcode.FromContext(ctx))
	for _, userID := range userIDs {
		placeholders = append(placeholders, "?")
		args = append(args, userID)
	}

	rows, err := r.db.QueryContext(ctx, fmt.Sprintf(`
		SELECT %s
		FROM host_profiles
		WHERE app_code = ? AND user_id IN (%s)
	`, hostProfileColumns, strings.Join(placeholders, ",")), args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	result := make(map[int64]hostdomain.HostProfile, len(userIDs))
	for rows.Next() {
		// 返回 map 以 user_id 为键，gateway 可以精确区分“非主播/缺失”和“主播但状态不可入账”。
		profile, err := scanHostProfile(rows)
		if err != nil {
			return nil, err
		}
		result[profile.UserID] = profile
	}
	return result, rows.Err()
}

// GetBDProfile 按 role 读取普通 BD 或 BD Leader 身份事实。
func (r *Repository) GetBDProfile(ctx context.Context, userID int64, role string) (hostdomain.BDProfile, error) {
	if role == hostdomain.BDRoleLeader {
		return queryBDLeaderProfile(ctx, r.db, "WHERE user_id = ?", userID)
	}
	return queryBDProfile(ctx, r.db, "WHERE user_id = ?", userID)
}

// GetCoinSellerProfile 读取币商身份事实。
func (r *Repository) GetCoinSellerProfile(ctx context.Context, userID int64) (hostdomain.CoinSellerProfile, error) {
	return queryCoinSellerProfile(ctx, r.db, "WHERE user_id = ?", userID)
}

// GetAgency 读取 Agency 当前事实。
func (r *Repository) GetAgency(ctx context.Context, agencyID int64) (hostdomain.Agency, error) {
	return queryAgency(ctx, r.db, "WHERE agency_id = ?", agencyID)
}

// ListActiveCoinSellersInMyRegion 返回当前用户所在区域的启用币商列表。
// 该查询从 users.region_id 出发过滤，避免把区域条件交给 gateway 或客户端传入。
func (r *Repository) ListActiveCoinSellersInMyRegion(ctx context.Context, userID int64) ([]hostdomain.CoinSellerListItem, error) {
	regionID, err := r.userRegion(ctx, r.db, userID, "")
	if err != nil {
		return nil, err
	}
	if regionID <= 0 {
		// 币商列表是区域内充值入口，缺少当前区域时不能降级成全局列表。
		return nil, xerr.New(xerr.InvalidArgument, "user region is required")
	}
	if err := requireActiveRegion(ctx, r.db, regionID); err != nil {
		return nil, err
	}

	rows, err := r.db.QueryContext(ctx, `
		SELECT
		  u.user_id,
		  u.current_display_user_id,
		  COALESCE(u.username, ''),
		  COALESCE(u.avatar, ''),
		  COALESCE(country.country_id, 0),
		  COALESCE(u.country, ''),
		  COALESCE(country.country_name, ''),
		  COALESCE(country.country_display_name, ''),
		  u.region_id,
		  COALESCE(region.region_code, ''),
		  COALESCE(region.name, ''),
		  seller.status,
		  seller.merchant_asset_type,
		  COALESCE(NULLIF(u.contact_info, ''), seller.contact_info, ''),
		  seller.can_manage_sub_coin_sellers,
		  seller.updated_at_ms
		FROM users u
		INNER JOIN coin_seller_profiles seller
		  ON seller.app_code = u.app_code AND seller.user_id = u.user_id
		LEFT JOIN countries country
		  ON country.app_code = u.app_code AND country.country_code = u.country
		LEFT JOIN regions region
		  ON region.app_code = u.app_code AND region.region_id = u.region_id
		WHERE u.app_code = ?
		  AND u.region_id = ?
		  AND u.status = 'active'
		  AND seller.status = ?
		  AND seller.merchant_asset_type = ?
		ORDER BY seller.updated_at_ms DESC, u.user_id DESC
	`, appcode.FromContext(ctx), regionID, hostdomain.CoinSellerStatusActive, hostdomain.CoinSellerMerchantAssetType)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	items := make([]hostdomain.CoinSellerListItem, 0)
	for rows.Next() {
		var item hostdomain.CoinSellerListItem
		if err := rows.Scan(
			&item.UserID,
			&item.DisplayUserID,
			&item.Username,
			&item.Avatar,
			&item.CountryID,
			&item.CountryCode,
			&item.CountryName,
			&item.CountryDisplayName,
			&item.RegionID,
			&item.RegionCode,
			&item.RegionName,
			&item.Status,
			&item.MerchantAssetType,
			&item.ContactInfo,
			&item.CanManageSubCoinSellers,
			&item.UpdatedAtMs,
		); err != nil {
			return nil, err
		}
		items = append(items, item)
	}
	return items, rows.Err()
}

// GetUserRoleSummary 用一组索引点查构造我的页角色摘要。
// 摘要只表达入口显隐需要的当前身份，不加载成员列表、团队统计或薪资数据。
func (r *Repository) GetUserRoleSummary(ctx context.Context, userID int64) (hostdomain.UserRoleSummary, error) {
	app := appcode.FromContext(ctx)
	summary := hostdomain.UserRoleSummary{UserID: userID}
	var hostStatus string
	var ownerAgencyID int64
	var bdUserID int64
	var bdStatus string
	var leaderUserID int64
	var leaderStatus string
	var managerUserID int64
	var coinSellerStatus string
	var merchantAssetType string

	err := r.db.QueryRowContext(ctx, `
		SELECT
		  COALESCE(h.status, ''),
		  COALESCE(a.agency_id, 0),
		  COALESCE(b.user_id, 0),
		  COALESCE(b.status, ''),
		  COALESCE(bl.user_id, 0),
		  COALESCE(bl.status, ''),
		  COALESCE(m.user_id, 0),
		  COALESCE(c.status, ''),
		  COALESCE(c.merchant_asset_type, '')
		FROM (SELECT ? AS user_id) u
		LEFT JOIN host_profiles h
		  ON h.app_code = ? AND h.user_id = u.user_id
		LEFT JOIN agencies a
		  ON a.app_code = ? AND a.owner_user_id = u.user_id AND a.status = ?
		LEFT JOIN bd_profiles b
		  ON b.app_code = ? AND b.user_id = u.user_id
		LEFT JOIN bd_leader_profiles bl
		  ON bl.app_code = ? AND bl.user_id = u.user_id
		LEFT JOIN manager_profiles m
		  ON m.app_code = ? AND m.user_id = u.user_id AND m.status = ?
		LEFT JOIN coin_seller_profiles c
		  ON c.app_code = ? AND c.user_id = u.user_id`,
		userID,
		app,
		app, hostdomain.AgencyStatusActive,
		app,
		app,
		app, hostdomain.ManagerStatusActive,
		app,
	).Scan(&hostStatus, &ownerAgencyID, &bdUserID, &bdStatus, &leaderUserID, &leaderStatus, &managerUserID, &coinSellerStatus, &merchantAssetType)
	if err != nil {
		return hostdomain.UserRoleSummary{}, err
	}

	summary.HostStatus = hostStatus
	summary.IsHost = hostStatus == hostdomain.HostStatusActive
	// Agency 身份只来自 active Agency owner 行；普通 Host 即使有 current_agency_id，也只是成员归属，不能暴露 Agency Center。
	summary.IsAgency = ownerAgencyID > 0
	if summary.IsAgency {
		summary.AgencyID = ownerAgencyID
	}
	// 经理身份来自独立 manager_profiles 表；Agency owner、Host、BD 或 BD Leader 身份都不会隐式获得经理中心入口。
	summary.IsManager = managerUserID > 0
	summary.BDStatus = bdStatus
	if bdUserID > 0 && bdStatus == hostdomain.BDStatusActive {
		summary.BDID = bdUserID
		summary.IsBD = true
	}
	if leaderUserID > 0 && leaderStatus == hostdomain.BDStatusActive {
		if summary.BDID == 0 {
			summary.BDID = leaderUserID
		}
		if summary.BDStatus == "" {
			summary.BDStatus = leaderStatus
		}
		summary.IsBDLeader = true
	}
	summary.CoinSellerStatus = coinSellerStatus
	summary.IsCoinSeller = coinSellerStatus == hostdomain.CoinSellerStatusActive && merchantAssetType == hostdomain.CoinSellerMerchantAssetType
	if err := r.db.QueryRowContext(ctx, `
		SELECT COUNT(*)
		FROM role_invitations
		WHERE app_code = ? AND target_user_id = ? AND status = ?`,
		app, userID, hostdomain.InvitationStatusPending,
	).Scan(&summary.PendingRoleInvitations); err != nil {
		return hostdomain.UserRoleSummary{}, err
	}

	return summary, nil
}

// HasActiveAgencyOwner 精确判断用户是否为 active Agency owner。
// 普通 Agency 成员只保留 host_profile.current_agency_id 归属，不会在 UserRoleSummary 中获得 Agency 入口。
func (r *Repository) HasActiveAgencyOwner(ctx context.Context, userID int64) (int64, bool, error) {
	agency, ok, err := queryActiveAgencyByOwner(ctx, r.db, userID, false)
	if err != nil || !ok {
		return 0, ok, err
	}
	return agency.AgencyID, true, nil
}

// HasActiveManagerProfile 精确判断用户是否拥有 active 经理身份。
// 经理是 BD Leader/Admin 的上级身份，不能从 Agency owner 或 BD/BD Leader 身份推导出来。
func (r *Repository) HasActiveManagerProfile(ctx context.Context, userID int64) (bool, error) {
	var id int64
	err := r.db.QueryRowContext(ctx, `
		SELECT user_id
		FROM manager_profiles
		WHERE app_code = ? AND user_id = ? AND status = ?
		LIMIT 1
	`, appcode.FromContext(ctx), userID, hostdomain.ManagerStatusActive).Scan(&id)
	if err == sql.ErrNoRows {
		return false, nil
	}
	if err != nil {
		return false, err
	}
	return id > 0, nil
}

// HasActiveManagerCapability 精确校验经理中心单项能力。
// 这里把 legacy 的 manager_resource_grant 收敛成任一资源赠送权限，写入口仍会按真实资源类型再校验一次。
func (r *Repository) HasActiveManagerCapability(ctx context.Context, userID int64, capability string) (bool, error) {
	columnSQL := ""
	switch capability {
	case hostservice.CapabilityManagerResourceGrant:
		columnSQL = "(can_grant_avatar_frame = 1 OR can_grant_vehicle = 1 OR can_grant_badge = 1)"
	case hostservice.CapabilityManagerGrantAvatarFrame:
		columnSQL = "can_grant_avatar_frame = 1"
	case hostservice.CapabilityManagerGrantVehicle:
		columnSQL = "can_grant_vehicle = 1"
	case hostservice.CapabilityManagerGrantBadge:
		columnSQL = "can_grant_badge = 1"
	case hostservice.CapabilityManagerGrantVIP:
		columnSQL = "can_grant_vip = 1"
	case hostservice.CapabilityManagerUpdateUserLevel:
		columnSQL = "can_update_user_level = 1"
	case hostservice.CapabilityManagerAddBDLeader:
		columnSQL = "can_add_bd_leader = 1"
	case hostservice.CapabilityManagerCrossRegion, hostservice.CapabilityManagerAddBDLeaderInRegion:
		columnSQL = "can_manage_cross_region = 1"
	case hostservice.CapabilityManagerAddAdmin:
		columnSQL = "can_add_admin = 1"
	case hostservice.CapabilityManagerAddSuperadmin:
		columnSQL = "can_add_superadmin = 1"
	case hostservice.CapabilityManagerBlockUser:
		columnSQL = "can_block_user = 1"
	case hostservice.CapabilityManagerTransferCountry:
		columnSQL = "can_transfer_user_country = 1"
	default:
		return false, nil
	}
	var id int64
	err := r.db.QueryRowContext(ctx, `
		SELECT user_id
		FROM manager_profiles
		WHERE app_code = ? AND user_id = ? AND status = ? AND `+columnSQL+`
		LIMIT 1
	`, appcode.FromContext(ctx), userID, hostdomain.ManagerStatusActive).Scan(&id)
	if err == sql.ErrNoRows {
		return false, nil
	}
	if err != nil {
		return false, err
	}
	return id > 0, nil
}

// ListBDLeaderBDs 读取 BD Leader 直属普通 BD；调用前先校验独立 Leader 行当前仍有效，避免停用后继续读取团队数据。
func (r *Repository) ListBDLeaderBDs(ctx context.Context, command hostservice.ListBDLeaderBDsCommand) ([]hostdomain.BDProfile, error) {
	leader, err := queryBDLeaderProfile(ctx, r.db, "WHERE user_id = ?", command.LeaderUserID)
	if err != nil {
		return nil, err
	}
	if leader.Status != hostdomain.BDStatusActive {
		return nil, xerr.New(xerr.PermissionDenied, "leader is not active")
	}
	status := command.Status
	if status == "" {
		status = hostdomain.BDStatusActive
	}
	clause, args := appScopedClause(ctx, "WHERE role = ? AND parent_leader_user_id = ? AND status = ? ORDER BY created_at_ms DESC, user_id DESC LIMIT ?", hostdomain.BDRoleBD, command.LeaderUserID, status, command.PageSize)
	return queryBDProfiles(ctx, r.db, clause, args...)
}

// ListBDLeaderAgencies 读取 BD Leader 团队 Agency；包含直属 leader 自己创建的 Agency 和下属普通 BD 创建的 Agency。
func (r *Repository) ListBDLeaderAgencies(ctx context.Context, command hostservice.ListBDLeaderAgenciesCommand) ([]hostdomain.Agency, error) {
	leader, err := queryBDLeaderProfile(ctx, r.db, "WHERE user_id = ?", command.LeaderUserID)
	if err != nil {
		return nil, err
	}
	if leader.Status != hostdomain.BDStatusActive {
		return nil, xerr.New(xerr.PermissionDenied, "leader is not active")
	}
	status := command.Status
	if status == "" {
		status = hostdomain.AgencyStatusActive
	}
	clause, args := appScopedClause(ctx, `WHERE status = ? AND (
		parent_bd_user_id = ?
		OR parent_bd_user_id IN (
			SELECT user_id
			FROM bd_profiles
			WHERE app_code = agencies.app_code
			  AND role = ?
			  AND parent_leader_user_id = ?
			  AND status = ?
		)
	) ORDER BY created_at_ms DESC, agency_id DESC LIMIT ?`, status, command.LeaderUserID, hostdomain.BDRoleBD, command.LeaderUserID, hostdomain.BDStatusActive, command.PageSize)
	return queryAgencies(ctx, r.db, clause, args...)
}

// ListBDAgencies 只读取一个有效普通 BD 直接拓展的 Agency，不包含同负责人下其他 BD 的数据。
func (r *Repository) ListBDAgencies(ctx context.Context, command hostservice.ListBDAgenciesCommand) ([]hostdomain.Agency, error) {
	bd, err := queryBDProfile(ctx, r.db, "WHERE user_id = ?", command.BDUserID)
	if err != nil {
		return nil, err
	}
	if bd.Status != hostdomain.BDStatusActive || bd.Role != hostdomain.BDRoleBD {
		return nil, xerr.New(xerr.PermissionDenied, "bd is not active")
	}
	status := command.Status
	if status == "" {
		status = hostdomain.AgencyStatusActive
	}
	clause, args := appScopedClause(ctx, "WHERE status = ? AND parent_bd_user_id = ? ORDER BY created_at_ms DESC, agency_id DESC LIMIT ?", status, command.BDUserID, command.PageSize)
	return queryAgencies(ctx, r.db, clause, args...)
}

// ListManagerTeamAgencies 读取经理整棵团队树内的有效 Agency 归属投影：
// 经理 → bd_leader_profiles.created_by_user_id → bd_profiles.parent_leader_user_id → agencies.parent_bd_user_id。
// 逐层先解析出上级 ID 列表再用单个 IN 查询 Agency，避免 OR + 相关子查询让优化器放弃
// idx_agencies_parent_bd 索引退化成整租户扫描；投影只取归属三列，不带成员数/区域等每行相关子查询。
func (r *Repository) ListManagerTeamAgencies(ctx context.Context, command hostservice.ListManagerTeamAgenciesCommand) (hostdomain.ManagerTeamAgencies, error) {
	// 经理身份停用后不能继续读取团队数据，与 BD Leader 列表接口的权限语义保持一致。
	active, err := r.HasActiveManagerProfile(ctx, command.ManagerUserID)
	if err != nil {
		return hostdomain.ManagerTeamAgencies{}, err
	}
	if !active {
		return hostdomain.ManagerTeamAgencies{}, xerr.New(xerr.PermissionDenied, "manager is not active")
	}
	app := appcode.FromContext(ctx)
	result := hostdomain.ManagerTeamAgencies{Agencies: []hostdomain.ManagerTeamAgency{}}
	// 上级链路每层都带截断上限：正常团队远小于该值，超限用 Truncated 显式标记而不是静默丢数据。
	leaderIDs, leadersTruncated, err := r.listManagerTeamUserIDs(ctx, `
		SELECT user_id
		FROM bd_leader_profiles
		WHERE app_code = ? AND created_by_user_id = ? AND status = ?
		ORDER BY user_id ASC
		LIMIT ?`, command.PageSize, app, command.ManagerUserID, hostdomain.BDStatusActive)
	if err != nil {
		return hostdomain.ManagerTeamAgencies{}, err
	}
	result.TotalBDLeaders = len(leaderIDs)
	result.Truncated = leadersTruncated
	if len(leaderIDs) == 0 {
		// 没有任何 BD Leader 时团队为空，直接返回零值统计。
		return result, nil
	}
	bdArgs := make([]any, 0, len(leaderIDs)+3)
	bdArgs = append(bdArgs, app, hostdomain.BDRoleBD, hostdomain.BDStatusActive)
	for _, id := range leaderIDs {
		bdArgs = append(bdArgs, id)
	}
	bdIDs, bdsTruncated, err := r.listManagerTeamUserIDs(ctx, `
		SELECT user_id
		FROM bd_profiles
		WHERE app_code = ? AND role = ? AND status = ?
		  AND parent_leader_user_id IN (`+placeholderList(len(leaderIDs))+`)
		ORDER BY user_id ASC
		LIMIT ?`, command.PageSize, bdArgs...)
	if err != nil {
		return hostdomain.ManagerTeamAgencies{}, err
	}
	result.TotalBDs = len(bdIDs)
	result.Truncated = result.Truncated || bdsTruncated

	// Agency 既可能直接挂 BD Leader 也可能挂下属 BD，合并成一个父级集合后单索引路径查询。
	parentIDs := append(append(make([]int64, 0, len(leaderIDs)+len(bdIDs)), leaderIDs...), bdIDs...)
	agencyArgs := make([]any, 0, len(parentIDs)+3)
	agencyArgs = append(agencyArgs, app, hostdomain.AgencyStatusActive)
	for _, id := range parentIDs {
		agencyArgs = append(agencyArgs, id)
	}
	agencyArgs = append(agencyArgs, command.PageSize+1)
	rows, err := r.db.QueryContext(ctx, `
		SELECT agency_id, owner_user_id, COALESCE(parent_bd_user_id, 0)
		FROM agencies
		WHERE app_code = ? AND status = ?
		  AND parent_bd_user_id IN (`+placeholderList(len(parentIDs))+`)
		ORDER BY agency_id ASC
		LIMIT ?`, agencyArgs...)
	if err != nil {
		return hostdomain.ManagerTeamAgencies{}, err
	}
	defer rows.Close()
	for rows.Next() {
		var agency hostdomain.ManagerTeamAgency
		if err := rows.Scan(&agency.AgencyID, &agency.OwnerUserID, &agency.ParentBDUserID); err != nil {
			return hostdomain.ManagerTeamAgencies{}, err
		}
		result.Agencies = append(result.Agencies, agency)
	}
	if err := rows.Err(); err != nil {
		return hostdomain.ManagerTeamAgencies{}, err
	}
	if len(result.Agencies) > command.PageSize {
		result.Truncated = true
		result.Agencies = result.Agencies[:command.PageSize]
	}
	return result, nil
}

// listManagerTeamUserIDs 执行“取上限 limit+1 行识别截断”的 ID 列表查询；
// query 末尾必须带 LIMIT ? 占位，本函数负责追加 limit+1 参数并返回截断后的 ID 集合和截断标记。
func (r *Repository) listManagerTeamUserIDs(ctx context.Context, query string, limit int, args ...any) ([]int64, bool, error) {
	rows, err := r.db.QueryContext(ctx, query, append(args, limit+1)...)
	if err != nil {
		return nil, false, err
	}
	defer rows.Close()
	ids := make([]int64, 0, limit)
	for rows.Next() {
		var id int64
		if err := rows.Scan(&id); err != nil {
			return nil, false, err
		}
		ids = append(ids, id)
	}
	if err := rows.Err(); err != nil {
		return nil, false, err
	}
	if len(ids) > limit {
		return ids[:limit], true, nil
	}
	return ids, false, nil
}

// placeholderList 生成 IN 子句占位符；调用方保证 count > 0。
func placeholderList(count int) string {
	if count <= 0 {
		return "NULL"
	}
	out := make([]byte, 0, count*2-1)
	for i := 0; i < count; i++ {
		if i > 0 {
			out = append(out, ',')
		}
		out = append(out, '?')
	}
	return string(out)
}

// ListRoleInvitations 读取目标用户收到的角色邀请消息；默认由业务层传入 pending 状态用于消息中心。
func (r *Repository) ListRoleInvitations(ctx context.Context, command hostservice.ListRoleInvitationsCommand) ([]hostdomain.RoleInvitation, error) {
	query := fmt.Sprintf(`SELECT %s FROM role_invitations WHERE app_code = ? AND target_user_id = ?`, roleInvitationColumns)
	args := []any{appcode.FromContext(ctx), command.TargetUserID}
	if command.Status != "" {
		query += " AND status = ?"
		args = append(args, command.Status)
	}
	// 消息中心优先展示最新邀请；同毫秒内用 invitation_id 保持稳定排序。
	query += " ORDER BY created_at_ms DESC, invitation_id DESC LIMIT ?"
	args = append(args, command.PageSize)
	return queryRoleInvitations(ctx, r.db, query, args...)
}

// queryPendingRoleInvitationByTarget 通过 pending_target_key 唯一索引读取目标当前邀请。
// 邀请创建事务传入 forUpdate=true，把“检查 pending + 创建/回放邀请”串行化，避免连续点击用不同 command_id 撞成泛化 409。
func queryPendingRoleInvitationByTarget(ctx context.Context, q sqlQueryer, invitationType string, targetUserID int64, forUpdate bool) (hostdomain.RoleInvitation, bool, error) {
	query := fmt.Sprintf(`SELECT %s FROM role_invitations WHERE app_code = ? AND pending_target_key = ?`, roleInvitationColumns)
	if forUpdate {
		query += " FOR UPDATE"
	}
	invitation, err := scanRoleInvitation(q.QueryRowContext(ctx, query, appcode.FromContext(ctx), fmt.Sprintf("%s:%d", invitationType, targetUserID)))
	if err == sql.ErrNoRows {
		return hostdomain.RoleInvitation{}, false, nil
	}
	if err != nil {
		return hostdomain.RoleInvitation{}, false, err
	}
	return invitation, true, nil
}

// ListAgencyMembers 读取 Agency 成员关系列表。
func (r *Repository) ListAgencyMembers(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyMembership, error) {
	query := fmt.Sprintf(`SELECT %s FROM agency_memberships WHERE app_code = ? AND agency_id = ?`, agencyMembershipColumns)
	args := []any{appcode.FromContext(ctx), agencyID}
	if status != "" {
		query += " AND status = ?"
		args = append(args, status)
	}
	query += " ORDER BY joined_at_ms DESC, membership_id DESC"
	return queryAgencyMemberships(ctx, r.db, query, args...)
}

// ListAgencyApplications 读取 Agency 申请列表。
func (r *Repository) ListAgencyApplications(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyApplication, error) {
	query := fmt.Sprintf(`SELECT %s FROM agency_applications WHERE app_code = ? AND agency_id = ?`, agencyApplicationColumns)
	args := []any{appcode.FromContext(ctx), agencyID}
	if status != "" {
		query += " AND status = ?"
		args = append(args, status)
	}
	query += " ORDER BY created_at_ms DESC, application_id DESC"
	return queryAgencyApplications(ctx, r.db, query, args...)
}

func queryHostProfile(ctx context.Context, q sqlQueryer, clause string, args ...any) (hostdomain.HostProfile, error) {
	clause, args = appScopedClause(ctx, clause, args...)
	profile, err := scanHostProfile(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM host_profiles %s`, hostProfileColumns, clause), args...))
	if err == sql.ErrNoRows {
		return hostdomain.HostProfile{}, xerr.New(xerr.NotFound, "host profile not found")
	}
	return profile, err
}

func queryHostProfileMaybe(ctx context.Context, q sqlQueryer, userID int64, lock bool) (hostdomain.HostProfile, bool, error) {
	clause := "WHERE user_id = ? AND status = ?"
	if lock {
		clause += " FOR UPDATE"
	}
	clause, args := appScopedClause(ctx, clause, userID, hostdomain.HostStatusActive)
	profile, err := scanHostProfile(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM host_profiles %s`, hostProfileColumns, clause), args...))
	if err == sql.ErrNoRows {
		return hostdomain.HostProfile{}, false, nil
	}
	if err != nil {
		return hostdomain.HostProfile{}, false, err
	}
	return profile, true, nil
}

func queryHostProfileByUserMaybe(ctx context.Context, q sqlQueryer, userID int64, lock bool) (hostdomain.HostProfile, bool, error) {
	clause := "WHERE user_id = ?"
	if lock {
		// 后台创建 Agency owner 时需要覆盖 active/disabled 任意主播身份行，避免重复插入主键。
		clause += " FOR UPDATE"
	}
	clause, args := appScopedClause(ctx, clause, userID)
	profile, err := scanHostProfile(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM host_profiles %s`, hostProfileColumns, clause), args...))
	if err == sql.ErrNoRows {
		return hostdomain.HostProfile{}, false, nil
	}
	if err != nil {
		return hostdomain.HostProfile{}, false, err
	}
	return profile, true, nil
}

func scanHostProfile(scanner rowScanner) (hostdomain.HostProfile, error) {
	var profile hostdomain.HostProfile
	err := scanner.Scan(
		&profile.UserID,
		&profile.Status,
		&profile.RegionID,
		&profile.CurrentAgencyID,
		&profile.CurrentMembershipID,
		&profile.CurrentAgencyOwnerUserID,
		&profile.Source,
		&profile.FirstBecameHostAtMs,
		&profile.CreatedAtMs,
		&profile.UpdatedAtMs,
	)
	return profile, err
}

func queryAgency(ctx context.Context, q sqlQueryer, clause string, args ...any) (hostdomain.Agency, error) {
	clause, args = appScopedClause(ctx, clause, args...)
	agency, err := scanAgency(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM agencies %s`, agencyColumns, clause), args...))
	if err == sql.ErrNoRows {
		return hostdomain.Agency{}, xerr.New(xerr.NotFound, "agency not found")
	}
	return agency, err
}

func queryAgencies(ctx context.Context, q sqlQueryer, clause string, args ...any) ([]hostdomain.Agency, error) {
	rows, err := q.QueryContext(ctx, fmt.Sprintf(`SELECT %s FROM agencies %s`, agencyColumns, clause), args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	agencies := make([]hostdomain.Agency, 0)
	for rows.Next() {
		agency, err := scanAgency(rows)
		if err != nil {
			return nil, err
		}
		agencies = append(agencies, agency)
	}
	return agencies, rows.Err()
}

func queryActiveAgencyByOwner(ctx context.Context, q sqlQueryer, ownerUserID int64, lock bool) (hostdomain.Agency, bool, error) {
	clause := "WHERE owner_user_id = ? AND status = ?"
	if lock {
		clause += " FOR UPDATE"
	}
	clause, args := appScopedClause(ctx, clause, ownerUserID, hostdomain.AgencyStatusActive)
	agency, err := scanAgency(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM agencies %s`, agencyColumns, clause), args...))
	if err == sql.ErrNoRows {
		return hostdomain.Agency{}, false, nil
	}
	if err != nil {
		return hostdomain.Agency{}, false, err
	}
	return agency, true, nil
}

func queryAgencyByOwnerMaybe(ctx context.Context, q sqlQueryer, ownerUserID int64, lock bool) (hostdomain.Agency, bool, error) {
	clause := "WHERE owner_user_id = ?"
	if lock {
		// 后台创建 Agency 时要锁住 owner 名下所有历史 Agency 行，避免 active/closed 切换和新建并发。
		clause += " FOR UPDATE"
	}
	clause, args := appScopedClause(ctx, clause, ownerUserID)
	agency, err := scanAgency(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM agencies %s`, agencyColumns, clause), args...))
	if err == sql.ErrNoRows {
		return hostdomain.Agency{}, false, nil
	}
	if err != nil {
		return hostdomain.Agency{}, false, err
	}
	return agency, true, nil
}

func scanAgency(scanner rowScanner) (hostdomain.Agency, error) {
	var agency hostdomain.Agency
	err := scanner.Scan(
		&agency.AgencyID,
		&agency.OwnerUserID,
		&agency.RegionID,
		&agency.ParentBDUserID,
		&agency.Name,
		&agency.Avatar,
		&agency.Status,
		&agency.JoinEnabled,
		&agency.MaxHosts,
		&agency.CreatedByUserID,
		&agency.CreatedAtMs,
		&agency.UpdatedAtMs,
		&agency.ActiveHostCount,
	)
	return agency, err
}

func scanSearchAgency(scanner rowScanner) (hostdomain.Agency, error) {
	var agency hostdomain.Agency
	err := scanner.Scan(
		&agency.AgencyID,
		&agency.OwnerUserID,
		&agency.RegionID,
		&agency.ParentBDUserID,
		&agency.Name,
		&agency.Avatar,
		&agency.Status,
		&agency.JoinEnabled,
		&agency.MaxHosts,
		&agency.CreatedByUserID,
		&agency.CreatedAtMs,
		&agency.UpdatedAtMs,
		&agency.ActiveHostCount,
	)
	return agency, err
}

func int64FromKeyword(keyword string) int64 {
	value, err := strconv.ParseInt(strings.TrimSpace(keyword), 10, 64)
	if err != nil {
		return 0
	}
	return value
}

func queryBDProfile(ctx context.Context, q sqlQueryer, clause string, args ...any) (hostdomain.BDProfile, error) {
	clause, args = appScopedClause(ctx, clause, args...)
	profile, err := scanBDProfile(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM bd_profiles %s`, bdProfileColumns, clause), args...))
	if err == sql.ErrNoRows {
		return hostdomain.BDProfile{}, xerr.New(xerr.NotFound, "bd profile not found")
	}
	return profile, err
}

func queryBDProfiles(ctx context.Context, q sqlQueryer, clause string, args ...any) ([]hostdomain.BDProfile, error) {
	rows, err := q.QueryContext(ctx, fmt.Sprintf(`SELECT %s FROM bd_profiles %s`, bdProfileColumns, clause), args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	profiles := make([]hostdomain.BDProfile, 0)
	for rows.Next() {
		profile, err := scanBDProfile(rows)
		if err != nil {
			return nil, err
		}
		profiles = append(profiles, profile)
	}
	return profiles, rows.Err()
}

func queryBDLeaderProfile(ctx context.Context, q sqlQueryer, clause string, args ...any) (hostdomain.BDProfile, error) {
	clause, args = appScopedClause(ctx, clause, args...)
	profile, err := scanBDProfile(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM bd_leader_profiles %s`, bdLeaderProfileColumns, clause), args...))
	if err == sql.ErrNoRows {
		return hostdomain.BDProfile{}, xerr.New(xerr.NotFound, "bd leader profile not found")
	}
	return profile, err
}

func queryBDProfileMaybe(ctx context.Context, q sqlQueryer, userID int64, lock bool) (hostdomain.BDProfile, bool, error) {
	clause := "WHERE user_id = ? AND status = ?"
	if lock {
		clause += " FOR UPDATE"
	}
	clause, args := appScopedClause(ctx, clause, userID, hostdomain.BDStatusActive)
	profile, err := scanBDProfile(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM bd_profiles %s`, bdProfileColumns, clause), args...))
	if err == sql.ErrNoRows {
		return hostdomain.BDProfile{}, false, nil
	}
	if err != nil {
		return hostdomain.BDProfile{}, false, err
	}
	return profile, true, nil
}

func queryBDLeaderProfileMaybe(ctx context.Context, q sqlQueryer, userID int64, lock bool) (hostdomain.BDProfile, bool, error) {
	clause := "WHERE user_id = ? AND status = ?"
	if lock {
		clause += " FOR UPDATE"
	}
	clause, args := appScopedClause(ctx, clause, userID, hostdomain.BDStatusActive)
	profile, err := scanBDProfile(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM bd_leader_profiles %s`, bdLeaderProfileColumns, clause), args...))
	if err == sql.ErrNoRows {
		return hostdomain.BDProfile{}, false, nil
	}
	if err != nil {
		return hostdomain.BDProfile{}, false, err
	}
	return profile, true, nil
}

func queryBDProfileByUserMaybe(ctx context.Context, q sqlQueryer, userID int64, lock bool) (hostdomain.BDProfile, bool, error) {
	clause := "WHERE user_id = ?"
	if lock {
		// 后台创建/停用角色使用主键锁，覆盖 active 和 disabled 两种状态。
		clause += " FOR UPDATE"
	}
	clause, args := appScopedClause(ctx, clause, userID)
	profile, err := scanBDProfile(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM bd_profiles %s`, bdProfileColumns, clause), args...))
	if err == sql.ErrNoRows {
		return hostdomain.BDProfile{}, false, nil
	}
	if err != nil {
		return hostdomain.BDProfile{}, false, err
	}
	return profile, true, nil
}

func queryBDLeaderProfileByUserMaybe(ctx context.Context, q sqlQueryer, userID int64, lock bool) (hostdomain.BDProfile, bool, error) {
	clause := "WHERE user_id = ?"
	if lock {
		// 后台创建/停用 Leader 使用主键锁，覆盖 active 和 disabled 两种状态。
		clause += " FOR UPDATE"
	}
	clause, args := appScopedClause(ctx, clause, userID)
	profile, err := scanBDProfile(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM bd_leader_profiles %s`, bdLeaderProfileColumns, clause), args...))
	if err == sql.ErrNoRows {
		return hostdomain.BDProfile{}, false, nil
	}
	if err != nil {
		return hostdomain.BDProfile{}, false, err
	}
	return profile, true, nil
}

func scanBDProfile(scanner rowScanner) (hostdomain.BDProfile, error) {
	var profile hostdomain.BDProfile
	err := scanner.Scan(
		&profile.UserID,
		&profile.Role,
		&profile.RegionID,
		&profile.ParentLeaderUserID,
		&profile.ParentOwnerUserID,
		&profile.Status,
		&profile.CreatedByUserID,
		&profile.CreatedAtMs,
		&profile.UpdatedAtMs,
	)
	return profile, err
}

func queryCoinSellerProfile(ctx context.Context, q sqlQueryer, clause string, args ...any) (hostdomain.CoinSellerProfile, error) {
	clause, args = appScopedClause(ctx, clause, args...)
	profile, err := scanCoinSellerProfile(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM coin_seller_profiles %s`, coinSellerProfileColumns, clause), args...))
	if err == sql.ErrNoRows {
		return hostdomain.CoinSellerProfile{}, xerr.New(xerr.NotFound, "coin_seller profile not found")
	}
	return profile, err
}

func queryCoinSellerProfileByUserMaybe(ctx context.Context, q sqlQueryer, userID int64, lock bool) (hostdomain.CoinSellerProfile, bool, error) {
	clause := "WHERE user_id = ?"
	if lock {
		// 币商身份可以和 Agency/BD/BD Leader 并存，但同一用户只能有一条币商身份事实。
		clause += " FOR UPDATE"
	}
	clause, args := appScopedClause(ctx, clause, userID)
	profile, err := scanCoinSellerProfile(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM coin_seller_profiles %s`, coinSellerProfileColumns, clause), args...))
	if err == sql.ErrNoRows {
		return hostdomain.CoinSellerProfile{}, false, nil
	}
	if err != nil {
		return hostdomain.CoinSellerProfile{}, false, err
	}
	return profile, true, nil
}

func scanCoinSellerProfile(scanner rowScanner) (hostdomain.CoinSellerProfile, error) {
	var profile hostdomain.CoinSellerProfile
	err := scanner.Scan(
		&profile.UserID,
		&profile.Status,
		&profile.MerchantAssetType,
		&profile.CanManageSubCoinSellers,
		&profile.CreatedByUserID,
		&profile.CreatedAtMs,
		&profile.UpdatedAtMs,
	)
	return profile, err
}

func queryAgencyMembership(ctx context.Context, q sqlQueryer, clause string, args ...any) (hostdomain.AgencyMembership, error) {
	clause, args = appScopedClause(ctx, clause, args...)
	membership, err := scanAgencyMembership(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM agency_memberships %s`, agencyMembershipColumns, clause), args...))
	if err == sql.ErrNoRows {
		return hostdomain.AgencyMembership{}, xerr.New(xerr.NotFound, "agency membership not found")
	}
	return membership, err
}

func queryActiveMembershipByHost(ctx context.Context, q sqlQueryer, hostUserID int64, lock bool) (hostdomain.AgencyMembership, bool, error) {
	clause := "WHERE host_user_id = ? AND status = ?"
	if lock {
		clause += " FOR UPDATE"
	}
	clause, args := appScopedClause(ctx, clause, hostUserID, hostdomain.MembershipStatusActive)
	membership, err := scanAgencyMembership(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM agency_memberships %s`, agencyMembershipColumns, clause), args...))
	if err == sql.ErrNoRows {
		return hostdomain.AgencyMembership{}, false, nil
	}
	if err != nil {
		return hostdomain.AgencyMembership{}, false, err
	}
	return membership, true, nil
}

func queryAgencyMemberships(ctx context.Context, q sqlQueryer, query string, args ...any) ([]hostdomain.AgencyMembership, error) {
	rows, err := q.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	memberships := make([]hostdomain.AgencyMembership, 0)
	for rows.Next() {
		membership, err := scanAgencyMembership(rows)
		if err != nil {
			return nil, err
		}
		memberships = append(memberships, membership)
	}
	return memberships, rows.Err()
}

func scanAgencyMembership(scanner rowScanner) (hostdomain.AgencyMembership, error) {
	var membership hostdomain.AgencyMembership
	err := scanner.Scan(
		&membership.MembershipID,
		&membership.AgencyID,
		&membership.HostUserID,
		&membership.RegionID,
		&membership.MembershipType,
		&membership.Status,
		&membership.JoinedAtMs,
		&membership.EndedAtMs,
		&membership.EndedByUserID,
		&membership.EndedReason,
		&membership.CreatedAtMs,
		&membership.UpdatedAtMs,
	)
	return membership, err
}

func queryAgencyApplication(ctx context.Context, q sqlQueryer, clause string, args ...any) (hostdomain.AgencyApplication, error) {
	clause, args = appScopedClause(ctx, clause, args...)
	application, err := scanAgencyApplication(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM agency_applications %s`, agencyApplicationColumns, clause), args...))
	if err == sql.ErrNoRows {
		return hostdomain.AgencyApplication{}, xerr.New(xerr.NotFound, "agency application not found")
	}
	return application, err
}

func queryAgencyApplications(ctx context.Context, q sqlQueryer, query string, args ...any) ([]hostdomain.AgencyApplication, error) {
	rows, err := q.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	applications := make([]hostdomain.AgencyApplication, 0)
	for rows.Next() {
		application, err := scanAgencyApplication(rows)
		if err != nil {
			return nil, err
		}
		applications = append(applications, application)
	}
	return applications, rows.Err()
}

func scanAgencyApplication(scanner rowScanner) (hostdomain.AgencyApplication, error) {
	var application hostdomain.AgencyApplication
	err := scanner.Scan(
		&application.ApplicationID,
		&application.CommandID,
		&application.ApplicantUserID,
		&application.AgencyID,
		&application.RegionID,
		&application.Status,
		&application.ReviewedByUserID,
		&application.ReviewReason,
		&application.ReviewedAtMs,
		&application.CreatedAtMs,
		&application.UpdatedAtMs,
	)
	return application, err
}

func queryRoleInvitation(ctx context.Context, q sqlQueryer, clause string, args ...any) (hostdomain.RoleInvitation, error) {
	clause, args = appScopedClause(ctx, clause, args...)
	invitation, err := scanRoleInvitation(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM role_invitations %s`, roleInvitationColumns, clause), args...))
	if err == sql.ErrNoRows {
		return hostdomain.RoleInvitation{}, xerr.New(xerr.NotFound, "role invitation not found")
	}
	return invitation, err
}

func queryRoleInvitations(ctx context.Context, q sqlQueryer, query string, args ...any) ([]hostdomain.RoleInvitation, error) {
	rows, err := q.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	invitations := make([]hostdomain.RoleInvitation, 0)
	for rows.Next() {
		invitation, err := scanRoleInvitation(rows)
		if err != nil {
			return nil, err
		}
		invitations = append(invitations, invitation)
	}
	return invitations, rows.Err()
}

func scanRoleInvitation(scanner rowScanner) (hostdomain.RoleInvitation, error) {
	var invitation hostdomain.RoleInvitation
	err := scanner.Scan(
		&invitation.InvitationID,
		&invitation.CommandID,
		&invitation.InvitationType,
		&invitation.Status,
		&invitation.InviterUserID,
		&invitation.InviterBDUserID,
		&invitation.TargetUserID,
		&invitation.ExternalOperatorUserID,
		&invitation.RegionID,
		&invitation.AgencyName,
		&invitation.ParentBDUserID,
		&invitation.ParentLeaderUserID,
		&invitation.ParentOwnerUserID,
		&invitation.ParentAgencyID,
		&invitation.ProcessedByUserID,
		&invitation.ProcessReason,
		&invitation.ProcessedAtMs,
		&invitation.CreatedAtMs,
		&invitation.UpdatedAtMs,
	)
	return invitation, err
}

func reviewResultByApplicationID(ctx context.Context, q sqlQueryer, applicationID int64) (hostdomain.ReviewAgencyApplicationResult, error) {
	application, err := queryAgencyApplication(ctx, q, "WHERE application_id = ?", applicationID)
	if err != nil {
		return hostdomain.ReviewAgencyApplicationResult{}, err
	}
	result := hostdomain.ReviewAgencyApplicationResult{Application: application}
	if application.Status == hostdomain.ApplicationStatusApproved {
		profile, err := queryHostProfile(ctx, q, "WHERE user_id = ?", application.ApplicantUserID)
		if err != nil {
			return hostdomain.ReviewAgencyApplicationResult{}, err
		}
		membership, err := queryAgencyMembership(ctx, q, "WHERE membership_id = ?", profile.CurrentMembershipID)
		if err != nil {
			return hostdomain.ReviewAgencyApplicationResult{}, err
		}
		result.HostProfile = profile
		result.Membership = membership
	}
	return result, nil
}

func kickResultByMembershipID(ctx context.Context, q sqlQueryer, membershipID int64) (hostdomain.KickAgencyHostResult, error) {
	membership, err := queryAgencyMembership(ctx, q, "WHERE membership_id = ?", membershipID)
	if err != nil {
		return hostdomain.KickAgencyHostResult{}, err
	}
	profile, err := queryHostProfile(ctx, q, "WHERE user_id = ?", membership.HostUserID)
	if err != nil {
		return hostdomain.KickAgencyHostResult{}, err
	}
	return hostdomain.KickAgencyHostResult{Membership: membership, HostProfile: profile}, nil
}

func processResultByInvitationID(ctx context.Context, q sqlQueryer, invitationID int64) (hostdomain.ProcessRoleInvitationResult, error) {
	invitation, err := queryRoleInvitation(ctx, q, "WHERE invitation_id = ?", invitationID)
	if err != nil {
		return hostdomain.ProcessRoleInvitationResult{}, err
	}
	result := hostdomain.ProcessRoleInvitationResult{Invitation: invitation}
	if invitation.Status != hostdomain.InvitationStatusAccepted {
		return result, nil
	}

	// 只有接受动作会创建后续身份事实；按邀请类型回填前端需要立即刷新的最新关系。
	switch invitation.InvitationType {
	case hostdomain.InvitationTypeAgency, hostdomain.InvitationTypeExternalAgency:
		if profile, err := queryHostProfile(ctx, q, "WHERE user_id = ?", invitation.TargetUserID); err == nil {
			result.HostProfile = profile
			if profile.CurrentMembershipID > 0 {
				result.Membership, _ = queryAgencyMembership(ctx, q, "WHERE membership_id = ?", profile.CurrentMembershipID)
			}
		}
		if agency, ok, err := queryActiveAgencyByOwner(ctx, q, invitation.TargetUserID, false); err != nil {
			return hostdomain.ProcessRoleInvitationResult{}, err
		} else if ok {
			result.Agency = agency
		}
	case hostdomain.InvitationTypeBD, hostdomain.InvitationTypeExternalBD:
		if bd, err := queryBDProfile(ctx, q, "WHERE user_id = ?", invitation.TargetUserID); err == nil {
			result.BDProfile = bd
		}
	case hostdomain.InvitationTypeHost, hostdomain.InvitationTypeExternalHost:
		if profile, err := queryHostProfile(ctx, q, "WHERE user_id = ?", invitation.TargetUserID); err == nil {
			result.HostProfile = profile
			if profile.CurrentMembershipID > 0 {
				result.Membership, _ = queryAgencyMembership(ctx, q, "WHERE membership_id = ?", profile.CurrentMembershipID)
			}
			if profile.CurrentAgencyID > 0 {
				result.Agency, _ = queryAgency(ctx, q, "WHERE agency_id = ?", profile.CurrentAgencyID)
			}
		}
	}
	return result, nil
}
