src/public/Configuration/Get-AitherConfigs.ps1

#Requires -Version 7.0

<#
.SYNOPSIS
    Analyze and ingest configuration from config.psd1

.DESCRIPTION
    Analyzes the config.psd1 file and provides easy access to configuration data.
    Can return the full configuration, specific sections, or print formatted output.
    This is the primary way to access configuration in automation scripts.

    This cmdlet automatically merges configuration from multiple sources in the correct order:
    1. Base config.psd1
    2. OS-specific config (config.windows.psd1, config.linux.psd1, config.macos.psd1)
    3. Local overrides (config.local.psd1)
    4. Custom config file (if specified)

    This ensures that local and OS-specific settings override base configuration appropriately.

.PARAMETER Section
    Optional section name to retrieve (e.g., 'Automation', 'Features', 'Core').
    If specified, returns only the requested section instead of the entire configuration.

    Common sections:
    - Automation: Script execution and orchestration settings
    - Features: Feature flags and dependencies
    - Core: Core platform settings
    - Logging: Logging configuration
    - EnvironmentConfiguration: Environment setup settings

.PARAMETER Key
    Optional key within a section to retrieve. Must be used together with -Section.
    Returns only the value of the specified key within the section.

    Example: Get-AitherConfigs -Section Features -Key Node
    Returns only the Node feature configuration.

.PARAMETER ConfigFile
    Path to a custom configuration file (defaults to config.psd1 in module root).
    If specified, this file is merged on top of all other configuration sources.
    Useful for environment-specific configurations or testing.

    The path can be absolute or relative to the module root.

.PARAMETER AsObject
    Return configuration as a PowerShell object (default behavior).
    This is the default and allows you to access configuration properties directly.

.PARAMETER Print
    Print formatted configuration to console instead of returning object.
    Useful for quickly viewing configuration without assigning to a variable.
    If Section or Key is specified, prints only that portion.

.PARAMETER Path
    Return only the path to the configuration file that would be used.
    Useful for verifying which config file will be loaded or for other file operations.

.INPUTS
    System.String
    You can pipe configuration file paths to Get-AitherConfigs.

.OUTPUTS
    Hashtable
    Returns configuration as a hashtable (PowerShell object) by default.

    System.String
    Returns a string path when -Path is used.

.EXAMPLE
    $config = Get-AitherConfigs
    $config.Automation.MaxConcurrency

    Gets the full configuration and accesses the MaxConcurrency setting.

.EXAMPLE
    $automation = Get-AitherConfigs -Section Automation
    $automation.MaxConcurrency

    Gets only the Automation section and accesses MaxConcurrency.

.EXAMPLE
    Get-AitherConfigs -Section Features -Key Node

    Gets only the Node feature configuration.

.EXAMPLE
    Get-AitherConfigs -Print

    Prints the entire configuration to the console in formatted JSON.

.EXAMPLE
    # In automation script
    $config = Get-AitherConfigs
    if ($config.Features.Node.Enabled) {
        # Install Node.js
    }

    Checks if Node feature is enabled before installing.

.EXAMPLE
    Get-AitherConfigs -ConfigFile './config.test.psd1'

    Loads configuration from a custom test configuration file.

.EXAMPLE
    './config.local.psd1' | Get-AitherConfigs

    Pipes a configuration file path to Get-AitherConfigs.

.NOTES
    This function is designed to be used by automation scripts to ingest configuration.
    It handles hierarchical merging of config files automatically.

    Configuration files use PowerShell Data (.psd1) format, which is a native PowerShell format
    that supports IntelliSense in IDEs and can be easily version controlled.

.LINK
    Set-AitherConfig
    Test-AitherConfig
    Export-AitherConfig
#>

