"""Utility functions for OS builder operations.

This module provides low-level utility functions used throughout the OS
builder for command execution, directory management, and chroot operations.
These functions abstract common operations and provide consistent error
handling and logging.

Example:
    Run a command with sudo:
        run_command(['apt-get', 'update'], sudo=True)
    
    Ensure directory exists:
        build_dir = ensure_dir(Path("build/os"))
    
    Check if command is available:
        if check_command('debootstrap'):
            print("debootstrap is installed")
"""

import subprocess
import shutil
from pathlib import Path
from typing import Optional
from pymergetic.os.builder.console import print_command


def run_command(
    cmd: list[str],
    cwd: Optional[Path] = None,
    check: bool = True,
    sudo: bool = False,
    capture_output: bool = False,
) -> subprocess.CompletedProcess:
    """Execute a shell command with optional sudo and output capture.
    
    Runs a command using subprocess, with support for sudo execution,
    working directory changes, and output capture. All commands are
    logged to stdout for debugging purposes.
    
    Args:
        cmd: List of command and arguments (e.g., ['ls', '-la', '/tmp']).
        cwd: Optional working directory for the command. If None, uses
             current working directory. Defaults to None.
        check: If True, raises CalledProcessError on non-zero exit code.
               If False, returns CompletedProcess regardless of exit code.
               Defaults to True.
        sudo: If True, prepends 'sudo' to the command. Requires sudo
              privileges and passwordless sudo configuration. Defaults to False.
        capture_output: If True, captures stdout and stderr. If False,
                       output is streamed to terminal. Defaults to False.
    
    Returns:
        subprocess.CompletedProcess: Result object containing returncode,
                                    stdout, stderr, and other attributes.
    
    Raises:
        subprocess.CalledProcessError: If check=True and command returns
                                       non-zero exit code.
        FileNotFoundError: If the command executable is not found.
    
    Example:
        Run a simple command:
            result = run_command(['ls', '-la'])
        
        Run with sudo:
            run_command(['chown', 'root:root', '/file'], sudo=True)
        
        Capture output:
            result = run_command(['dpkg', '-L', 'package'],
                               capture_output=True)
            print(result.stdout.decode())
    """
    if sudo:
        cmd = ['sudo'] + cmd
    
    print_command(cmd)
    
    result = subprocess.run(
        cmd,
        cwd=str(cwd) if cwd else None,
        check=check,
        capture_output=capture_output,
    )
    
    return result


def ensure_dir(path: Path) -> Path:
    """Create directory and all parent directories if they don't exist.
    
    Creates the specified directory path, including any necessary parent
    directories. If the directory already exists, no error is raised.
    This is equivalent to `mkdir -p` in shell.
    
    Args:
        path: Path to the directory to create. Can be absolute or relative.
    
    Returns:
        Path: The same path object that was passed in, for method chaining.
    
    Raises:
        PermissionError: If the directory cannot be created due to
                       insufficient permissions.
        OSError: If the path exists but is not a directory.
    
    Example:
        Create build directory:
            build_dir = ensure_dir(Path("build/os"))
        
        Create nested directories:
            ensure_dir(Path("build/os/chroot/boot/grub"))
    """
    path.mkdir(parents=True, exist_ok=True)
    return path


def check_command(cmd: str) -> bool:
    """Check if a command is available in the system PATH.
    
    Uses shutil.which() to determine if the specified command exists
    and is executable. This is useful for checking prerequisites before
    running build operations.
    
    Args:
        cmd: Command name to check (e.g., 'debootstrap', 'mksquashfs').
    
    Returns:
        bool: True if the command is found in PATH, False otherwise.
    
    Example:
        Check if debootstrap is installed:
            if check_command('debootstrap'):
                print("debootstrap is available")
            else:
                print("Please install debootstrap")
    """
    return shutil.which(cmd) is not None


def require_command(cmd: str) -> None:
    """Require that a command is available, raising an error if not found.
    
    Checks if a command exists in PATH. If not found, raises RuntimeError
    with a descriptive message. This is used for validating build
    prerequisites.
    
    Args:
        cmd: Command name to check (e.g., 'grub-mkrescue').
    
    Raises:
        RuntimeError: If the command is not found in PATH.
    
    Example:
        Validate required tools:
            require_command('debootstrap')
            require_command('mksquashfs')
            require_command('grub-mkrescue')
    """
    if not check_command(cmd):
        raise RuntimeError(f"Required command not found: {cmd}")


def copy_tree(src: Path, dst: Path, sudo: bool = False) -> None:
    """Recursively copy a directory tree.
    
    Copies the entire directory tree from source to destination. If sudo
    is required (e.g., for copying files owned by root), uses the run_command
    function with sudo. Otherwise, uses shutil.copytree for efficiency.
    
    Args:
        src: Source directory path.
        dst: Destination directory path. Parent directories are created
             if they don't exist.
        sudo: If True, uses sudo for the copy operation. Defaults to False.
    
    Raises:
        FileNotFoundError: If the source directory does not exist.
        PermissionError: If insufficient permissions to read source or
                       write to destination.
        OSError: If destination already exists and is not a directory.
    
    Example:
        Copy assets to chroot:
            copy_tree(Path("assets"), Path("chroot/opt/assets"), sudo=True)
    """
    if sudo:
        run_command(['cp', '-r', str(src), str(dst)], sudo=True)
    else:
        shutil.copytree(src, dst, dirs_exist_ok=True)


def mount_chroot_filesystems(chroot_dir: Path) -> None:
    """Mount essential virtual filesystems in the chroot.
    
    Binds /proc, /sys, and /dev from the host into the chroot directory.
    These filesystems are required for many operations within the chroot,
    such as package installation, kernel module operations, and device access.
    
    The mounts use bind mounts, so they share the same underlying filesystem
    as the host. This is safe and necessary for chroot operations.
    
    Args:
        chroot_dir: Path to the chroot directory where filesystems will
                   be mounted.
    
    Note:
        - Requires sudo privileges
        - Mounts are created with check=False, so failures are logged but
          don't raise exceptions (some mounts may already exist)
        - Always call unmount_chroot_filesystems() when done
    
    Example:
        Mount filesystems before package installation:
            mount_chroot_filesystems(Path("build/os/chroot"))
            # ... perform chroot operations ...
            unmount_chroot_filesystems(Path("build/os/chroot"))
    """
    run_command(['mount', '--bind', '/proc', str(chroot_dir / 'proc')], sudo=True, check=False)
    run_command(['mount', '--bind', '/sys', str(chroot_dir / 'sys')], sudo=True, check=False)
    run_command(['mount', '--bind', '/dev', str(chroot_dir / 'dev')], sudo=True, check=False)


def unmount_chroot_filesystems(chroot_dir: Path) -> None:
    """Unmount virtual filesystems from the chroot.
    
    Unmounts /proc, /sys, and /dev that were previously mounted in the
    chroot directory. This should be called after chroot operations are
    complete to clean up mount points.
    
    Args:
        chroot_dir: Path to the chroot directory where filesystems are
                   mounted.
    
    Note:
        - Requires sudo privileges
        - Uses lazy unmount (-l flag equivalent) to handle busy filesystems
        - Failures are logged but don't raise exceptions (mounts may
          already be unmounted or not exist)
    
    Example:
        Clean up after chroot operations:
            unmount_chroot_filesystems(Path("build/os/chroot"))
    """
    run_command(['umount', str(chroot_dir / 'proc')], sudo=True, check=False)
    run_command(['umount', str(chroot_dir / 'sys')], sudo=True, check=False)
    run_command(['umount', str(chroot_dir / 'dev')], sudo=True, check=False)

