package zddc

import (
	"io/fs"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"sync"
)

// scanCache memoizes ScanZddcFiles results keyed by fsRoot.
//
// The walk is the dominant cost on every profile-page render in a real
// archive (thousands of directories). Caching is safe because:
//   - The set of directories with .zddc files only changes via the writer
//     helpers (CreateOrUpdateFile / DeleteFile) or external file events
//     observed by the archive watcher; both call InvalidateScanCache.
//   - Cache values are immutable []string slices; readers reuse them.
//
// Errors are NOT cached — callers always get a fresh attempt after a walk
// failure so a transient permissions blip doesn't poison the cache.
var scanCache sync.Map // map[string][]string

// ScanZddcFiles walks fsRoot and returns every directory that contains a
// .zddc file, sorted by path. Reserved-prefix directories ('.', '_') are
// pruned from the walk — they hide internal/scaffolding state from the
// project picker and should likewise not surface in the editor's tree
// view. fsRoot itself is not subject to the prefix rule (it is the root).
//
// The returned paths are absolute (rooted under fsRoot). On any walk
// error the partial result accumulated so far is returned alongside the
// error so callers can degrade gracefully. Successful results are cached
// in memory; subsequent calls return the cached slice until
// InvalidateScanCache is called (the .zddc writer helpers and the archive
// watcher both invalidate on file events).
func ScanZddcFiles(fsRoot string) ([]string, error) {
	fsRoot = filepath.Clean(fsRoot)

	if cached, ok := scanCache.Load(fsRoot); ok {
		return cached.([]string), nil
	}

	var dirs []string
	err := filepath.WalkDir(fsRoot, func(path string, d fs.DirEntry, walkErr error) error {
		if walkErr != nil {
			// A permission error on a single subtree shouldn't kill the walk.
			if os.IsPermission(walkErr) {
				return nil
			}
			return walkErr
		}
		if !d.IsDir() {
			return nil
		}
		// Prune reserved-prefix subtrees, but never the root itself.
		if path != fsRoot {
			name := d.Name()
			if strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") {
				return filepath.SkipDir
			}
		}
		// Does this directory contain a .zddc file?
		if _, err := os.Stat(filepath.Join(path, ".zddc")); err == nil {
			dirs = append(dirs, path)
		}
		return nil
	})
	sort.Strings(dirs)

	if err == nil {
		scanCache.Store(fsRoot, dirs)
	}
	return dirs, err
}

// InvalidateScanCache drops every cached ScanZddcFiles result. Called by
// the writer helpers when a .zddc file is created or deleted, and by the
// archive watcher on .zddc filesystem events. Modifications to existing
// .zddc files don't change the scan output — but distinguishing create
// from modify in fsnotify is fragile, so we treat any event as
// invalidating; a single extra walk-on-next-call is cheap compared to
// silent staleness.
func InvalidateScanCache() {
	scanCache.Range(func(key, _ any) bool {
		scanCache.Delete(key)
		return true
	})
}
