package message

import (
	"context"
	"encoding/json"
	"fmt"
	"net/url"
	"strings"

	"hyapp/pkg/xerr"
	messagedomain "hyapp/services/activity-service/internal/domain/message"
)

const (
	defaultNoticeProducer = "activity-service"

	VIPDailyCoinRebateNoticeTemplateID      = "vip_daily_coin_rebate_available"
	VIPDailyCoinRebateNoticeTemplateVersion = "v2"
	VIPDailyCoinRebateNoticeTitle           = "VIP Coin Cashback"
)

// NoticeCommand is the common producer-facing command for one user-visible system/activity message.
type NoticeCommand struct {
	TargetUserID      int64
	Producer          string
	ProducerEventID   string
	ProducerEventType string
	AggregateType     string
	AggregateID       string
	TemplateID        string
	TemplateVersion   string
	Title             string
	Summary           string
	Body              string
	IconURL           string
	ImageURL          string
	ImageURLs         []string
	RewardItems       []messagedomain.RewardItem
	Actions           []messagedomain.InboxAction
	ActionType        string
	ActionParam       string
	Priority          int32
	SentAtMS          int64
	ExpireAtMS        int64
	Metadata          map[string]any
	MetadataJSON      string
}

// NoticeResult keeps the inbox row and the idempotent create flag together.
type NoticeResult struct {
	Message messagedomain.InboxMessage
	Created bool
}

// VIPDailyCoinRebateNoticeCopy 使用钱包金额事实生成统一英文快照；金额缺失时仍返回英文兜底，
// 供读取历史 v1 中文消息时关闭旧文案泄漏，而不伪造未知的返现金额。
func VIPDailyCoinRebateNoticeCopy(coinAmount int64) (string, string) {
	if coinAmount <= 0 {
		return VIPDailyCoinRebateNoticeTitle, "Your daily VIP cashback is ready. Claim it before it expires."
	}
	return VIPDailyCoinRebateNoticeTitle, fmt.Sprintf("Your daily VIP cashback of %d coins is ready. Claim it before it expires.", coinAmount)
}

// normalizeVIPDailyCoinRebateNoticeCopy keeps already-persisted v1 inbox snapshots compatible with
// the English-only client contract. The rebate ID and amount still come from the immutable wallet event.
func normalizeVIPDailyCoinRebateNoticeCopy(message messagedomain.InboxMessage) messagedomain.InboxMessage {
	if message.TemplateID != VIPDailyCoinRebateNoticeTemplateID || message.ActionType != "vip_coin_rebate_claim" {
		return message
	}
	var actionParam struct {
		CoinAmount int64  `json:"coin_amount"`
		RebateID   string `json:"rebate_id"`
	}
	_ = json.Unmarshal([]byte(message.ActionParam), &actionParam)
	message.Title, message.Summary = VIPDailyCoinRebateNoticeCopy(actionParam.CoinAmount)
	message.Body = message.Summary
	if strings.TrimSpace(actionParam.RebateID) != "" {
		message.Actions = []messagedomain.InboxAction{VIPDailyCoinRebateNoticeAction(actionParam.RebateID)}
		message.ActionType = ""
		message.ActionParam = ""
	}
	return message
}

// VIPDailyCoinRebateNoticeAction 把 VIP 返现映射成普通 inbox 操作；Flutter 不识别 VIP，
// 只展示后端 label 并执行同源 API。稳定 command_id 让超时重试仍命中钱包幂等结果。
func VIPDailyCoinRebateNoticeAction(rebateID string) messagedomain.InboxAction {
	rebateID = strings.TrimSpace(rebateID)
	return messagedomain.InboxAction{
		Action: "claim",
		Label:  "Claim",
		Style:  "primary",
		API: messagedomain.InboxActionAPI{
			Method: "POST",
			Path:   "/api/v1/vip/coin-rebates/" + url.PathEscape(rebateID) + "/claim",
			Body: map[string]any{
				"command_id": "vip_rebate_claim_" + rebateID,
			},
		},
	}
}

// NoticeFanoutCommand is the common command for background system/activity broadcasts.
type NoticeFanoutCommand struct {
	CommandID         string
	MessageType       string
	TargetScope       string
	TargetUserID      int64
	UserIDs           []int64
	RegionID          int64
	Country           string
	ProducerEventType string
	AggregateType     string
	AggregateID       string
	TemplateID        string
	TemplateVersion   string
	Title             string
	Summary           string
	Body              string
	IconURL           string
	ImageURL          string
	Actions           []messagedomain.InboxAction
	ActionType        string
	ActionParam       string
	Priority          int32
	SentAtMS          int64
	ExpireAtMS        int64
	Metadata          map[string]any
	MetadataJSON      string
	BatchSize         int32
	CreatedBy         string
}

