package service

import (
	"context"
	"strings"
	"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"
)

const (
	rtcEventAudioStarted = "audio_started"
	rtcEventAudioStopped = "audio_stopped"
	rtcEventRoomExited   = "room_exited"
)

// ApplyRTCEvent 把已验签的腾讯 RTC 服务端事件落到 Room Cell。
func (s *Service) ApplyRTCEvent(ctx context.Context, req *roomv1.ApplyRTCEventRequest) (*roomv1.ApplyRTCEventResponse, error) {
	ctx = contextFromMeta(ctx, req.GetMeta())
	cmd := command.ApplyRTCEvent{
		Base:            baseFromMeta(req.GetMeta()),
		TargetUserID:    req.GetTargetUserId(),
		EventType:       strings.TrimSpace(req.GetEventType()),
		EventTimeMS:     req.GetEventTimeMs(),
		Reason:          strings.TrimSpace(req.GetReason()),
		Source:          strings.TrimSpace(req.GetSource()),
		ExternalEventID: strings.TrimSpace(req.GetExternalEventId()),
	}
	if cmd.TargetUserID == 0 {
		// gateway 正常会显式传 target_user_id；兜底使用 actor 保持内部调用更易测试。
		cmd.TargetUserID = cmd.ActorUserID()
	}
	if cmd.Source == "" {
		cmd.Source = "rtc_webhook"
	}

	result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
		if cmd.TargetUserID <= 0 || cmd.EventType == "" || cmd.EventTimeMS <= 0 {
			return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "rtc event is incomplete")
		}

		switch cmd.EventType {
		case rtcEventAudioStarted:
			return s.applyRTCAudioStarted(now, current, cmd)
		case rtcEventAudioStopped:
			return s.applyRTCAudioStopped(now, current, cmd)
		case rtcEventRoomExited:
			return s.applyRTCRoomExited(now, current, cmd)
		default:
			return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "rtc event type is unsupported")
		}
	})
	if err != nil {
		return nil, err
	}

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

func (s *Service) applyRTCAudioStarted(now time.Time, current *state.RoomState, cmd command.ApplyRTCEvent) (mutationResult, []outbox.Record, error) {
	if err := requireActiveRoom(current); err != nil {
		return mutationResult{}, nil, err
	}
	if _, exists := current.OnlineUsers[cmd.TargetUserID]; !exists {
		// RTC 事件不拥有业务进房事实；用户不在业务 presence 时只忽略旧事件。
		return mutationResult{snapshot: current.ToProto()}, nil, nil
	}

	seat, exists := current.SeatByUser(cmd.TargetUserID)
	if !exists {
		return mutationResult{snapshot: current.ToProto()}, nil, nil
	}
	if !rtcEventMatchesCurrentMicSession(seat, cmd.EventTimeMS, s.micPublishTimeout) {
		return mutationResult{snapshot: current.ToProto(), seatNo: seat.SeatNo}, nil, nil
	}
	if seat.PublishState == state.MicPublishPublishing {
		return mutationResult{snapshot: current.ToProto(), seatNo: seat.SeatNo}, nil, nil
	}
	if seat.PublishState != state.MicPublishPending {
		return mutationResult{snapshot: current.ToProto(), seatNo: seat.SeatNo}, nil, nil
	}

	index := current.SeatIndex(seat.SeatNo)
	current.MicSeats[index].PublishState = state.MicPublishPublishing
	current.MicSeats[index].LastPublishEventTimeMS = cmd.EventTimeMS
	current.Version++

	micEvent, err := outbox.Build(current.RoomID, "RoomMicChanged", current.Version, now, &roomeventsv1.RoomMicChanged{
		ActorUserId:        cmd.ActorUserID(),
		TargetUserId:       cmd.TargetUserID,
		FromSeat:           seat.SeatNo,
		ToSeat:             seat.SeatNo,
		Action:             "publish_confirmed",
		MicSessionId:       seat.MicSessionID,
		PublishState:       state.MicPublishPublishing,
		PublishDeadlineMs:  seat.PublishDeadlineMS,
		PublishEventTimeMs: cmd.EventTimeMS,
	})
	if err != nil {
		return mutationResult{}, nil, err
	}

	return mutationResult{
		snapshot: current.ToProto(),
		seatNo:   seat.SeatNo,
		syncEvent: &tencentim.RoomEvent{
			EventID:      micEvent.EventID,
			RoomID:       current.RoomID,
			EventType:    "room_mic_publish_confirmed",
			ActorUserID:  cmd.ActorUserID(),
			TargetUserID: cmd.TargetUserID,
			SeatNo:       seat.SeatNo,
			RoomVersion:  current.Version,
			Attributes: map[string]string{
				"action":                "publish_confirmed",
				"publish_state":         state.MicPublishPublishing,
				"mic_session_id":        seat.MicSessionID,
				"publish_event_time_ms": int64String(cmd.EventTimeMS),
				"source":                cmd.Source,
				"external_event_id":     cmd.ExternalEventID,
			},
		},
	}, []outbox.Record{micEvent}, nil
}

