package service

import (
	"context"
	"time"

	roomeventsv1 "hyapp.local/api/proto/events/room/v1"
	roomv1 "hyapp.local/api/proto/room/v1"
	"hyapp/pkg/tencentim"
	"hyapp/pkg/xerr"
	"hyapp/services/room-service/internal/room/command"
	"hyapp/services/room-service/internal/room/outbox"
	"hyapp/services/room-service/internal/room/rank"
	"hyapp/services/room-service/internal/room/state"
)

// MuteUser 修改用户禁言状态。
func (s *Service) MuteUser(ctx context.Context, req *roomv1.MuteUserRequest) (*roomv1.MuteUserResponse, error) {
	ctx = contextFromMeta(ctx, req.GetMeta())
	// 禁言状态由 Room Cell 持有；提交后的 durable outbox 分别投影腾讯 IM 原生群成员禁言和 RTC 音频上行闸门。
	cmd := command.MuteUser{
		Base:         baseFromMeta(req.GetMeta()),
		TargetUserID: req.GetTargetUserId(),
		Muted:        req.GetMuted(),
	}

	result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
		if err := requireActiveRoom(current); err != nil {
			return mutationResult{}, nil, err
		}
		if cmd.TargetUserID <= 0 {
			return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "target_user_id is required")
		}
		if cmd.TargetUserID == cmd.ActorUserID() {
			return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "cannot mute self")
		}
		if err := requireActorPresent(current, cmd.ActorUserID()); err != nil {
			return mutationResult{}, nil, err
		}
		if err := canManageTarget(current, cmd.ActorUserID(), cmd.TargetUserID); err != nil {
			return mutationResult{}, nil, err
		}
		if cmd.Muted {
			if err := s.requireTargetWithoutVIPBenefit(ctx, req.GetMeta().GetRequestId(), cmd.TargetUserID, vipBenefitAntiMute); err != nil {
				// anti_mute 只阻止普通房主/管理员新增禁言；解除既有禁言必须始终允许。
				return mutationResult{}, nil, err
			}
		}
		if current.MuteUsers[cmd.TargetUserID] == cmd.Muted {
			// 重复设置同一禁言状态是业务 no-op，仍由 pipeline 写 command log 做幂等保护。
			return mutationResult{snapshot: current.ToProto()}, nil, nil
		}

		var mutedSeat *state.MicSeat
		if cmd.Muted {
			// map set 是公屏、上麦和开麦权限的统一事实；麦位静音只是这个事实的 RTC 投影。
			current.MuteUsers[cmd.TargetUserID] = true
			if seat, exists := current.SeatByUser(cmd.TargetUserID); exists && !seat.MicMuted {
				// 房间禁言不能只依赖目标客户端执行 muteLocalAudio；先把当前麦位标成 muted，
				// 让快照和 room_mic_mute_changed 都保持静音，目标用户解除房间禁言前也不能自行开麦。
				index := current.SeatIndex(seat.SeatNo)
				current.MicSeats[index].MicMuted = true
				updated := current.MicSeats[index]
				mutedSeat = &updated
			}
		} else {
			// 解除房间禁言只恢复“允许开麦”；不替用户自动打开本地麦克风，当前麦位仍需显式 SetMicMute(false)。
			delete(current.MuteUsers, cmd.TargetUserID)
		}

		current.Version++

		// 禁言变更进入 outbox，外部系统可以做审核和操作审计。
		muteEvent, err := outbox.Build(current.RoomID, "RoomUserMuted", current.Version, now, &roomeventsv1.RoomUserMuted{
			ActorUserId:  cmd.ActorUserID(),
			TargetUserId: cmd.TargetUserID,
			Muted:        cmd.Muted,
		})
		if err != nil {
			return mutationResult{}, nil, err
		}
		records := make([]outbox.Record, 0, 2)
		if mutedSeat != nil {
			micChanged := roomMicChangedEvent(&roomeventsv1.RoomMicChanged{
				ActorUserId:       cmd.ActorUserID(),
				TargetUserId:      cmd.TargetUserID,
				FromSeat:          mutedSeat.SeatNo,
				ToSeat:            mutedSeat.SeatNo,
				Action:            "mic_mute",
				MicSessionId:      mutedSeat.MicSessionID,
				PublishState:      mutedSeat.PublishState,
				PublishDeadlineMs: mutedSeat.PublishDeadlineMS,
				MicMuted:          true,
				SeatStatus:        mutedSeat.SeatStatus(),
				TargetGiftValue:   roomUserGiftValue(current, cmd.TargetUserID),
			}, command.UserDisplayProfile{})
			micEvent, err := outbox.Build(current.RoomID, "RoomMicChanged", current.Version, now, micChanged)
			if err != nil {
				return mutationResult{}, nil, err
			}
			records = append(records, micEvent)
		}
		records = append(records, muteEvent)

		return mutationResult{
			snapshot: current.ToProto(),
			syncEvent: &tencentim.RoomEvent{
				EventID:      muteEvent.EventID,
				RoomID:       current.RoomID,
				EventType:    "room_user_muted",
				ActorUserID:  tencentim.FormatOptionalUserID(cmd.ActorUserID()),
				TargetUserID: tencentim.FormatOptionalUserID(cmd.TargetUserID),
				RoomVersion:  current.Version,
			},
		}, records, nil
	})
	if err != nil {
		return nil, err
	}

	return &roomv1.MuteUserResponse{
		Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
		Room:   result.snapshot,
	}, nil
}

