package file

import (
	"fmt"
	"path/filepath"
	"strings"
)

// validatePath checks that absPath is under one of the allowed paths.
// Deny-by-default: if allowedPaths is empty, no paths are allowed.
// Resolves symlinks to prevent traversal via ../  or symlink escapes.
func validatePath(absPath string, allowedPaths []string) error {
	if len(allowedPaths) == 0 {
		return fmt.Errorf("file: no allowed paths configured, all operations denied")
	}

	// Resolve symlinks on the requested path to get the real path.
	// If the file doesn't exist yet, resolve the parent directory.
	realPath, err := resolveReal(absPath)
	if err != nil {
		return fmt.Errorf("file: cannot resolve path %q: %w", absPath, err)
	}

	for _, allowed := range allowedPaths {
		a, err := filepath.Abs(allowed)
		if err != nil {
			continue
		}
		// Resolve symlinks on the allowed path too.
		realAllowed, err := resolveReal(a)
		if err != nil {
			continue
		}
		// Ensure the real path is strictly under the allowed directory.
		// Add trailing separator to prevent /opt matching /opt1234.
		if strings.HasPrefix(realPath, realAllowed+string(filepath.Separator)) || realPath == realAllowed {
			return nil
		}
	}
	return fmt.Errorf("path %q not under any allowed path", absPath)
}

// validateWritePath checks path validity AND that writing is allowed.
func validateWritePath(absPath string, allowedPaths []string, readOnly bool) error {
	if readOnly {
		return fmt.Errorf("file: write operations denied (read_only mode)")
	}
	return validatePath(absPath, allowedPaths)
}

// resolveReal resolves symlinks for a path.
// If the exact path doesn't exist, it walks up the tree to find the deepest
// existing ancestor, resolves its symlinks, and appends the remaining segments.
// This prevents partial traversal attacks via symlinks in non-existent paths.
func resolveReal(path string) (string, error) {
	real, err := filepath.EvalSymlinks(path)
	if err == nil {
		return filepath.Clean(real), nil
	}

	// Walk up to find the deepest existing ancestor.
	cleaned := filepath.Clean(path)
	var tail []string
	cur := cleaned
	for {
		parent := filepath.Dir(cur)
		tail = append([]string{filepath.Base(cur)}, tail...)
		realParent, err := filepath.EvalSymlinks(parent)
		if err == nil {
			// Found an existing ancestor — rebuild the path.
			result := realParent
			for _, seg := range tail {
				result = filepath.Join(result, seg)
			}
			return filepath.Clean(result), nil
		}
		if parent == cur {
			// Reached the root without finding an existing ancestor.
			return "", fmt.Errorf("cannot resolve any ancestor of %q", path)
		}
		cur = parent
	}
}
