package filemanager

import (
	"encoding/base64"
	"fmt"
	"io"
	"io/fs"
	"log"
	"os"
	"path/filepath"
	"strings"
	"time"
)

const (
	// maxReadSize is the maximum file size allowed for GetContent (5 MB).
	// This guards against accidentally reading large binary files into memory.
	maxReadSize = 5 * 1024 * 1024

	// maxSearchResults caps the number of results returned by Search.
	maxSearchResults = 100

	// storageName is the VueFinder storage identifier used in all path strings.
	storageName = "server"

	// adapterName is the VueFinder adapter field included in every list/search response.
	adapterName = "local"
)

// FileManager handles sandboxed filesystem operations for the game server
// install directory. All operations are confined to installDir — any path
// that resolves outside that boundary is rejected with an error.
type FileManager struct {
	// installDir is the absolute, symlink-resolved root of the jail.
	// It is set once at construction and never changes.
	installDir string
}

// New creates a FileManager jailed to installDir. The directory is cleaned and
// made absolute but NOT required to exist at construction time — the daemon may
// start before the install completes.
func New(installDir string) *FileManager {
	// Clean and make absolute so comparisons are deterministic even if the
	// caller passed a relative or un-normalised path.
	abs, err := filepath.Abs(filepath.Clean(installDir))
	if err != nil {
		// Abs only fails on systems where os.Getwd() fails; fall back to raw value.
		log.Printf("filemanager: warning: could not make installDir absolute (%v), using raw value", err)
		abs = filepath.Clean(installDir)
	}
	return &FileManager{installDir: abs}
}

// ---------------------------------------------------------------------------
// Public API — one method per VueFinder operation
// ---------------------------------------------------------------------------

// List returns the contents of the directory identified by storagePath.
func (fm *FileManager) List(storagePath string) (*ListResponse, error) {
	abs, rel, err := fm.parseAndResolve(storagePath)
	if err != nil {
		return nil, err
	}

	entries, err := os.ReadDir(abs)
	if err != nil {
		return nil, fmt.Errorf("cannot read directory: %w", err)
	}

	files, err := buildFileItems(entries, abs, rel, fm.installDir)
	if err != nil {
		return nil, err
	}

	return &ListResponse{
		Adapter:  adapterName,
		Storages: []string{storageName},
		Dirname:  toStoragePath(rel),
		Files:    files,
	}, nil
}

// Delete removes every item listed in items from within storagePath's directory.
// After deletion it returns a fresh listing of the parent directory.
func (fm *FileManager) Delete(storagePath string, items []string) (*ListResponse, error) {
	parentAbs, parentRel, err := fm.parseAndResolve(storagePath)
	if err != nil {
		return nil, err
	}

	for _, item := range items {
		itemAbs, _, err := fm.parseAndResolve(item)
		if err != nil {
			return nil, fmt.Errorf("invalid item path %q: %w", item, err)
		}
		if err := os.RemoveAll(itemAbs); err != nil {
			return nil, fmt.Errorf("failed to delete %q: %w", item, err)
		}
		log.Printf("filemanager: deleted %s", itemAbs)
	}

	// Return a fresh listing of the parent so the frontend can update immediately.
	entries, err := os.ReadDir(parentAbs)
	if err != nil {
		return nil, fmt.Errorf("cannot re-read directory after delete: %w", err)
	}
	files, err := buildFileItems(entries, parentAbs, parentRel, fm.installDir)
	if err != nil {
		return nil, err
	}
	return &ListResponse{
		Adapter:  adapterName,
		Storages: []string{storageName},
		Dirname:  toStoragePath(parentRel),
		Files:    files,
	}, nil
}