// CreateSystemNotice is the small public helper for account, wallet, room and application messages.
func (s *Service) CreateSystemNotice(ctx context.Context, cmd NoticeCommand) (NoticeResult, error) {
	return s.CreateNotice(ctx, messagedomain.SectionSystem, cmd)
}

// CreateActivityNotice is the small public helper for activity, reward and campaign messages.
func (s *Service) CreateActivityNotice(ctx context.Context, cmd NoticeCommand) (NoticeResult, error) {
	return s.CreateNotice(ctx, messagedomain.SectionActivity, cmd)
}

// CreateNotice normalizes producer defaults and metadata before delegating to the inbox owner.
func (s *Service) CreateNotice(ctx context.Context, messageType string, cmd NoticeCommand) (NoticeResult, error) {
	metadataJSON, err := noticeMetadataJSON(cmd.Metadata, cmd.MetadataJSON)
	if err != nil {
		return NoticeResult{}, err
	}
	// The existing CreateInboxMessage method is still the only place that writes user_inbox_messages.
	// This helper only narrows the public call shape so producers use the same idempotency and content rules.
	message, created, err := s.CreateInboxMessage(ctx, messagedomain.CreateInput{
		TargetUserID:      cmd.TargetUserID,
		Producer:          firstNoticeText(cmd.Producer, defaultNoticeProducer),
		ProducerEventID:   cmd.ProducerEventID,
		ProducerEventType: cmd.ProducerEventType,
		MessageType:       messageType,
		AggregateType:     cmd.AggregateType,
		AggregateID:       cmd.AggregateID,
		TemplateID:        cmd.TemplateID,
		TemplateVersion:   cmd.TemplateVersion,
		Title:             cmd.Title,
		Summary:           cmd.Summary,
		Body:              cmd.Body,
		IconURL:           cmd.IconURL,
		ImageURL:          cmd.ImageURL,
		ImageURLs:         cmd.ImageURLs,
		RewardItems:       cmd.RewardItems,
		Actions:           cmd.Actions,
		ActionType:        cmd.ActionType,
		ActionParam:       cmd.ActionParam,
		Priority:          cmd.Priority,
		SentAtMS:          cmd.SentAtMS,
		ExpireAtMS:        cmd.ExpireAtMS,
		MetadataJSON:      metadataJSON,
	})
	if err != nil {
		return NoticeResult{}, err
	}
	return NoticeResult{Message: message, Created: created}, nil
}

// CreateNoticeFanout records a broadcast command for the fanout worker instead of looping in the caller.
func (s *Service) CreateNoticeFanout(ctx context.Context, cmd NoticeFanoutCommand) (messagedomain.FanoutJob, bool, error) {
	messageType := normalizeSection(cmd.MessageType)
	if messageType == "" {
		messageType = messagedomain.SectionSystem
	}
	targetFilterJSON, err := noticeTargetFilterJSON(cmd)
	if err != nil {
		return messagedomain.FanoutJob{}, false, err
	}
	templateSnapshotJSON, err := noticeTemplateSnapshotJSON(cmd)
	if err != nil {
		return messagedomain.FanoutJob{}, false, err
	}
	// command_id is the broadcast idempotency key. Retrying the same admin/activity command returns the
	// existing job; changing payload under the same command_id is rejected by CreateFanoutJob.
	return s.CreateFanoutJob(ctx, messagedomain.CreateFanoutInput{
		CommandID:            strings.TrimSpace(cmd.CommandID),
		MessageType:          messageType,
		TargetScope:          strings.TrimSpace(cmd.TargetScope),
		TargetFilterJSON:     targetFilterJSON,
		TemplateSnapshotJSON: templateSnapshotJSON,
		BatchSize:            cmd.BatchSize,
		CreatedBy:            strings.TrimSpace(cmd.CreatedBy),
	})
}

