package host

import (
	"context"
	"database/sql"
	"encoding/json"

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

const userOutboxEventRoleInvitationCreated = "RoleInvitationCreated"

// InviteAgency 创建 Agency 角色邀请；非全域策略校验 inviter BD 和目标用户区域一致。
func (r *Repository) InviteAgency(ctx context.Context, command hostservice.InviteAgencyCommand) (hostdomain.RoleInvitation, error) {
	regionMatchRequired, err := r.roleRegionMatchRequired(ctx)
	if err != nil {
		return hostdomain.RoleInvitation{}, err
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return hostdomain.RoleInvitation{}, err
	}
	defer tx.Rollback()

	// 邀请是可重试写命令；命中幂等结果时返回既有邀请，
	// 不重新校验当前 BD/用户状态，保持“已提交结果可回放”的语义。
	if existing, ok, err := commandResult(ctx, tx, command.CommandID); err != nil {
		return hostdomain.RoleInvitation{}, err
	} else if ok {
		if err := requireCommandResult(existing, hostdomain.CommandTypeInviteAgency, hostdomain.ResultTypeInvitation); err != nil {
			return hostdomain.RoleInvitation{}, err
		}
		return queryRoleInvitation(ctx, tx, "WHERE invitation_id = ?", existing.ResultID)
	}

	// InviteAgency 没有显式角色字段；普通 BD 行优先承载 BD Center 邀请，普通 BD 不可用时再使用 Leader 行承载负责人直属邀请。
	// 两种身份都用 FOR UPDATE 串行化停用/换区和邀请创建，避免旧权限在同一事务窗口内继续写入。
	inviter, err := queryActiveAgencyInviterProfile(ctx, tx, command.InviterUserID)
	if err != nil {
		return hostdomain.RoleInvitation{}, err
	}
	// 目标用户还不是 Host/Agency 时没有领域表行，所以锁 users 行来串行化目标身份变更。
	targetRegionID, err := r.userRegion(ctx, tx, command.TargetUserID, "FOR UPDATE")
	if err != nil {
		return hostdomain.RoleInvitation{}, err
	}
	if regionMatchRequired && targetRegionID != inviter.RegionID {
		return hostdomain.RoleInvitation{}, xerr.New(xerr.RegionMismatch, "target region does not match inviter region")
	}
	// Agency owner 和 Host 成员归属是两套身份；目标已有 active Host 绑定时仍允许成为 Agency owner，
	// 但接受邀请时不会覆盖原 Host 当前归属。一个用户只能拥有一个有效 Agency，这里仍先行拦截。
	if _, ok, err := queryActiveAgencyByOwner(ctx, tx, command.TargetUserID, true); err != nil {
		return hostdomain.RoleInvitation{}, err
	} else if ok {
		return hostdomain.RoleInvitation{}, xerr.New(xerr.Conflict, "target already owns active agency")
	}
	// H5 在首个响应丢失、弹窗重新打开等情况下可能为同一目标生成新的 command_id。
	// 同一 BD 的 pending 邀请属于同一业务动作，记录新命令到既有结果并按成功回放；其他 BD 仍不能覆盖目标已收到的邀请。
	if pending, ok, err := queryPendingRoleInvitationByTarget(ctx, tx, hostdomain.InvitationTypeAgency, command.TargetUserID, true); err != nil {
		return hostdomain.RoleInvitation{}, err
	} else if ok {
		if pending.InviterBDUserID != inviter.UserID {
			return hostdomain.RoleInvitation{}, xerr.New(xerr.Conflict, "target already has pending agency invitation")
		}
		if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{
			CommandID:   command.CommandID,
			CommandType: hostdomain.CommandTypeInviteAgency,
			ResultType:  hostdomain.ResultTypeInvitation,
			ResultID:    pending.InvitationID,
			CreatedAtMs: command.NowMs,
		}); err != nil {
			return hostdomain.RoleInvitation{}, mapHostDuplicateError(err)
		}
		if err := tx.Commit(); err != nil {
			return hostdomain.RoleInvitation{}, err
		}
		return pending, nil
	}

	// BD 发出的 Agency 邀请要记录直接上级 BD 和负责人链路；
	// 如果本次以负责人身份邀请，则直接 BD 和负责人都指向 Leader 自己。
	parentLeaderUserID := inviter.ParentLeaderUserID
	if inviter.Role == hostdomain.BDRoleLeader {
		parentLeaderUserID = inviter.UserID
	}
	invitation := hostdomain.RoleInvitation{
		InvitationID:       command.InvitationID,
		CommandID:          command.CommandID,
		InvitationType:     hostdomain.InvitationTypeAgency,
		Status:             hostdomain.InvitationStatusPending,
		InviterUserID:      command.InviterUserID,
		InviterBDUserID:    inviter.UserID,
		TargetUserID:       command.TargetUserID,
		RegionID:           targetRegionID,
		AgencyName:         command.AgencyName,
		ParentBDUserID:     inviter.UserID,
		ParentLeaderUserID: parentLeaderUserID,
		CreatedAtMs:        command.NowMs,
		UpdatedAtMs:        command.NowMs,
	}
	// 邀请创建只落 pending 消息；Agency、owner Host 和 owner membership 必须等目标用户接受时再同事务创建。
	if err := insertRoleInvitation(ctx, tx, invitation); err != nil {
		return hostdomain.RoleInvitation{}, mapHostDuplicateError(err)
	}
	if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{
		CommandID:   command.CommandID,
		CommandType: hostdomain.CommandTypeInviteAgency,
		ResultType:  hostdomain.ResultTypeInvitation,
		ResultID:    invitation.InvitationID,
		CreatedAtMs: command.NowMs,
	}); err != nil {
		return hostdomain.RoleInvitation{}, mapHostDuplicateError(err)
	}
	if err := insertRoleInvitationCreatedOutbox(ctx, tx, invitation, command.NowMs); err != nil {
		return hostdomain.RoleInvitation{}, err
	}
	if err := tx.Commit(); err != nil {
		return hostdomain.RoleInvitation{}, err
	}

	return invitation, nil
}