// Rename renames item inside storagePath to newName.
// newName must be a bare filename, not a path — slashes are rejected.
func (fm *FileManager) Rename(storagePath string, item string, newName string) (*ListResponse, error) {
	if strings.ContainsAny(newName, "/\\") {
		return nil, fmt.Errorf("new name must not contain path separators")
	}
	if newName == "" || newName == "." || newName == ".." {
		return nil, fmt.Errorf("invalid new name %q", newName)
	}

	parentAbs, parentRel, err := fm.parseAndResolve(storagePath)
	if err != nil {
		return nil, err
	}

	itemAbs, _, err := fm.parseAndResolve(item)
	if err != nil {
		return nil, fmt.Errorf("invalid item path %q: %w", item, err)
	}

	destAbs := filepath.Join(parentAbs, newName)
	// Verify the destination is also inside the jail before committing.
	if err := fm.checkWithinJail(destAbs); err != nil {
		return nil, fmt.Errorf("destination escapes jail: %w", err)
	}

	if err := os.Rename(itemAbs, destAbs); err != nil {
		return nil, fmt.Errorf("rename failed: %w", err)
	}
	log.Printf("filemanager: renamed %s -> %s", itemAbs, destAbs)

	entries, err := os.ReadDir(parentAbs)
	if err != nil {
		return nil, fmt.Errorf("cannot re-read directory after rename: %w", err)
	}
	files, err := buildFileItems(entries, parentAbs, parentRel, fm.installDir)
	if err != nil {
		return nil, err
	}
	return &ListResponse{
		Adapter:  adapterName,
		Storages: []string{storageName},
		Dirname:  toStoragePath(parentRel),
		Files:    files,
	}, nil
}

// CreateFolder creates a new directory named name inside storagePath.
func (fm *FileManager) CreateFolder(storagePath string, name string) (*ListResponse, error) {
	if err := validateBareName(name); err != nil {
		return nil, err
	}

	parentAbs, parentRel, err := fm.parseAndResolve(storagePath)
	if err != nil {
		return nil, err
	}

	newDirAbs := filepath.Join(parentAbs, name)
	if err := fm.checkWithinJail(newDirAbs); err != nil {
		return nil, fmt.Errorf("target escapes jail: %w", err)
	}
	if err := os.MkdirAll(newDirAbs, 0755); err != nil {
		return nil, fmt.Errorf("failed to create directory: %w", err)
	}
	log.Printf("filemanager: created directory %s", newDirAbs)

	entries, err := os.ReadDir(parentAbs)
	if err != nil {
		return nil, fmt.Errorf("cannot re-read directory after mkdir: %w", err)
	}
	files, err := buildFileItems(entries, parentAbs, parentRel, fm.installDir)
	if err != nil {
		return nil, err
	}
	return &ListResponse{
		Adapter:  adapterName,
		Storages: []string{storageName},
		Dirname:  toStoragePath(parentRel),
		Files:    files,
	}, nil
}

