"""Configuration management using Pydantic and YAML.

This module provides type-safe configuration management for the OS builder.
It uses Pydantic for validation and YAML for human-readable configuration
files. Configuration can be loaded from YAML files or environment variables.

Example:
    Load configuration from YAML:
        config = Config.from_yaml(Path("config.yaml"))
    
    Use default configuration:
        config = Config()
    
    Save configuration to YAML:
        config.to_yaml(Path("output.yaml"))
"""

import os
import yaml
from pathlib import Path
from typing import Optional, List
from pydantic import BaseModel, Field
from pydantic_settings import BaseSettings, SettingsConfigDict


class BuildSettings(BaseModel):
    """Build directory configuration settings.
    
    Defines the directory structure used during the build process. All paths
    are relative to the project root and are resolved to absolute paths
    when accessed via the Config properties.
    
    Attributes:
        build_dir: Main build output directory containing all build artifacts.
        chroot_dir: Directory where the Debian chroot environment is created.
        iso_dir: Staging directory for ISO contents before final image creation.
    
    Example:
        Custom build directories:
            build:
              build_dir: "custom_build"
              chroot_dir: "custom_build/chroot"
              iso_dir: "custom_build/iso"
    """
    build_dir: str = Field(default="build/os", description="Build output directory")
    chroot_dir: str = Field(default="build/os/chroot", description="Chroot directory")
    iso_dir: str = Field(default="build/os/iso", description="ISO staging directory")


class OSSettings(BaseModel):
    """Operating system configuration settings.
    
    Defines the target Debian release, system hostname, architecture, and
    packages to install. This configuration is used by the base module to
    create the chroot environment.
    
    Attributes:
        release: Debian release name (e.g., "trixie", "bookworm", "bullseye").
        hostname: System hostname that will be set in the built ISO.
        architecture: Target CPU architecture (e.g., "amd64", "arm64").
        packages: List of Debian package names to install in addition to
                 base packages. Base packages (live-boot, kernel, etc.) are
                 always installed automatically.
        desktop: Optional desktop environment package name. If None, no
                desktop environment is installed (minimal system).
    
    Example:
        Configure for Debian Trixie with custom packages:
            os:
              release: trixie
              hostname: my-node
              architecture: amd64
              packages:
                - curl
                - vim
                - htop
              desktop: null
    """
    release: str = Field(default="trixie", description="Debian release")
    hostname: str = Field(default="pymergetic-node", description="System hostname")
    architecture: str = Field(default="amd64", description="Target architecture")
    packages: List[str] = Field(
        default_factory=lambda: ["ca-certificates", "openssl", "curl", "screen"],
        description="Base packages to install"
    )
    desktop: Optional[str] = Field(default=None, description="Desktop environment (optional)")


