// Package outbox 实现公共 Outbox 的持久化与投递基础设施。
package outbox

import (
	"context"
	stderrors "errors"
	"strings"
	"time"

	"github.com/bytedance/sonic"
	"github.com/google/uuid"
	"gorm.io/datatypes"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"

	"github.com/break/junhong_cmp_fiber/internal/model"
	"github.com/break/junhong_cmp_fiber/pkg/asynctask"
	"github.com/break/junhong_cmp_fiber/pkg/constants"
)

// Envelope 是业务 UseCase 在事务内追加的公共事件信封。
type Envelope struct {
	EventID        string `json:"event_id"`
	EventType      string `json:"event_type"`
	PayloadVersion int    `json:"payload_version"`
	AggregateType  string `json:"aggregate_type"`
	AggregateID    string `json:"aggregate_id"`
	ResourceType   string `json:"resource_type"`
	ResourceID     string `json:"resource_id"`
	BusinessKey    string `json:"business_key,omitempty"`
	RequestID      string `json:"request_id,omitempty"`
	CorrelationID  string `json:"correlation_id,omitempty"`
	Payload        any    `json:"payload"`
}

// Repository 通过调用方显式传入的 GORM 事务句柄追加事件。
type Repository struct{}

// NewRepository 创建公共 Outbox Repository。
func NewRepository() *Repository {
	return &Repository{}
}

// Append 在业务事务中持久化稳定事件身份和必要快照。
func (r *Repository) Append(ctx context.Context, tx *gorm.DB, envelope Envelope) (*model.OutboxEvent, error) {
	return r.append(ctx, tx, envelope, false)
}

// AppendIdempotent 在稳定 EventID 重投时保持原事件，不重复创建事实。
func (r *Repository) AppendIdempotent(ctx context.Context, tx *gorm.DB, envelope Envelope) (*model.OutboxEvent, error) {
	return r.append(ctx, tx, envelope, true)
}

func (r *Repository) append(ctx context.Context, tx *gorm.DB, envelope Envelope, idempotent bool) (*model.OutboxEvent, error) {
	if tx == nil {
		return nil, stderrors.New("Outbox 追加必须传入 GORM 事务句柄")
	}
	if strings.TrimSpace(envelope.EventType) == "" || strings.TrimSpace(envelope.AggregateType) == "" ||
		strings.TrimSpace(envelope.AggregateID) == "" || strings.TrimSpace(envelope.ResourceType) == "" ||
		strings.TrimSpace(envelope.ResourceID) == "" {
		return nil, stderrors.New("Outbox 事件类型和资源定位不能为空")
	}
	if err := asynctask.ValidatePayload(envelope.Payload); err != nil {
		return nil, err
	}
	payload, err := sonic.Marshal(envelope.Payload)
	if err != nil {
		return nil, err
	}
	if envelope.EventID == "" {
		envelope.EventID = uuid.NewString()
	}
	if envelope.PayloadVersion <= 0 {
		envelope.PayloadVersion = 1
	}
	now := time.Now().UTC()
	event := &model.OutboxEvent{
		EventID: envelope.EventID, EventType: envelope.EventType, PayloadVersion: envelope.PayloadVersion,
		AggregateType: envelope.AggregateType, AggregateID: envelope.AggregateID,
		ResourceType: envelope.ResourceType, ResourceID: envelope.ResourceID, BusinessKey: envelope.BusinessKey,
		RequestID: envelope.RequestID, CorrelationID: envelope.CorrelationID, Payload: datatypes.JSON(payload),
		Status: constants.OutboxStatusPending, MaxRetries: constants.OutboxDefaultMaxRetries, NextAttemptAt: now,
	}
	create := tx.WithContext(ctx)
	if idempotent {
		create = create.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "event_id"}}, DoNothing: true})
	}
	if err := create.Create(event).Error; err != nil {
		return nil, err
	}
	return event, nil
}