function Get-AitherConfigs {
    [OutputType([Hashtable], [System.String])]
    [CmdletBinding(DefaultParameterSetName = 'Object')]
    param(
        [Parameter(ParameterSetName = 'Object')]
        [Parameter(ParameterSetName = 'Print')]
        [string]$Section,

        [Parameter(ParameterSetName = 'Object')]
        [Parameter(ParameterSetName = 'Print')]
        [string]$Key,

        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string]$ConfigFile,

        [Parameter(ParameterSetName = 'Object')]
        [switch]$AsObject,

        [Parameter(ParameterSetName = 'Print')]
        [switch]$Print,

        [Parameter(ParameterSetName = 'Path')]
        [switch]$Path
    )

    begin {
        # Get module root first
        # Try Get-AitherModuleRoot (private function, so use try/catch instead of Get-Command)
        try {
            $moduleRoot = Get-AitherModuleRoot
        }
        catch {
            # Fallback to finding AitherZero directory
            $curr = $PSScriptRoot
            while ($curr -and -not (Test-Path (Join-Path $curr 'AitherZero.psd1'))) {
                $curr = Split-Path $curr -Parent
            }
            $moduleRoot = if ($curr) { $curr } else { $env:AITHERZERO_ROOT }
        }

        # Import-ConfigDataFile is loaded from AitherZero/Private/ during module initialization
        # No need to import aithercore modules

        if (-not $ConfigFile) {
            # Try both possible config locations (handles running from repo root or module dir)
            $possiblePaths = @(
                (Join-Path $moduleRoot 'config/config.psd1'),           # When moduleRoot is AitherZero dir
                (Join-Path $moduleRoot 'AitherZero/config/config.psd1') # When moduleRoot is repo root
            )
            $ConfigFile = $possiblePaths | Where-Object { Test-Path $_ } | Select-Object -First 1
            if (-not $ConfigFile) {
                # Fallback to standard path if neither exists
                $ConfigFile = Join-Path $moduleRoot 'AitherZero/config/config.psd1'
            }
        }
        elseif (-not [System.IO.Path]::IsPathRooted($ConfigFile)) {
            $ConfigFile = Join-Path $moduleRoot $ConfigFile
        }

        # During module import validation, skip config loading if called with empty parameters
        if ($PSCmdlet.MyInvocation.InvocationName -eq '.' -and -not $Section -and -not $Key -and -not $Print -and -not $Path) {
            return
        }
    }

    process {
        try {
            # Return path if requested
            if ($Path) {
                return $ConfigFile
            }

            # Load and merge configuration files
            # Determine base path for config files (handle both repo root and module dir)
            $configBasePath = if (Test-Path (Join-Path $moduleRoot 'config/config.psd1')) {
                $moduleRoot  # moduleRoot is the AitherZero directory
            }
            else {
                Join-Path $moduleRoot 'AitherZero'  # moduleRoot is repo root
            }

            $baseConfigPath = Join-Path $configBasePath 'config/config.psd1'
            $localConfigPath = Join-Path $configBasePath 'config/config.local.psd1'

            # Detect OS for OS-specific configuration
            $osConfigPath = $null
            if ($IsWindows -or $PSVersionTable.Platform -eq 'Win32NT') {
                $osConfigPath = Join-Path $configBasePath 'config/config.windows.psd1'
            }
            elseif ($IsLinux) {
                $osConfigPath = Join-Path $configBasePath 'config/config.linux.psd1'
            }
            elseif ($IsMacOS) {
                $osConfigPath = Join-Path $configBasePath 'config/config.macos.psd1'
            }

            # Helper function to load config file
            function Import-ConfigDataFile {
                param([string]$Path)
                if (-not (Test-Path $Path)) {
                    return $null
                }
                try {
                    $content = Get-Content -Path $Path -Raw -ErrorAction Stop
                    if ([string]::IsNullOrWhiteSpace($content)) {
                        return $null
                    }
                    $scriptBlock = [scriptblock]::Create($content)
                    $data = & $scriptBlock
                    if ($data -is [hashtable]) {
                        # Validate hashtable has valid keys (empty hashtable is valid)
                        if ($data.Keys.Count -gt 0) {
                            $hasValidKeys = $false
                            foreach ($key in $data.Keys) {
                                if (-not [string]::IsNullOrWhiteSpace($key)) {
                                    $hasValidKeys = $true
                                    break
                                }
                            }
                            if (-not $hasValidKeys) {
                                Write-AitherLog -Level Warning -Message "Config file $Path contains only empty keys. Skipping." -Source 'Get-AitherConfigs'
                                return $null
                            }
                        }
                        # Empty hashtable is valid - return it
                        return $data
                    }
                    return $null
                }
                catch {
                    Write-AitherLog -Level Warning -Message "Failed to load config file $Path : $($_.Exception.Message)" -Source 'Get-AitherConfigs' -Exception $_
                    return $null
                }
            }

            # Helper function to deep merge hashtables
            function Merge-Configuration {
                param(
                    [hashtable]$Current,
                    [hashtable]$New
                )
                if (-not $Current) { return $New }
                if (-not $New) { return $Current }

                $merged = $Current.Clone()
                foreach ($key in $New.Keys) {
                    if ($merged.ContainsKey($key)) {
                        if ($merged[$key] -is [hashtable] -and $New[$key] -is [hashtable]) {
                            $merged[$key] = Merge-Configuration -Current $merged[$key] -New $New[$key]
                        }
                        else {
                            $merged[$key] = $New[$key]
                        }
                    }
                    else {
                        # Handle dot notation keys in $New (e.g., "AI.Ollama.Enabled")
                        if ($key -match '\.') {
                            $parts = $key.Split('.')
                            $currentLevel = $merged
                            for ($i = 0; $i -lt $parts.Count - 1; $i++) {
                                $part = $parts[$i]
                                if (-not $currentLevel.ContainsKey($part) -or $currentLevel[$part] -isnot [hashtable]) {
                                    $currentLevel[$part] = @{}
                                }
                                $currentLevel = $currentLevel[$part]
                            }
                            $finalKey = $parts[-1]
                            $currentLevel[$finalKey] = $New[$key]
                        }
                        else {
                            $merged[$key] = $New[$key]
                        }
                    }
                }
                return $merged
            }

            # Start with base configuration
            $config = $null
            if (Test-Path $baseConfigPath) {
                $config = Import-ConfigDataFile -Path $baseConfigPath
            }
            else {
                throw "Base configuration file not found: $baseConfigPath"
            }

            # Merge OS-specific configuration
            if ($osConfigPath -and (Test-Path $osConfigPath)) {
                $osConfig = Import-ConfigDataFile -Path $osConfigPath
                if ($osConfig) {
                    $config = Merge-Configuration -Current $config -New $osConfig
                }
            }

            # Merge domain configurations
            $domainsPath = Join-Path $moduleRoot 'AitherZero/config/domains'
            if (Test-Path $domainsPath) {
                $domainFiles = Get-ChildItem -Path $domainsPath -Filter '*.psd1' -File
                foreach ($file in $domainFiles) {
                    $domainConfig = Import-ConfigDataFile -Path $file.FullName
                    if ($domainConfig) {
                        $config = Merge-Configuration -Current $config -New $domainConfig
                    }
                }
            }

            # Merge local overrides
            if (Test-Path $localConfigPath) {
                $localConfig = Import-ConfigDataFile -Path $localConfigPath
                if ($localConfig) {
                    $config = Merge-Configuration -Current $config -New $localConfig
                }
            }

            # Merge custom config file (highest priority)
            if ($ConfigFile -and (Test-Path $ConfigFile)) {
                $customConfig = Import-ConfigDataFile -Path $ConfigFile
                if ($customConfig) {
                    $config = Merge-Configuration -Current $config -New $customConfig
                }
            }

            # Filter by section if specified
            if ($Section) {
                if ($config.ContainsKey($Section)) {
                    $config = $config[$Section]
                }
                else {
                    Write-AitherLog -Level Warning -Message "Section '$Section' not found in configuration" -Source 'Get-AitherConfigs'
                    return $null
                }
            }

            # Filter by key if specified
            if ($Key) {
                if ($config -is [hashtable] -and $config.ContainsKey($Key)) {
                    $config = $config[$Key]
                }
                else {
                    Write-AitherLog -Level Warning -Message "Key '$Key' not found in configuration" -Source 'Get-AitherConfigs'
                    return $null
                }
            }

            # Print or return
            if ($Print) {
                if ($Section -or $Key) {
                    $config | Format-List
                }
                else {
                    $config | ConvertTo-Json -Depth 10 | ForEach-Object { Write-AitherLog -Level Information -Message $_ -Source 'Get-AitherConfigs' }
                }
            }
            else {
                return $config
            }
        }
        catch {
            Invoke-AitherErrorHandler -ErrorRecord $_ -Operation "Loading configuration" -Parameters $PSBoundParameters -ThrowOnError
        }
    }

}