func (s *Service) applyRTCAudioStopped(now time.Time, current *state.RoomState, cmd command.ApplyRTCEvent) (mutationResult, []outbox.Record, error) {
	seat, exists := current.SeatByUser(cmd.TargetUserID)
	if !exists {
		return mutationResult{snapshot: current.ToProto()}, nil, nil
	}
	if !rtcStopEventMatchesCurrentMicSession(seat, cmd.EventTimeMS, s.micPublishTimeout) {
		return mutationResult{snapshot: current.ToProto(), seatNo: seat.SeatNo}, nil, nil
	}

	index := current.SeatIndex(seat.SeatNo)
	micSessionID := current.MicSeats[index].MicSessionID
	current.ClearMicSession(index)
	current.Version++

	micEvent, err := outbox.Build(current.RoomID, "RoomMicChanged", current.Version, now, &roomeventsv1.RoomMicChanged{
		ActorUserId:        cmd.ActorUserID(),
		TargetUserId:       cmd.TargetUserID,
		FromSeat:           seat.SeatNo,
		ToSeat:             0,
		Action:             "down",
		MicSessionId:       micSessionID,
		PublishState:       state.MicPublishIdle,
		Reason:             cmd.Reason,
		PublishEventTimeMs: cmd.EventTimeMS,
	})
	if err != nil {
		return mutationResult{}, nil, err
	}

	return mutationResult{
		snapshot: current.ToProto(),
		seatNo:   seat.SeatNo,
		syncEvent: &tencentim.RoomEvent{
			EventID:      micEvent.EventID,
			RoomID:       current.RoomID,
			EventType:    "room_mic_down",
			ActorUserID:  cmd.ActorUserID(),
			TargetUserID: cmd.TargetUserID,
			SeatNo:       seat.SeatNo,
			RoomVersion:  current.Version,
			Attributes: map[string]string{
				"action":                "down",
				"publish_state":         state.MicPublishIdle,
				"mic_session_id":        micSessionID,
				"reason":                cmd.Reason,
				"publish_event_time_ms": int64String(cmd.EventTimeMS),
				"source":                cmd.Source,
				"external_event_id":     cmd.ExternalEventID,
			},
		},
	}, []outbox.Record{micEvent}, nil
}

func (s *Service) applyRTCRoomExited(now time.Time, current *state.RoomState, cmd command.ApplyRTCEvent) (mutationResult, []outbox.Record, error) {
	if user, exists := current.OnlineUsers[cmd.TargetUserID]; exists && user.LastSeenAtMS > cmd.EventTimeMS {
		// 腾讯可能重试或延迟投递旧退房事件；如果业务 presence 已被更新到事件之后，保留当前重连和麦位状态。
		return mutationResult{snapshot: current.ToProto()}, nil, nil
	}

	_, onSeat := current.SeatByUser(cmd.TargetUserID)
	if !onSeat {
		// RTC 退房只代表媒体频道状态，不拥有 room-service 业务 presence。
		return mutationResult{snapshot: current.ToProto()}, nil, nil
	}

	// 用户仍可能停留在房间页继续重连；这里只释放当前 mic_session，业务离房只能由 LeaveRoom、kick、close 或 stale worker 触发。
	return s.applyRTCAudioStopped(now, current, cmd)
}

func rtcEventMatchesCurrentMicSession(seat state.MicSeat, eventTimeMS int64, timeout time.Duration) bool {
	if !rtcStopEventMatchesCurrentMicSession(seat, eventTimeMS, timeout) {
		return false
	}
	if seat.PublishDeadlineMS > 0 && eventTimeMS > seat.PublishDeadlineMS {
		return false
	}

	return true
}

func rtcStopEventMatchesCurrentMicSession(seat state.MicSeat, eventTimeMS int64, timeout time.Duration) bool {
	if seat.MicSessionID == "" || eventTimeMS <= seat.LastPublishEventTimeMS {
		return false
	}
	if createdAtMS := rtcMicSessionCreatedAtMS(seat, timeout); createdAtMS > 0 && eventTimeMS < createdAtMS {
		return false
	}

	return true
}

func rtcMicSessionCreatedAtMS(seat state.MicSeat, timeout time.Duration) int64 {
	timeoutMS := timeout.Milliseconds()
	if seat.PublishDeadlineMS <= 0 || timeoutMS <= 0 {
		return 0
	}

	return seat.PublishDeadlineMS - timeoutMS
}
