package workspace

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

	"golang.org/x/sync/singleflight"

	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
)

const (
	// defaultBranchDiffCacheCapacity bounds the process-wide commit-pair LRU
	// for the shared branch-diff projection (changed files + line stats). A
	// single review session touches one or two commit pairs, so a modest cap
	// yields ~100% hit rate; the cap exists to bound memory when a daemon
	// wanders across many repos or branches, not to tune hit rate.
	defaultBranchDiffCacheCapacity = 64
)

// branchDiffKey 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, so a cached entry is
// correct until evicted by LRU pressure (no TTL / invalidation needed).
type branchDiffKey struct {
	baseCommit string
	headCommit string
}

// branchDiffEntry is the cached projection of a branch diff between an
// immutable commit pair: the per-file change list (with per-file tokens) and
// aggregate line stats. It holds only plain derived structs — never go-git
// object graphs (those reference the repo storer and are unsafe to cache
// across repo re-opens). The per-file diff TEXT (blob content) is NOT cached
// here; it is produced on demand via direct blob lookup.
type branchDiffEntry struct {
	files      []types.ReviewFile
	lineStats  reviewLineStats
	baseCommit string
	headCommit string
}

// branchDiffCache is a process-wide LRU keyed by resolved commit pair. A miss
// or eviction simply recomputes on the next call — it never affects
// correctness. The expensive work (one DiffTree + one line-stats patch pass)
// happens at most once per commit pair.
type branchDiffCache struct {
	cap   int
	mu    sync.Mutex
	items map[branchDiffKey]*list.Element
	order *list.List

	flights singleflight.Group
}

type branchDiffCacheEntry struct {
	key   branchDiffKey
	value branchDiffEntry
}

var defaultBranchDiffCache = newBranchDiffCache(defaultBranchDiffCacheCapacity)

func newBranchDiffCache(capacity int) *branchDiffCache {
	if capacity <= 0 {
		capacity = defaultBranchDiffCacheCapacity
	}
	return &branchDiffCache{
		cap:   capacity,
		items: make(map[branchDiffKey]*list.Element),
		order: list.New(),
	}
}

func (c *branchDiffCache) lookup(key branchDiffKey) (branchDiffEntry, bool) {
	if c == nil {
		return branchDiffEntry{}, false
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	el, ok := c.items[key]
	if !ok {
		return branchDiffEntry{}, false
	}
	c.order.MoveToFront(el)
	return el.Value.(*branchDiffCacheEntry).value, true
}

func (c *branchDiffCache) store(key branchDiffKey, value branchDiffEntry) {
	if c == nil {
		return
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if el, ok := c.items[key]; ok {
		el.Value.(*branchDiffCacheEntry).value = value
		c.order.MoveToFront(el)
		return
	}
	entry := &branchDiffCacheEntry{key: key, value: value}
	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.(*branchDiffCacheEntry)
		c.order.Remove(oldest)
		delete(c.items, evicted.key)
	}
}

func (c *branchDiffCache) len() int {
	if c == nil {
		return 0
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.order.Len()
}

// getOrCreate returns the cached diff projection 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 + one line-stats pass.
func (c *branchDiffCache) getOrCreate(key branchDiffKey, load func() (branchDiffEntry, error)) (branchDiffEntry, error) {
	if c == nil {
		return load()
	}
	if value, ok := c.lookup(key); ok {
		return value, nil
	}
	value, err, _ := c.flights.Do(flightKeyForBranchDiff(key), func() (any, error) {
		if value, ok := c.lookup(key); ok {
			return value, nil
		}
		entry, err := load()
		if err != nil {
			return branchDiffEntry{}, err
		}
		c.store(key, entry)
		return entry, nil
	})
	if err != nil {
		return branchDiffEntry{}, err
	}
	return value.(branchDiffEntry), nil
}

func flightKeyForBranchDiff(key branchDiffKey) string {
	var buf bytes.Buffer
	writeTokenString(&buf, branchDiffFlightKeyDomainV1)
	writeTokenString(&buf, key.baseCommit)
	writeTokenString(&buf, key.headCommit)
	return buf.String()
}

// loadBranchDiff resolves the review branch commit pair and returns the cached
// branch-diff projection (changed files + line stats). On a cache miss it runs
// one DiffTree + one line-stats pass; every review consumer of the branch diff
// (snapshot, files, fingerprint) shares this single computation per commit pair.
func loadBranchDiff(repo reviewRepoState) (branchDiffEntry, error) {
	baseCommit, headCommit, err := resolveReviewBranchCommitPair(repo)
	if err != nil {
		return branchDiffEntry{}, err
	}
	return getBranchDiffAtCommits(repo, baseCommit, headCommit)
}

// getBranchDiffAtCommits returns the cached branch-diff projection for an
// already-resolved, immutable commit pair. Callers that must interpose between
// resolving the pair and loading the diff (notably the fingerprint TOCTOU test
// hook) resolve the pair themselves and call this entry point so the
// resolve→load gap stays observable while still sharing the cache.
func getBranchDiffAtCommits(repo reviewRepoState, baseCommit, headCommit string) (branchDiffEntry, error) {
	return defaultBranchDiffCache.getOrCreate(branchDiffKey{baseCommit: baseCommit, headCommit: headCommit}, func() (branchDiffEntry, error) {
		branchChanges, err := loadReviewBranchChangesAtCommits(repo, baseCommit, headCommit)
		if err != nil {
			return branchDiffEntry{}, err
		}
		files := changesToReviewFiles(branchChanges.changes)
		lineStats, err := reviewBranchLineStatsFromChanges(branchChanges)
		if err != nil {
			return branchDiffEntry{}, err
		}
		return branchDiffEntry{
			files:      files,
			lineStats:  lineStats,
			baseCommit: baseCommit,
			headCommit: headCommit,
		}, nil
	})
}
