// Package installer provides installation utilities for MPV.Rocks.
// This file contains defensive validation utilities to help catch potential issues
// during development and improve code quality.
//
// SECURITY NOTE:
// These validation functions are DEFENSIVE PROGRAMMING UTILITIES, not security guarantees.
// Go's exec.Command already handles proper argument escaping, and the installer avoids shell commands.
// These utilities are intended for:
// - Catching accidental mistakes during development
// - Code review and linting
// - Preventing common path manipulation errors
//
// These should NOT be used as the sole security mechanism.
// They are designed to be permissive (block obvious issues) rather than restrictive (block everything).
package installer

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"strings"
)

// Unsafe characters that could cause issues in file paths
// \x00 (null byte) is particularly dangerous as it can cause silent truncation
const unsafePathChars = "\x00"

// Shell metacharacters that could enable command injection if shell commands were used
const shellMetachars = "`$;|<>()&\n\r\t"

// Maximum reasonable path length to prevent potential issues
const maxPathLength = 4096

// ValidatePath checks if a path contains potentially unsafe characters or patterns.
//
// This is a defensive check to catch obvious issues before file operations.
// It does NOT replace proper sanitization and is not a security guarantee.
//
// Purpose:
// - Detect null bytes that could cause path truncation
// - Validate reasonable path length
// - Normalize paths using filepath.Clean()
//
// Note: filepath.Join() and os.Open() already handle most path traversal attempts safely.
// This function is for catching accidental issues during development.
//
// Returns:
//   - nil if path passes validation
//   - error with descriptive message if validation fails
//
// Example:
//
//	err := ValidatePath("/home/user/.config/mpv")
//	if err != nil {
//	    return fmt.Errorf("invalid path: %w", err)
//	}
func ValidatePath(path string) error {
	if path == "" {
		return errors.New("path cannot be empty")
	}

	// Check for null bytes (most dangerous character)
	if strings.Contains(path, "\x00") {
		return errors.New("path contains null byte (\\x00) which can cause truncation")
	}

	// Check for other unsafe characters
	if ContainsSpecialChars(path, unsafePathChars) {
		return errors.New("path contains unsafe characters")
	}

	// Check path length
	if len(path) > maxPathLength {
		return fmt.Errorf("path length (%d) exceeds maximum allowed length (%d)", len(path), maxPathLength)
	}

	// Normalize path to check for obvious traversal patterns
	cleanPath := filepath.Clean(path)

	// Check for obvious relative path traversal (though filepath.Join handles this)
	// This is defensive - the actual operation would still be safe, but we warn developers
	if strings.Contains(cleanPath, "../") {
		// Don't error on this, just return info - it might be intentional
		// We're being permissive, not restrictive
		// This is logged/warned, not blocked
		// Commented out as we're being permissive:
		// return errors.New("path contains parent directory references (..)")
	}

	return nil
}

// ValidatePathForWrite performs additional validation for paths that will be written to.
//
// In addition to ValidatePath() checks, this validates:
// - Path is not a device file
// - Path is not a system directory
// - Path is reasonably safe for write operations
//
// Purpose:
// - Prevent accidental writes to system directories
// - Prevent accidental writes to device files
// - Catch configuration errors during development
//
// Note: The actual file write operations should still use proper error handling.
// This function is defensive to catch obvious mistakes early.
//
// Returns:
//   - nil if path passes validation
//   - error with descriptive message if validation fails
//
// Example:
//
//	err := ValidatePathForWrite("/home/user/.config/mpv/mpv.conf")
//	if err != nil {
//	    return fmt.Errorf("path not safe for writing: %w", err)
//	}
func ValidatePathForWrite(path string) error {
	// First, run basic path validation
	if err := ValidatePath(path); err != nil {
		return err
	}

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

	// Platform-specific system directory checks
	switch runtime.GOOS {
	case "windows":
		if err := validateWindowsPathForWrite(absPath); err != nil {
			return err
		}
	case "linux", "darwin":
		if err := validateUnixPathForWrite(absPath); err != nil {
			return err
		}
	default:
		// For unknown platforms, skip platform-specific checks
		// Be permissive rather than blocking on unfamiliar systems
	}

	return nil
}

// validateWindowsPathForWrite checks Windows-specific paths for safety.
//
// Blocks writes to:
// - Windows directory (C:\Windows)
// - System32 directory
// - Program Files directories
// - Root of drive (C:\)
//
// This is defensive to prevent accidental system modification.
func validateWindowsPathForWrite(absPath string) error {
	// Check for Windows directory
	if strings.Contains(strings.ToLower(absPath), `\windows\system32`) {
		return errors.New("cannot write to Windows system directory")
	}

	if strings.Contains(strings.ToLower(absPath), `\windows`) {
		return errors.New("cannot write to Windows directory")
	}

	// Check for Program Files
	if strings.Contains(strings.ToLower(absPath), `\program files`) {
		return errors.New("cannot write to Program Files directory")
	}

	// Check for root of drive (C:\, D:\, etc.)
	if len(absPath) == 3 && absPath[1] == ':' && (absPath[2] == '\\' || absPath[2] == '/') {
		return errors.New("cannot write to root of drive")
	}

	return nil
}

