// Package cpnotice consumes user-service CP outbox facts and delivers the IM side effects.
package cpnotice

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"strconv"
	"time"

	"hyapp/pkg/appcode"
	"hyapp/pkg/imgroup"
	"hyapp/pkg/logx"
	"hyapp/pkg/tencentim"
	"hyapp/pkg/usermq"
)

const (
	eventApplicationCreated  = "UserCPApplicationCreated"
	eventApplicationAccepted = "UserCPApplicationAccepted"
	eventApplicationRejected = "UserCPApplicationRejected"
	eventRelationshipCreated = "UserCPRelationshipCreated"

	channelTencentIMC2C         = "tencent_im_c2c"
	channelTencentIMC2CAcceptor = "tencent_im_c2c_acceptor"
	channelTencentIMRoom        = "tencent_im_room"
	channelTencentIMRegion      = "tencent_im_region"

	noticeTypeApplicationCreated  = "cp_application_created"
	noticeTypeApplicationAccepted = "cp_application_accepted"
	noticeTypeApplicationRejected = "cp_application_rejected"
	noticeTypeRelationshipCreated = "cp_relationship_created"
)

// Repository owns notice_delivery_events for CP IM deliveries; user-service remains the CP fact owner.
type Repository interface {
	ClaimCPDelivery(ctx context.Context, workerID string, delivery CPDelivery, lockTTL time.Duration) (CPDelivery, bool, error)
	MarkCPDelivered(ctx context.Context, delivery CPDelivery, deliveredPayload []byte, nowMs int64) error
	MarkCPRetryable(ctx context.Context, delivery CPDelivery, retryCount int, nextRetryAtMS int64, lastErr string, nowMs int64) error
	MarkCPFailed(ctx context.Context, delivery CPDelivery, retryCount int, lastErr string, nowMs int64) error
}

// Publisher is the Tencent IM boundary needed by CP notices.
type Publisher interface {
	PublishUserCustomMessage(ctx context.Context, message tencentim.CustomUserMessage) error
	PublishGroupCustomMessage(ctx context.Context, message tencentim.CustomGroupMessage) error
	EnsureGroup(ctx context.Context, spec tencentim.GroupSpec) error
}

// Config carries process-level delivery settings that must match gateway broadcast group generation.
type Config struct {
	NodeID        string
	GroupIDPrefix string
}

// Service maps CP facts to concrete IM messages and stores per-channel delivery state.
type Service struct {
	cfg        Config
	repository Repository
	publisher  Publisher
}

// New creates CP notice service.
func New(cfg Config, repository Repository, publisher Publisher) *Service {
	return &Service{cfg: cfg, repository: repository, publisher: publisher}
}

// ProcessUserOutboxMessage handles one user_outbox MQ message. Unknown events are acknowledged so that unrelated
// user-service facts can share the same topic without notice-service poisoning the consumer group.
func (s *Service) ProcessUserOutboxMessage(ctx context.Context, message usermq.UserOutboxMessage, options CPNoticeWorkerOptions) (bool, error) {
	if !isCPNoticeEvent(message.EventType) {
		return false, nil
	}
	options = normalizeCPNoticeWorkerOptions(options, s.cfg.NodeID)
	if s.repository == nil {
		return true, fmt.Errorf("notice repository is not configured")
	}
	if s.publisher == nil {
		return true, fmt.Errorf("notice publisher is not configured")
	}
	deliveries, err := deliveriesFromMessage(message, s.cfg.GroupIDPrefix)
	if err != nil {
		return true, err
	}
	for _, delivery := range deliveries {
		claimedDelivery, claimed, err := s.repository.ClaimCPDelivery(ctx, options.WorkerID, delivery, options.LockTTL)
		if err != nil || !claimed {
			return true, err
		}
		if err := s.publishDelivery(ctx, claimedDelivery, options); err != nil {
			return true, err
		}
	}
	return true, nil
}

