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 userOutboxEventAgencyHostRemoved = "AgencyHostRemoved"

// ApplyToAgency 创建待处理申请；用户行和 Agency 行在同一事务内锁定。
func (r *Repository) ApplyToAgency(ctx context.Context, command hostservice.ApplyToAgencyCommand) (hostdomain.AgencyApplication, error) {
	regionMatchRequired, err := r.roleRegionMatchRequired(ctx)
	if err != nil {
		return hostdomain.AgencyApplication{}, err
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return hostdomain.AgencyApplication{}, err
	}
	defer tx.Rollback()

	// command_id 是客户端写请求的幂等键；只要上一轮事务已经提交，
	// 重试就按持久化的结果 ID 回放，避免重复创建申请或消耗新 ID。
	if existing, ok, err := commandResult(ctx, tx, command.CommandID); err != nil {
		return hostdomain.AgencyApplication{}, err
	} else if ok {
		if err := requireCommandResult(existing, hostdomain.CommandTypeApplyToAgency, hostdomain.ResultTypeApplication); err != nil {
			return hostdomain.AgencyApplication{}, err
		}
		return queryAgencyApplication(ctx, tx, "WHERE application_id = ?", existing.ResultID)
	}

	// 申请人此时可能还没有 host_profiles 行，因此以 users 行作为“按用户串行化”的主锁。
	// 区域从用户表读取，作为后续 Host/成员关系的当前投影；全域策略允许跨区但不会迁移申请人区域。
	userRegionID, err := r.userRegion(ctx, tx, command.UserID, "FOR UPDATE")
	if err != nil {
		return hostdomain.AgencyApplication{}, err
	}
	if userRegionID <= 0 {
		return hostdomain.AgencyApplication{}, xerr.New(xerr.InvalidArgument, "user region is required")
	}

	// Agency 行必须锁住：加入开关、状态、拥有者当前区域是申请可创建性的条件，
	// 如果后台同时禁用加入或用户迁移区域，这里必须和申请创建串行。
	agency, err := queryAgency(ctx, tx, "WHERE agency_id = ? FOR UPDATE", command.AgencyID)
	if err != nil {
		return hostdomain.AgencyApplication{}, err
	}
	if agency.Status != hostdomain.AgencyStatusActive || !agency.JoinEnabled {
		return hostdomain.AgencyApplication{}, xerr.New(xerr.Conflict, "agency is not joinable")
	}
	if regionMatchRequired && agency.RegionID != userRegionID {
		return hostdomain.AgencyApplication{}, xerr.New(xerr.PermissionDenied, "agency region does not match user region")
	}
	// 第一阶段规则是一个 Host 同时只能归属一个有效 Agency；
	// 这里在同事务内查询并加锁，防止并发申请绕过唯一归属约束。
	if _, ok, err := queryActiveMembershipByHost(ctx, tx, command.UserID, true); err != nil {
		return hostdomain.AgencyApplication{}, err
	} else if ok {
		return hostdomain.AgencyApplication{}, xerr.New(xerr.Conflict, "user already has active agency membership")
	}

	application := hostdomain.AgencyApplication{
		ApplicationID:   command.ApplicationID,
		CommandID:       command.CommandID,
		ApplicantUserID: command.UserID,
		AgencyID:        command.AgencyID,
		RegionID:        userRegionID,
		Status:          hostdomain.ApplicationStatusPending,
		CreatedAtMs:     command.NowMs,
		UpdatedAtMs:     command.NowMs,
	}
	// 申请记录和 command_result 同事务提交：如果申请记录成功但幂等记录失败，
	// 客户端重试会失去可回放结果，因此两者必须作为一个原子写入单元。
	if err := insertAgencyApplication(ctx, tx, application); err != nil {
		return hostdomain.AgencyApplication{}, mapHostDuplicateError(err)
	}
	if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{
		CommandID:   command.CommandID,
		CommandType: hostdomain.CommandTypeApplyToAgency,
		ResultType:  hostdomain.ResultTypeApplication,
		ResultID:    application.ApplicationID,
		CreatedAtMs: command.NowMs,
	}); err != nil {
		return hostdomain.AgencyApplication{}, mapHostDuplicateError(err)
	}
	if err := tx.Commit(); err != nil {
		return hostdomain.AgencyApplication{}, err
	}

	return application, nil
}

