package content

import (
	"context"
	"fmt"
	"strings"
	"time"
	"unicode/utf8"

	"gorm.io/gorm"
)

const (
	MaxOutboxLeaseBatch  = 100
	MaxOutboxAttempts    = 10
	maxDeliveryLastError = 4096
)

// LeaseDueOutbox claims at most one bounded batch. Competing workers are
// fenced by the row's current status and expired lease condition.
func (s *Store) LeaseDueOutbox(ctx context.Context, now time.Time, leaseOwner string, leaseUntil time.Time, limit int) ([]Outbox, error) {
	if err := s.ensureDB(); err != nil {
		return nil, err
	}
	leaseOwner = strings.TrimSpace(leaseOwner)
	if leaseOwner == "" || limit <= 0 || limit > MaxOutboxLeaseBatch || !leaseUntil.After(now) {
		return nil, fmt.Errorf("content outbox lease request is invalid")
	}
	var leased []Outbox
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var candidates []Outbox
		if err := tx.Select("id", "workspace_id", "content_id", "version_id", "event_type", "attempts").
			Where("status = ? AND next_attempt_at <= ? AND (lease_until IS NULL OR lease_until <= ?)", OutboxStatusPending, now, now).
			Order("next_attempt_at ASC, id ASC").Limit(limit).Find(&candidates).Error; err != nil {
			return err
		}
		for _, candidate := range candidates {
			result := tx.Model(&Outbox{}).
				Where("id = ? AND status = ? AND next_attempt_at <= ? AND (lease_until IS NULL OR lease_until <= ?)", candidate.ID, OutboxStatusPending, now, now).
				Updates(map[string]any{"lease_owner": leaseOwner, "lease_until": leaseUntil, "updated_at": now})
			if result.Error != nil {
				return result.Error
			}
			if result.RowsAffected == 1 {
				candidate.LeaseOwner = leaseOwner
				candidate.LeaseUntil = &leaseUntil
				candidate.UpdatedAt = now
				leased = append(leased, candidate)
			}
		}
		return nil
	})
	if err != nil {
		return nil, fmt.Errorf("lease content outbox: %w", err)
	}
	return leased, nil
}

func (s *Store) MarkOutboxDelivered(ctx context.Context, row Outbox, leaseOwner string, now time.Time) (bool, error) {
	if err := s.ensureDB(); err != nil {
		return false, err
	}
	if row.LeaseUntil == nil {
		return false, nil
	}
	result := s.db.WithContext(ctx).Model(&Outbox{}).
		Where("id = ? AND status = ? AND lease_owner = ? AND lease_until = ?", strings.TrimSpace(row.ID), OutboxStatusPending, strings.TrimSpace(leaseOwner), *row.LeaseUntil).
		Updates(map[string]any{"status": OutboxStatusDelivered, "lease_owner": "", "lease_until": nil, "last_error": "", "updated_at": now})
	if result.Error != nil {
		return false, fmt.Errorf("mark content outbox delivered: %w", result.Error)
	}
	return result.RowsAffected == 1, nil
}

// MarkOutboxAttemptFailed releases one owned lease and schedules bounded
// exponential retry, or terminally fails the row after MaxOutboxAttempts.
func (s *Store) MarkOutboxAttemptFailed(ctx context.Context, row Outbox, leaseOwner string, failure error, now time.Time) (bool, error) {
	if err := s.ensureDB(); err != nil {
		return false, err
	}
	attempts := row.Attempts + 1
	status := OutboxStatusPending
	nextAttempt := now.Add(deliveryRetryBackoff(attempts))
	if attempts >= MaxOutboxAttempts {
		status = OutboxStatusFailed
		nextAttempt = now
	}
	lastError := ""
	if failure != nil {
		lastError = truncateDeliveryError(failure.Error())
	}
	if row.LeaseUntil == nil {
		return false, nil
	}
	result := s.db.WithContext(ctx).Model(&Outbox{}).
		Where("id = ? AND status = ? AND lease_owner = ? AND lease_until = ?", strings.TrimSpace(row.ID), OutboxStatusPending, strings.TrimSpace(leaseOwner), *row.LeaseUntil).
		Updates(map[string]any{
			"status": status, "attempts": attempts, "next_attempt_at": nextAttempt,
			"lease_owner": "", "lease_until": nil, "last_error": lastError, "updated_at": now,
		})
	if result.Error != nil {
		return false, fmt.Errorf("mark content outbox attempt failed: %w", result.Error)
	}
	return result.RowsAffected == 1, nil
}

func truncateDeliveryError(value string) string {
	value = strings.TrimSpace(value)
	if len(value) <= maxDeliveryLastError {
		return value
	}
	end := 0
	for end < len(value) {
		_, size := utf8.DecodeRuneInString(value[end:])
		if end+size > maxDeliveryLastError {
			break
		}
		end += size
	}
	return value[:end]
}

func deliveryRetryBackoff(attempt int) time.Duration {
	if attempt < 1 {
		attempt = 1
	}
	if attempt > 6 {
		attempt = 6
	}
	return time.Second * time.Duration(1<<(attempt-1))
}
