// Package fs provides filesystem abstraction using spf13/afero for testability.
// It allows swapping the real filesystem with an in-memory mock for unit tests.
package fs

import (
	"io"
	"os"
	"path/filepath"
	"time"

	"github.com/spf13/afero"
)

// FS is the global filesystem interface used throughout the application.
// By default, it uses the real OS filesystem.
// For testing, use SetFS(afero.NewMemMapFs()) to use an in-memory filesystem.
var FS afero.Fs = afero.NewOsFs()

// SetFS sets the global filesystem (useful for testing)
func SetFS(fs afero.Fs) {
	FS = fs
}

// ResetFS resets to the real OS filesystem
func ResetFS() {
	FS = afero.NewOsFs()
}

// NewMemMapFs creates a new in-memory filesystem for testing
func NewMemMapFs() afero.Fs {
	return afero.NewMemMapFs()
}

// NewReadOnlyFs wraps a filesystem to make it read-only
func NewReadOnlyFs(base afero.Fs) afero.Fs {
	return afero.NewReadOnlyFs(base)
}

// NewBasePathFs creates a filesystem rooted at a specific path
func NewBasePathFs(base afero.Fs, path string) afero.Fs {
	return afero.NewBasePathFs(base, path)
}

// --- File Operations (use global FS) ---

// Create creates a file
func Create(name string) (afero.File, error) {
	return FS.Create(name)
}

// Open opens a file for reading
func Open(name string) (afero.File, error) {
	return FS.Open(name)
}

// OpenFile opens a file with specified flags and permissions
func OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) {
	return FS.OpenFile(name, flag, perm)
}

// Remove removes a file or empty directory
func Remove(name string) error {
	return FS.Remove(name)
}

// RemoveAll removes a path and any children it contains
func RemoveAll(path string) error {
	return FS.RemoveAll(path)
}

// Rename renames (moves) a file
func Rename(oldname, newname string) error {
	return FS.Rename(oldname, newname)
}

// Stat returns file info
func Stat(name string) (os.FileInfo, error) {
	return FS.Stat(name)
}

// Chmod changes file mode
func Chmod(name string, mode os.FileMode) error {
	return FS.Chmod(name, mode)
}

// Chown changes file ownership (may not work on all filesystems)
func Chown(name string, uid, gid int) error {
	return FS.Chown(name, uid, gid)
}

// Chtimes changes file access and modification times
func Chtimes(name string, atime, mtime time.Time) error {
	return FS.Chtimes(name, atime, mtime)
}

// --- Directory Operations ---

// Mkdir creates a directory
func Mkdir(name string, perm os.FileMode) error {
	return FS.Mkdir(name, perm)
}

// MkdirAll creates a directory and all parents
func MkdirAll(path string, perm os.FileMode) error {
	return FS.MkdirAll(path, perm)
}

// ReadDir reads a directory
func ReadDir(dirname string) ([]os.FileInfo, error) {
	return afero.ReadDir(FS, dirname)
}

// --- File Content Operations ---

// ReadFile reads an entire file
func ReadFile(filename string) ([]byte, error) {
	return afero.ReadFile(FS, filename)
}

// WriteFile writes data to a file
func WriteFile(filename string, data []byte, perm os.FileMode) error {
	return afero.WriteFile(FS, filename, data, perm)
}

// --- Existence Checks ---

// Exists checks if a file or directory exists
func Exists(path string) (bool, error) {
	return afero.Exists(FS, path)
}

// DirExists checks if a directory exists
func DirExists(path string) (bool, error) {
	return afero.DirExists(FS, path)
}

// IsDir checks if path is a directory
func IsDir(path string) (bool, error) {
	return afero.IsDir(FS, path)
}

// IsEmpty checks if a directory is empty
func IsEmpty(path string) (bool, error) {
	return afero.IsEmpty(FS, path)
}

// --- Utility Functions ---

// Walk walks a directory tree
func Walk(root string, walkFn filepath.WalkFunc) error {
	return afero.Walk(FS, root, walkFn)
}

// Glob returns the names of all files matching pattern
func Glob(pattern string) ([]string, error) {
	return afero.Glob(FS, pattern)
}

// TempDir creates a temporary directory
func TempDir(dir, prefix string) (string, error) {
	return afero.TempDir(FS, dir, prefix)
}

// TempFile creates a temporary file
func TempFile(dir, pattern string) (afero.File, error) {
	return afero.TempFile(FS, dir, pattern)
}

// CopyFile copies a file from src to dst
func CopyFile(src, dst string) error {
	srcFile, err := FS.Open(src)
	if err != nil {
		return err
	}
	defer srcFile.Close()

	srcInfo, err := srcFile.Stat()
	if err != nil {
		return err
	}

	dstFile, err := FS.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, srcInfo.Mode())
	if err != nil {
		return err
	}
	defer dstFile.Close()

	_, err = io.Copy(dstFile, srcFile)
	return err
}

// FileSize returns the size of a file
func FileSize(path string) (int64, error) {
	info, err := FS.Stat(path)
	if err != nil {
		return 0, err
	}
	return info.Size(), nil
}

// --- Testing Helpers ---

// WithMemFs executes a function with an in-memory filesystem, then restores the original
func WithMemFs(fn func(fs afero.Fs)) {
	original := FS
	memFs := afero.NewMemMapFs()
	FS = memFs
	defer func() { FS = original }()
	fn(memFs)
}

// SetupTestDir creates a test directory structure in-memory
func SetupTestDir(files map[string]string) afero.Fs {
	memFs := afero.NewMemMapFs()
	for path, content := range files {
		dir := filepath.Dir(path)
		if dir != "." && dir != "/" {
			_ = memFs.MkdirAll(dir, 0755)
		}
		_ = afero.WriteFile(memFs, path, []byte(content), 0644)
	}
	return memFs
}
