package mysql

import (
	"context"
	"strings"
	"time"

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

	filemanager "wucher/internal/domain/file_manager"
)

func (r *FileManagerFileRepository) WithTransaction(
	ctx context.Context,
	fn func(repo filemanager.FileRepository, outbox filemanager.FileProcessingOutboxWriter) error,
) error {
	if fn == nil {
		return nil
	}
	return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		txRepo := &FileManagerFileRepository{db: tx}
		return fn(txRepo, txRepo)
	})
}

func (r *FileManagerFileRepository) CreateFileProcessingOutboxMessage(ctx context.Context, message *filemanager.FileProcessingOutboxMessage) error {
	if message == nil {
		return gorm.ErrInvalidData
	}
	return r.db.WithContext(ctx).Create(message).Error
}

func (r *FileManagerFileRepository) ClaimPendingFileProcessingOutboxMessages(
	ctx context.Context,
	limit int,
	now time.Time,
	lockTTL time.Duration,
	workerID string,
) ([]filemanager.FileProcessingOutboxMessage, 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 []filemanager.FileProcessingOutboxMessage
	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 < ?))",
				filemanager.FileProcessingOutboxStatusPending,
				now,
				filemanager.FileProcessingOutboxStatusProcessing,
				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(&filemanager.FileProcessingOutboxMessage{}).
			Where("id IN ?", ids).
			Updates(map[string]any{
				"status":     filemanager.FileProcessingOutboxStatusProcessing,
				"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 = filemanager.FileProcessingOutboxStatusProcessing
			messages[i].Attempts++
			ts := now
			messages[i].LockedAt = &ts
			messages[i].LockedBy = workerID
			messages[i].UpdatedAt = now
		}
		return nil
	})
	return messages, err
}

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

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

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

func (r *FileManagerFileRepository) CountPendingFileProcessingOutboxMessages(ctx context.Context, now time.Time) (int64, error) {
	var count int64
	err := r.db.WithContext(ctx).
		Model(&filemanager.FileProcessingOutboxMessage{}).
		Where(
			"(status = ? AND available_at <= ?) OR status = ?",
			filemanager.FileProcessingOutboxStatusPending,
			now.UTC(),
			filemanager.FileProcessingOutboxStatusProcessing,
		).
		Count(&count).Error
	return count, err
}