// ReviewAgencyApplication 审核申请；通过路径同时固化 Host 身份和成员关系。
func (r *Repository) ReviewAgencyApplication(ctx context.Context, command hostservice.ReviewAgencyApplicationCommand) (hostdomain.ReviewAgencyApplicationResult, error) {
	regionMatchRequired, err := r.roleRegionMatchRequired(ctx)
	if err != nil {
		return hostdomain.ReviewAgencyApplicationResult{}, err
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return hostdomain.ReviewAgencyApplicationResult{}, err
	}
	defer tx.Rollback()

	// 审核动作可能由 App 重试触发；命中 command_result 时按申请 ID 重建完整返回值，
	// 不再重复执行通过/拒绝状态机。
	if existing, ok, err := commandResult(ctx, tx, command.CommandID); err != nil {
		return hostdomain.ReviewAgencyApplicationResult{}, err
	} else if ok {
		if err := requireCommandResult(existing, hostdomain.CommandTypeReviewAgencyApplication, hostdomain.ResultTypeApplication); err != nil {
			return hostdomain.ReviewAgencyApplicationResult{}, err
		}
		return reviewResultByApplicationID(ctx, tx, existing.ResultID)
	}

	// 申请行是审核状态机的核心锁；同一个待处理申请只能被一个事务推进到终态。
	application, err := queryAgencyApplication(ctx, tx, "WHERE application_id = ? FOR UPDATE", command.ApplicationID)
	if err != nil {
		return hostdomain.ReviewAgencyApplicationResult{}, err
	}
	if application.Status != hostdomain.ApplicationStatusPending {
		return hostdomain.ReviewAgencyApplicationResult{}, xerr.New(xerr.Conflict, "application is not pending")
	}

	// 当前阶段的授权边界是 Agency 拥有者；后续如果引入管理员代审，
	// 应该在这里扩展授权规则，而不是绕过持久化层的事务检查。
	agency, err := queryAgency(ctx, tx, "WHERE agency_id = ? FOR UPDATE", application.AgencyID)
	if err != nil {
		return hostdomain.ReviewAgencyApplicationResult{}, err
	}
	if agency.OwnerUserID != command.ReviewerUserID {
		return hostdomain.ReviewAgencyApplicationResult{}, xerr.New(xerr.PermissionDenied, "reviewer is not agency owner")
	}
	if agency.Status != hostdomain.AgencyStatusActive {
		return hostdomain.ReviewAgencyApplicationResult{}, xerr.New(xerr.Conflict, "agency is not active")
	}

	// 申请创建后申请人或 Agency 拥有者区域都可能被后台调整；通过前重新锁申请人 users 行。
	// 非全域策略继续按 Agency 当前拥有者区域复验；全域策略只跳过匹配，不恢复区域快照或修改 users。
	regionID, err := r.userRegion(ctx, tx, application.ApplicantUserID, "FOR UPDATE")
	if err != nil {
		return hostdomain.ReviewAgencyApplicationResult{}, err
	}
	if regionMatchRequired && regionID != agency.RegionID {
		return hostdomain.ReviewAgencyApplicationResult{}, xerr.New(xerr.PermissionDenied, "application region no longer matches")
	}

	application.ReviewedByUserID = command.ReviewerUserID
	application.ReviewReason = command.Reason
	application.ReviewedAtMs = command.NowMs
	application.UpdatedAtMs = command.NowMs

	result := hostdomain.ReviewAgencyApplicationResult{Application: application}
	if command.Decision == hostdomain.ApplicationDecisionReject {
		// 拒绝只推进申请终态，不创建 host_profile，不改成员关系，
		// 这样被拒用户之后仍可以重新申请或被邀请。
		application.Status = hostdomain.ApplicationStatusRejected
		if err := updateAgencyApplicationReview(ctx, tx, application); err != nil {
			return hostdomain.ReviewAgencyApplicationResult{}, err
		}
		result.Application = application
	} else {
		// 通过前再次检查有效成员关系，覆盖“申请后已通过其他路径入会”的并发场景。
		if _, ok, err := queryActiveMembershipByHost(ctx, tx, application.ApplicantUserID, true); err != nil {
			return hostdomain.ReviewAgencyApplicationResult{}, err
		} else if ok {
			return hostdomain.ReviewAgencyApplicationResult{}, xerr.New(xerr.Conflict, "user already has active agency membership")
		}
		// host_profile 是用户成为主播的长期身份事实；成员关系是当前 Agency 归属事实。
		// 两者和申请审核结果必须原子提交，否则列表页和身份页会看到不一致状态。
		hostProfile, created, err := ensureHostProfileForMembership(ctx, tx, application.ApplicantUserID, regionID, command.MembershipID, application.AgencyID, hostdomain.HostSourceApplication, command.NowMs)
		if err != nil {
			return hostdomain.ReviewAgencyApplicationResult{}, err
		}
		membership := hostdomain.AgencyMembership{
			MembershipID:   command.MembershipID,
			AgencyID:       application.AgencyID,
			HostUserID:     application.ApplicantUserID,
			RegionID:       regionID,
			MembershipType: hostdomain.MembershipTypeMember,
			Status:         hostdomain.MembershipStatusActive,
			JoinedAtMs:     command.NowMs,
			CreatedAtMs:    command.NowMs,
			UpdatedAtMs:    command.NowMs,
		}
		if err := insertAgencyMembership(ctx, tx, membership); err != nil {
			return hostdomain.ReviewAgencyApplicationResult{}, mapHostDuplicateError(err)
		}
		application.Status = hostdomain.ApplicationStatusApproved
		if err := updateAgencyApplicationReview(ctx, tx, application); err != nil {
			return hostdomain.ReviewAgencyApplicationResult{}, err
		}
		// 事件箱是派生读模型/异步集成的可靠通知，不参与业务事实判定；
		// 但必须和事实同事务写入，避免事实提交后事件丢失。
		if created {
			if err := insertHostOutbox(ctx, tx, command.EventID, "HostCreated", "host_profile", application.ApplicantUserID, command.NowMs); err != nil {
				return hostdomain.ReviewAgencyApplicationResult{}, err
			}
		}
		if err := insertHostOutbox(ctx, tx, command.EventID+1, "AgencyMembershipChanged", "agency_membership", membership.MembershipID, command.NowMs); err != nil {
			return hostdomain.ReviewAgencyApplicationResult{}, err
		}
		result = hostdomain.ReviewAgencyApplicationResult{
			Application: application,
			HostProfile: hostProfile,
			Membership:  membership,
		}
	}
	// command_result 放在状态机成功之后写入；如果前面的业务写失败，
	// 不会留下“已处理”的幂等记录误导下一次重试。
	if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{
		CommandID:   command.CommandID,
		CommandType: hostdomain.CommandTypeReviewAgencyApplication,
		ResultType:  hostdomain.ResultTypeApplication,
		ResultID:    application.ApplicationID,
		CreatedAtMs: command.NowMs,
	}); err != nil {
		return hostdomain.ReviewAgencyApplicationResult{}, mapHostDuplicateError(err)
	}
	if err := tx.Commit(); err != nil {
		return hostdomain.ReviewAgencyApplicationResult{}, err
	}

	return result, nil
}

