// Package luckygiftmq defines the RocketMQ wire contract owned by lucky-gift-service.
package luckygiftmq

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

	"google.golang.org/protobuf/proto"
	luckygifteventsv1 "hyapp.local/api/proto/events/luckygift/v1"
)

const (
	MessageTypeLuckyGiftOutboxEvent = "lucky_gift_outbox_event"
	EventTypeLuckyGiftDrawn         = "LuckyGiftDrawn"
	TagLuckyGiftDrawn               = EventTypeLuckyGiftDrawn

	// LuckyGiftDisplayMinMultiplierPPM keeps room and regional top-banner policy on the same exact 10x boundary.
	// The owner still publishes every granted positive reward; consumers apply this presentation threshold independently.
	LuckyGiftDisplayMinMultiplierPPM int64 = 10_000_000
)

// LuckyGiftOutboxMessage is the inspectable JSON wrapper around one protobuf owner envelope.
// Metadata is duplicated outside envelope only for RocketMQ diagnostics and routing; the protobuf envelope remains authoritative.
type LuckyGiftOutboxMessage struct {
	MessageType  string `json:"message_type"`
	AppCode      string `json:"app_code"`
	EventID      string `json:"event_id"`
	EventType    string `json:"event_type"`
	DrawID       string `json:"draw_id"`
	OccurredAtMS int64  `json:"occurred_at_ms"`
	Envelope     []byte `json:"envelope"`
}

// EventTypeTag validates an owner-controlled event type before it becomes a broker-side typed Tag.
// Only registered fact names are accepted so malformed outbox data cannot publish to an unconsumed routing key.
func EventTypeTag(eventType string) (string, error) {
	switch tag := strings.TrimSpace(eventType); tag {
	case EventTypeLuckyGiftDrawn:
		return tag, nil
	case "":
		return "", errors.New("lucky gift event_type tag is required")
	default:
		return "", fmt.Errorf("unsupported lucky gift event_type tag %q", eventType)
	}
}

// EncodeLuckyGiftOutboxMessage serializes the protobuf envelope without converting int64 business identifiers through JSON numbers.
func EncodeLuckyGiftOutboxMessage(envelope *luckygifteventsv1.EventEnvelope) ([]byte, error) {
	if err := validateLuckyGiftEnvelope(envelope); err != nil {
		return nil, err
	}
	envelopeBytes, err := proto.Marshal(envelope)
	if err != nil {
		return nil, err
	}
	return json.Marshal(LuckyGiftOutboxMessage{
		MessageType:  MessageTypeLuckyGiftOutboxEvent,
		AppCode:      envelope.GetAppCode(),
		EventID:      envelope.GetEventId(),
		EventType:    envelope.GetEventType(),
		DrawID:       envelope.GetDrawId(),
		OccurredAtMS: envelope.GetOccurredAtMs(),
		Envelope:     envelopeBytes,
	})
}

// DecodeLuckyGiftOutboxMessage restores and validates the owner envelope.
// Wrapper/envelope equality is checked explicitly so operators never diagnose one set of visible metadata while consumers process another.
func DecodeLuckyGiftOutboxMessage(body []byte) (*luckygifteventsv1.EventEnvelope, LuckyGiftOutboxMessage, error) {
	var message LuckyGiftOutboxMessage
	if err := json.Unmarshal(body, &message); err != nil {
		return nil, LuckyGiftOutboxMessage{}, err
	}
	if message.MessageType != MessageTypeLuckyGiftOutboxEvent {
		return nil, LuckyGiftOutboxMessage{}, errors.New("unexpected lucky gift outbox message_type")
	}
	if len(message.Envelope) == 0 {
		return nil, LuckyGiftOutboxMessage{}, errors.New("lucky gift outbox envelope is required")
	}
	var envelope luckygifteventsv1.EventEnvelope
	if err := proto.Unmarshal(message.Envelope, &envelope); err != nil {
		return nil, LuckyGiftOutboxMessage{}, err
	}
	if err := validateLuckyGiftEnvelope(&envelope); err != nil {
		return nil, LuckyGiftOutboxMessage{}, err
	}
	if message.AppCode != envelope.GetAppCode() ||
		message.EventID != envelope.GetEventId() ||
		message.EventType != envelope.GetEventType() ||
		message.DrawID != envelope.GetDrawId() ||
		message.OccurredAtMS != envelope.GetOccurredAtMs() {
		return nil, LuckyGiftOutboxMessage{}, errors.New("lucky gift outbox wrapper does not match envelope")
	}
	return &envelope, message, nil
}

func validateLuckyGiftEnvelope(envelope *luckygifteventsv1.EventEnvelope) error {
	if envelope == nil {
		return errors.New("lucky gift outbox envelope is required")
	}
	if strings.TrimSpace(envelope.GetAppCode()) == "" ||
		strings.TrimSpace(envelope.GetEventId()) == "" ||
		strings.TrimSpace(envelope.GetDrawId()) == "" ||
		envelope.GetOccurredAtMs() <= 0 ||
		len(envelope.GetBody()) == 0 {
		return errors.New("lucky gift outbox envelope is incomplete")
	}
	// draw_id 是一次中奖事实的天然幂等键；固定 event_id 规则后，所有消费者都能用同一个键收敛重投，
	// 也避免某个 producer 临时生成随机 ID 导致 room/activity 各自重复派生外部飘屏。
	if envelope.GetEventId() != "lucky_gift_drawn:"+envelope.GetDrawId() {
		return errors.New("lucky gift outbox event_id does not match draw_id")
	}
	if _, err := EventTypeTag(envelope.GetEventType()); err != nil {
		return err
	}

	// The current topic carries only granted winning facts. Validate the protobuf body here so poison data fails before broker publish
	// and is retried from the owner outbox instead of being acknowledged and rejected independently by every consumer.
	var drawn luckygifteventsv1.LuckyGiftDrawn
	if err := proto.Unmarshal(envelope.GetBody(), &drawn); err != nil {
		return fmt.Errorf("decode lucky gift drawn body: %w", err)
	}
	if strings.TrimSpace(drawn.GetDrawId()) == "" ||
		drawn.GetDrawId() != envelope.GetDrawId() ||
		strings.TrimSpace(drawn.GetCommandId()) == "" ||
		strings.TrimSpace(drawn.GetRoomId()) == "" ||
		strings.TrimSpace(drawn.GetGiftId()) == "" ||
		drawn.GetGiftCount() <= 0 ||
		drawn.GetSenderUserId() <= 0 ||
		drawn.GetTargetUserId() <= 0 ||
		drawn.GetCoinSpent() <= 0 ||
		drawn.GetMultiplierPpm() <= 0 ||
		drawn.GetEffectiveRewardCoins() <= 0 ||
		strings.ToLower(strings.TrimSpace(drawn.GetRewardStatus())) != "granted" ||
		drawn.GetDrawCreatedAtMs() <= 0 ||
		drawn.GetDrawCreatedAtMs() != envelope.GetOccurredAtMs() ||
		drawn.GetRewardGrantedAtMs() <= 0 {
		return errors.New("lucky gift drawn body is incomplete")
	}
	return nil
}
