// Package autonumber implements metadata-driven identifier assignment for NexusCRM.
package autonumber

import (
	"sync"
	"time"

	"golang.org/x/sync/semaphore"
)

// semaphoreCache is a per-counter semaphore registry keyed by
// (fieldID, scopeKey). Semantics: size-1 `*semaphore.Weighted` per key
// (mutex semantics), shared across goroutines.
//
// V2-C3 Round-2 review — eviction policy: NEVER-EVICT.
//
// The previous LRU+TTL design could evict an entry whose semaphore
// was currently held by a goroutine in the fallback path. The next
// caller for the same key would allocate a brand-new semaphore and
// enter the fallback in parallel with the prior holder — defeating
// the size-1 serialization that the fallback relies on to keep the
// FOR UPDATE lane correct under TiDB optimistic-mode regressions.
//
// Memory argument: one entry ≈ 96B (string key + list element +
// weighted pointer + timestamps). At the PRD's upper bound of
// 132 fields × 2 scope classes × 2 UTC-day rollover = ≤ 528 entries ≈
// ~50 KB steady-state — not a memory problem. If scope=per_day is
// re-activated with ∞ day buckets, the entry count grows O(days);
// at that point a time-windowed GC that ONLY evicts entries with
// `sem.TryAcquire(1) == true` can be added (currently out of scope).
type semaphoreCache struct {
	// entries holds *semaphore.Weighted values keyed by
	// semaphoreCacheKey(fieldID, scopeKey).
	entries sync.Map
	// nowFunc is retained for API compatibility with older callers
	// that passed a clock; no eviction → field unused but harmless.
	nowFunc func() time.Time
}

// newSemaphoreCache constructs the cache. `fieldCount` and `nowFunc`
// are retained for call-site compatibility — the never-evict policy
// does not use them (fieldCount previously sized an LRU cap;
// nowFunc previously drove TTL).
func newSemaphoreCache(_ int, nowFunc func() time.Time) *semaphoreCache {
	if nowFunc == nil {
		nowFunc = time.Now
	}
	return &semaphoreCache{nowFunc: nowFunc}
}

// Get returns (possibly after allocation) the per-counter semaphore
// for the given (fieldID, scopeKey) tuple. Same key → same semaphore
// instance for the lifetime of the process.
func (c *semaphoreCache) Get(fieldID, scopeKey string) *semaphore.Weighted {
	key := semaphoreCacheKey(fieldID, scopeKey)
	// Fast path — load without allocating.
	if v, ok := c.entries.Load(key); ok {
		return v.(*semaphore.Weighted)
	}
	// Slow path — allocate, attempt store. A concurrent winner may
	// race ahead; LoadOrStore returns the winner's semaphore and
	// discards ours (gc'd immediately).
	sem := semaphore.NewWeighted(1)
	actual, _ := c.entries.LoadOrStore(key, sem)
	return actual.(*semaphore.Weighted)
}

// Len returns the current cache size. O(n) via sync.Map.Range — used
// only by tests.
func (c *semaphoreCache) Len() int {
	n := 0
	c.entries.Range(func(_, _ any) bool {
		n++
		return true
	})
	return n
}

// semaphoreCacheKey produces the key used by the cache. The NUL byte
// separator matches PRD §5.2 `counterKey = f.ID + "\x00" + scopeKey`.
func semaphoreCacheKey(fieldID, scopeKey string) string {
	// NUL byte guarantees fieldID/scopeKey disambiguation without
	// allocating intermediate formatters.
	return fieldID + "\x00" + scopeKey
}
