// Package opstate provides a mutex-guarded registry for long-running
// operations (e.g. async VM create, async image build). A registry stores
// operations by ID and can prune completed ones after a retention window.
package opstate

import (
	"sync"
	"time"
)

// AsyncOp is the protocol each operation type must satisfy. Implementations
// own their own concurrency for the returned values — the registry treats
// them as opaque.
type AsyncOp interface {
	ID() string
	IsDone() bool
	UpdatedAt() time.Time
	Cancel()
}

// Registry is a mutex-guarded map of in-flight operations keyed by op ID.
// One registry per operation kind; each owns its own lock.
type Registry[T AsyncOp] struct {
	mu   sync.Mutex
	byID map[string]T
}

// Insert adds op keyed by its ID.
func (r *Registry[T]) Insert(op T) {
	r.mu.Lock()
	defer r.mu.Unlock()
	if r.byID == nil {
		r.byID = map[string]T{}
	}
	r.byID[op.ID()] = op
}

// Get returns the operation with the given ID, if present.
func (r *Registry[T]) Get(id string) (T, bool) {
	r.mu.Lock()
	defer r.mu.Unlock()
	op, ok := r.byID[id]
	return op, ok
}

// List returns a snapshot of every operation currently in the
// registry — both pending and (un-pruned) completed. Callers filter
// by IsDone() if they care about state. The slice is freshly
// allocated; mutating it doesn't affect the registry.
//
// Used by `banger update`'s preflight to detect in-flight operations
// before swapping binaries.
func (r *Registry[T]) List() []T {
	r.mu.Lock()
	defer r.mu.Unlock()
	out := make([]T, 0, len(r.byID))
	for _, op := range r.byID {
		out = append(out, op)
	}
	return out
}

// Prune drops completed operations last updated before the cutoff.
func (r *Registry[T]) Prune(before time.Time) {
	r.mu.Lock()
	defer r.mu.Unlock()
	for id, op := range r.byID {
		if !op.IsDone() {
			continue
		}
		if op.UpdatedAt().Before(before) {
			delete(r.byID, id)
		}
	}
}