func (s *Service) publishDelivery(ctx context.Context, delivery CPDelivery, options CPNoticeWorkerOptions) error {
	publishCtx, cancel := context.WithTimeout(ctx, options.PublishTimeout)
	defer cancel()
	var err error
	switch delivery.Channel {
	case channelTencentIMC2C, channelTencentIMC2CAcceptor:
		err = s.publisher.PublishUserCustomMessage(publishCtx, tencentim.CustomUserMessage{
			ToAccount:   tencentim.FormatUserID(delivery.TargetUserID),
			FromAccount: tencentim.FormatUserID(delivery.SenderUserID),
			EventID:     delivery.EventID,
			Desc:        delivery.NoticeType,
			Ext:         "cp_relation_notice",
			PayloadJSON: delivery.PayloadJSON,
		})
	case channelTencentIMRoom:
		err = s.publisher.PublishGroupCustomMessage(publishCtx, tencentim.CustomGroupMessage{
			GroupID:     delivery.GroupID,
			EventID:     delivery.EventID,
			Desc:        delivery.NoticeType,
			Ext:         "cp_room_notice",
			PayloadJSON: delivery.PayloadJSON,
		})
	case channelTencentIMRegion:
		// 区域群是长期播报群；发送前幂等创建，避免本地真实测试或新区域缺群导致关系主流程事实无法通知。
		if ensureErr := s.publisher.EnsureGroup(publishCtx, tencentim.GroupSpec{
			GroupID:         delivery.GroupID,
			Name:            delivery.GroupID,
			Type:            tencentim.DefaultGroupType,
			ApplyJoinOption: "FreeAccess",
		}); ensureErr != nil {
			err = ensureErr
			break
		}
		err = s.publisher.PublishGroupCustomMessage(publishCtx, tencentim.CustomGroupMessage{
			GroupID:     delivery.GroupID,
			EventID:     delivery.EventID,
			Desc:        delivery.NoticeType,
			Ext:         "cp_region_broadcast",
			PayloadJSON: delivery.PayloadJSON,
		})
	default:
		err = fmt.Errorf("unsupported cp notice channel %q", delivery.Channel)
	}
	nowMs := time.Now().UnixMilli()
	if err == nil {
		if markErr := s.repository.MarkCPDelivered(ctx, delivery, delivery.PayloadJSON, nowMs); markErr != nil {
			return markErr
		}
		logx.Info(ctx, "notice_cp_delivered",
			slog.String("event_id", delivery.EventID),
			slog.String("app_code", delivery.AppCode),
			slog.String("notice_type", delivery.NoticeType),
			slog.String("channel", delivery.Channel),
			slog.Int64("target_user_id", delivery.TargetUserID),
			slog.String("group_id", delivery.GroupID),
		)
		return nil
	}
	return s.markFailed(ctx, delivery, options, err)
}

func (s *Service) markFailed(ctx context.Context, delivery CPDelivery, options CPNoticeWorkerOptions, cause error) error {
	nowMs := time.Now().UnixMilli()
	nextRetryCount := delivery.RetryCount + 1
	if nextRetryCount >= options.MaxRetryCount {
		if err := s.repository.MarkCPFailed(ctx, delivery, nextRetryCount, cause.Error(), nowMs); err != nil {
			return err
		}
		logx.Error(ctx, "notice_cp_dead_letter", cause,
			slog.String("event_id", delivery.EventID),
			slog.String("app_code", delivery.AppCode),
			slog.String("notice_type", delivery.NoticeType),
			slog.String("channel", delivery.Channel),
			slog.Int("retry_count", nextRetryCount),
		)
		return nil
	}
	nextRetryAtMS := nowMs + cpNoticeBackoff(nextRetryCount, options).Milliseconds()
	if err := s.repository.MarkCPRetryable(ctx, delivery, nextRetryCount, nextRetryAtMS, cause.Error(), nowMs); err != nil {
		return err
	}
	logx.Error(ctx, "notice_cp_retryable", cause,
		slog.String("event_id", delivery.EventID),
		slog.String("app_code", delivery.AppCode),
		slog.String("notice_type", delivery.NoticeType),
		slog.String("channel", delivery.Channel),
		slog.Int("retry_count", nextRetryCount),
		slog.Int64("next_retry_at_ms", nextRetryAtMS),
	)
	return nil
}

func isCPNoticeEvent(eventType string) bool {
	switch eventType {
	case eventApplicationCreated, eventApplicationAccepted, eventApplicationRejected, eventRelationshipCreated:
		return true
	default:
		return false
	}
}

