// Package roommq defines RocketMQ payloads owned by room-service.
package roommq

import (
	"encoding/json"
	"errors"
	"fmt"
	"sort"
	"strings"

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

const (
	MessageTypeRoomOutboxEvent     = "room_outbox_event"
	MessageTypeRoomRocketLaunchDue = "room_rocket_launch_due"

	// TagRoomOutboxEvent 是 typed tag 上线前所有房间事实共用的 legacy Tag。
	// 迁移窗口内 consumer 必须订阅 legacy || relevant typed tags，等所有旧消息
	// 和重试消息排空后才允许移除 legacy，不能直接切换造成存量事实不可见。
	TagRoomOutboxEvent = "room_outbox_event"

	// 以下事件类型是当前 room topic 下游实际使用的稳定事实名。producer 的
	// typed 模式直接使用 envelope.event_type，consumer 复用这些常量拼 selector，
	// 避免 producer/consumer 两侧手写字符串漂移后静默漏消费。
	EventTypeRoomGiftSent            = "RoomGiftSent"
	EventTypeRoomUserJoined          = "RoomUserJoined"
	EventTypeRoomUserLeft            = "RoomUserLeft"
	EventTypeRoomUserKicked          = "RoomUserKicked"
	EventTypeRoomMicChanged          = "RoomMicChanged"
	EventTypeRoomRocketIgnited       = "RoomRocketIgnited"
	EventTypeRoomRocketRewardGranted = "RoomRocketRewardGranted"
	EventTypeRoomPasswordChanged     = "RoomPasswordChanged"

	TagRoomRocketLaunchDue = "room_rocket_launch_due"
)

// EventTagMode 控制 room outbox 的 Broker Tag；它只改变 MQ 路由，不改变消息体协议。
type EventTagMode string

const (
	// EventTagModeLegacy 是安全默认值，保证缺少新配置的旧环境继续发布统一 Tag。
	EventTagModeLegacy EventTagMode = "legacy"
	// EventTagModeEventType 只应在所有 consumer 完成兼容订阅滚动后显式启用。
	EventTagModeEventType EventTagMode = "event_type"
)

// EventTypeTag 把一个受控的 room event_type 转成 RocketMQ Tag。事件类型来自
// owner outbox 而不是用户输入，但仍需拒绝表达式操作符和标点，避免坏数据扩大
// 订阅范围或发布成 consumer 无法安全表达的 Tag。
func EventTypeTag(eventType string) (string, error) {
	tag := strings.TrimSpace(eventType)
	if tag == "" {
		return "", errors.New("room event_type tag is required")
	}
	if tag == TagRoomOutboxEvent {
		return "", errors.New("room event_type cannot reuse the legacy room tag")
	}
	for _, char := range tag {
		if (char >= 'a' && char <= 'z') ||
			(char >= 'A' && char <= 'Z') ||
			(char >= '0' && char <= '9') ||
			char == '_' || char == '-' {
			continue
		}
		return "", fmt.Errorf("room event_type %q cannot be used as a RocketMQ tag", eventType)
	}
	return tag, nil
}

// LegacyCompatibleTagExpression 构造无损迁移 selector：legacy 分支消费切换前
// 已存在 Broker 的消息，typed 分支只接收当前 consumer 真正处理的事件。排序和
// 去重保证同一 consumer group 的所有副本注册完全相同的订阅元数据。
func LegacyCompatibleTagExpression(eventTypes ...string) (string, error) {
	typedTags := make([]string, 0, len(eventTypes))
	seen := make(map[string]struct{}, len(eventTypes))
	for _, eventType := range eventTypes {
		tag, err := EventTypeTag(eventType)
		if err != nil {
			return "", err
		}
		if _, exists := seen[tag]; exists {
			continue
		}
		seen[tag] = struct{}{}
		typedTags = append(typedTags, tag)
	}
	if len(typedTags) == 0 {
		return "", errors.New("at least one typed room event tag is required")
	}
	sort.Strings(typedTags)
	return TagRoomOutboxEvent + " || " + strings.Join(typedTags, " || "), nil
}

// RoomOutboxMessage is the MQ representation of one durable room_outbox fact.
type RoomOutboxMessage struct {
	MessageType  string `json:"message_type"`
	AppCode      string `json:"app_code"`
	EventID      string `json:"event_id"`
	EventType    string `json:"event_type"`
	RoomID       string `json:"room_id"`
	RoomVersion  int64  `json:"room_version"`
	OccurredAtMS int64  `json:"occurred_at_ms"`
	Envelope     []byte `json:"envelope"`
}

// RoomRocketLaunchDueMessage wakes room-service at the configured launch_at_ms.
type RoomRocketLaunchDueMessage struct {
	MessageType string `json:"message_type"`
	AppCode     string `json:"app_code"`
	RoomID      string `json:"room_id"`
	RocketID    string `json:"rocket_id"`
	Level       int32  `json:"level"`
	LaunchAtMS  int64  `json:"launch_at_ms"`
	ResetAtMS   int64  `json:"reset_at_ms"`
	CommandID   string `json:"command_id"`
}

// EncodeRoomOutboxMessage serializes the protobuf envelope into an MQ JSON body.
func EncodeRoomOutboxMessage(envelope *roomeventsv1.EventEnvelope) ([]byte, error) {
	if envelope == nil {
		return nil, errors.New("room outbox envelope is required")
	}
	envelopeBytes, err := proto.Marshal(envelope)
	if err != nil {
		return nil, err
	}
	return json.Marshal(RoomOutboxMessage{
		MessageType:  MessageTypeRoomOutboxEvent,
		AppCode:      envelope.GetAppCode(),
		EventID:      envelope.GetEventId(),
		EventType:    envelope.GetEventType(),
		RoomID:       envelope.GetRoomId(),
		RoomVersion:  envelope.GetRoomVersion(),
		OccurredAtMS: envelope.GetOccurredAtMs(),
		Envelope:     envelopeBytes,
	})
}

// DecodeRoomOutboxMessage validates the MQ body and restores the protobuf fact.
func DecodeRoomOutboxMessage(body []byte) (*roomeventsv1.EventEnvelope, RoomOutboxMessage, error) {
	var message RoomOutboxMessage
	if err := json.Unmarshal(body, &message); err != nil {
		return nil, RoomOutboxMessage{}, err
	}
	if message.MessageType != MessageTypeRoomOutboxEvent {
		return nil, RoomOutboxMessage{}, errors.New("unexpected room outbox message_type")
	}
	var envelope roomeventsv1.EventEnvelope
	if err := proto.Unmarshal(message.Envelope, &envelope); err != nil {
		return nil, RoomOutboxMessage{}, err
	}
	if envelope.GetEventId() == "" || envelope.GetEventType() == "" || envelope.GetRoomId() == "" {
		return nil, RoomOutboxMessage{}, errors.New("room outbox envelope is incomplete")
	}
	return &envelope, message, nil
}

// EncodeRoomRocketLaunchDueMessage serializes a launch wakeup command.
func EncodeRoomRocketLaunchDueMessage(message RoomRocketLaunchDueMessage) ([]byte, error) {
	message.MessageType = MessageTypeRoomRocketLaunchDue
	if message.AppCode == "" || message.RoomID == "" || message.RocketID == "" || message.Level <= 0 || message.LaunchAtMS <= 0 {
		return nil, errors.New("room rocket launch due message is incomplete")
	}
	return json.Marshal(message)
}

// DecodeRoomRocketLaunchDueMessage validates a launch wakeup body.
func DecodeRoomRocketLaunchDueMessage(body []byte) (RoomRocketLaunchDueMessage, error) {
	var message RoomRocketLaunchDueMessage
	if err := json.Unmarshal(body, &message); err != nil {
		return RoomRocketLaunchDueMessage{}, err
	}
	if message.MessageType != MessageTypeRoomRocketLaunchDue {
		return RoomRocketLaunchDueMessage{}, errors.New("unexpected room rocket message_type")
	}
	if message.AppCode == "" || message.RoomID == "" || message.RocketID == "" || message.Level <= 0 || message.LaunchAtMS <= 0 {
		return RoomRocketLaunchDueMessage{}, errors.New("room rocket launch due message is incomplete")
	}
	return message, nil
}