// CreateFile creates an empty file named name inside storagePath.
func (fm *FileManager) CreateFile(storagePath string, name string) (*ListResponse, error) {
	if err := validateBareName(name); err != nil {
		return nil, err
	}

	parentAbs, parentRel, err := fm.parseAndResolve(storagePath)
	if err != nil {
		return nil, err
	}

	newFileAbs := filepath.Join(parentAbs, name)
	if err := fm.checkWithinJail(newFileAbs); err != nil {
		return nil, fmt.Errorf("target escapes jail: %w", err)
	}

	f, err := os.OpenFile(newFileAbs, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
	if err != nil {
		return nil, fmt.Errorf("failed to create file: %w", err)
	}
	f.Close()
	log.Printf("filemanager: created file %s", newFileAbs)

	entries, err := os.ReadDir(parentAbs)
	if err != nil {
		return nil, fmt.Errorf("cannot re-read directory after mkfile: %w", err)
	}
	files, err := buildFileItems(entries, parentAbs, parentRel, fm.installDir)
	if err != nil {
		return nil, err
	}
	return &ListResponse{
		Adapter:  adapterName,
		Storages: []string{storageName},
		Dirname:  toStoragePath(parentRel),
		Files:    files,
	}, nil
}

// GetContent reads and returns the UTF-8 content of the file at storagePath.
// Reading is capped at maxReadSize (5 MB) to avoid loading large binaries.
func (fm *FileManager) GetContent(storagePath string) (string, error) {
	abs, _, err := fm.parseAndResolve(storagePath)
	if err != nil {
		return "", err
	}

	info, err := os.Stat(abs)
	if err != nil {
		return "", fmt.Errorf("cannot stat file: %w", err)
	}
	if info.IsDir() {
		return "", fmt.Errorf("path is a directory, not a file")
	}
	if info.Size() > maxReadSize {
		return "", fmt.Errorf("file size %d bytes exceeds read limit of %d bytes", info.Size(), maxReadSize)
	}

	data, err := os.ReadFile(abs)
	if err != nil {
		return "", fmt.Errorf("cannot read file: %w", err)
	}
	return string(data), nil
}

// SaveContent overwrites the file at storagePath with content.
func (fm *FileManager) SaveContent(storagePath string, content string) error {
	abs, _, err := fm.parseAndResolve(storagePath)
	if err != nil {
		return err
	}

	// Ensure parent directory exists.
	if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil {
		return fmt.Errorf("cannot create parent directory: %w", err)
	}

	if err := os.WriteFile(abs, []byte(content), 0644); err != nil {
		return fmt.Errorf("cannot write file: %w", err)
	}
	log.Printf("filemanager: saved %d bytes to %s", len(content), abs)
	return nil
}

// Search walks the directory at storagePath and returns files whose names
// contain filter (case-insensitive). Results are capped at maxSearchResults.
func (fm *FileManager) Search(storagePath string, filter string) (*SearchResponse, error) {
	rootAbs, rootRel, err := fm.parseAndResolve(storagePath)
	if err != nil {
		return nil, err
	}

	lowerFilter := strings.ToLower(filter)
	var results []FileItem

	err = filepath.WalkDir(rootAbs, func(path string, d fs.DirEntry, walkErr error) error {
		if walkErr != nil {
			// Skip entries that produce errors (e.g. permission denied) rather than aborting.
			log.Printf("filemanager: search: skipping %s: %v", path, walkErr)
			return nil
		}
		if path == rootAbs {
			// Skip the root itself.
			return nil
		}

		// Security: verify every path we touch is within the jail.
		if err := fm.checkWithinJail(path); err != nil {
			log.Printf("filemanager: search: path %s escaped jail, skipping", path)
			return filepath.SkipDir
		}

		if strings.Contains(strings.ToLower(d.Name()), lowerFilter) {
			rel, relErr := filepath.Rel(fm.installDir, path)
			if relErr != nil {
				return nil
			}
			item, buildErr := buildFileItem(d, path, rel)
			if buildErr != nil {
				return nil
			}
			results = append(results, item)
			if len(results) >= maxSearchResults {
				return io.EOF // Signal early exit; WalkDir treats this as a stop, not an error.
			}
		}
		return nil
	})
	// WalkDir returns io.EOF only if we injected it; suppress it here.
	if err != nil && err != io.EOF {
		return nil, fmt.Errorf("search walk failed: %w", err)
	}

	_ = rootRel // rootRel used for Dirname below.
	return &SearchResponse{
		Adapter:  adapterName,
		Storages: []string{storageName},
		Dirname:  toStoragePath(rootRel),
		Files:    results,
	}, nil
}

// Move moves every item in items into destination.
// After the move it returns a fresh listing of destination.
func (fm *FileManager) Move(storagePath string, items []string, destination string) (*ListResponse, error) {
	destAbs, destRel, err := fm.parseAndResolve(destination)
	if err != nil {
		return nil, fmt.Errorf("invalid destination %q: %w", destination, err)
	}

	// Ensure destination directory exists.
	if err := os.MkdirAll(destAbs, 0755); err != nil {
		return nil, fmt.Errorf("cannot create destination directory: %w", err)
	}

	for _, item := range items {
		itemAbs, _, err := fm.parseAndResolve(item)
		if err != nil {
			return nil, fmt.Errorf("invalid item path %q: %w", item, err)
		}
		targetAbs := filepath.Join(destAbs, filepath.Base(itemAbs))
		if err := fm.checkWithinJail(targetAbs); err != nil {
			return nil, fmt.Errorf("move target escapes jail: %w", err)
		}
		if err := os.Rename(itemAbs, targetAbs); err != nil {
			// os.Rename fails across filesystems; fall back to copy+delete.
			if err2 := copyRecursive(itemAbs, targetAbs); err2 != nil {
				return nil, fmt.Errorf("failed to move %q: %w", item, err2)
			}
			if err2 := os.RemoveAll(itemAbs); err2 != nil {
				return nil, fmt.Errorf("moved %q but failed to remove source: %w", item, err2)
			}
		}
		log.Printf("filemanager: moved %s -> %s", itemAbs, targetAbs)
	}

	entries, err := os.ReadDir(destAbs)
	if err != nil {
		return nil, fmt.Errorf("cannot read destination after move: %w", err)
	}
	files, err := buildFileItems(entries, destAbs, destRel, fm.installDir)
	if err != nil {
		return nil, err
	}
	return &ListResponse{
		Adapter:  adapterName,
		Storages: []string{storageName},
		Dirname:  toStoragePath(destRel),
		Files:    files,
	}, nil
}

// Copy copies every item in items into destination.
// After the copy it returns a fresh listing of destination.
func (fm *FileManager) Copy(storagePath string, items []string, destination string) (*ListResponse, error) {
	destAbs, destRel, err := fm.parseAndResolve(destination)
	if err != nil {
		return nil, fmt.Errorf("invalid destination %q: %w", destination, err)
	}

	if err := os.MkdirAll(destAbs, 0755); err != nil {
		return nil, fmt.Errorf("cannot create destination directory: %w", err)
	}

	for _, item := range items {
		itemAbs, _, err := fm.parseAndResolve(item)
		if err != nil {
			return nil, fmt.Errorf("invalid item path %q: %w", item, err)
		}
		targetAbs := filepath.Join(destAbs, filepath.Base(itemAbs))
		if err := fm.checkWithinJail(targetAbs); err != nil {
			return nil, fmt.Errorf("copy target escapes jail: %w", err)
		}
		if err := copyRecursive(itemAbs, targetAbs); err != nil {
			return nil, fmt.Errorf("failed to copy %q: %w", item, err)
		}
		log.Printf("filemanager: copied %s -> %s", itemAbs, targetAbs)
	}

	entries, err := os.ReadDir(destAbs)
	if err != nil {
		return nil, fmt.Errorf("cannot read destination after copy: %w", err)
	}
	files, err := buildFileItems(entries, destAbs, destRel, fm.installDir)
	if err != nil {
		return nil, err
	}
	return &ListResponse{
		Adapter:  adapterName,
		Storages: []string{storageName},
		Dirname:  toStoragePath(destRel),
		Files:    files,
	}, nil
}

// Upload decodes the base64-encoded content and writes it as filename inside storagePath.
// After the write it returns a fresh listing of the parent directory.
func (fm *FileManager) Upload(storagePath string, filename string, content []byte) (*ListResponse, error) {
	if err := validateBareName(filename); err != nil {
		return nil, err
	}

	parentAbs, parentRel, err := fm.parseAndResolve(storagePath)
	if err != nil {
		return nil, err
	}

	destAbs := filepath.Join(parentAbs, filename)
	if err := fm.checkWithinJail(destAbs); err != nil {
		return nil, fmt.Errorf("upload target escapes jail: %w", err)
	}
	if err := os.MkdirAll(parentAbs, 0755); err != nil {
		return nil, fmt.Errorf("cannot create upload directory: %w", err)
	}
	if err := os.WriteFile(destAbs, content, 0644); err != nil {
		return nil, fmt.Errorf("cannot write uploaded file: %w", err)
	}
	log.Printf("filemanager: uploaded %d bytes to %s", len(content), destAbs)

	entries, err := os.ReadDir(parentAbs)
	if err != nil {
		return nil, fmt.Errorf("cannot re-read directory after upload: %w", err)
	}
	files, err := buildFileItems(entries, parentAbs, parentRel, fm.installDir)
	if err != nil {
		return nil, err
	}
	return &ListResponse{
		Adapter:  adapterName,
		Storages: []string{storageName},
		Dirname:  toStoragePath(parentRel),
		Files:    files,
	}, nil
}

// ---------------------------------------------------------------------------
// Path parsing and security helpers
// ---------------------------------------------------------------------------

// ParsePath converts a VueFinder storage path ("server://relative/path") into
// the absolute filesystem path within the jail.
//
// Rules:
//   - Must have the form "server://<relative>" where relative may be empty.
//   - The storage identifier must be "server" — anything else is rejected.
//   - The resulting absolute path must be within installDir.
func (fm *FileManager) ParsePath(storagePath string) (string, error) {
	abs, _, err := fm.parseAndResolve(storagePath)
	return abs, err
}

// parseAndResolve parses a storage path and resolves it to an absolute path.
// Returns (absolutePath, relativePath, error). relativePath is relative to
// installDir and uses the OS path separator.
func (fm *FileManager) parseAndResolve(storagePath string) (absPath string, relPath string, err error) {
	const sep = "://"

	idx := strings.Index(storagePath, sep)
	if idx < 0 {
		// Tolerate a bare relative path for internal calls, but flag it.
		return "", "", fmt.Errorf("invalid storage path %q: missing storage prefix (expected %q://...)", storagePath, storageName)
	}

	storage := storagePath[:idx]
	if storage != storageName {
		return "", "", fmt.Errorf("unknown storage %q: only %q is supported", storage, storageName)
	}

	relative := storagePath[idx+len(sep):]

	return fm.resolvePath(relative)
}

// resolvePath resolves a relative path (which may be empty, representing the
// root) to an absolute path inside the jail.  It runs filepath.EvalSymlinks on
// the resolved path and then re-verifies the prefix to prevent symlink escapes.
// Parent-traversal via ".." is neutralised by filepath.Clean before joining.
func (fm *FileManager) resolvePath(relativePath string) (absPath string, relPath string, err error) {
	// filepath.Clean neutralises ".." sequences before we ever join.
	cleaned := filepath.Clean(relativePath)
	if cleaned == "." {
		cleaned = ""
	}

	var absolute string
	if cleaned == "" {
		absolute = fm.installDir
	} else {
		absolute = filepath.Join(fm.installDir, cleaned)
	}

	// First prefix check — fast path before hitting the filesystem.
	if !strings.HasPrefix(absolute, fm.installDir+string(filepath.Separator)) && absolute != fm.installDir {
		return "", "", fmt.Errorf("path %q escapes install directory", relativePath)
	}

	// Resolve symlinks so we can do an authoritative prefix check.
	resolved, symlinkErr := filepath.EvalSymlinks(absolute)
	if symlinkErr != nil {
		// EvalSymlinks fails if the path does not exist yet (e.g. a new file).
		// In that case fall back to the unresolved absolute path; the subsequent
		// prefix check is still valid because we already cleaned ".." away.
		resolved = absolute
	}

	// Authoritative prefix check on the symlink-resolved path.
	if !strings.HasPrefix(resolved, fm.installDir+string(filepath.Separator)) && resolved != fm.installDir {
		return "", "", fmt.Errorf("path %q resolves outside install directory (possible symlink escape)", relativePath)
	}

	// Compute relative portion for use in VueFinder response paths.
	rel, relErr := filepath.Rel(fm.installDir, resolved)
	if relErr != nil {
		rel = cleaned
	}
	if rel == "." {
		rel = ""
	}

	return resolved, rel, nil
}

// checkWithinJail verifies that an absolute path (already joined but not yet
// symlink-resolved) stays within the jail. Used for destination paths that may
// not exist yet.
func (fm *FileManager) checkWithinJail(absPath string) error {
	clean := filepath.Clean(absPath)
	if !strings.HasPrefix(clean, fm.installDir+string(filepath.Separator)) && clean != fm.installDir {
		return fmt.Errorf("path %q is outside the install directory", absPath)
	}
	return nil
}

// ---------------------------------------------------------------------------
// VueFinder path formatting helpers
// ---------------------------------------------------------------------------

// toStoragePath converts a relative path (relative to installDir) to the
// "server://..." format expected by VueFinder. An empty relative path maps to
// "server://".
func toStoragePath(relPath string) string {
	// Normalise OS separators to forward slashes for the JSON response.
	fwd := filepath.ToSlash(relPath)
	return storageName + "://" + fwd
}

// ---------------------------------------------------------------------------
// FileItem construction helpers
// ---------------------------------------------------------------------------

// buildFileItems builds the slice of FileItem values for a directory listing.
func buildFileItems(entries []fs.DirEntry, dirAbs string, dirRel string, installDir string) ([]FileItem, error) {
	items := make([]FileItem, 0, len(entries))
	for _, entry := range entries {
		entryPath := filepath.Join(dirAbs, entry.Name())
		entryRel := filepath.Join(dirRel, entry.Name())
		if dirRel == "" {
			entryRel = entry.Name()
		}

		item, err := buildFileItem(entry, entryPath, entryRel)
		if err != nil {
			log.Printf("filemanager: warning: skipping %s: %v", entryPath, err)
			continue
		}
		items = append(items, item)
	}
	return items, nil
}

// buildFileItem builds a single FileItem from a DirEntry.
// entryAbs is the absolute path; entryRel is relative to installDir.
func buildFileItem(entry fs.DirEntry, entryAbs string, entryRel string) (FileItem, error) {
	info, err := entry.Info()
	if err != nil {
		return FileItem{}, fmt.Errorf("cannot stat %s: %w", entryAbs, err)
	}

	itemType := "file"
	ext := ""
	size := info.Size()

	if entry.IsDir() {
		itemType = "dir"
		size = 0
	} else {
		raw := filepath.Ext(entry.Name())
		if len(raw) > 0 {
			ext = raw[1:] // strip the leading dot
		}
	}

	// Normalise to forward slashes for the JSON path field.
	fwdRel := filepath.ToSlash(entryRel)

	return FileItem{
		Type:         itemType,
		Path:         storageName + "://" + fwdRel,
		Basename:     entry.Name(),
		Extension:    ext,
		Storage:      storageName,
		FileSize:     size,
		LastModified: info.ModTime().Format("2006-01-02 15:04:05"),
	}, nil
}

// ---------------------------------------------------------------------------
// Recursive copy helper
// ---------------------------------------------------------------------------

// copyRecursive copies src to dst. If src is a directory it is copied
// recursively. dst must not exist.
func copyRecursive(src, dst string) error {
	srcInfo, err := os.Lstat(src)
	if err != nil {
		return fmt.Errorf("cannot stat source: %w", err)
	}

	if srcInfo.IsDir() {
		if err := os.MkdirAll(dst, srcInfo.Mode()); err != nil {
			return fmt.Errorf("cannot create destination directory: %w", err)
		}
		entries, err := os.ReadDir(src)
		if err != nil {
			return fmt.Errorf("cannot read source directory: %w", err)
		}
		for _, entry := range entries {
			if err := copyRecursive(filepath.Join(src, entry.Name()), filepath.Join(dst, entry.Name())); err != nil {
				return err
			}
		}
		return nil
	}

	// Regular file copy.
	srcFile, err := os.Open(src)
	if err != nil {
		return fmt.Errorf("cannot open source file: %w", err)
	}
	defer srcFile.Close()

	dstFile, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, srcInfo.Mode())
	if err != nil {
		return fmt.Errorf("cannot create destination file: %w", err)
	}
	defer dstFile.Close()

	if _, err := io.Copy(dstFile, srcFile); err != nil {
		return fmt.Errorf("copy failed: %w", err)
	}
	return nil
}

