// Package fileutil provides secure file operation utilities to prevent path traversal attacks.
package fileutil

import (
	"crypto/rand"
	"encoding/base64"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"
)

// SecurePathValidator provides path traversal protection with symlink resolution.
type SecurePathValidator struct {
	BasePath string
}

// NewSecurePathValidator creates a new path validator for a base directory.
func NewSecurePathValidator(basePath string) *SecurePathValidator {
	return &SecurePathValidator{BasePath: basePath}
}

// ValidatePath ensures resolved path is within base directory.
// It resolves symlinks and returns the canonical absolute path.
func (v *SecurePathValidator) ValidatePath(inputPath string) (string, error) {
	if v.BasePath == "" {
		return "", fmt.Errorf("base path not set")
	}

	// Clean the path to remove . and ..
	cleaned := filepath.Clean(inputPath)

	// Get absolute base path and resolve any symlinks (critical for macOS /tmp -> /private/tmp)
	baseAbs, err := filepath.Abs(v.BasePath)
	if err != nil {
		return "", fmt.Errorf("failed to get absolute base path: %w", err)
	}
	// Resolve symlinks in base path for accurate comparison
	baseResolved, err := filepath.EvalSymlinks(baseAbs)
	if err != nil {
		// Base path may not exist yet, use as-is
		baseResolved = baseAbs
	}

	// If cleaned is already absolute, check if it's within base
	var absPath string
	if filepath.IsAbs(cleaned) {
		// For absolute paths, try to resolve symlinks
		resolvedInput, err := filepath.EvalSymlinks(cleaned)
		if err != nil {
			// Path doesn't exist - try to resolve parent directories to handle macOS /private prefix
			dir := filepath.Dir(cleaned)
			resolvedDir, dirErr := filepath.EvalSymlinks(dir)
			if dirErr == nil {
				// Parent resolved successfully, use resolved parent + base name
				base := filepath.Base(cleaned)
				resolvedInput = filepath.Join(resolvedDir, base)
			} else {
				// Can't resolve parent either, use cleaned as-is
				resolvedInput = cleaned
			}
		}
		absPath = resolvedInput
	} else {
		// Join with RESOLVED base path if relative (for consistent handling on macOS)
		absPath = filepath.Join(baseResolved, cleaned)
	}

	// FIRST: Check path boundaries before resolving symlinks
	// This catches path traversal attempts even if the path doesn't exist
	baseWithSep := baseResolved + string(filepath.Separator)
	if !strings.HasPrefix(absPath+string(filepath.Separator), baseWithSep) && absPath != baseResolved {
		return "", fmt.Errorf("path escapes base directory: %s (base is %s)", inputPath, baseResolved)
	}

	// Resolve symlinks - critical for security
	resolved, err := filepath.EvalSymlinks(absPath)
	if err != nil {
		// If the file doesn't exist, we still need to check the directory path
		// Try to resolve the parent directory
		dir := filepath.Dir(absPath)
		resolvedDir, dirErr := filepath.EvalSymlinks(dir)
		if dirErr != nil {
			// Path doesn't exist and parent can't be resolved - this is ok for new files
			// as long as the path itself is within bounds (which we checked above)
			return absPath, nil
		}
		// Reconstruct the path with resolved directory
		base := filepath.Base(absPath)
		resolved = filepath.Join(resolvedDir, base)
	}

	// Get absolute resolved path
	resolvedAbs, err := filepath.Abs(resolved)
	if err != nil {
		return "", fmt.Errorf("failed to get absolute resolved path: %w", err)
	}

	// SECOND: Verify resolved path is still within base (symlink escape check)
	if resolvedAbs != baseResolved && !strings.HasPrefix(resolvedAbs+string(filepath.Separator), baseWithSep) {
		return "", fmt.Errorf("path escapes base directory: %s (resolved to %s, base is %s)", inputPath, resolvedAbs, baseResolved)
	}

	return resolvedAbs, nil
}

// SecureFileRead securely reads a file after cleaning the path to prevent path traversal.
func SecureFileRead(path string) ([]byte, error) {
	return os.ReadFile(filepath.Clean(path))
}

// SecureFileWrite securely writes a file after cleaning the path to prevent path traversal.
func SecureFileWrite(path string, data []byte, perm os.FileMode) error {
	return os.WriteFile(filepath.Clean(path), data, perm)
}

// SecureOpenFile securely opens a file after cleaning the path to prevent path traversal.
func SecureOpenFile(path string, flag int, perm os.FileMode) (*os.File, error) {
	return os.OpenFile(filepath.Clean(path), flag, perm)
}

