package workspace

import (
	"bytes"
	"container/list"
	"sync"

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

const (
	// defaultBranchSignatureCacheCapacity bounds the process-wide commit-pair
	// LRU. A single review session typically touches one or two commit pairs,
	// so a small cap is sufficient; the cap exists to prevent unbounded growth
	// when an agent wanders across many repos or branches.
	defaultBranchSignatureCacheCapacity = 64
)

// branchSignatureKey identifies a branch diff by its resolved commit pair.
// Both fields are full git commit hashes — never symbolic refs — so equal
// keys guarantee equal diff content. Commits are immutable, which means a
// cached entry is correct until evicted by LRU pressure.
type branchSignatureKey struct {
	baseCommit string
	headCommit string
}

// branchSignature is the cached projection of a branch diff: an opaque token
// (authoritative for content equality), a file count, and the FULL resolved
// commit pair. Both commits are exposed alongside the token so the fingerprint
// response (HeadCommit) and the repository identity token (reviewRepositoryToken,
// which consumes baseCommit) can share the SAME resolved pair as BranchToken
// instead of re-deriving it from repo.headCommit / repo.mergeBase, which can
// move independently of baseRef/headRef in direct mode. Without this, a
// concurrent ref move between loadReviewRepoState and computeBranchSignature
// would produce a self-inconsistent fingerprint: HeadCommit from the old
// resolve, BranchToken from the new.
type branchSignature struct {
	token      string
	fileCount  int
	baseCommit string
	headCommit string
}

// branchSignatureCache is a process-wide LRU keyed by resolved commit pair.
// It only avoids repeated DiffTree + token derivation work — it never affects
// correctness, so a miss or eviction simply recomputes on the next call.
type branchSignatureCache struct {
	cap   int
	mu    sync.Mutex
	items map[branchSignatureKey]*list.Element
	order *list.List

	flights singleflight.Group
}

type branchSignatureEntry struct {
	key branchSignatureKey
	sig branchSignature
}

var defaultBranchSignatureCache = newBranchSignatureCache(defaultBranchSignatureCacheCapacity)

func newBranchSignatureCache(capacity int) *branchSignatureCache {
	if capacity <= 0 {
		capacity = defaultBranchSignatureCacheCapacity
	}
	return &branchSignatureCache{
		cap:   capacity,
		items: make(map[branchSignatureKey]*list.Element),
		order: list.New(),
	}
}

// lookup returns the cached signature for key, marking it most-recently-used.
// The ok flag is false when the cache is nil or the key is absent.
func (c *branchSignatureCache) lookup(key branchSignatureKey) (branchSignature, bool) {
	if c == nil {
		return branchSignature{}, false
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	el, ok := c.items[key]
	if !ok {
		return branchSignature{}, false
	}
	c.order.MoveToFront(el)
	return el.Value.(*branchSignatureEntry).sig, true
}

// store caches sig for key, evicting the least-recently-used entry when the
// cache is at capacity. A no-op when c is nil.
func (c *branchSignatureCache) store(key branchSignatureKey, sig branchSignature) {
	if c == nil {
		return
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if el, ok := c.items[key]; ok {
		el.Value.(*branchSignatureEntry).sig = sig
		c.order.MoveToFront(el)
		return
	}
	entry := &branchSignatureEntry{key: key, sig: sig}
	el := c.order.PushFront(entry)
	c.items[key] = el
	for c.order.Len() > c.cap {
		oldest := c.order.Back()
		if oldest == nil {
			break
		}
		evicted := oldest.Value.(*branchSignatureEntry)
		c.order.Remove(oldest)
		delete(c.items, evicted.key)
	}
}

// len returns the current entry count. Test-only.
func (c *branchSignatureCache) len() int {
	if c == nil {
		return 0
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.order.Len()
}

// getOrCreate returns the cached signature for key, or computes it via load
// when absent. Concurrent in-flight loads for the same key share a single
// load call via singleflight, so a burst of cold misses collapses to one
// DiffTree pass.
func (c *branchSignatureCache) getOrCreate(key branchSignatureKey, load func() (branchSignature, error)) (branchSignature, error) {
	if c == nil {
		return load()
	}
	if sig, ok := c.lookup(key); ok {
		return sig, nil
	}
	value, err, _ := c.flights.Do(flightKeyForBranchSignature(key), func() (any, error) {
		// Re-check after acquiring the flight — another caller may have just
		// stored a fresh entry.
		if sig, ok := c.lookup(key); ok {
			return sig, nil
		}
		sig, err := load()
		if err != nil {
			return branchSignature{}, err
		}
		c.store(key, sig)
		return sig, nil
	})
	if err != nil {
		return branchSignature{}, err
	}
	return value.(branchSignature), nil
}

// flightKeyForBranchSignature builds the singleflight key for a branch
// signature lookup. Production commit hashes are always hex ([0-9a-f]{40,64}),
// so a NUL delimiter would be safe today — but the encoder discipline used
// everywhere else in this package (length-prefix + domain tag) makes the
// boundary unambiguous for ANY input shape, including the synthetic strings
// tests throw at the cache. Reusing writeTokenString here keeps the flight
// key in the same shape discipline as the tokens themselves and removes the
// implicit "hashes never contain NUL" assumption.
func flightKeyForBranchSignature(key branchSignatureKey) string {
	var buf bytes.Buffer
	writeTokenString(&buf, branchSignatureFlightKeyDomainV1)
	writeTokenString(&buf, key.baseCommit)
	writeTokenString(&buf, key.headCommit)
	return buf.String()
}
