package apicontract

import (
	"sync"
	"time"
)

// IdempotencyStore makes a non-idempotent operation safely retryable: a
// client supplies an idempotency key; the first call executes and the
// result is cached; retries with the same key return the cached result
// WITHOUT re-executing. This is how "create order" / "charge card" APIs
// survive client retries (gw-06) and at-least-once delivery (pa-03/05)
// without double-applying.
type IdempotencyStore struct {
	mu    sync.Mutex
	m     map[string]record
	ttl   time.Duration
	nowFn func() time.Time
}

type record struct {
	result  any
	expires time.Time
}

func NewIdempotencyStore(ttl time.Duration) *IdempotencyStore {
	return &IdempotencyStore{m: map[string]record{}, ttl: ttl, nowFn: time.Now}
}

// Do runs fn at most once per key (within the TTL). Returns the result,
// whether it was served from cache, and any error. A failed fn is NOT
// cached, so the client can legitimately retry it.
func (s *IdempotencyStore) Do(key string, fn func() (any, error)) (result any, cached bool, err error) {
	s.mu.Lock()
	if r, ok := s.m[key]; ok && s.nowFn().Before(r.expires) {
		s.mu.Unlock()
		return r.result, true, nil
	}
	s.mu.Unlock()

	res, err := fn()
	if err != nil {
		return nil, false, err // don't cache failures
	}
	s.mu.Lock()
	s.m[key] = record{result: res, expires: s.nowFn().Add(s.ttl)}
	s.mu.Unlock()
	return res, false, nil
}