// SecureReadDir reads directory contents with path validation.
func (v *SecurePathValidator) SecureReadDir(dirPath string) ([]os.DirEntry, error) {
	validatedPath, err := v.ValidatePath(dirPath)
	if err != nil {
		return nil, fmt.Errorf("directory path validation failed: %w", err)
	}
	return os.ReadDir(validatedPath)
}

// SecureCreateTemp creates a temporary file within the base directory.
func (v *SecurePathValidator) SecureCreateTemp(pattern string) (*os.File, string, error) {
	validatedPath, err := v.ValidatePath("")
	if err != nil {
		return nil, "", fmt.Errorf("base directory validation failed: %w", err)
	}

	// Generate secure random suffix
	randomBytes := make([]byte, 16)
	if _, err := rand.Read(randomBytes); err != nil {
		return nil, "", fmt.Errorf("failed to generate random bytes: %w", err)
	}
	randomSuffix := base64.URLEncoding.EncodeToString(randomBytes)

	// Create temp file
	if pattern == "" {
		pattern = "tmp"
	}
	fileName := fmt.Sprintf("%s_%s", pattern, randomSuffix)
	fullPath := filepath.Join(validatedPath, fileName)

	file, err := os.Create(fullPath)
	if err != nil {
		return nil, "", fmt.Errorf("failed to create temp file: %w", err)
	}

	return file, fullPath, nil
}

// WriteFileSafe writes data to a file with fsync and removes partial files on error.
// This ensures crash safety: either the file is complete and synced, or it doesn't exist.
func WriteFileSafe(path string, data []byte, perm os.FileMode) error {
	f, err := os.OpenFile(filepath.Clean(path), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, perm)
	if err != nil {
		return fmt.Errorf("open: %w", err)
	}

	if _, err := f.Write(data); err != nil {
		f.Close()
		os.Remove(path) // remove partial write
		return fmt.Errorf("write: %w", err)
	}

	// CRITICAL: fsync ensures data is flushed to disk before returning
	if err := f.Sync(); err != nil {
		f.Close()
		os.Remove(path) // remove unsynced file
		return fmt.Errorf("fsync: %w", err)
	}

	// Close can fail on some filesystems (NFS, network-backed volumes)
	if err := f.Close(); err != nil {
		os.Remove(path) // remove file if close failed
		return fmt.Errorf("close: %w", err)
	}

	return nil
}

// OpenFileNoFollow opens a file with O_NOFOLLOW flag to prevent symlink attacks.
// This ensures the open fails if the final path component is a symlink.
// On Windows, this falls back to regular open (no O_NOFOLLOW equivalent).
func OpenFileNoFollow(path string, flag int, perm os.FileMode) (*os.File, error) {
	cleanPath := filepath.Clean(path)

	// Add O_NOFOLLOW on Unix systems
	// This causes open to fail if the final path component is a symlink
	flag |= O_NOFOLLOW

	return os.OpenFile(cleanPath, flag, perm)
}

// O_NOFOLLOW is the flag to prevent following symlinks.
// This is set per-platform in the build tags below.
const O_NOFOLLOW = o_NOFOLLOW

// ValidatePathStrict validates that a path is safe for use.
// It checks for path traversal attempts, resolves symlinks, and ensures
// the final resolved path stays within the base directory.
// Returns the cleaned, validated path or an error if validation fails.
func ValidatePathStrict(filePath, baseDir string) (string, error) {
	// Clean the input path
	cleanPath := filepath.Clean(filePath)

	// Reject absolute paths
	if filepath.IsAbs(cleanPath) {
		return "", fmt.Errorf("absolute paths not allowed: %s", filePath)
	}

	// Detect path traversal attempts
	if strings.HasPrefix(cleanPath, "..") || strings.Contains(cleanPath, "../") {
		return "", fmt.Errorf("path traversal attempt detected: %s", filePath)
	}

	// Build full path and verify it stays within base directory
	fullPath := filepath.Join(baseDir, cleanPath)
	resolvedPath, err := filepath.EvalSymlinks(fullPath)
	if err != nil {
		// If file doesn't exist yet, check the directory path
		dir := filepath.Dir(fullPath)
		resolvedDir, err := filepath.EvalSymlinks(dir)
		if err != nil {
			return "", fmt.Errorf("failed to resolve path: %w", err)
		}
		if !strings.HasPrefix(resolvedDir+string(filepath.Separator), baseDir+string(filepath.Separator)) {
			return "", fmt.Errorf("path %q escapes base directory", filePath)
		}
		// New file path is valid (parent directory is within base)
		return fullPath, nil
	}

	// Check resolved path is still within base directory
	if !strings.HasPrefix(resolvedPath+string(filepath.Separator), baseDir+string(filepath.Separator)) {
		return "", fmt.Errorf("resolved path %q escapes base directory", filePath)
	}

	return resolvedPath, nil
}

