package http

import (
	"context"
	"sync"
	"time"

	"gitea.dcglab.co.uk/steve/restic-manager/internal/api"
)

// treeCacheTTL is how long a per-session cached directory listing
// stays valid. The whole point of the cache is to make re-expanding
// nodes within the same wizard session snappy; 30 minutes covers a
// generous wizard interaction window without holding stale data
// indefinitely.
const treeCacheTTL = 30 * time.Minute

// treeCacheKey identifies one cached listing. session_id scopes
// entries to a single browser session so two operators don't share
// view state; snapshot_id + path identify the directory inside the
// snapshot.
type treeCacheKey struct {
	SessionID  string
	HostID     string
	SnapshotID string
	Path       string
}

type treeCacheEntry struct {
	Result    api.TreeListResultPayload
	ExpiresAt time.Time
}

// treeCache is a per-process map of synchronously fetched directory
// listings. Concurrency is light (a few entries per active wizard
// session) so a single mutex is fine.
type treeCache struct {
	mu      sync.Mutex
	entries map[treeCacheKey]treeCacheEntry
}

func newTreeCache() *treeCache {
	return &treeCache{entries: make(map[treeCacheKey]treeCacheEntry)}
}

// Get returns a cached entry if one exists and hasn't expired.
func (c *treeCache) Get(k treeCacheKey, now time.Time) (api.TreeListResultPayload, bool) {
	c.mu.Lock()
	defer c.mu.Unlock()
	e, ok := c.entries[k]
	if !ok {
		return api.TreeListResultPayload{}, false
	}
	if now.After(e.ExpiresAt) {
		delete(c.entries, k)
		return api.TreeListResultPayload{}, false
	}
	return e.Result, true
}

// Put records a fresh listing under k. Caller is responsible for
// having validated the result first (Error == "").
func (c *treeCache) Put(k treeCacheKey, result api.TreeListResultPayload, now time.Time) {
	c.mu.Lock()
	c.entries[k] = treeCacheEntry{
		Result:    result,
		ExpiresAt: now.Add(treeCacheTTL),
	}
	c.mu.Unlock()
}

// Sweep deletes expired entries. Called opportunistically from the
// wizard handler — no separate goroutine needed; cache size is small.
func (c *treeCache) Sweep(now time.Time) {
	c.mu.Lock()
	for k, e := range c.entries {
		if now.After(e.ExpiresAt) {
			delete(c.entries, k)
		}
	}
	c.mu.Unlock()
}

// fetchTreeWithCache returns a directory listing — cache hit, or a
// synchronous tree.list RPC against the agent on miss. On agent error
// (not transport error), the result is returned as-is with Error set
// rather than cached, so a transient failure doesn't poison subsequent
// requests for the same path.
//
//nolint:unused // wired in by the wizard handler in the next slice
func (s *Server) fetchTreeWithCache(ctx context.Context, sessionID, hostID, snapshotID, path string) (api.TreeListResultPayload, error) {
	now := time.Now()
	k := treeCacheKey{SessionID: sessionID, HostID: hostID, SnapshotID: snapshotID, Path: path}
	if cached, ok := s.treeCache.Get(k, now); ok {
		return cached, nil
	}

	reply, err := s.deps.Hub.SendRPC(ctx, hostID, api.MsgTreeList,
		api.TreeListRequestPayload{SnapshotID: snapshotID, Path: path},
		30*time.Second)
	if err != nil {
		return api.TreeListResultPayload{}, err
	}
	var result api.TreeListResultPayload
	if perr := reply.UnmarshalPayload(&result); perr != nil {
		return api.TreeListResultPayload{}, perr
	}
	if result.Error == "" {
		s.treeCache.Put(k, result, now)
	}
	return result, nil
}