// InviteBD 创建 BD Leader 到 BD 的邀请。
func (r *Repository) InviteBD(ctx context.Context, command hostservice.InviteBDCommand) (hostdomain.RoleInvitation, error) {
	regionMatchRequired, err := r.roleRegionMatchRequired(ctx)
	if err != nil {
		return hostdomain.RoleInvitation{}, err
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return hostdomain.RoleInvitation{}, err
	}
	defer tx.Rollback()

	// BD 邀请也以 command_id 去重；已成功的邀请不会因为负责人后续状态变化而回放失败。
	if existing, ok, err := commandResult(ctx, tx, command.CommandID); err != nil {
		return hostdomain.RoleInvitation{}, err
	} else if ok {
		if err := requireCommandResult(existing, hostdomain.CommandTypeInviteBD, hostdomain.ResultTypeInvitation); err != nil {
			return hostdomain.RoleInvitation{}, err
		}
		return queryRoleInvitation(ctx, tx, "WHERE invitation_id = ?", existing.ResultID)
	}

	// 只有有效 BD 负责人能发展 BD；锁住独立 Leader 表行让停用和邀请创建串行。
	inviter, err := queryBDLeaderProfile(ctx, tx, "WHERE user_id = ? FOR UPDATE", command.InviterUserID)
	if err != nil {
		return hostdomain.RoleInvitation{}, err
	}
	if inviter.Status != hostdomain.BDStatusActive {
		return hostdomain.RoleInvitation{}, xerr.New(xerr.PermissionDenied, "inviter is not active bd leader")
	}
	// 邀请创建时锁目标用户当前区域；非全域策略要求和 Leader 同区，BD 投影始终来自目标 users 当前区域。
	targetRegionID, err := r.userRegion(ctx, tx, command.TargetUserID, "FOR UPDATE")
	if err != nil {
		return hostdomain.RoleInvitation{}, err
	}
	if regionMatchRequired && targetRegionID != inviter.RegionID {
		return hostdomain.RoleInvitation{}, xerr.New(xerr.RegionMismatch, "target region does not match leader region")
	}
	// 只有 active 普通 BD 才算已有权限；disabled 行可以重新发邀请，
	// 接受时会复用原主键行恢复 active，避免历史停用记录阻断重新授权。
	if _, ok, err := queryBDProfileMaybe(ctx, tx, command.TargetUserID, true); err != nil {
		return hostdomain.RoleInvitation{}, err
	} else if ok {
		return hostdomain.RoleInvitation{}, xerr.New(xerr.Conflict, "target already has bd profile")
	}

	invitation := hostdomain.RoleInvitation{
		InvitationID:       command.InvitationID,
		CommandID:          command.CommandID,
		InvitationType:     hostdomain.InvitationTypeBD,
		Status:             hostdomain.InvitationStatusPending,
		InviterUserID:      command.InviterUserID,
		InviterBDUserID:    inviter.UserID,
		TargetUserID:       command.TargetUserID,
		RegionID:           targetRegionID,
		ParentLeaderUserID: inviter.UserID,
		CreatedAtMs:        command.NowMs,
		UpdatedAtMs:        command.NowMs,
	}
	// 新 BD 邀请只产生待确认消息；BD 身份要等目标用户接受后才创建。
	if err := insertRoleInvitation(ctx, tx, invitation); err != nil {
		return hostdomain.RoleInvitation{}, mapHostDuplicateError(err)
	}
	if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{
		CommandID:   command.CommandID,
		CommandType: hostdomain.CommandTypeInviteBD,
		ResultType:  hostdomain.ResultTypeInvitation,
		ResultID:    invitation.InvitationID,
		CreatedAtMs: command.NowMs,
	}); err != nil {
		return hostdomain.RoleInvitation{}, mapHostDuplicateError(err)
	}
	if err := insertRoleInvitationCreatedOutbox(ctx, tx, invitation, command.NowMs); err != nil {
		return hostdomain.RoleInvitation{}, err
	}
	if err := tx.Commit(); err != nil {
		return hostdomain.RoleInvitation{}, err
	}

	return invitation, nil
}

