package engine

import (
	"context"
	"log/slog"
	"time"

	"tangled.org/core/spindle/db"
	"tangled.org/core/spindle/storage"
)

const (
	cachePruneBatch    = 100
	cachePendingMaxAge = time.Hour
)

func StartCachePruner(ctx context.Context, l *slog.Logger, index *db.DB, store storage.Storage, retention, interval time.Duration) {
	if store == nil || interval <= 0 {
		return
	}
	go func() {
		prune := func() {
			total := 0
			for {
				n, err := PruneCaches(ctx, index, store, time.Now(), retention, cachePendingMaxAge, cachePruneBatch)
				total += n
				if err != nil {
					l.Warn("cache prune failed", "count", total, "err", err)
					return
				}
				if n < cachePruneBatch {
					if total > 0 {
						l.Info("pruned cache entries", "count", total)
					}
					return
				}
			}
		}
		prune()
		ticker := time.NewTicker(interval)
		defer ticker.Stop()
		for {
			select {
			case <-ctx.Done():
				return
			case <-ticker.C:
				prune()
			}
		}
	}()
}

func PruneCaches(ctx context.Context, index *db.DB, store storage.Storage, now time.Time, retention, pendingMaxAge time.Duration, limit int) (int, error) {
	readyBefore := time.Unix(0, 0)
	if retention > 0 {
		readyBefore = now.Add(-retention)
	}
	entries, err := index.ExpiredCacheEntries(ctx, readyBefore, now.Add(-pendingMaxAge), limit)
	if err != nil {
		return 0, err
	}
	pruned := 0
	for _, entry := range entries {
		if entry.State != "deleting" {
			claimed, err := index.ClaimCacheEntry(ctx, entry.ID, entry.State, entry.LastUsedAt)
			if err != nil {
				return pruned, err
			}
			if !claimed {
				continue
			}
		}
		if err := store.Delete(ctx, entry.StorageKey); err != nil {
			if entry.State != "deleting" {
				_ = index.RestoreCacheEntryState(context.WithoutCancel(ctx), entry.ID, entry.State)
			}
			return pruned, err
		}
		if err := index.DeleteCacheEntry(ctx, entry.ID); err != nil {
			return pruned, err
		}
		pruned++
	}
	return pruned, nil
}