class Config(BaseSettings):
    """Main configuration class using Pydantic Settings.
    
    This class manages all configuration for the OS builder, combining OS
    settings, build settings, and module configuration. It provides methods
    to load from YAML files and save to YAML files, with support for
    environment variable overrides.
    
    Configuration can be loaded via:
    - YAML file (via from_yaml() method)
    - Environment variables (PYMERGETIC_* prefix)
    - Default values (if nothing is specified)
    
    Attributes:
        os: OS configuration settings (release, hostname, packages, etc.).
        build: Build directory configuration.
        modules: List of module names to enable during the build process.
    
    Example:
        Load from YAML file:
            config = Config.from_yaml(Path("config.yaml"))
        
        Load from environment variable:
            os.environ["PYMERGETIC_OS_BUILD_CONFIG"] = "config.yaml"
            config = Config.from_yaml()
        
        Use default configuration:
            config = Config()
        
        Access configuration values:
            print(config.release)  # "trixie"
            print(config.build_dir)  # Path object
    """
    
    model_config = SettingsConfigDict(
        env_prefix="PYMERGETIC_",
        env_nested_delimiter="__",
        case_sensitive=False,
    )
    
    # OS settings
    os: OSSettings = Field(default_factory=OSSettings)
    
    # Build settings
    build: BuildSettings = Field(default_factory=BuildSettings)
    
    # Enabled modules (for future use)
    modules: List[str] = Field(default_factory=lambda: ["base"], description="Enabled modules")
    
    @classmethod
    def from_yaml(cls, config_file: Optional[Path] = None) -> "Config":
        """Load configuration from a YAML file.
        
        Loads and parses a YAML configuration file, merging it with default
        values. The configuration file must be provided either as a parameter
        or via the PYMERGETIC_OS_BUILD_CONFIG environment variable.
        
        If no configuration file is provided and the environment variable is
        not set, returns a Config instance with all default values.
        
        Args:
            config_file: Optional path to YAML configuration file. Can be a
                        string or Path object. If None, checks the
                        PYMERGETIC_OS_BUILD_CONFIG environment variable.
                        Defaults to None.
        
        Returns:
            Config: A new Config instance loaded from the YAML file or
                   with default values if no file is provided.
        
        Raises:
            FileNotFoundError: If the specified config file does not exist.
            yaml.YAMLError: If the YAML file is malformed or cannot be parsed.
            ValueError: If the configuration data is invalid or doesn't match
                       the expected schema.
        
        Example:
            Load from file path:
                config = Config.from_yaml(Path("pymergetic_os.yaml"))
            
            Load from environment variable:
                os.environ["PYMERGETIC_OS_BUILD_CONFIG"] = "config.yaml"
                config = Config.from_yaml()
            
            Use defaults:
                config = Config.from_yaml()  # No file, no env var
        """
        # Check environment variable if no file provided
        if config_file is None:
            env_config = os.getenv("PYMERGETIC_OS_BUILD_CONFIG")
            if env_config:
                config_file = Path(env_config)
            else:
                # No config provided - return defaults
                return cls()
        
        # Convert to Path if string
        if isinstance(config_file, str):
            config_file = Path(config_file)
        
        # Check if file exists
        if not config_file.exists():
            raise FileNotFoundError(f"Config file not found: {config_file}")
        
        # Load and parse YAML
        with open(config_file, 'r') as f:
            data = yaml.safe_load(f) or {}
        
        # Merge with defaults
        return cls(**data)
    
    def to_yaml(self, output_file) -> None:
        """Serialize configuration to YAML format.
        
        Writes the current configuration to a YAML file or file-like object.
        The output format matches the input format expected by from_yaml(),
        making it suitable for creating template configuration files.
        
        Args:
            output_file: Destination for YAML output. Can be:
                       - A string or Path object (file path)
                       - A file-like object (e.g., sys.stdout, open file)
        
        Raises:
            IOError: If the file cannot be written (when output_file is a path).
            AttributeError: If the file-like object doesn't support writing.
        
        Example:
            Save to file:
                config.to_yaml(Path("output.yaml"))
            
            Print to stdout:
                config.to_yaml(sys.stdout)
            
            Use in CLI sample command:
                config = Config()
                config.to_yaml(sys.stdout)  # Prints sample config
        """
        data = {
            'os': {
                'release': self.os.release,
                'hostname': self.os.hostname,
                'architecture': self.os.architecture,
                'packages': self.os.packages,
                'desktop': self.os.desktop,
            },
            'build': {
                'build_dir': str(self.build.build_dir),
                'chroot_dir': str(self.build.chroot_dir),
                'iso_dir': str(self.build.iso_dir),
            },
            'modules': self.modules,
        }
        
        # Handle Path objects or strings - open file
        if isinstance(output_file, (str, Path)):
            with open(output_file, 'w') as f:
                yaml.dump(data, f, default_flow_style=False, sort_keys=False)
        else:
            # File-like object (e.g., sys.stdout)
            yaml.dump(data, output_file, default_flow_style=False, sort_keys=False)
    
    # Convenience properties for backward compatibility
    @property
    def release(self) -> str:
        """Get the Debian release name.
        
        Returns:
            str: The Debian release (e.g., "trixie", "bookworm").
        """
        return self.os.release
    
    @property
    def hostname(self) -> str:
        """Get the system hostname.
        
        Returns:
            str: The hostname that will be set in the built ISO.
        """
        return self.os.hostname
    
    @property
    def architecture(self) -> str:
        """Get the target architecture.
        
        Returns:
            str: The CPU architecture (e.g., "amd64", "arm64").
        """
        return self.os.architecture
    
    @property
    def packages(self) -> List[str]:
        """Get the list of packages to install.
        
        Returns:
            List[str]: List of Debian package names.
        """
        return self.os.packages
    
    @property
    def desktop(self) -> Optional[str]:
        """Get the desktop environment package name.
        
        Returns:
            Optional[str]: Desktop environment package name, or None if
                          no desktop environment is configured.
        """
        return self.os.desktop
    
    @property
    def build_dir(self) -> Path:
        """Get the build directory as an absolute Path.
        
        Returns:
            Path: Absolute path to the build output directory.
        """
        return Path(self.build.build_dir).resolve()
    
    @property
    def chroot_dir(self) -> Path:
        """Get the chroot directory as an absolute Path.
        
        Returns:
            Path: Absolute path to the chroot directory.
        """
        return Path(self.build.chroot_dir).resolve()
    
    @property
    def iso_dir(self) -> Path:
        """Get the ISO staging directory as an absolute Path.
        
        Returns:
            Path: Absolute path to the ISO staging directory.
        """
        return Path(self.build.iso_dir).resolve()