// InviteHost 创建 Agency owner 到目标用户的 Host 入会邀请。
func (r *Repository) InviteHost(ctx context.Context, command hostservice.InviteHostCommand) (hostdomain.RoleInvitation, error) {
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return hostdomain.RoleInvitation{}, err
	}
	defer tx.Rollback()

	// 发送邀请是独立写命令；重试只回放同一条 pending/终态邀请，不重新读取 owner 当前权限。
	if existing, ok, err := commandResult(ctx, tx, command.CommandID); err != nil {
		return hostdomain.RoleInvitation{}, err
	} else if ok {
		if err := requireCommandResult(existing, hostdomain.CommandTypeInviteHost, hostdomain.ResultTypeInvitation); err != nil {
			return hostdomain.RoleInvitation{}, err
		}
		return queryRoleInvitation(ctx, tx, "WHERE invitation_id = ?", existing.ResultID)
	}

	// Agency owner 是邀请 Host 的唯一授权身份；锁 active agency 行可以让关闭、删除、禁入和邀请创建串行。
	agency, ok, err := queryActiveAgencyByOwner(ctx, tx, command.InviterUserID, true)
	if err != nil {
		return hostdomain.RoleInvitation{}, err
	}
	if !ok || !agency.JoinEnabled {
		return hostdomain.RoleInvitation{}, xerr.New(xerr.PermissionDenied, "inviter agency is not active or joinable")
	}
	// 目标用户可能还没有 host_profile，所以用 users 行作为目标身份变化的串行化锁。
	targetRegionID, err := r.userRegion(ctx, tx, command.TargetUserID, "FOR UPDATE")
	if err != nil {
		return hostdomain.RoleInvitation{}, err
	}
	if targetRegionID != agency.RegionID {
		return hostdomain.RoleInvitation{}, xerr.New(xerr.RegionMismatch, "target region does not match agency region")
	}
	// Host 入会邀请不能绕过“一人一个有效 Agency 归属”的硬约束，接受时还会再次校验当前状态。
	if _, ok, err := queryActiveMembershipByHost(ctx, tx, command.TargetUserID, true); err != nil {
		return hostdomain.RoleInvitation{}, err
	} else if ok {
		return hostdomain.RoleInvitation{}, xerr.New(xerr.Conflict, "target already has active agency membership")
	}
	if _, ok, err := queryActiveAgencyByOwner(ctx, tx, command.TargetUserID, true); err != nil {
		return hostdomain.RoleInvitation{}, err
	} else if ok {
		return hostdomain.RoleInvitation{}, xerr.New(xerr.Conflict, "target already owns active agency")
	}

	invitation := hostdomain.RoleInvitation{
		InvitationID:    command.InvitationID,
		CommandID:       command.CommandID,
		InvitationType:  hostdomain.InvitationTypeHost,
		Status:          hostdomain.InvitationStatusPending,
		InviterUserID:   command.InviterUserID,
		InviterBDUserID: agency.ParentBDUserID,
		TargetUserID:    command.TargetUserID,
		RegionID:        targetRegionID,
		AgencyName:      agency.Name,
		ParentBDUserID:  agency.ParentBDUserID,
		CreatedAtMs:     command.NowMs,
		UpdatedAtMs:     command.NowMs,
	}
	if err := insertRoleInvitation(ctx, tx, invitation); err != nil {
		return hostdomain.RoleInvitation{}, mapHostDuplicateError(err)
	}
	if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{
		CommandID:   command.CommandID,
		CommandType: hostdomain.CommandTypeInviteHost,
		ResultType:  hostdomain.ResultTypeInvitation,
		ResultID:    invitation.InvitationID,
		CreatedAtMs: command.NowMs,
	}); err != nil {
		return hostdomain.RoleInvitation{}, mapHostDuplicateError(err)
	}
	if err := insertRoleInvitationCreatedOutbox(ctx, tx, invitation, command.NowMs); err != nil {
		return hostdomain.RoleInvitation{}, err
	}
	if err := tx.Commit(); err != nil {
		return hostdomain.RoleInvitation{}, err
	}

	return invitation, nil
}

// GetRoleInvitation 读取单条邀请并校验 actor 只能看到自己发出或收到的邀请。
func (r *Repository) GetRoleInvitation(ctx context.Context, actorUserID int64, invitationID int64) (hostdomain.RoleInvitation, error) {
	invitation, err := queryRoleInvitation(ctx, r.db, "WHERE invitation_id = ?", invitationID)
	if err != nil {
		return hostdomain.RoleInvitation{}, err
	}
	if actorUserID != invitation.InviterUserID && actorUserID != invitation.TargetUserID {
		return hostdomain.RoleInvitation{}, xerr.New(xerr.PermissionDenied, "role invitation is not visible to actor")
	}
	return invitation, nil
}

type roleInvitationUserSnapshot struct {
	UserID        int64  `json:"user_id"`
	DisplayUserID string `json:"display_user_id"`
	Username      string `json:"username"`
	Avatar        string `json:"avatar"`
}

type roleInvitationCreatedPayload struct {
	InvitationID           int64                      `json:"invitation_id"`
	InvitationType         string                     `json:"invitation_type"`
	Status                 string                     `json:"status"`
	InviterUserID          int64                      `json:"inviter_user_id"`
	InviterBDUserID        int64                      `json:"inviter_bd_user_id"`
	TargetUserID           int64                      `json:"target_user_id"`
	ExternalOperatorUserID int64                      `json:"external_operator_user_id,omitempty"`
	RegionID               int64                      `json:"region_id"`
	AgencyName             string                     `json:"agency_name"`
	ParentBDUserID         int64                      `json:"parent_bd_user_id"`
	ParentLeaderUserID     int64                      `json:"parent_leader_user_id"`
	ParentOwnerUserID      int64                      `json:"parent_owner_user_id,omitempty"`
	ParentAgencyID         int64                      `json:"parent_agency_id,omitempty"`
	Inviter                roleInvitationUserSnapshot `json:"inviter"`
	Target                 roleInvitationUserSnapshot `json:"target"`
	CreatedAtMS            int64                      `json:"created_at_ms"`
}

