src/public/Configuration/Set-AitherConfig.ps1

#Requires -Version 7.0

<#
.SYNOPSIS
    Set configuration values in AitherZero configuration files

.DESCRIPTION
    Updates configuration values in config.psd1 or creates local overrides in config.local.psd1.
    This cmdlet allows you to modify configuration settings programmatically without manually
    editing configuration files.

    By default, changes are saved to config.local.psd1 (local overrides) which is gitignored
    and takes precedence over the main config.psd1. This prevents modifying version-controlled
    configuration files.

.PARAMETER Section
    Configuration section name. This parameter is REQUIRED and identifies which section
    of the configuration to modify.

    Common sections:
    - Core: Core platform settings (Environment, Profile, etc.)
    - Automation: Script execution and orchestration settings
    - Features: Feature flags and component settings
    - Logging: Logging configuration
    - SSHKeyManagement: SSH key management settings
    - PSSessionManagement: PSSession management settings

    Examples:
    - "Core"
    - "Automation"
    - "Features"

.PARAMETER Key
    Key within the section to set. This parameter is REQUIRED and can be a simple key
    or a nested key path using dot notation.

    Examples:
    - "Environment" - Simple key in Core section
    - "MaxConcurrency" - Simple key in Automation section
    - "Node.Enabled" - Nested key (Node feature's Enabled property)
    - "DefaultPort.WinRM" - Nested key in PSSessionManagement section

    For nested keys, use dot notation: "Section.Subsection.Key"

.PARAMETER Value
    Value to set for the configuration key. Can be any PowerShell object type:
    - Strings: "Production", "Development"
    - Booleans: $true, $false
    - Numbers: 8, 30, 3600
    - Arrays: @("Item1", "Item2")
    - Hashtables: @{ Key = "Value" }

    If not specified, the key will be removed from the configuration.

.PARAMETER Path
    Path to a specific configuration file. If not specified, defaults to config.local.psd1
    (for local overrides) or config.psd1 (if -Global is specified).

    Can be:
    - Relative path: "config.test.psd1"
    - Absolute path: "C:\Configs\custom.psd1"

    Use this to save to a custom configuration file for testing or environment-specific configs.

.PARAMETER Local
    Save to config.local.psd1 (default behavior). This is the recommended approach as
    config.local.psd1 is gitignored and won't affect version control.

    Local overrides take precedence over the main config.psd1, so your changes will
    be applied when configuration is loaded.

.PARAMETER Global
    Save to main config.psd1. Use with caution as this modifies version-controlled files.
    Only use this when you want to make permanent changes to the base configuration.

    Warning: Changes to config.psd1 will affect all users and environments unless overridden.

.INPUTS
    System.String
    You can pipe configuration section names to Set-AitherConfig.

.OUTPUTS
    PSCustomObject
    Returns the updated configuration section or key with the new value.

.EXAMPLE
    Set-AitherConfig -Section Core -Key Environment -Value 'Production'

    Sets the Environment setting in the Core section to 'Production' in config.local.psd1.

.EXAMPLE
    Set-AitherConfig -Section Automation -Key MaxConcurrency -Value 8

    Sets MaxConcurrency to 8 in the Automation section.

.EXAMPLE
    Set-AitherConfig -Section Features -Key Node -Key Enabled -Value $true

    Sets the Node feature's Enabled property to $true. Note: This example shows nested keys
    but the actual syntax uses dot notation in the Key parameter.

.EXAMPLE
    Set-AitherConfig -Section PSSessionManagement -Key "DefaultPort.WinRM" -Value 5985

    Sets a nested configuration value using dot notation.

.EXAMPLE
    "Core", "Automation" | Set-AitherConfig -Key "Verbose" -Value $true

    Sets the Verbose key in multiple sections by piping section names.

.EXAMPLE
    Set-AitherConfig -Section Logging -Key RetentionDays -Value 60 -Global

    Sets retention days in the main config.psd1 (use with caution).

.NOTES
    By default, saves to config.local.psd1 to avoid modifying version-controlled files.
    Local overrides are automatically merged when configuration is loaded via Get-AitherConfigs.

    Configuration precedence (highest to lowest):
    1. Command-line parameters
    2. Environment variables (AITHERZERO_*)
    3. config.local.psd1 (local overrides)
    4. config.psd1 (base configuration)

    Changes take effect immediately for new operations. Existing processes may need to
    reload configuration.

.LINK
    Get-AitherConfigs
    Test-AitherConfig
    Export-AitherConfig
    Compare-AitherConfig
#>

function Set-AitherConfig {
[OutputType([PSCustomObject])]
[CmdletBinding(SupportsShouldProcess)]
param(
    [Parameter(Mandatory=$false, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName, HelpMessage="Configuration section name (e.g., Core, Automation).")]
    [AllowEmptyString()]
    [ArgumentCompleter({
        param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)

        if (Get-Command Get-AitherConfigs -ErrorAction SilentlyContinue) {
            $config = Get-AitherConfigs
            $config.Keys |
                Where-Object { $_ -like "$wordToComplete*" } |
                ForEach-Object {
                    [System.Management.Automation.CompletionResult]::new(
                        $_,
                        $_,
                        [System.Management.Automation.CompletionResultType]::ParameterValue,
                        "Configuration Section"
                    )
                }
        }
    })]
    [string]$Section,

    [Parameter(Mandatory=$false, Position = 1, HelpMessage="Key within the section to set (dot notation supported).")]
    [AllowEmptyString()]
    [string]$Key,

    [Parameter(HelpMessage="Value to set for the configuration key.")]
    [object]$Value,

    [Parameter()]
    [string]$Path,

    [Parameter()]
    [switch]$Local,

    [Parameter()]
    [switch]$Global,

    [Parameter(HelpMessage = "Show command output in console.")]
    [switch]$ShowOutput
)

begin {
    # Manage logging targets for this execution
    $originalLogTargets = $script:AitherLogTargets
    if ($ShowOutput) {
        if ($script:AitherLogTargets -notcontains 'Console') {
            $script:AitherLogTargets += 'Console'
        }
    }
    else {
        # Ensure Console is NOT in targets if ShowOutput is not specified
        $script:AitherLogTargets = $script:AitherLogTargets | Where-Object { $_ -ne 'Console' }
    }

    $moduleRoot = Get-AitherModuleRoot

    if (-not $Path) {
        if ($Global) {
            $Path = Join-Path $moduleRoot 'AitherZero/config/config.psd1'
        }
        else {
            $Path = Join-Path $moduleRoot 'AitherZero/config/config.local.psd1'
        }
    }
    elseif (-not [System.IO.Path]::IsPathRooted($Path)) {
        $Path = Join-Path $moduleRoot $Path
    }
}

process { try {
        # During module validation, parameters may be empty - skip validation
        if ($PSCmdlet.MyInvocation.InvocationName -eq '.' -and ([string]::IsNullOrWhiteSpace($Section) -or [string]::IsNullOrWhiteSpace($Key))) {
            return
        }

        # Validate required parameters
        if ([string]::IsNullOrWhiteSpace($Section)) {
            throw "Section parameter is required"
        }
        if ([string]::IsNullOrWhiteSpace($Key)) {
            throw "Key parameter is required"
        }

        # Load existing config or create new
        $config = @{}
        if (Test-Path $Path) {
            try {
                $content = Get-Content -Path $Path -Raw
                if (-not [string]::IsNullOrWhiteSpace($content)) {
                    $scriptBlock = [scriptblock]::Create($content)
                    $config = & $scriptBlock
                    if (-not ($config -is [hashtable])) {
                        $config = @{}
                    }
                    # Validate config has valid keys
                    $hasValidKeys = $false
                    foreach ($k in $config.Keys) {
                        if (-not [string]::IsNullOrWhiteSpace($k)) {
                            $hasValidKeys = $true
                            break
                        }
                    }
                    if (-not $hasValidKeys) {
                        $config = @{}
                    }
                }
            }
            catch {
                Write-AitherLog -Level Warning -Message "Failed to load existing config, creating new: $_" -Source 'Set-AitherConfig' -Exception $_
                $config = @{}
            }
        }

        # Ensure section exists
        if (-not $config.ContainsKey($Section)) {
            $config[$Section] = @{}
        }

        # Set value
        if ($config[$Section] -isnot [hashtable]) {
            $config[$Section] = @{}
        }

        # Handle nested keys (dot notation)
        $current = $config[$Section]
        $keyParts = $Key.Split('.')

        for ($i = 0; $i -lt $keyParts.Count - 1; $i++) {
            $part = $keyParts[$i]
            if (-not $current.ContainsKey($part) -or $current[$part] -isnot [hashtable]) {
                $current[$part] = @{}
            }
            $current = $current[$part]
        }

        $finalKey = $keyParts[-1]
        $current[$finalKey] = $Value

        # Convert to PowerShell data file format
        function ConvertTo-PowerShellData {
            param([hashtable]$Hashtable, [int]$Depth = 0)

            $indent = ' ' * $Depth
            $result = "@{`n"

            foreach ($key in $Hashtable.Keys | Sort-Object) {
                $value = $Hashtable[$key]

                if ($value -is [hashtable]) {
                    $result += "$indent $key = $(ConvertTo-PowerShellData -Hashtable $value -Depth ($Depth + 1))`n"
                }
                elseif ($value -is [array]) {
                    $arrayStr = $value | ForEach-Object {
                        if ($_ -is [string]) {
                            "'$_'"
                        }
                        elseif ($_ -is [bool]) {
                            "`$$_"
                        }
                        else {
                            $_
                        }
                    }
                    $result += "$indent $key = @($($arrayStr -join ', '))`n"
                }
                elseif ($value -is [string]) {
                    $result += "$indent $key = '$value'`n"
                }
                elseif ($value -is [bool]) {
                    $result += "$indent $key = `$$value`n"
                }
                else {
                    $result += "$indent $key = $value`n"
                }
            }

            $result += "$indent}"
            return $result
        }

        # Save config
        $content = ConvertTo-PowerShellData -Hashtable $config

        if ($PSCmdlet.ShouldProcess($Path, "Update configuration")) {
            Set-Content -Path $Path -Value $content -Encoding UTF8
            Write-AitherLog -Message "Configuration updated: $Section.$Key = $Value" -Level Information -Source 'Set-AitherConfig'
            Write-AitherLog -Message "Saved to: $Path" -Level Information -Source 'Set-AitherConfig'
        }
    }
    catch {
        Invoke-AitherErrorHandler -ErrorRecord $_ -Operation "Setting configuration: $Section.$Key" -Parameters $PSBoundParameters -ThrowOnError
    }
    finally {
        # Restore original log targets
        $script:AitherLogTargets = $originalLogTargets
    }
}

}