// KickUser 把目标用户踢出房间并移除其麦位与 presence。
func (s *Service) KickUser(ctx context.Context, req *roomv1.KickUserRequest) (*roomv1.KickUserResponse, error) {
	ctx = contextFromMeta(ctx, req.GetMeta())
	// KickUser 同时修改 presence、麦位和 ban 集合，是典型 Room Cell 强关联命令。
	cmd := command.KickUser{
		Base:         baseFromMeta(req.GetMeta()),
		TargetUserID: req.GetTargetUserId(),
		DurationMS:   req.GetDurationMs(),
	}

	shouldKickRTC := false
	result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
		if err := requireActiveRoom(current); err != nil {
			return mutationResult{}, nil, err
		}
		if cmd.TargetUserID <= 0 {
			return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "target_user_id is required")
		}
		if cmd.TargetUserID == cmd.ActorUserID() {
			return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "cannot kick self")
		}
		if cmd.DurationMS < 0 {
			return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "duration_ms must be non-negative")
		}
		if err := requireActorPresent(current, cmd.ActorUserID()); err != nil {
			return mutationResult{}, nil, err
		}
		if err := canManageTarget(current, cmd.ActorUserID(), cmd.TargetUserID); err != nil {
			return mutationResult{}, nil, err
		}
		if err := s.requireTargetWithoutVIPBenefit(ctx, req.GetMeta().GetRequestId(), cmd.TargetUserID, vipBenefitAntiKick); err != nil {
			// 平台封禁、风控和后台治理使用独立 SystemEvictUser；只有普通房间治理受 anti_kick 保护。
			return mutationResult{}, nil, err
		}

		_, inRoom := current.OnlineUsers[cmd.TargetUserID]
		_, onSeat := current.SeatByUser(cmd.TargetUserID)
		// 只有目标确实还在业务房间或麦位上，才需要请求 TRTC 服务端移除实时音频连接。
		shouldKickRTC = inRoom || onSeat

		var releasedSeatNo int32
		var releasedMicSessionID string
		if seat, exists := current.SeatByUser(cmd.TargetUserID); exists {
			// 被踢用户如果在麦上，必须先释放麦位，避免恢复后出现幽灵占位。
			index := current.SeatIndex(seat.SeatNo)
			releasedSeatNo = seat.SeatNo
			releasedMicSessionID = current.MicSeats[index].MicSessionID
			current.ClearMicSession(index)
		}

		// ban 集合会让后续 JoinRoom 和 VerifyRoomPresence 都失败。
		delete(current.OnlineUsers, cmd.TargetUserID)
		delete(current.AdminUsers, cmd.TargetUserID)
		expiresAtMS := int64(0)
		nowMS := now.UnixMilli()
		if cmd.DurationMS > 0 {
			expiresAtMS = nowMS + cmd.DurationMS
			if expiresAtMS <= nowMS {
				return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "duration_ms is too large")
			}
		}
		current.BanUsers[cmd.TargetUserID] = state.UserModerationState{
			UserID:      cmd.TargetUserID,
			ActorUserID: cmd.ActorUserID(),
			CreatedAtMS: nowMS,
			ExpiresAtMS: expiresAtMS,
		}
		current.Version++

		kickEvent, err := outbox.Build(current.RoomID, "RoomUserKicked", current.Version, now, &roomeventsv1.RoomUserKicked{
			ActorUserId:  cmd.ActorUserID(),
			TargetUserId: cmd.TargetUserID,
		})
		if err != nil {
			return mutationResult{}, nil, err
		}
		records := make([]outbox.Record, 0, 2)
		if releasedMicSessionID != "" {
			// 踢人释放麦位必须也生成 down 事实，时长聚合不能从 RoomUserKicked 反推麦位结束。
			micEvent, err := buildMicDownEventForSeat(current.RoomID, current.Version, now, cmd.ActorUserID(), cmd.TargetUserID, releasedSeatNo, releasedMicSessionID, "kick")
			if err != nil {
				return mutationResult{}, nil, err
			}
			records = append(records, micEvent)
		}
		records = append(records, kickEvent)

		return mutationResult{
			snapshot: current.ToProto(),
			syncEvent: &tencentim.RoomEvent{
				EventID:      kickEvent.EventID,
				RoomID:       current.RoomID,
				EventType:    "room_user_kicked",
				ActorUserID:  tencentim.FormatOptionalUserID(cmd.ActorUserID()),
				TargetUserID: tencentim.FormatOptionalUserID(cmd.TargetUserID),
				RoomVersion:  current.Version,
			},
		}, records, nil
	})
	if err != nil {
		return nil, err
	}

	resp := &roomv1.KickUserResponse{
		Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
		Room:   result.snapshot,
	}
	if result.applied && shouldKickRTC && s.rtcUserRemover != nil {
		// RTC 是房间外连接态：踢人事实已提交后再调用 TRTC，失败不回滚 Room Cell，但要回传给调用方做告警。
		if err := s.rtcUserRemover.RemoveUserByStrRoomID(ctx, cmd.RoomID(), cmd.TargetUserID); err != nil {
			resp.RtcKickError = err.Error()
		} else {
			resp.RtcKicked = true
		}
	}

	return resp, nil
}