// KickAgencyHost 只结束普通成员关系并清空当前 Agency 指针，长期 Host 身份保持不变。
func (r *Repository) KickAgencyHost(ctx context.Context, command hostservice.KickAgencyHostCommand) (hostdomain.KickAgencyHostResult, error) {
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return hostdomain.KickAgencyHostResult{}, err
	}
	defer tx.Rollback()

	// 踢人也是写命令：同一个 command_id 重试时返回上次结束的成员关系，
	// 不会重复写 ended_at 或再次投递成员关系变更事件。
	if existing, ok, err := commandResult(ctx, tx, command.CommandID); err != nil {
		return hostdomain.KickAgencyHostResult{}, err
	} else if ok {
		if err := requireCommandResult(existing, hostdomain.CommandTypeKickAgencyHost, hostdomain.ResultTypeMembership); err != nil {
			return hostdomain.KickAgencyHostResult{}, err
		}
		return kickResultByMembershipID(ctx, tx, existing.ResultID)
	}

	// 第一阶段只允许 Agency 拥有者管理成员；Agency 行加锁后再校验拥有者，
	// 避免所有权变更和成员管理在同一时刻交叉。
	agency, err := queryAgency(ctx, tx, "WHERE agency_id = ? FOR UPDATE", command.AgencyID)
	if err != nil {
		return hostdomain.KickAgencyHostResult{}, err
	}
	if agency.OwnerUserID != command.OperatorUserID {
		return hostdomain.KickAgencyHostResult{}, xerr.New(xerr.PermissionDenied, "operator is not agency owner")
	}
	if agency.OwnerUserID == command.HostUserID {
		return hostdomain.KickAgencyHostResult{}, xerr.New(xerr.Conflict, "agency owner cannot be kicked")
	}
	// host_profile 是长期主播身份，Agency membership 只是当前组织归属；这里锁住身份行，
	// 只为在同一事务内清空指向被结束关系的冗余指针，不改变 status，也不触碰 wallet-service 的钻石事实。
	hostProfile, err := queryHostProfile(ctx, tx, "WHERE user_id = ? FOR UPDATE", command.HostUserID)
	if err != nil {
		return hostdomain.KickAgencyHostResult{}, err
	}
	// 成员关系行是被结束的事实本体；只处理有效普通成员，
	// 拥有者成员关系不能通过踢人路径结束。
	membership, err := queryAgencyMembership(ctx, tx, "WHERE agency_id = ? AND host_user_id = ? AND status = ? FOR UPDATE", command.AgencyID, command.HostUserID, hostdomain.MembershipStatusActive)
	if err != nil {
		return hostdomain.KickAgencyHostResult{}, err
	}
	if membership.MembershipType == hostdomain.MembershipTypeOwner {
		return hostdomain.KickAgencyHostResult{}, xerr.New(xerr.Conflict, "owner membership cannot be kicked")
	}

	membership.Status = hostdomain.MembershipStatusEnded
	membership.EndedAtMs = command.NowMs
	membership.EndedByUserID = command.OperatorUserID
	membership.EndedReason = command.Reason
	membership.UpdatedAtMs = command.NowMs
	if err := endAgencyMembership(ctx, tx, membership); err != nil {
		return hostdomain.KickAgencyHostResult{}, err
	}
	if hostProfile.CurrentMembershipID == membership.MembershipID {
		// 只清理仍指向本次 ended membership 的当前归属，避免并发重绑后旧命令误清新关系。
		// Host status、历史钻石和结算进度都不属于 Agency 归属，因此全部保持原值。
		hostProfile.CurrentAgencyID = 0
		hostProfile.CurrentMembershipID = 0
		hostProfile.UpdatedAtMs = command.NowMs
		if err := updateHostCurrentMembership(ctx, tx, hostProfile); err != nil {
			return hostdomain.KickAgencyHostResult{}, err
		}
	}
	if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{
		CommandID:   command.CommandID,
		CommandType: hostdomain.CommandTypeKickAgencyHost,
		ResultType:  hostdomain.ResultTypeMembership,
		ResultID:    membership.MembershipID,
		CreatedAtMs: command.NowMs,
	}); err != nil {
		return hostdomain.KickAgencyHostResult{}, mapHostDuplicateError(err)
	}
	if err := insertHostOutbox(ctx, tx, command.EventID, "AgencyMembershipChanged", "agency_membership", membership.MembershipID, command.NowMs); err != nil {
		return hostdomain.KickAgencyHostResult{}, err
	}
	// 系统消息消费的是和成员关系结束同事务提交的 user_outbox 快照。HTTP 返回后再同步调用
	// activity-service 会让“关系已移除、通知调用失败”变成不可重试的半成功状态，也无法覆盖 MQ 重投。
	if err := insertAgencyHostRemovedOutbox(ctx, tx, membership, command.OperatorUserID, command.NowMs); err != nil {
		return hostdomain.KickAgencyHostResult{}, err
	}
	if err := tx.Commit(); err != nil {
		return hostdomain.KickAgencyHostResult{}, err
	}

	return hostdomain.KickAgencyHostResult{Membership: membership, HostProfile: hostProfile}, nil
}