func deliveriesFromMessage(message usermq.UserOutboxMessage, groupIDPrefix string) ([]CPDelivery, error) {
	appCode := appcode.Normalize(message.AppCode)
	root, err := decodeObject(message.PayloadJSON)
	if err != nil {
		return nil, err
	}
	switch message.EventType {
	case eventApplicationCreated:
		application, err := applicationFromObject(root)
		if err != nil {
			return nil, err
		}
		basePayload, err := cpApplicationPayload(message, application, noticeTypeApplicationCreated, "pending")
		if err != nil {
			return nil, err
		}
		roomPayload := cloneJSONMap(basePayload)
		roomPayload["recipient_user_id"] = strconv.FormatInt(application.Target.UserID, 10)
		return []CPDelivery{
			{
				AppCode:      appCode,
				EventID:      message.EventID,
				EventType:    message.EventType,
				Channel:      channelTencentIMC2C,
				NoticeType:   noticeTypeApplicationCreated,
				SenderUserID: application.Requester.UserID,
				TargetUserID: application.Target.UserID,
				PayloadJSON:  mustJSON(basePayload),
				CreatedAtMS:  message.OccurredAtMS,
			},
			{
				AppCode:      appCode,
				EventID:      message.EventID,
				EventType:    message.EventType,
				Channel:      channelTencentIMRoom,
				NoticeType:   noticeTypeApplicationCreated,
				TargetUserID: application.Target.UserID,
				GroupID:      application.RoomID,
				PayloadJSON:  mustJSON(roomPayload),
				CreatedAtMS:  message.OccurredAtMS,
			},
		}, nil
	case eventApplicationAccepted:
		application, err := applicationFromObject(root)
		if err != nil {
			return nil, err
		}
		payload, err := cpApplicationPayload(message, application, noticeTypeApplicationAccepted, "accepted")
		if err != nil {
			return nil, err
		}
		return applicationAcceptedDeliveries(appCode, message, application, payload), nil
	case eventApplicationRejected:
		application, err := applicationFromObject(firstObject(root, "application", "Application"))
		if err != nil {
			return nil, err
		}
		payload, err := cpApplicationPayload(message, application, noticeTypeApplicationRejected, "rejected")
		if err != nil {
			return nil, err
		}
		payload["reason"] = stringValue(root, "reason", "Reason")
		return applicationDecisionDeliveries(appCode, message, application, payload, noticeTypeApplicationRejected), nil
	case eventRelationshipCreated:
		application, err := applicationFromObject(firstObject(root, "application", "Application"))
		if err != nil {
			return nil, err
		}
		relationship := firstObject(root, "relationship", "Relationship")
		relationshipID := stringValue(relationship, "relationship_id", "RelationshipID")
		groupID, err := imgroup.RegionBroadcastGroupIDWithPrefix(groupIDPrefix, appCode, application.RoomRegionID)
		if err != nil {
			return nil, err
		}
		return relationshipCreatedDelivery(appCode, message, application, relationshipID, groupID)
	default:
		return nil, nil
	}
}

func applicationDecisionDeliveries(appCode string, message usermq.UserOutboxMessage, application cpApplication, payload map[string]any, noticeType string) []CPDelivery {
	// 同意/拒绝结果必须同步覆盖两个入口：
	// 1. A 在私聊里看到 C2C 结果；
	// 2. A 在申请来源房间上下文看到房间 IM，客户端用 recipient_user_id 只给 A 展示按钮态变化。
	deliveries := []CPDelivery{{
		AppCode:      appCode,
		EventID:      message.EventID,
		EventType:    message.EventType,
		Channel:      channelTencentIMC2C,
		NoticeType:   noticeType,
		SenderUserID: application.Target.UserID,
		TargetUserID: application.Requester.UserID,
		PayloadJSON:  mustJSON(payload),
		CreatedAtMS:  message.OccurredAtMS,
	}}
	if application.RoomID == "" {
		return deliveries
	}
	roomPayload := cloneJSONMap(payload)
	roomPayload["recipient_user_id"] = strconv.FormatInt(application.Requester.UserID, 10)
	deliveries = append(deliveries, CPDelivery{
		AppCode:      appCode,
		EventID:      message.EventID,
		EventType:    message.EventType,
		Channel:      channelTencentIMRoom,
		NoticeType:   noticeType,
		TargetUserID: application.Requester.UserID,
		GroupID:      application.RoomID,
		PayloadJSON:  mustJSON(roomPayload),
		CreatedAtMS:  message.OccurredAtMS,
	})
	return deliveries
}

func applicationAcceptedDeliveries(appCode string, message usermq.UserOutboxMessage, application cpApplication, payload map[string]any) []CPDelivery {
	// 同意成功后双方都要在 C2C 会话里看到一条消息；notice_delivery_events 的幂等键只有 event_id + channel，
	// 所以第二条 C2C 使用独立内部 channel，但 publishDelivery 仍按腾讯 IM 单聊投递。
	requesterPayload := cloneJSONMap(payload)
	requesterPayload["c2c_notice_role"] = "accepted_to_requester"
	requesterPayload["message_text"] = "我同意了你"
	acceptorPayload := cloneJSONMap(payload)
	acceptorPayload["c2c_notice_role"] = "accepted_to_acceptor"
	acceptorPayload["message_text"] = "对方已同意你的申请"
	deliveries := []CPDelivery{
		{
			AppCode:      appCode,
			EventID:      message.EventID,
			EventType:    message.EventType,
			Channel:      channelTencentIMC2C,
			NoticeType:   noticeTypeApplicationAccepted,
			SenderUserID: application.Target.UserID,
			TargetUserID: application.Requester.UserID,
			PayloadJSON:  mustJSON(requesterPayload),
			CreatedAtMS:  message.OccurredAtMS,
		},
		{
			AppCode:      appCode,
			EventID:      message.EventID,
			EventType:    message.EventType,
			Channel:      channelTencentIMC2CAcceptor,
			NoticeType:   noticeTypeApplicationAccepted,
			SenderUserID: application.Requester.UserID,
			TargetUserID: application.Target.UserID,
			PayloadJSON:  mustJSON(acceptorPayload),
			CreatedAtMS:  message.OccurredAtMS,
		},
	}
	if application.RoomID == "" {
		return deliveries
	}
	roomPayload := cloneJSONMap(payload)
	roomPayload["recipient_user_id"] = strconv.FormatInt(application.Requester.UserID, 10)
	deliveries = append(deliveries, CPDelivery{
		AppCode:      appCode,
		EventID:      message.EventID,
		EventType:    message.EventType,
		Channel:      channelTencentIMRoom,
		NoticeType:   noticeTypeApplicationAccepted,
		TargetUserID: application.Requester.UserID,
		GroupID:      application.RoomID,
		PayloadJSON:  mustJSON(roomPayload),
		CreatedAtMS:  message.OccurredAtMS,
	})
	return deliveries
}

