package service

import (
	"context"
	"fmt"
	"strings"
	"time"

	"google.golang.org/protobuf/proto"
	roomeventsv1 "hyapp.local/api/proto/events/room/v1"
	"hyapp/pkg/appcode"
)

const (
	// forbid_send_msg 官方单接口上限为 200 QPS；双节点各 100 QPS，避免共享 SDKAppID 被单服务打满。
	imMuteProjectionInterval = 10 * time.Millisecond
	// 云 API 执行期间若 Room Cell 状态继续变化，最多在同一门闩内追赶三次，之后交给 outbox 退避重试。
	imMuteProjectionMaxAttempts = 3
	// REST client 单次上限为 5s；总预算额外覆盖进程内门闩、限速和最新快照复核。
	imMuteProjectionTimeout = 6 * time.Second
)

type imMuteProjectionKey struct {
	appCode string
	roomID  string
	userID  int64
}

// imMuteProjectionGate 让同一 app/room/user 的旧 mute 和新 unmute 在单进程内串行，
// 同时允许等待者遵守 context deadline；Room Cell 最新状态仍是每次云端写入的唯一输入。
type imMuteProjectionGate struct {
	token chan struct{}
	refs  int
}

// syncIMUserMuteFromOutbox 只消费已经随 Room Cell 命令原子提交的 RoomUserMuted 事实。
// 普通群消息不会调用该路径，因此腾讯 IM 回调关闭后，共享 SDKAppID 的其它项目不再依赖本服务延迟或可用性。
func (s *Service) syncIMUserMuteFromOutbox(ctx context.Context, envelope *roomeventsv1.EventEnvelope) error {
	if s == nil || s.imUserMuter == nil || envelope == nil || envelope.GetEventType() != "RoomUserMuted" {
		return nil
	}

	var event roomeventsv1.RoomUserMuted
	if err := proto.Unmarshal(envelope.GetBody(), &event); err != nil {
		return fmt.Errorf("decode im mute outbox event: %w", err)
	}
	if event.GetTargetUserId() <= 0 || strings.TrimSpace(envelope.GetRoomId()) == "" {
		return fmt.Errorf("im mute outbox event is incomplete")
	}
	return s.reconcileIMUserMute(ctx, envelope.GetRoomId(), event.GetTargetUserId())
}

// reconcileIMUserMute 在每次 forbid_send_msg 前后重读 Room Cell。
// outbox 可能因网络失败重放旧 mute 事件，不能直接采用历史 payload.muted，否则会覆盖较新的 unmute。
func (s *Service) reconcileIMUserMute(parent context.Context, roomID string, userID int64) error {
	if s == nil || s.imUserMuter == nil {
		return nil
	}
	roomID = strings.TrimSpace(roomID)
	if roomID == "" || userID <= 0 {
		return fmt.Errorf("im mute reconcile target is incomplete")
	}

	ctx, cancel := context.WithTimeout(parent, imMuteProjectionTimeout)
	defer cancel()
	release, err := s.acquireIMMuteProjection(ctx, imMuteProjectionKey{
		appCode: appcode.FromContext(ctx),
		roomID:  roomID,
		userID:  userID,
	})
	if err != nil {
		return err
	}
	defer release()

	for attempt := 1; attempt <= imMuteProjectionMaxAttempts; attempt++ {
		before, err := s.currentSnapshot(ctx, roomID)
		if err != nil {
			return err
		}
		if before == nil {
			// 房间已被物理清理时群也不再是活跃投递通道，历史禁言投影可以直接结束。
			return nil
		}
		desiredMuted := containsID(before.GetMuteUserIds(), userID)
		if err := s.waitIMMuteProjectionRate(ctx); err != nil {
			return err
		}
		if err := s.imUserMuter.SetRoomGroupMemberMuted(ctx, roomID, userID, desiredMuted); err != nil {
			return err
		}

		after, err := s.currentSnapshot(ctx, roomID)
		if err != nil {
			return err
		}
		if after == nil || containsID(after.GetMuteUserIds(), userID) == desiredMuted {
			return nil
		}
		// 状态在腾讯请求飞行期间变化；保持同一 keyed gate 并立即投影最新值，避免本轮留下旧云端状态。
	}
	return fmt.Errorf("im mute state kept changing during %d reconcile attempts", imMuteProjectionMaxAttempts)
}

func (s *Service) acquireIMMuteProjection(ctx context.Context, key imMuteProjectionKey) (func(), error) {
	s.imMuteGateMu.Lock()
	if s.imMuteGates == nil {
		s.imMuteGates = make(map[imMuteProjectionKey]*imMuteProjectionGate)
	}
	gate := s.imMuteGates[key]
	if gate == nil {
		gate = &imMuteProjectionGate{token: make(chan struct{}, 1)}
		gate.token <- struct{}{}
		s.imMuteGates[key] = gate
	}
	gate.refs++
	s.imMuteGateMu.Unlock()

	select {
	case <-ctx.Done():
		s.releaseIMMuteProjectionRef(key, gate)
		return nil, ctx.Err()
	case <-gate.token:
		return func() {
			gate.token <- struct{}{}
			s.releaseIMMuteProjectionRef(key, gate)
		}, nil
	}
}

func (s *Service) releaseIMMuteProjectionRef(key imMuteProjectionKey, gate *imMuteProjectionGate) {
	s.imMuteGateMu.Lock()
	defer s.imMuteGateMu.Unlock()
	gate.refs--
	if gate.refs == 0 && s.imMuteGates[key] == gate {
		delete(s.imMuteGates, key)
	}
}

func (s *Service) waitIMMuteProjectionRate(ctx context.Context) error {
	s.imMuteRateMu.Lock()
	now := time.Now()
	slot := now
	if s.imMuteNextCall.After(slot) {
		slot = s.imMuteNextCall
	}
	s.imMuteNextCall = slot.Add(imMuteProjectionInterval)
	s.imMuteRateMu.Unlock()

	wait := time.Until(slot)
	if wait <= 0 {
		return nil
	}
	timer := time.NewTimer(wait)
	defer timer.Stop()
	select {
	case <-ctx.Done():
		return ctx.Err()
	case <-timer.C:
		return nil
	}
}
