// Package activitymq defines the RocketMQ fact contract owned by activity-service.
package activitymq

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

const (
	MessageTypeActivityTemplateFact = "activity_template_fact"
	TagActivityTemplateFact         = MessageTypeActivityTemplateFact

	EventTypeGiftScored      = "GiftScored"
	EventTypeTaskProgressed  = "TaskProgressed"
	EventTypeTaskClaimed     = "TaskClaimed"
	EventTypeActivityVisited = "ActivityVisited"
)

// ActivityTemplateFactMessage is a self-contained immutable activity fact. Every
// dimension needed by statistics is carried in the message so consumers never
// need to read activity-service owner tables.
type ActivityTemplateFactMessage struct {
	MessageType      string `json:"message_type"`
	AppCode          string `json:"app_code"`
	EventID          string `json:"event_id"`
	EventType        string `json:"event_type"`
	TemplateID       string `json:"template_id"`
	TemplateCode     string `json:"template_code"`
	PublishedVersion int64  `json:"published_version"`
	StatDay          string `json:"stat_day"`
	RegionID         int64  `json:"region_id"`
	UserID           int64  `json:"user_id"`
	GiftID           string `json:"gift_id"`
	TaskKey          string `json:"task_key"`
	TaskType         string `json:"task_type"`
	GiftCountDelta   int64  `json:"gift_count_delta"`
	CoinAmountDelta  int64  `json:"coin_amount_delta"`
	ProgressDelta    int64  `json:"progress_delta"`
	CompletedDelta   int64  `json:"completed_delta"`
	ClaimedDelta     int64  `json:"claimed_delta"`
	VisitCountDelta  int64  `json:"visit_count_delta"`
	OccurredAtMS     int64  `json:"occurred_at_ms"`
	AttributionAtMS  int64  `json:"attribution_at_ms"`
}

// EventTypeTag accepts only committed owner facts. Keeping the allow-list here
// prevents a malformed database value from becoming a broker routing expression.
func EventTypeTag(eventType string) (string, error) {
	switch tag := strings.TrimSpace(eventType); tag {
	case EventTypeGiftScored, EventTypeTaskProgressed, EventTypeTaskClaimed, EventTypeActivityVisited:
		return tag, nil
	case "":
		return "", errors.New("activity event_type tag is required")
	default:
		return "", fmt.Errorf("unsupported activity event_type tag %q", eventType)
	}
}

// LegacyCompatibleTagExpression keeps messages published before typed tags were
// enabled consumable while making all new deliveries broker-filtered and explicit.
func LegacyCompatibleTagExpression(eventTypes ...string) (string, error) {
	tags := 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{}{}
		tags = append(tags, tag)
	}
	if len(tags) == 0 {
		return "", errors.New("at least one typed activity event tag is required")
	}
	sort.Strings(tags)
	return TagActivityTemplateFact + " || " + strings.Join(tags, " || "), nil
}

// EncodeActivityTemplateFactMessage normalizes the wrapper discriminator and
// rejects poison facts before they can leave the transactional outbox relay.
func EncodeActivityTemplateFactMessage(message ActivityTemplateFactMessage) ([]byte, error) {
	message.MessageType = MessageTypeActivityTemplateFact
	message = normalizeActivityTemplateFact(message)
	if err := validateActivityTemplateFact(message); err != nil {
		return nil, err
	}
	return json.Marshal(message)
}

// DecodeActivityTemplateFactMessage applies the same validation at every ingest
// boundary, including replay tooling that may bypass the original producer.
func DecodeActivityTemplateFactMessage(body []byte) (ActivityTemplateFactMessage, error) {
	var message ActivityTemplateFactMessage
	if err := json.Unmarshal(body, &message); err != nil {
		return ActivityTemplateFactMessage{}, err
	}
	if message.MessageType != MessageTypeActivityTemplateFact {
		return ActivityTemplateFactMessage{}, errors.New("unexpected activity template fact message_type")
	}
	message = normalizeActivityTemplateFact(message)
	if err := validateActivityTemplateFact(message); err != nil {
		return ActivityTemplateFactMessage{}, err
	}
	return message, nil
}

func normalizeActivityTemplateFact(message ActivityTemplateFactMessage) ActivityTemplateFactMessage {
	message.AppCode = strings.TrimSpace(message.AppCode)
	message.EventID = strings.TrimSpace(message.EventID)
	message.EventType = strings.TrimSpace(message.EventType)
	message.TemplateID = strings.TrimSpace(message.TemplateID)
	message.TemplateCode = strings.TrimSpace(message.TemplateCode)
	message.StatDay = strings.TrimSpace(message.StatDay)
	message.GiftID = strings.TrimSpace(message.GiftID)
	message.TaskKey = strings.TrimSpace(message.TaskKey)
	message.TaskType = strings.TrimSpace(message.TaskType)
	if message.AttributionAtMS <= 0 {
		message.AttributionAtMS = message.OccurredAtMS
	}
	return message
}

func validateActivityTemplateFact(message ActivityTemplateFactMessage) error {
	if message.AppCode == "" ||
		message.EventID == "" ||
		message.TemplateID == "" ||
		message.TemplateCode == "" ||
		message.PublishedVersion <= 0 ||
		message.RegionID < 0 ||
		message.UserID <= 0 ||
		message.OccurredAtMS <= 0 {
		return errors.New("activity template fact is incomplete")
	}
	if _, err := EventTypeTag(message.EventType); err != nil {
		return err
	}
	if message.GiftCountDelta < 0 || message.CoinAmountDelta < 0 ||
		message.ProgressDelta < 0 || message.CompletedDelta < 0 || message.ClaimedDelta < 0 || message.VisitCountDelta < 0 {
		return errors.New("activity template fact deltas cannot be negative")
	}
	parsedDay, err := time.Parse("2006-01-02", message.StatDay)
	if err != nil || parsedDay.Format("2006-01-02") != message.StatDay {
		return errors.New("activity template fact stat_day must be a UTC YYYY-MM-DD date")
	}
	// Task claims can be executed days after completion. stat_day remains the task earning day while
	// occurred_at_ms is the truthful wallet-grant time; attribution_at_ms proves the owner-selected bucket.
	daySourceMS := message.OccurredAtMS
	if message.EventType == EventTypeTaskClaimed {
		daySourceMS = message.AttributionAtMS
	}
	if time.UnixMilli(daySourceMS).UTC().Format("2006-01-02") != message.StatDay {
		return errors.New("activity template fact stat_day does not match attribution time")
	}
	switch message.EventType {
	case EventTypeGiftScored:
		if message.GiftID == "" || message.GiftCountDelta <= 0 {
			return errors.New("GiftScored requires gift_id and positive gift_count_delta")
		}
	case EventTypeTaskProgressed:
		if message.TaskKey == "" || message.TaskType == "" ||
			(message.ProgressDelta == 0 && message.CompletedDelta == 0) {
			return errors.New("TaskProgressed requires task identity and a progress or completed delta")
		}
	case EventTypeTaskClaimed:
		if message.TaskKey == "" || message.TaskType == "" || message.ClaimedDelta <= 0 {
			return errors.New("TaskClaimed requires task identity and positive claimed_delta")
		}
	case EventTypeActivityVisited:
		if message.VisitCountDelta <= 0 {
			return errors.New("ActivityVisited requires positive visit_count_delta")
		}
	}
	return nil
}