// UnbanUser 解除房间 ban；不会恢复 presence、管理员、麦位或 RTC 推流事实。
func (s *Service) UnbanUser(ctx context.Context, req *roomv1.UnbanUserRequest) (*roomv1.UnbanUserResponse, error) {
	ctx = contextFromMeta(ctx, req.GetMeta())
	cmd := command.UnbanUser{
		Base:         baseFromMeta(req.GetMeta()),
		TargetUserID: req.GetTargetUserId(),
	}

	result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
		if err := requireActiveRoom(current); err != nil {
			return mutationResult{}, nil, err
		}
		if cmd.TargetUserID <= 0 {
			return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "target_user_id is required")
		}
		if err := requireOwnerPresent(current, cmd.ActorUserID()); err != nil {
			return mutationResult{}, nil, err
		}
		if _, exists := current.BanUsers[cmd.TargetUserID]; !exists {
			// 重复 unban 是 no-op，仍写 command log 确保 command_id 幂等可判定。
			return mutationResult{snapshot: current.ToProto()}, nil, nil
		}

		delete(current.BanUsers, cmd.TargetUserID)
		current.Version++

		unbanEvent, err := outbox.Build(current.RoomID, "RoomUserUnbanned", current.Version, now, &roomeventsv1.RoomUserUnbanned{
			ActorUserId:  cmd.ActorUserID(),
			TargetUserId: cmd.TargetUserID,
		})
		if err != nil {
			return mutationResult{}, nil, err
		}

		return mutationResult{
			snapshot: current.ToProto(),
			syncEvent: &tencentim.RoomEvent{
				EventID:      unbanEvent.EventID,
				RoomID:       current.RoomID,
				EventType:    "room_user_unbanned",
				ActorUserID:  tencentim.FormatOptionalUserID(cmd.ActorUserID()),
				TargetUserID: tencentim.FormatOptionalUserID(cmd.TargetUserID),
				RoomVersion:  current.Version,
			},
		}, []outbox.Record{unbanEvent}, nil
	})
	if err != nil {
		return nil, err
	}

	return &roomv1.UnbanUserResponse{
		Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
		Room:   result.snapshot,
	}, nil
}