type agencyHostRemovedPayload struct {
	MembershipID int64                      `json:"membership_id"`
	AgencyID     int64                      `json:"agency_id"`
	HostUserID   int64                      `json:"host_user_id"`
	Operator     roleInvitationUserSnapshot `json:"operator"`
	RemovedAtMS  int64                      `json:"removed_at_ms"`
}

func insertAgencyHostRemovedOutbox(ctx context.Context, tx *sql.Tx, membership hostdomain.AgencyMembership, operatorUserID int64, nowMs int64) error {
	// current_display_user_id 是用户当前对外展示号：存在有效靓号时为靓号，否则为永久短 ID。
	// 在关系事务内固化操作者资料，避免异步消费时昵称或靓号变化导致通知内容不再对应移除当时的事实。
	operator, err := roleInvitationUserSnapshotByID(ctx, tx, operatorUserID)
	if err != nil {
		return err
	}
	payloadBytes, err := json.Marshal(agencyHostRemovedPayload{
		MembershipID: membership.MembershipID,
		AgencyID:     membership.AgencyID,
		HostUserID:   membership.HostUserID,
		Operator:     operator,
		RemovedAtMS:  nowMs,
	})
	if err != nil {
		return err
	}
	_, 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"),
		userOutboxEventAgencyHostRemoved,
		hostdomain.ResultTypeMembership,
		membership.MembershipID,
		string(payloadBytes),
		nowMs,
		nowMs,
	)
	return err
}