func insertRoleInvitationCreatedOutbox(ctx context.Context, tx *sql.Tx, invitation hostdomain.RoleInvitation, nowMs int64) error {
	inviter, err := roleInvitationUserSnapshotByID(ctx, tx, invitation.InviterUserID)
	if err != nil {
		return err
	}
	target, err := roleInvitationUserSnapshotByID(ctx, tx, invitation.TargetUserID)
	if err != nil {
		return err
	}
	payloadBytes, err := json.Marshal(roleInvitationCreatedPayload{
		InvitationID:           invitation.InvitationID,
		InvitationType:         invitation.InvitationType,
		Status:                 invitation.Status,
		InviterUserID:          invitation.InviterUserID,
		InviterBDUserID:        invitation.InviterBDUserID,
		TargetUserID:           invitation.TargetUserID,
		ExternalOperatorUserID: invitation.ExternalOperatorUserID,
		RegionID:               invitation.RegionID,
		AgencyName:             invitation.AgencyName,
		ParentBDUserID:         invitation.ParentBDUserID,
		ParentLeaderUserID:     invitation.ParentLeaderUserID,
		ParentOwnerUserID:      invitation.ParentOwnerUserID,
		ParentAgencyID:         invitation.ParentAgencyID,
		Inviter:                inviter,
		Target:                 target,
		CreatedAtMS:            invitation.CreatedAtMs,
	})
	if err != nil {
		return err
	}
	// 邀请创建和 user_outbox 同事务提交；RocketMQ 失败时只影响异步确认 IM，业务邀请本身仍可由 outbox worker 补偿投递。
	_, err = tx.ExecContext(ctx, `
		INSERT INTO user_outbox (
			app_code, event_id, event_type, aggregate_type, aggregate_id,
			status, worker_id, lock_until_ms, retry_count, next_retry_at_ms,
			last_error, payload_json, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, 'pending', '', 0, 0, 0, '', CAST(? AS JSON), ?, ?)`,
		appcode.FromContext(ctx),
		idgen.New("uout"),
		userOutboxEventRoleInvitationCreated,
		hostdomain.ResultTypeInvitation,
		invitation.InvitationID,
		string(payloadBytes),
		nowMs,
		nowMs,
	)
	return err
}

func roleInvitationUserSnapshotByID(ctx context.Context, tx *sql.Tx, userID int64) (roleInvitationUserSnapshot, error) {
	var snapshot roleInvitationUserSnapshot
	err := tx.QueryRowContext(ctx, `
		SELECT user_id, COALESCE(current_display_user_id, ''), COALESCE(username, ''), COALESCE(avatar, '')
		FROM users
		WHERE app_code = ? AND user_id = ?`,
		appcode.FromContext(ctx), userID,
	).Scan(&snapshot.UserID, &snapshot.DisplayUserID, &snapshot.Username, &snapshot.Avatar)
	if err == sql.ErrNoRows {
		return roleInvitationUserSnapshot{}, xerr.New(xerr.NotFound, "user not found")
	}
	return snapshot, err
}

func queryActiveAgencyInviterProfile(ctx context.Context, tx *sql.Tx, userID int64) (hostdomain.BDProfile, error) {
	if profile, ok, err := queryBDProfileMaybe(ctx, tx, userID, true); err != nil || ok {
		return profile, err
	}
	if profile, ok, err := queryBDLeaderProfileMaybe(ctx, tx, userID, true); err != nil || ok {
		return profile, err
	}
	return hostdomain.BDProfile{}, xerr.New(xerr.PermissionDenied, "inviter bd is not active")
}

