package mysql

import (
	"context"
	"strings"
	"time"

	"gorm.io/gorm"
	"gorm.io/gorm/clause"

	"wucher/internal/domain/auth"
)

func (r *AuthRepository) WithTransaction(ctx context.Context, fn func(repo auth.Repository, outbox auth.EmailOutboxWriter) error) error {
	if fn == nil {
		return nil
	}
	return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		txRepo := &AuthRepository{db: tx}
		return fn(txRepo, txRepo)
	})
}

func (r *AuthRepository) CreateEmailOutboxMessage(ctx context.Context, message *auth.EmailOutboxMessage) error {
	if message == nil {
		return gorm.ErrInvalidData
	}
	return r.db.WithContext(ctx).Create(message).Error
}

func (r *AuthRepository) ClaimPendingEmailOutboxMessages(
	ctx context.Context,
	limit int,
	now time.Time,
	lockTTL time.Duration,
	workerID string,
) ([]auth.EmailOutboxMessage, error) {
	if limit <= 0 {
		limit = 1
	}
	if lockTTL <= 0 {
		lockTTL = time.Minute
	}
	now = now.UTC()
	staleBefore := now.Add(-lockTTL)
	workerID = strings.TrimSpace(workerID)
	if workerID == "" {
		workerID = "worker"
	}

	var messages []auth.EmailOutboxMessage
	err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		if err := tx.
			Clauses(clause.Locking{Strength: "UPDATE", Options: "SKIP LOCKED"}).
			Where(
				"((status = ? AND available_at <= ?) OR (status = ? AND locked_at IS NOT NULL AND locked_at < ?))",
				auth.EmailOutboxStatusPending,
				now,
				auth.EmailOutboxStatusProcessing,
				staleBefore,
			).
			Order("available_at ASC, created_at ASC").
			Limit(limit).
			Find(&messages).Error; err != nil {
			return err
		}
		if len(messages) == 0 {
			return nil
		}

		ids := make([][]byte, 0, len(messages))
		for i := range messages {
			ids = append(ids, messages[i].ID)
		}

		if err := tx.Model(&auth.EmailOutboxMessage{}).
			Where("id IN ?", ids).
			Updates(map[string]any{
				"status":     auth.EmailOutboxStatusProcessing,
				"locked_at":  now,
				"locked_by":  workerID,
				"updated_at": now,
				"attempts":   gorm.Expr("attempts + ?", 1),
			}).Error; err != nil {
			return err
		}

		for i := range messages {
			messages[i].Status = auth.EmailOutboxStatusProcessing
			messages[i].Attempts++
			ts := now
			messages[i].LockedAt = &ts
			messages[i].LockedBy = workerID
			messages[i].UpdatedAt = now
		}
		return nil
	})
	return messages, err
}

func (r *AuthRepository) MarkEmailOutboxMessagePublished(ctx context.Context, id []byte, publishedAt time.Time) error {
	publishedAt = publishedAt.UTC()
	return r.db.WithContext(ctx).
		Model(&auth.EmailOutboxMessage{}).
		Where("id = ?", id).
		Updates(map[string]any{
			"status":       auth.EmailOutboxStatusPublished,
			"published_at": publishedAt,
			"locked_at":    nil,
			"locked_by":    "",
			"last_error":   "",
			"updated_at":   publishedAt,
		}).Error
}

func (r *AuthRepository) MarkEmailOutboxMessageRetry(ctx context.Context, id []byte, availableAt time.Time, lastErr string) error {
	availableAt = availableAt.UTC()
	return r.db.WithContext(ctx).
		Model(&auth.EmailOutboxMessage{}).
		Where("id = ?", id).
		Updates(map[string]any{
			"status":       auth.EmailOutboxStatusPending,
			"available_at": availableAt,
			"locked_at":    nil,
			"locked_by":    "",
			"last_error":   truncateOutboxError(strings.TrimSpace(lastErr), 4096),
			"updated_at":   time.Now().UTC(),
		}).Error
}

func (r *AuthRepository) MarkEmailOutboxMessageDead(ctx context.Context, id []byte, failedAt time.Time, lastErr string) error {
	failedAt = failedAt.UTC()
	return r.db.WithContext(ctx).
		Model(&auth.EmailOutboxMessage{}).
		Where("id = ?", id).
		Updates(map[string]any{
			"status":       auth.EmailOutboxStatusDead,
			"available_at": failedAt,
			"locked_at":    nil,
			"locked_by":    "",
			"last_error":   truncateOutboxError(strings.TrimSpace(lastErr), 4096),
			"updated_at":   failedAt,
		}).Error
}

func (r *AuthRepository) CountPendingEmailOutboxMessages(ctx context.Context, now time.Time) (int64, error) {
	var count int64
	err := r.db.WithContext(ctx).
		Model(&auth.EmailOutboxMessage{}).
		Where(
			"(status = ? AND available_at <= ?) OR status = ?",
			auth.EmailOutboxStatusPending,
			now.UTC(),
			auth.EmailOutboxStatusProcessing,
		).
		Count(&count).Error
	return count, err
}

func truncateOutboxError(v string, max int) string {
	if max <= 0 || len(v) <= max {
		return v
	}
	return v[:max]
}