// SecureTempFile creates a temporary file with secure permissions (0600).
// Unlike os.CreateTemp which uses 0644, this ensures only the owner can read/write.
// The file is created in the specified directory with the given pattern.
func SecureTempFile(dir, pattern string) (*os.File, error) {
	if dir == "" {
		dir = os.TempDir()
	}

	// Ensure temp directory exists with secure permissions
	if err := os.MkdirAll(dir, 0700); err != nil {
		return nil, fmt.Errorf("failed to create temp directory: %w", err)
	}

	// Generate unique filename
	timestamp := time.Now().UnixNano()
	randomBytes := make([]byte, 8)
	if _, err := rand.Read(randomBytes); err != nil {
		return nil, fmt.Errorf("failed to generate random filename: %w", err)
	}
	unique := fmt.Sprintf("%d-%x", timestamp, randomBytes)

	filename := strings.Replace(pattern, "*", unique, 1)
	if filename == pattern {
		// No wildcard in pattern, append unique suffix
		filename = pattern + "." + unique
	}

	path := filepath.Join(dir, filename)

	// Create file with restrictive permissions (0600)
	// Use O_EXCL to prevent race conditions
	f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_RDWR, 0600)
	if err != nil {
		return nil, fmt.Errorf("failed to create secure temp file: %w", err)
	}

	return f, nil
}

// SecureFileRemove securely removes a file by overwriting it with zeros before deletion.
// This prevents recovery of sensitive data from the filesystem.
// For large files, this truncates to zero first, then removes.
func SecureFileRemove(path string) error {
	// Get file info
	info, err := os.Stat(path)
	if err != nil {
		if os.IsNotExist(err) {
			return nil // File doesn't exist, nothing to do
		}
		return fmt.Errorf("failed to stat file: %w", err)
	}

	// Skip directories and special files
	if !info.Mode().IsRegular() {
		return os.Remove(path)
	}

	// Open file for writing
	f, err := os.OpenFile(path, os.O_WRONLY, 0)
	if err != nil {
		return fmt.Errorf("failed to open file for secure removal: %w", err)
	}
	defer f.Close()

	// Overwrite with zeros (for files up to 10MB)
	const maxOverwriteSize = 10 * 1024 * 1024
	if info.Size() <= maxOverwriteSize {
		zeros := make([]byte, 4096)
		remaining := info.Size()
		for remaining > 0 {
			toWrite := int64(len(zeros))
			if remaining < toWrite {
				toWrite = remaining
			}
			if _, err := f.Write(zeros[:toWrite]); err != nil {
				// Continue to removal even if overwrite fails
				break
			}
			remaining -= toWrite
		}
		// Sync to ensure zeros hit disk
		f.Sync()
	}

	// Close and remove
	f.Close()
	return os.Remove(path)
}

// AtomicSymlink creates a symlink atomically with proper error handling.
// On Unix systems, it uses symlinkat with AT_SYMLINK_NOFOLLOW where available.
// This prevents race conditions and symlink attacks during creation.
func AtomicSymlink(oldPath, newPath string) error {
	// Clean both paths
	oldPath = filepath.Clean(oldPath)
	newPath = filepath.Clean(newPath)

	// Ensure target exists (but don't follow symlinks)
	_, err := os.Lstat(oldPath)
	if err != nil {
		return fmt.Errorf("symlink target %q does not exist: %w", oldPath, err)
	}

	// Remove existing symlink if it exists
	if info, err := os.Lstat(newPath); err == nil {
		if info.Mode()&os.ModeSymlink != 0 {
			if err := os.Remove(newPath); err != nil {
				return fmt.Errorf("failed to remove existing symlink: %w", err)
			}
		} else {
			return fmt.Errorf("newPath %q exists and is not a symlink", newPath)
		}
	}

	// Create the symlink
	if err := os.Symlink(oldPath, newPath); err != nil {
		return fmt.Errorf("failed to create symlink: %w", err)
	}

	return nil
}