func relationshipCreatedDelivery(appCode string, message usermq.UserOutboxMessage, application cpApplication, relationshipID string, groupID string) ([]CPDelivery, error) {
	payload, err := cpRelationshipBroadcastPayload(message, application, relationshipID, groupID)
	if err != nil {
		return nil, err
	}
	return []CPDelivery{{
		AppCode:      appCode,
		EventID:      message.EventID,
		EventType:    message.EventType,
		Channel:      channelTencentIMRegion,
		NoticeType:   noticeTypeRelationshipCreated,
		TargetUserID: 0,
		GroupID:      groupID,
		PayloadJSON:  mustJSON(payload),
		CreatedAtMS:  message.OccurredAtMS,
	}}, nil
}

func cpApplicationPayload(message usermq.UserOutboxMessage, application cpApplication, noticeType string, status string) (map[string]any, error) {
	if application.ApplicationID == "" || application.Requester.UserID <= 0 || application.Target.UserID <= 0 {
		return nil, fmt.Errorf("cp application payload is incomplete")
	}
	return map[string]any{
		"event_id":             message.EventID,
		"event_type":           noticeType,
		"source_event_type":    message.EventType,
		"app_code":             appcode.Normalize(message.AppCode),
		"application_id":       application.ApplicationID,
		"relation_type":        application.RelationType,
		"status":               status,
		"requester":            userPayload(application.Requester),
		"target":               userPayload(application.Target),
		"room_id":              application.RoomID,
		"room_region_id":       application.RoomRegionID,
		"gift":                 giftPayload(application.Gift),
		"expires_at_ms":        application.ExpiresAtMS,
		"decided_at_ms":        application.DecidedAtMS,
		"source_created_at_ms": message.OccurredAtMS,
	}, nil
}

func cpRelationshipBroadcastPayload(message usermq.UserOutboxMessage, application cpApplication, relationshipID string, groupID string) (map[string]any, error) {
	if application.Requester.UserID <= 0 || application.Target.UserID <= 0 || application.RoomRegionID <= 0 {
		return nil, fmt.Errorf("cp relationship broadcast payload is incomplete")
	}
	return map[string]any{
		"event_id":             message.EventID,
		"event_type":           noticeTypeRelationshipCreated,
		"source_event_type":    message.EventType,
		"app_code":             appcode.Normalize(message.AppCode),
		"relationship_id":      relationshipID,
		"application_id":       application.ApplicationID,
		"relation_type":        application.RelationType,
		"requester":            userPayload(application.Requester),
		"target":               userPayload(application.Target),
		"gift":                 giftPayload(application.Gift),
		"room_id":              application.RoomID,
		"room_region_id":       application.RoomRegionID,
		"group_id":             groupID,
		"source_created_at_ms": message.OccurredAtMS,
	}, nil
}

func userPayload(user cpUser) map[string]any {
	return map[string]any{
		"user_id":         strconv.FormatInt(user.UserID, 10),
		"display_user_id": user.DisplayUserID,
		"username":        user.Username,
		"avatar":          user.Avatar,
	}
}

func giftPayload(gift cpGift) map[string]any {
	return map[string]any{
		"gift_id":            gift.GiftID,
		"gift_name":          gift.GiftName,
		"gift_icon_url":      gift.GiftIconURL,
		"gift_animation_url": gift.GiftAnimationURL,
		"gift_count":         gift.GiftCount,
		"gift_value":         gift.GiftValue,
		"billing_receipt_id": gift.BillingReceiptID,
	}
}

func mustJSON(payload map[string]any) json.RawMessage {
	body, _ := json.Marshal(payload)
	return body
}

func cloneJSONMap(input map[string]any) map[string]any {
	out := make(map[string]any, len(input))
	for key, value := range input {
		out[key] = value
	}
	return out
}