// ProcessRoleInvitation 处理角色邀请，接受路径在同一事务内创建 Host/Agency/BD 角色事实。
func (r *Repository) ProcessRoleInvitation(ctx context.Context, command hostservice.ProcessRoleInvitationCommand) (hostdomain.ProcessRoleInvitationResult, error) {
	// 策略必须在占用事务连接前解析；否则缓存 miss 时并发事务可能耗尽连接池并互相等待第二条连接。
	regionMatchRequired, err := r.roleRegionMatchRequired(ctx)
	if err != nil {
		return hostdomain.ProcessRoleInvitationResult{}, err
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return hostdomain.ProcessRoleInvitationResult{}, err
	}
	defer tx.Rollback()

	// 接受、拒绝、取消共用同一个处理命令类型；重试时通过 invitation_id 重建结果，
	// 不重复创建 Agency、成员关系或 BD 身份行。
	if existing, ok, err := commandResult(ctx, tx, command.CommandID); err != nil {
		return hostdomain.ProcessRoleInvitationResult{}, err
	} else if ok {
		if err := requireCommandResult(existing, hostdomain.CommandTypeProcessRoleInvitation, hostdomain.ResultTypeInvitation); err != nil {
			return hostdomain.ProcessRoleInvitationResult{}, err
		}
		return processResultByInvitationID(ctx, tx, existing.ResultID)
	}

	// 邀请行是邀请状态机的唯一锁；待处理状态只能被推进一次。
	invitation, err := queryRoleInvitation(ctx, tx, "WHERE invitation_id = ? FOR UPDATE", command.InvitationID)
	if err != nil {
		return hostdomain.ProcessRoleInvitationResult{}, err
	}
	if invitation.Status != hostdomain.InvitationStatusPending {
		return hostdomain.ProcessRoleInvitationResult{}, xerr.New(xerr.Conflict, "invitation is not pending")
	}
	// 取消属于发起人撤回；接受/拒绝属于目标用户处理。
	// 这两个授权分支不要下沉到传输层，否则批处理或内部调用会绕过规则。
	if command.Action == hostdomain.InvitationActionCancel && command.ActorUserID != invitation.InviterUserID {
		return hostdomain.ProcessRoleInvitationResult{}, xerr.New(xerr.PermissionDenied, "only inviter can cancel invitation")
	}
	if command.Action != hostdomain.InvitationActionCancel && command.ActorUserID != invitation.TargetUserID {
		return hostdomain.ProcessRoleInvitationResult{}, xerr.New(xerr.PermissionDenied, "only target can process invitation")
	}

	invitation.ProcessedByUserID = command.ActorUserID
	invitation.ProcessReason = command.Reason
	invitation.ProcessedAtMs = command.NowMs
	invitation.UpdatedAtMs = command.NowMs
	result := hostdomain.ProcessRoleInvitationResult{Invitation: invitation}

	switch command.Action {
	case hostdomain.InvitationActionReject:
		// 拒绝只改变邀请终态，不创建任何角色事实。
		invitation.Status = hostdomain.InvitationStatusRejected
	case hostdomain.InvitationActionCancel:
		// 取消同样只关闭邀请，用于邀请人在目标处理前撤回。
		invitation.Status = hostdomain.InvitationStatusCancelled
	case hostdomain.InvitationActionAccept:
		invitation.Status = hostdomain.InvitationStatusAccepted
		if invitation.InvitationType == hostdomain.InvitationTypeAgency {
			// Agency 邀请的接受会一次性创建 Agency 拥有者身份、Agency 和拥有者成员关系。
			hostProfileChanged, membershipCreated, err := r.acceptAgencyInvitation(ctx, tx, command, &invitation, &result, regionMatchRequired)
			if err != nil {
				return hostdomain.ProcessRoleInvitationResult{}, err
			}
			// 事件 ID 在业务层预分配；这里按固定偏移写多条事件箱记录，
			// 让同一个接受事务中的派生事件保持可追踪顺序。
			if hostProfileChanged {
				if err := insertHostOutbox(ctx, tx, command.EventID, "HostCreated", "host_profile", invitation.TargetUserID, command.NowMs); err != nil {
					return hostdomain.ProcessRoleInvitationResult{}, err
				}
			}
			if err := insertHostOutbox(ctx, tx, command.EventID+1, "AgencyCreated", "agency", command.AgencyID, command.NowMs); err != nil {
				return hostdomain.ProcessRoleInvitationResult{}, err
			}
			if membershipCreated {
				if err := insertHostOutbox(ctx, tx, command.EventID+2, "AgencyMembershipChanged", "agency_membership", command.MembershipID, command.NowMs); err != nil {
					return hostdomain.ProcessRoleInvitationResult{}, err
				}
			}
		} else if invitation.InvitationType == hostdomain.InvitationTypeExternalAgency {
			// 外管 Agency 邀请使用独立类型和授权校验；旧 agency 分支不读取 external_* 元数据。
			hostProfileChanged, membershipCreated, err := r.acceptExternalAgencyInvitation(ctx, tx, command, &invitation, &result)
			if err != nil {
				return hostdomain.ProcessRoleInvitationResult{}, err
			}
			if hostProfileChanged {
				if err := insertHostOutbox(ctx, tx, command.EventID, "HostCreated", "host_profile", invitation.TargetUserID, command.NowMs); err != nil {
					return hostdomain.ProcessRoleInvitationResult{}, err
				}
			}
			if err := insertHostOutbox(ctx, tx, command.EventID+1, "AgencyCreated", "agency", command.AgencyID, command.NowMs); err != nil {
				return hostdomain.ProcessRoleInvitationResult{}, err
			}
			if membershipCreated {
				if err := insertHostOutbox(ctx, tx, command.EventID+2, "AgencyMembershipChanged", "agency_membership", command.MembershipID, command.NowMs); err != nil {
					return hostdomain.ProcessRoleInvitationResult{}, err
				}
			}
		} else if invitation.InvitationType == hostdomain.InvitationTypeBD {
			// BD 邀请的接受只创建 bd_profile；BD 不需要 host_profile。
			if err := r.acceptBDInvitation(ctx, tx, command, &invitation, &result, regionMatchRequired); err != nil {
				return hostdomain.ProcessRoleInvitationResult{}, err
			}
			if err := insertHostOutbox(ctx, tx, command.EventID, "BDCreated", "bd_profile", invitation.TargetUserID, command.NowMs); err != nil {
				return hostdomain.ProcessRoleInvitationResult{}, err
			}
		} else if invitation.InvitationType == hostdomain.InvitationTypeExternalBD {
			// 外管 BD 只写 parent_owner_user_id，不进入 App 的 parent_leader_user_id 树。
			if err := r.acceptExternalBDInvitation(ctx, tx, command, &invitation, &result); err != nil {
				return hostdomain.ProcessRoleInvitationResult{}, err
			}
			if err := insertHostOutbox(ctx, tx, command.EventID, "BDCreated", "bd_profile", invitation.TargetUserID, command.NowMs); err != nil {
				return hostdomain.ProcessRoleInvitationResult{}, err
			}
		} else if invitation.InvitationType == hostdomain.InvitationTypeHost {
			// Host 邀请只创建或复用 Host 身份，并把当前成员关系挂到发起 Agency。
			hostProfileCreated, err := r.acceptHostInvitation(ctx, tx, command, &invitation, &result)
			if err != nil {
				return hostdomain.ProcessRoleInvitationResult{}, err
			}
			if hostProfileCreated {
				if err := insertHostOutbox(ctx, tx, command.EventID, "HostCreated", "host_profile", invitation.TargetUserID, command.NowMs); err != nil {
					return hostdomain.ProcessRoleInvitationResult{}, err
				}
			}
			if err := insertHostOutbox(ctx, tx, command.EventID+1, "AgencyMembershipChanged", "agency_membership", command.MembershipID, command.NowMs); err != nil {
				return hostdomain.ProcessRoleInvitationResult{}, err
			}
		} else if invitation.InvitationType == hostdomain.InvitationTypeExternalHost {
			// 外管 Host 先验证固化的 Agency，再走和普通 Host 相同的身份及 membership 原子写入。
			hostProfileCreated, err := r.acceptExternalHostInvitation(ctx, tx, command, &invitation, &result)
			if err != nil {
				return hostdomain.ProcessRoleInvitationResult{}, err
			}
			if hostProfileCreated {
				if err := insertHostOutbox(ctx, tx, command.EventID, "HostCreated", "host_profile", invitation.TargetUserID, command.NowMs); err != nil {
					return hostdomain.ProcessRoleInvitationResult{}, err
				}
			}
			if err := insertHostOutbox(ctx, tx, command.EventID+1, "AgencyMembershipChanged", "agency_membership", command.MembershipID, command.NowMs); err != nil {
				return hostdomain.ProcessRoleInvitationResult{}, err
			}
		} else {
			return hostdomain.ProcessRoleInvitationResult{}, xerr.New(xerr.InvalidArgument, "invitation type is invalid")
		}
	}
	// 邀请终态必须在角色事实之后更新；如果事实创建失败，待处理状态保留给后续重试/处理。
	if err := updateRoleInvitationProcessed(ctx, tx, invitation); err != nil {
		return hostdomain.ProcessRoleInvitationResult{}, err
	}
	result.Invitation = invitation
	if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{
		CommandID:   command.CommandID,
		CommandType: hostdomain.CommandTypeProcessRoleInvitation,
		ResultType:  hostdomain.ResultTypeInvitation,
		ResultID:    invitation.InvitationID,
		CreatedAtMs: command.NowMs,
	}); err != nil {
		return hostdomain.ProcessRoleInvitationResult{}, mapHostDuplicateError(err)
	}
	if err := tx.Commit(); err != nil {
		return hostdomain.ProcessRoleInvitationResult{}, err
	}

	return result, nil
}