// validateUnixPathForWrite checks Unix-specific paths for safety.
//
// Blocks writes to:
// - /bin, /sbin, /usr/bin, /usr/sbin (executable directories)
// - /etc (system configuration)
// - /dev (device files)
// - /proc, /sys (pseudo-filesystems)
// - Root directory (/)
//
// This is defensive to prevent accidental system modification.
func validateUnixPathForWrite(absPath string) error {
	// List of system directories that should never be written to
	systemDirs := []string{
		"/bin", "/sbin",
		"/usr/bin", "/usr/sbin",
		"/usr/local/bin", "/usr/local/sbin",
		"/etc",
		"/dev",
		"/proc",
		"/sys",
		"/boot",
		"/lib", "/lib64",
	}

	// Check each system directory
	for _, sysDir := range systemDirs {
		if absPath == sysDir {
			return fmt.Errorf("cannot write to system directory: %s", sysDir)
		}
		if strings.HasPrefix(absPath, sysDir+"/") {
			return fmt.Errorf("cannot write to system directory: %s", sysDir)
		}
	}

	// Check for root directory
	if absPath == "/" {
		return errors.New("cannot write to root directory")
	}

	return nil
}

// ValidateShellCommand checks for shell metacharacters and command substitution patterns.
//
// SECURITY NOTE:
// This function is intended for CODE REVIEW and TESTING, not production use.
// The installer avoids shell commands entirely and uses Go's exec.Command with proper escaping.
// Use this function in tests or linters to detect accidental shell command usage.
//
// Checks for:
// - Shell metacharacters: `, $, ;, &, |, (, ), <, >, newlines, tabs
// - Command substitution: $(...) and backticks
// - Pipes and chaining
//
// Purpose:
// - Detect accidental shell command inclusion during code review
// - Catch patterns that would be dangerous if shell commands were used
// - Provide warnings for code that should be reviewed
//
// DO NOT use this in production code to "sanitize" commands.
// The correct approach is to NOT use shell commands at all.
//
// Returns:
//   - nil if no shell metacharacters found (command appears safe)
//   - error with descriptive message if shell metacharacters found
//
// Example (in tests):
//
//	// This should be caught during code review
//	cmd := "mpv --config=" + userConfigPath
//	if err := ValidateShellCommand(cmd); err != nil {
//	    t.Logf("WARNING: Shell command detected: %v", err)
//	    t.Logf("Use exec.Command with properly escaped arguments instead")
//	}
func ValidateShellCommand(cmd string) error {
	// Check for shell metacharacters
	if ContainsSpecialChars(cmd, shellMetachars) {
		return errors.New("command contains shell metacharacters that could enable injection")
	}

	// Check for command substitution with $()
	if strings.Contains(cmd, "$(") {
		return errors.New("command contains command substitution pattern $()")
	}

	// Check for backticks (another form of command substitution)
	if strings.Contains(cmd, "`") {
		return errors.New("command contains backticks for command substitution")
	}

	// Check for multiple commands chained with &&
	if strings.Contains(cmd, "&&") {
		return errors.New("command contains command chaining (&&)")
	}

	return nil
}

// ContainsSpecialChars checks if input contains any characters from the provided set.
//
// This is a helper function used by other validation functions.
//
// Parameters:
//   - input: string to check
//   - chars: string containing characters to look for (each character is checked individually)
//
// Returns:
//   - true if any character from chars is found in input
//   - false if input contains none of the specified characters
//
// Example:
//
//	unsafe := ContainsSpecialChars("/path/to/file", "\x00")
//	if unsafe {
//	    log.Println("Path contains null byte")
//	}
func ContainsSpecialChars(input, chars string) bool {
	if input == "" || chars == "" {
		return false
	}

	return strings.ContainsAny(input, chars)
}

// IsSafePathForConfig checks if a path is safe for storing configuration files.
//
// This is a convenience function that combines validation with config-specific checks.
// It's useful for validating paths that will store MPV configuration files.
//
// Parameters:
//   - path: path to validate
//
// Returns:
//   - nil if path is safe for config storage
//   - error with descriptive message if path is unsafe
//
// Example:
//
//	configPath := filepath.Join(homeDir, ".config", "mpv", "mpv.conf")
//	if err := IsSafePathForConfig(configPath); err != nil {
//	    return fmt.Errorf("unsafe config path: %w", err)
//	}
func IsSafePathForConfig(path string) error {
	// Basic validation
	if err := ValidatePathForWrite(path); err != nil {
		return err
	}

	// Additional config-specific checks
	absPath, err := filepath.Abs(path)
	if err != nil {
		return fmt.Errorf("failed to get absolute path: %w", err)
	}

	// Check if parent directory exists and is writable
	parentDir := filepath.Dir(absPath)
	if _, err := os.Stat(parentDir); os.IsNotExist(err) {
		// Parent directory doesn't exist - this might be okay
		// but we should create it first
		if err := os.MkdirAll(parentDir, 0755); err != nil {
			return fmt.Errorf("failed to create parent directory: %w", err)
		}
	}

	return nil
}
