package auth

import (
	"context"
	"encoding/json"
	"errors"
	"strings"
	"time"

	"gorm.io/gorm"

	"wucher/internal/shared/pkg/uuidv7"
	"wucher/internal/queue"
)

const (
	EmailOutboxStatusPending    = "pending"
	EmailOutboxStatusProcessing = "processing"
	EmailOutboxStatusPublished  = "published"
	EmailOutboxStatusDead       = "dead"
)

type EmailOutboxMessage struct {
	ID              []byte     `gorm:"type:binary(16);primaryKey;column:id"`
	MessageID       string     `gorm:"type:varchar(64);uniqueIndex;not null;column:message_id"`
	Kind            string     `gorm:"type:varchar(64);index;not null;column:kind"`
	JobType         string     `gorm:"type:varchar(64);index;not null;column:job_type"`
	Body            []byte     `gorm:"type:longblob;not null;column:body"`
	Attributes      []byte     `gorm:"type:longblob;column:attributes"`
	MessageGroupID  string     `gorm:"type:varchar(128);column:message_group_id"`
	DeduplicationID string     `gorm:"type:varchar(128);column:deduplication_id"`
	Status          string     `gorm:"type:varchar(32);index;not null;column:status"`
	Attempts        int        `gorm:"not null;default:0;column:attempts"`
	AvailableAt     time.Time  `gorm:"index;not null;column:available_at"`
	LockedAt        *time.Time `gorm:"index;column:locked_at"`
	LockedBy        string     `gorm:"type:varchar(64);column:locked_by"`
	PublishedAt     *time.Time `gorm:"column:published_at"`
	LastError       string     `gorm:"type:text;column:last_error"`
	CorrelationID   string     `gorm:"type:varchar(64);index;column:correlation_id"`
	TraceID         string     `gorm:"type:varchar(64);index;column:trace_id"`
	CreatedAt       time.Time
	UpdatedAt       time.Time
}

func (EmailOutboxMessage) TableName() string { return "email_outbox_messages" }

func (m *EmailOutboxMessage) BeforeCreate(_ *gorm.DB) error {
	if len(m.ID) == 0 {
		m.ID = uuidv7.MustBytes()
	}
	if strings.TrimSpace(m.Status) == "" {
		m.Status = EmailOutboxStatusPending
	}
	if m.AvailableAt.IsZero() {
		m.AvailableAt = time.Now().UTC()
	}
	return nil
}

func NewEmailOutboxMessage(now time.Time, message *queue.OutboundMessage) (*EmailOutboxMessage, error) {
	if message == nil {
		return nil, errors.New("outbound message is required")
	}
	if strings.TrimSpace(message.ID) == "" {
		return nil, errors.New("outbound message id is required")
	}
	if len(message.Body) == 0 {
		return nil, errors.New("outbound message body is required")
	}

	attributes, err := json.Marshal(message.Attributes)
	if err != nil {
		return nil, err
	}
	if now.IsZero() {
		now = time.Now().UTC()
	}

	kind := strings.TrimSpace(message.Attributes["message_kind"])
	if kind == "" {
		kind = queue.EmailKind
	}
	jobType := strings.TrimSpace(message.Attributes["job_type"])
	if jobType == "" {
		jobType = "generic"
	}

	return &EmailOutboxMessage{
		MessageID:       strings.TrimSpace(message.ID),
		Kind:            kind,
		JobType:         jobType,
		Body:            append([]byte(nil), message.Body...),
		Attributes:      append([]byte(nil), attributes...),
		MessageGroupID:  strings.TrimSpace(message.MessageGroupID),
		DeduplicationID: strings.TrimSpace(message.DeduplicationID),
		Status:          EmailOutboxStatusPending,
		AvailableAt:     now.UTC(),
		CorrelationID:   strings.TrimSpace(message.Attributes["correlation_id"]),
		TraceID:         strings.TrimSpace(message.Attributes["trace_id"]),
	}, nil
}

func (m *EmailOutboxMessage) OutboundMessage() (*queue.OutboundMessage, error) {
	if m == nil {
		return nil, errors.New("email outbox message is required")
	}
	attributes := map[string]string{}
	if len(m.Attributes) > 0 {
		if err := json.Unmarshal(m.Attributes, &attributes); err != nil {
			return nil, err
		}
	}
	if strings.TrimSpace(m.MessageID) == "" {
		return nil, errors.New("message id is required")
	}
	if len(m.Body) == 0 {
		return nil, errors.New("message body is required")
	}
	return &queue.OutboundMessage{
		ID:              m.MessageID,
		Body:            append([]byte(nil), m.Body...),
		Attributes:      attributes,
		MessageGroupID:  strings.TrimSpace(m.MessageGroupID),
		DeduplicationID: strings.TrimSpace(m.DeduplicationID),
	}, nil
}

type EmailOutboxWriter interface {
	CreateEmailOutboxMessage(ctx context.Context, message *EmailOutboxMessage) error
}

type EmailOutboxRepository interface {
	EmailOutboxWriter
	ClaimPendingEmailOutboxMessages(ctx context.Context, limit int, now time.Time, lockTTL time.Duration, workerID string) ([]EmailOutboxMessage, error)
	MarkEmailOutboxMessagePublished(ctx context.Context, id []byte, publishedAt time.Time) error
	MarkEmailOutboxMessageRetry(ctx context.Context, id []byte, availableAt time.Time, lastErr string) error
	MarkEmailOutboxMessageDead(ctx context.Context, id []byte, failedAt time.Time, lastErr string) error
	CountPendingEmailOutboxMessages(ctx context.Context, now time.Time) (int64, error)
}

type TransactionalRepository interface {
	WithTransaction(ctx context.Context, fn func(repo Repository, outbox EmailOutboxWriter) error) error
}