func (r *Repository) acceptAgencyInvitation(ctx context.Context, tx *sql.Tx, command hostservice.ProcessRoleInvitationCommand, invitation *hostdomain.RoleInvitation, result *hostdomain.ProcessRoleInvitationResult, regionMatchRequired bool) (bool, bool, error) {
	// 邀请可能放置一段时间后才被接受，所以目标用户和邀请 BD 都读取 users 当前区域。
	// 非全域策略用它们重新对齐；全域策略允许跨区，且邀请表仍不保存区域快照、不迁移目标用户。
	targetRegionID, err := r.userRegion(ctx, tx, invitation.TargetUserID, "FOR UPDATE")
	if err != nil {
		return false, false, err
	}
	inviterRegionID, err := userRegionValue(ctx, tx, invitation.InviterBDUserID, "")
	if err != nil {
		return false, false, err
	}
	if regionMatchRequired && targetRegionID != inviterRegionID {
		return false, false, xerr.New(xerr.RegionMismatch, "target region no longer matches invitation")
	}
	// 目标已有 Host 绑定时保留绑定，不再把 Host 当前归属切到自己新建的 Agency。
	// 这里仍锁住 active membership，避免接受邀请期间另一条入会流程改写当前关系。
	activeMembership, hasActiveMembership, err := queryActiveMembershipByHost(ctx, tx, invitation.TargetUserID, true)
	if err != nil {
		return false, false, err
	}
	// 同一用户不能拥有多个有效 Agency；这里和唯一索引共同处理并发接受。
	if _, ok, err := queryActiveAgencyByOwner(ctx, tx, invitation.TargetUserID, true); err != nil {
		return false, false, err
	} else if ok {
		return false, false, xerr.New(xerr.Conflict, "target already owns active agency")
	}

	// Agency、拥有者 host_profile、拥有者成员关系是同一业务事实的三张表投影，
	// 必须全部创建成功后邀请才能进入已接受状态。
	agencyName, agencyAvatar, err := defaultAgencyProfileFromOwner(ctx, tx, invitation.TargetUserID, invitation.AgencyName)
	if err != nil {
		return false, false, err
	}
	agency := hostdomain.Agency{
		AgencyID:       command.AgencyID,
		OwnerUserID:    invitation.TargetUserID,
		RegionID:       targetRegionID,
		ParentBDUserID: invitation.ParentBDUserID,
		Name:           agencyName,
		Avatar:         agencyAvatar,
		Status:         hostdomain.AgencyStatusActive,
		JoinEnabled:    true,
		// max_hosts 是历史兼容字段，当前业务不再限制主播数量。
		MaxHosts:        0,
		CreatedByUserID: invitation.InviterUserID,
		CreatedAtMs:     command.NowMs,
		UpdatedAtMs:     command.NowMs,
	}
	if err := insertAgency(ctx, tx, agency); err != nil {
		return false, false, mapHostDuplicateError(err)
	}
	if hasActiveMembership {
		// 已有 active Host 绑定时只恢复/修正 host_profile 指向原 membership，不插入 owner membership，
		// 防止同一 Host 同时拥有两条 active agency_memberships。
		hostProfile, hostProfileChanged, err := ensureHostProfileForExistingMembership(ctx, tx, invitation.TargetUserID, targetRegionID, activeMembership, hostdomain.HostSourceAgencyInvitation, command.NowMs)
		if err != nil {
			return false, false, err
		}
		result.HostProfile = hostProfile
		result.Agency = agency
		result.Membership = activeMembership
		return hostProfileChanged, false, nil
	}
	hostProfile, hostProfileCreated, err := ensureHostProfileForMembership(ctx, tx, invitation.TargetUserID, targetRegionID, command.MembershipID, agency.AgencyID, hostdomain.HostSourceAgencyInvitation, command.NowMs)
	if err != nil {
		return false, false, err
	}
	membership := hostdomain.AgencyMembership{
		MembershipID:   command.MembershipID,
		AgencyID:       agency.AgencyID,
		HostUserID:     invitation.TargetUserID,
		RegionID:       targetRegionID,
		MembershipType: hostdomain.MembershipTypeOwner,
		Status:         hostdomain.MembershipStatusActive,
		JoinedAtMs:     command.NowMs,
		CreatedAtMs:    command.NowMs,
		UpdatedAtMs:    command.NowMs,
	}
	if err := insertAgencyMembership(ctx, tx, membership); err != nil {
		return false, false, mapHostDuplicateError(err)
	}
	result.HostProfile = hostProfile
	result.Agency = agency
	result.Membership = membership
	return hostProfileCreated, true, nil
}

