package memorysearch

import (
	"context"
	"fmt"
	"sync/atomic"

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

// BuildLimiter gates the memorysearch build path against a daemon-wide
// concurrency budget. A Manager owns one instance and injects it into
// every Reconciler it constructs; the Reconciler calls Acquire immediately
// before Build and Release on every exit path (success, build error, ctx
// cancel).
//
// Contract:
//   - Acquire returns a non-nil error if the context is cancelled before
//     a slot is available. In that case Release MUST NOT be called.
//   - After a successful Acquire, Release MUST be called exactly once.
//
// The interface is intentionally narrow so tests can inject a fake that
// observes in-flight count and blocks at deterministic points. The
// semaphore-backed production implementation never exposes its
// *semaphore.Weighted pointer through this interface.
type BuildLimiter interface {
	Acquire(ctx context.Context) error
	Release()
}

// NewBuildLimiter returns a BuildLimiter backed by a single shared
// semaphore with the given concurrency. concurrency is clamped to a
// positive value; Manager construction enforces the upper bound.
func NewBuildLimiter(concurrency int) BuildLimiter {
	if concurrency < 1 {
		concurrency = 1
	}
	return &semaphoreLimiter{sem: semaphore.NewWeighted(int64(concurrency))}
}

type semaphoreLimiter struct {
	sem *semaphore.Weighted
}

func (l *semaphoreLimiter) Acquire(ctx context.Context) error {
	if err := l.sem.Acquire(ctx, 1); err != nil {
		return fmt.Errorf("memory search: build limiter: %w", err)
	}
	return nil
}

func (l *semaphoreLimiter) Release() {
	l.sem.Release(1)
}

// countingLimiter is a test fake that wraps another BuildLimiter and
// exposes the current and peak in-flight counts via atomics. Tests use
// it to assert the reconciler never exceeds the configured concurrency
// and to observe exact acquire/release ordering without coupling to the
// semaphore implementation.
type countingLimiter struct {
	inner    BuildLimiter
	inflight atomic.Int64
	peak     atomic.Int64
}

func newCountingLimiter(inner BuildLimiter) *countingLimiter {
	return &countingLimiter{inner: inner}
}

func (c *countingLimiter) Acquire(ctx context.Context) error {
	if err := c.inner.Acquire(ctx); err != nil {
		return err
	}
	now := c.inflight.Add(1)
	for {
		old := c.peak.Load()
		if now <= old {
			break
		}
		if c.peak.CompareAndSwap(old, now) {
			break
		}
	}
	return nil
}

func (c *countingLimiter) Release() {
	c.inflight.Add(-1)
	c.inner.Release()
}

func (c *countingLimiter) Inflight() int64 { return c.inflight.Load() }
func (c *countingLimiter) Peak() int64     { return c.peak.Load() }