func noticeTargetFilterJSON(cmd NoticeFanoutCommand) (string, error) {
	scope := strings.TrimSpace(cmd.TargetScope)
	var payload map[string]any
	switch scope {
	case messagedomain.TargetScopeSingleUser:
		if cmd.TargetUserID <= 0 {
			return "", xerr.New(xerr.InvalidArgument, "target_user_id is required")
		}
		payload = map[string]any{"target_user_id": cmd.TargetUserID}
	case messagedomain.TargetScopeUserIDs:
		userIDs := positiveUniqueInt64s(cmd.UserIDs)
		if len(userIDs) == 0 {
			return "", xerr.New(xerr.InvalidArgument, "user_ids is required")
		}
		payload = map[string]any{"user_ids": userIDs}
	case messagedomain.TargetScopeRegion:
		if cmd.RegionID <= 0 {
			return "", xerr.New(xerr.InvalidArgument, "region_id is required")
		}
		payload = map[string]any{"region_id": cmd.RegionID}
	case messagedomain.TargetScopeCountry:
		country := strings.TrimSpace(cmd.Country)
		if country == "" {
			return "", xerr.New(xerr.InvalidArgument, "country is required")
		}
		payload = map[string]any{"country": country}
	case messagedomain.TargetScopeAllActiveUsers, messagedomain.TargetScopeAllRegistered:
		payload = map[string]any{}
	default:
		return "", xerr.New(xerr.InvalidArgument, "target_scope is invalid")
	}
	encoded, err := json.Marshal(payload)
	if err != nil {
		return "", err
	}
	return string(encoded), nil
}

func noticeTemplateSnapshotJSON(cmd NoticeFanoutCommand) (string, error) {
	metadataJSON, err := noticeMetadataJSON(cmd.Metadata, cmd.MetadataJSON)
	if err != nil {
		return "", err
	}
	payload := map[string]any{
		"producer_event_type": strings.TrimSpace(cmd.ProducerEventType),
		"aggregate_type":      strings.TrimSpace(cmd.AggregateType),
		"aggregate_id":        strings.TrimSpace(cmd.AggregateID),
		"template_id":         strings.TrimSpace(cmd.TemplateID),
		"template_version":    strings.TrimSpace(cmd.TemplateVersion),
		"title":               strings.TrimSpace(cmd.Title),
		"summary":             strings.TrimSpace(cmd.Summary),
		"body":                strings.TrimSpace(cmd.Body),
		"icon_url":            strings.TrimSpace(cmd.IconURL),
		"image_url":           strings.TrimSpace(cmd.ImageURL),
		"actions":             cmd.Actions,
		"action_type":         strings.TrimSpace(cmd.ActionType),
		"action_param":        strings.TrimSpace(cmd.ActionParam),
		"priority":            cmd.Priority,
		"sent_at_ms":          cmd.SentAtMS,
		"expire_at_ms":        cmd.ExpireAtMS,
	}
	if metadataJSON != "" {
		var parsed any
		if err := json.Unmarshal([]byte(metadataJSON), &parsed); err != nil {
			return "", xerr.New(xerr.InvalidArgument, "metadata_json is invalid")
		}
		payload["metadata_json"] = parsed
	}
	encoded, err := json.Marshal(payload)
	if err != nil {
		return "", err
	}
	return string(encoded), nil
}

func noticeMetadataJSON(metadata map[string]any, metadataJSON string) (string, error) {
	metadataJSON = strings.TrimSpace(metadataJSON)
	if len(metadata) > 0 && metadataJSON != "" {
		return "", xerr.New(xerr.InvalidArgument, "metadata and metadata_json cannot both be set")
	}
	if metadataJSON != "" {
		if !json.Valid([]byte(metadataJSON)) {
			return "", xerr.New(xerr.InvalidArgument, "metadata_json is invalid")
		}
		return metadataJSON, nil
	}
	if len(metadata) == 0 {
		return "", nil
	}
	encoded, err := json.Marshal(metadata)
	if err != nil {
		return "", err
	}
	return string(encoded), nil
}

func positiveUniqueInt64s(values []int64) []int64 {
	seen := make(map[int64]struct{}, len(values))
	out := make([]int64, 0, len(values))
	for _, value := range values {
		if value <= 0 {
			continue
		}
		if _, exists := seen[value]; exists {
			continue
		}
		seen[value] = struct{}{}
		out = append(out, value)
	}
	return out
}

func firstNoticeText(values ...string) string {
	for _, value := range values {
		if trimmed := strings.TrimSpace(value); trimmed != "" {
			return trimmed
		}
	}
	return ""
}