func (r *Repository) acceptHostInvitation(ctx context.Context, tx *sql.Tx, command hostservice.ProcessRoleInvitationCommand, invitation *hostdomain.RoleInvitation, result *hostdomain.ProcessRoleInvitationResult) (bool, error) {
	// Host 邀请绑定的是发起人当前 active Agency，而不是客户端传入的 agency_id；
	// 这样邀请消息无法被目标用户篡改成加入其他团队。
	agency, ok, err := queryActiveAgencyByOwner(ctx, tx, invitation.InviterUserID, true)
	if err != nil {
		return false, err
	}
	if !ok || !agency.JoinEnabled {
		return false, xerr.New(xerr.Conflict, "inviter agency is no longer active or joinable")
	}
	targetRegionID, err := r.userRegion(ctx, tx, invitation.TargetUserID, "FOR UPDATE")
	if err != nil {
		return false, err
	}
	if targetRegionID != agency.RegionID {
		return false, xerr.New(xerr.RegionMismatch, "target region no longer matches agency")
	}
	// 接受时再次检查有效归属和 Agency owner 身份，覆盖邀请发出后用户已经加入或创建团队的情况。
	if _, ok, err := queryActiveMembershipByHost(ctx, tx, invitation.TargetUserID, true); err != nil {
		return false, err
	} else if ok {
		return false, xerr.New(xerr.Conflict, "target already has active agency membership")
	}
	if _, ok, err := queryActiveAgencyByOwner(ctx, tx, invitation.TargetUserID, true); err != nil {
		return false, err
	} else if ok {
		return false, xerr.New(xerr.Conflict, "target already owns active agency")
	}

	hostProfile, hostProfileCreated, err := ensureHostProfileForMembership(ctx, tx, invitation.TargetUserID, targetRegionID, command.MembershipID, agency.AgencyID, hostdomain.HostSourceHostInvitation, command.NowMs)
	if err != nil {
		return false, err
	}
	membership := hostdomain.AgencyMembership{
		MembershipID:   command.MembershipID,
		AgencyID:       agency.AgencyID,
		HostUserID:     invitation.TargetUserID,
		RegionID:       targetRegionID,
		MembershipType: hostdomain.MembershipTypeMember,
		Status:         hostdomain.MembershipStatusActive,
		JoinedAtMs:     command.NowMs,
		CreatedAtMs:    command.NowMs,
		UpdatedAtMs:    command.NowMs,
	}
	if err := insertAgencyMembership(ctx, tx, membership); err != nil {
		return false, mapHostDuplicateError(err)
	}
	result.HostProfile = hostProfile
	result.Agency = agency
	result.Membership = membership
	return hostProfileCreated, nil
}