// ---------------------------------------------------------------------------
// Input validation helpers
// ---------------------------------------------------------------------------

// validateBareName rejects names that are empty, are "." or "..", or contain
// path separators. This is used to validate user-supplied filenames for create,
// rename, and upload operations.
func validateBareName(name string) error {
	if name == "" {
		return fmt.Errorf("name must not be empty")
	}
	if name == "." || name == ".." {
		return fmt.Errorf("name %q is not allowed", name)
	}
	if strings.ContainsAny(name, "/\\") {
		return fmt.Errorf("name %q must not contain path separators", name)
	}
	return nil
}

// DecodeBase64 is a convenience wrapper used by the handler when the upload
// content arrives as a base64 string.
func DecodeBase64(encoded string) ([]byte, error) {
	// Accept both standard and URL-safe base64, with or without padding.
	decoded, err := base64.StdEncoding.DecodeString(encoded)
	if err != nil {
		// Try URL-safe encoding as a fallback.
		decoded, err = base64.URLEncoding.DecodeString(encoded)
		if err != nil {
			// Try raw (no padding) variants.
			decoded, err = base64.RawStdEncoding.DecodeString(encoded)
			if err != nil {
				return nil, fmt.Errorf("cannot base64-decode upload content: %w", err)
			}
		}
	}
	return decoded, nil
}

// suppress unused import warning — time is used in buildFileItem via ModTime.Format
var _ = time.Now
