package winfsp

import (
	"strings"
	"sync"
	"time"

	"github.com/seaweedfs/go-fuse/v2/fuse"
)

// pathCache stands in for the dentry and attribute caches the kernel provides
// on the unix mounts. WinFsp addresses every operation by path, so without it
// each operation walks the whole path through Lookup again, and in a directory
// the filer has not listed yet every one of those lookups is a filer round
// trip.
//
// The cache owns one lookup reference per entry, the way the kernel holds one
// until it sends FORGET. An evicted reference sits out one sweep in the
// graveyard before it is returned, so an operation that resolved just before
// the eviction is not left holding a reclaimed inode.
type pathCache struct {
	ttl    time.Duration
	forget func(inode uint64)

	mu      sync.Mutex
	entries map[string]*pathCacheEntry
	// Two graveyard generations: an appended reference always survives the
	// sweep of the call that appended it, so a caller still using the inode -
	// a walk mid-resolution, an operation that looked it up just before - is
	// never holding a reference the same call just returned.
	graveyard     []uint64
	prevGraveyard []uint64
	lastSweep     time.Time
	// gen counts purges. A resolve holds no lock across its Lookup RPC, so a
	// purge can run between the RPC and the insert; the insert then carries
	// exactly the name the purge removed and has to be discarded. The recent
	// purges are kept by key so only a purge that covers the inserted name
	// discards it: unrelated churn must not starve resolveAndSteal into EIO.
	gen          uint64
	recentPurges []purgeRecord
}

type purgeRecord struct {
	key    string
	prefix bool
}

// maxRecentPurges bounds the purges remembered for the covers check. A resolve
// older than the window is discarded without one, which only costs a retry.
const maxRecentPurges = 128

func (r purgeRecord) covers(key string) bool {
	if r.key == key {
		return true
	}
	if !r.prefix {
		return false
	}
	// An empty key with prefix is the whole cache: purge clears every entry
	// for it, so it has to cover every in-flight insert too.
	return r.key == "" || strings.HasPrefix(key, r.key+"/")
}

// purgedSince reports whether any purge after gen covers key.
func (c *pathCache) purgedSince(gen uint64, key string) bool {
	dropped := c.gen - gen
	if dropped == 0 {
		return false
	}
	if dropped > uint64(len(c.recentPurges)) {
		return true
	}
	for _, r := range c.recentPurges[uint64(len(c.recentPurges))-dropped:] {
		if r.covers(key) {
			return true
		}
	}
	return false
}

type pathCacheEntry struct {
	inode   uint64
	attr    fuse.Attr
	expires time.Time
}

// maxCachedPaths bounds the references parked here. Overflow clears the whole
// cache rather than tracking recency: entries expire within ttl anyway, so
// exact eviction order buys nothing.
const maxCachedPaths = 64 << 10

func newPathCache(ttl time.Duration, forget func(inode uint64)) *pathCache {
	return &pathCache{
		ttl:       ttl,
		forget:    forget,
		entries:   map[string]*pathCacheEntry{},
		lastSweep: time.Now(),
	}
}

// cacheKey canonicalises a WinFsp path. The root maps to "", which is never
// cached: its inode is fixed and holds no reference.
func cacheKey(path string) string {
	return strings.Join(splitPath(path), "/")
}

// lookup reports the cached inode and attributes for key. The entry stays in
// the cache; the inode remains valid at least one sweep past its expiry.
func (c *pathCache) lookup(key string) (inode uint64, attr fuse.Attr, ok bool) {
	if key == "" {
		return 0, fuse.Attr{}, false
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	entry, found := c.entries[key]
	if !found || time.Now().After(entry.expires) {
		return 0, fuse.Attr{}, false
	}
	return entry.inode, entry.attr, true
}

// snapshot returns the purge generation. A caller about to resolve outside
// the lock passes it back to insert, which discards the entry if any purge ran
// in between.
func (c *pathCache) snapshot() uint64 {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.gen
}

// insert takes ownership of one lookup reference on inode, in every outcome:
// an entry a covering purge outdated goes to the graveyard instead of the map,
// so the caller may keep using the inode for at least one sweep either way.
func (c *pathCache) insert(key string, inode uint64, attr fuse.Attr, gen uint64) {
	if key == "" {
		c.forget(inode)
		return
	}
	var pending []uint64
	c.mu.Lock()
	if c.purgedSince(gen, key) {
		c.graveyard = append(c.graveyard, inode)
		pending = c.sweepLocked()
		c.mu.Unlock()
		c.forgetAll(pending)
		return
	}
	if existing, found := c.entries[key]; found {
		c.graveyard = append(c.graveyard, existing.inode)
	} else if len(c.entries) >= maxCachedPaths {
		for _, entry := range c.entries {
			c.graveyard = append(c.graveyard, entry.inode)
		}
		c.entries = map[string]*pathCacheEntry{}
	}
	c.entries[key] = &pathCacheEntry{inode: inode, attr: attr, expires: time.Now().Add(c.ttl)}
	pending = c.sweepLocked()
	c.mu.Unlock()
	c.forgetAll(pending)
}

// steal removes key and hands its reference to the caller.
func (c *pathCache) steal(key string) (inode uint64, ok bool) {
	c.mu.Lock()
	defer c.mu.Unlock()
	entry, found := c.entries[key]
	if !found {
		return 0, false
	}
	delete(c.entries, key)
	return entry.inode, true
}

// purge drops key, and everything under it when prefix is set, which a rename
// or removal of a directory needs: the children's cached paths name entries
// that are no longer there.
func (c *pathCache) purge(key string, prefix bool) {
	var pending []uint64
	c.mu.Lock()
	// Recorded even when the map holds nothing: the point is as much the
	// in-flight resolve about to insert this very name.
	c.gen++
	c.recentPurges = append(c.recentPurges, purgeRecord{key: key, prefix: prefix})
	if len(c.recentPurges) > maxRecentPurges {
		c.recentPurges = append(c.recentPurges[:0], c.recentPurges[len(c.recentPurges)-maxRecentPurges:]...)
	}
	if entry, found := c.entries[key]; found {
		c.graveyard = append(c.graveyard, entry.inode)
		delete(c.entries, key)
	}
	if prefix {
		under := key + "/"
		for k, entry := range c.entries {
			if key == "" || strings.HasPrefix(k, under) {
				c.graveyard = append(c.graveyard, entry.inode)
				delete(c.entries, k)
			}
		}
	}
	pending = c.sweepLocked()
	c.mu.Unlock()
	c.forgetAll(pending)
}

// sweepLocked returns the generation before last for the caller to forget
// outside the lock, and retires the current one. Sweeps run at most once per
// ttl and an appended reference sits out the sweep of its own call, so every
// reference rests for at least one full ttl after its last possible use.
func (c *pathCache) sweepLocked() []uint64 {
	now := time.Now()
	if now.Sub(c.lastSweep) < c.ttl {
		return nil
	}
	c.lastSweep = now
	pending := c.prevGraveyard
	c.prevGraveyard = c.graveyard
	c.graveyard = nil
	for key, entry := range c.entries {
		if now.After(entry.expires) {
			// Returned by the next sweep, a full ttl away: served from the map
			// until a moment ago, so someone may still be holding it.
			c.prevGraveyard = append(c.prevGraveyard, entry.inode)
			delete(c.entries, key)
		}
	}
	return pending
}

func (c *pathCache) forgetAll(inodes []uint64) {
	for _, inode := range inodes {
		c.forget(inode)
	}
}