func (r *Repository) acceptBDInvitation(ctx context.Context, tx *sql.Tx, command hostservice.ProcessRoleInvitationCommand, invitation *hostdomain.RoleInvitation, result *hostdomain.ProcessRoleInvitationResult, regionMatchRequired bool) error {
	// BD 邀请同样读取目标用户和发起 Leader 的 users 当前区域；非全域策略阻止漂移/跨区。
	targetRegionID, err := r.userRegion(ctx, tx, invitation.TargetUserID, "FOR UPDATE")
	if err != nil {
		return err
	}
	leaderRegionID, err := userRegionValue(ctx, tx, invitation.InviterBDUserID, "")
	if err != nil {
		return err
	}
	if regionMatchRequired && targetRegionID != leaderRegionID {
		return xerr.New(xerr.RegionMismatch, "target region no longer matches invitation")
	}
	// BD 身份是独立角色事实，不依赖 host_profile；active BD 不能被覆盖，
	// disabled BD 不算当前权限，接受邀请时恢复并绑定到本次 Leader。
	if existing, ok, err := queryBDProfileByUserMaybe(ctx, tx, invitation.TargetUserID, true); err != nil {
		return err
	} else if ok {
		if existing.Status == hostdomain.BDStatusActive {
			return xerr.New(xerr.Conflict, "target already has bd profile")
		}
		existing.ParentLeaderUserID = invitation.ParentLeaderUserID
		existing.Status = hostdomain.BDStatusActive
		existing.UpdatedAtMs = command.NowMs
		if err := updateBDProfileForActivation(ctx, tx, existing); err != nil {
			return err
		}
		result.BDProfile = existing
		return nil
	}
	bd := hostdomain.BDProfile{
		UserID:             invitation.TargetUserID,
		Role:               hostdomain.BDRoleBD,
		RegionID:           targetRegionID,
		ParentLeaderUserID: invitation.ParentLeaderUserID,
		Status:             hostdomain.BDStatusActive,
		CreatedByUserID:    invitation.InviterUserID,
		CreatedAtMs:        command.NowMs,
		UpdatedAtMs:        command.NowMs,
	}
	if err := insertBDProfile(ctx, tx, bd); err != nil {
		return mapHostDuplicateError(err)
	}
	result.BDProfile = bd
	return nil
}

func (r *Repository) acceptExternalAgencyInvitation(ctx context.Context, tx *sql.Tx, command hostservice.ProcessRoleInvitationCommand, invitation *hostdomain.RoleInvitation, result *hostdomain.ProcessRoleInvitationResult) (bool, bool, error) {
	// 外管邀请先按固化的 operator/owner/BD/region 快照重验授权，再复用 Agency 事实的原子落库流程。
	// 普通 invitation_type=agency 的接受函数不读取任何 external_* 字段，确保旧 App 链路保持原语义。
	targetRegionID, err := r.userRegion(ctx, tx, invitation.TargetUserID, "FOR UPDATE")
	if err != nil {
		return false, false, err
	}
	if err := validateExternalAgencyInvitationAcceptance(ctx, tx, *invitation, targetRegionID); err != nil {
		return false, false, err
	}
	return r.acceptAgencyInvitation(ctx, tx, command, invitation, result, true)
}

func (r *Repository) acceptExternalHostInvitation(ctx context.Context, tx *sql.Tx, command hostservice.ProcessRoleInvitationCommand, invitation *hostdomain.RoleInvitation, result *hostdomain.ProcessRoleInvitationResult) (bool, error) {
	// 外管 Host 邀请固化的是明确 agency_id；接受时先确认 Agency 及其 BD owner 都未漂移。
	// 旧 Host 落库函数只认 Agency owner，因此仅在局部副本中换成已验证的 owner，不修改邀请审计事实。
	targetRegionID, err := r.userRegion(ctx, tx, invitation.TargetUserID, "FOR UPDATE")
	if err != nil {
		return false, err
	}
	agency, err := validateExternalHostInvitationAcceptance(ctx, tx, *invitation, targetRegionID)
	if err != nil {
		return false, err
	}
	materializationInvitation := *invitation
	materializationInvitation.InviterUserID = agency.OwnerUserID
	return r.acceptHostInvitation(ctx, tx, command, &materializationInvitation, result)
}

func (r *Repository) acceptExternalBDInvitation(ctx context.Context, tx *sql.Tx, command hostservice.ProcessRoleInvitationCommand, invitation *hostdomain.RoleInvitation, result *hostdomain.ProcessRoleInvitationResult) error {
	// 外管 BD 不属于 App 的 BD Leader 树：parent_leader_user_id 必须为空，
	// 只有 parent_owner_user_id 记录外管团队归属，避免 Manager/BD Leader 查询被新关系污染。
	targetRegionID, err := r.userRegion(ctx, tx, invitation.TargetUserID, "FOR UPDATE")
	if err != nil {
		return err
	}
	if err := validateExternalBDInvitationAcceptance(ctx, tx, *invitation, targetRegionID); err != nil {
		return err
	}
	if existing, ok, err := queryBDProfileByUserMaybe(ctx, tx, invitation.TargetUserID, true); err != nil {
		return err
	} else if ok {
		if existing.Status == hostdomain.BDStatusActive {
			return xerr.New(xerr.Conflict, "target already has bd profile")
		}
		existing.ParentLeaderUserID = 0
		existing.ParentOwnerUserID = invitation.ParentOwnerUserID
		existing.Status = hostdomain.BDStatusActive
		existing.UpdatedAtMs = command.NowMs
		if err := updateExternalBDProfileForActivation(ctx, tx, existing); err != nil {
			return err
		}
		result.BDProfile = existing
		return nil
	}
	bd := hostdomain.BDProfile{
		UserID:            invitation.TargetUserID,
		Role:              hostdomain.BDRoleBD,
		RegionID:          targetRegionID,
		ParentOwnerUserID: invitation.ParentOwnerUserID,
		Status:            hostdomain.BDStatusActive,
		CreatedByUserID:   invitation.ExternalOperatorUserID,
		CreatedAtMs:       command.NowMs,
		UpdatedAtMs:       command.NowMs,
	}
	if err := insertExternalBDProfile(ctx, tx, bd); err != nil {
		return mapHostDuplicateError(err)
	}
	result.BDProfile = bd
	return nil
}
