Private/Invoke-WinClatConfigurationChange.ps1

Function Invoke-WinClatConfigurationChange {

    <#

    .SYNOPSIS
        Applies confirmed global configuration plans and returns the verified results (pass 2).

    .DESCRIPTION
        The change pass shared by Set-WinClatConfiguration, Import-WinClatConfiguration, and Reset-WinClatConfiguration (DESIGN.md, sections 7.3, 7.7, and 7.8). Plans come from Get-WinClatConfigurationPlan, after the public function has confirmed them.

        Computers whose plans write the same settings are changed with one Invoke-WinClatRemote call, because it sends the same arguments to every computer in a call. The Set script writes the settings, reads the configuration back, and returns one WinClat.SetResult per setting. A warning is written for every written setting that Group Policy controls.

        Settings that already matched (the plan's Unchanged settings, from -OnlyChanged) are returned as Success rows built from the configuration read in pass 1, with EffectiveValueChanged $false and a message saying they were not written. No warning is written for them, because nothing was written. Reported settings (the plan's Reported settings, from -Report) are returned the same way, with no RequestedValue and the message from the plan.

        When Permit is written as disabled and the change takes effect, the Set script on the target says in the message whether CLAT stopped everywhere or a manually enabled instance is still translating, and how to stop it (DESIGN.md, section 7.3).

        Rows are returned per computer in plan order, with each computer's settings in the order Permit, Pref64FromDns, Pref64FromRA, AutoEnable.

    .PARAMETER Plan
        The WinClat.ConfigurationPlan objects to apply. Plans with no settings to write return only their unchanged rows.

    .PARAMETER Credential
        Credentials used to connect to remote computers.

    .PARAMETER ThrottleLimit
        Maximum number of concurrent remote connections.

    .PARAMETER Cmdlet
        The $PSCmdlet of the public function, so errors and warnings are attributed to it.

    .INPUTS
        None. This function does not accept pipeline input.

    .OUTPUTS
        WinClat.SetResult.

    .EXAMPLE
        Invoke-WinClatConfigurationChange -Plan $Confirmed -Cmdlet $PSCmdlet

        Applies the confirmed plans and returns one WinClat.SetResult per computer per setting.

    .LINK
        https://learn.microsoft.com/windows-server/administration/windows-commands/netsh

    .NOTES
        Version: 1.0.0
        Creation Date: October 28, 2026
        Last Updated: October 28, 2026
        Author: Richard Hicks
        Organization: Richard M. Hicks Consulting, Inc.
        Contact: rich@richardhicks.com
        Website: https://www.richardhicks.com/

    #>


    [CmdletBinding()]
    [OutputType('WinClat.SetResult')]

    Param (

        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [object[]]$Plan,

        [PSCredential]$Credential,

        [int]$ThrottleLimit = 32,

        [System.Management.Automation.PSCmdlet]$Cmdlet

    )

    If (-not $Cmdlet) {

        $Cmdlet = $PSCmdlet

    }

    $SettingOrder = @('Permit', 'Pref64FromDns', 'Pref64FromRA', 'AutoEnable')
    $Results = @{}
    $Batches = [ordered]@{}

    ForEach ($Item in $Plan) {

        $Results[$Item.ComputerName] = New-Object -TypeName 'System.Collections.Generic.List[object]'

        If ($Item.Change.Count -eq 0) {

            Continue

        }

        $Signature = @($SettingOrder | Where-Object -FilterScript { $Item.Change.ContainsKey($_) } | ForEach-Object -Process { "$_=$($Item.Change[$_])" }) -join ';'

        If (-not $Batches.Contains($Signature)) {

            $Batches[$Signature] = @{ Change = $Item.Change; Computers = New-Object -TypeName 'System.Collections.Generic.List[string]' }

        }

        $Batches[$Signature].Computers.Add($Item.ComputerName)

    }

    ForEach ($Batch in $Batches.Values) {

        $Change = $Batch.Change

        # One row per setting for a computer that could not be reached between the two passes. Invoke-WinClatRemote
        # calls it during this iteration, so it reads this batch's $Change.
        $NewFailure = {

            Param ($Name, $Status, $Message)

            ForEach ($Setting in $SettingOrder) {

                If ($Change.ContainsKey($Setting)) {

                    ConvertTo-WinClatSetResult -ComputerName $Name -Status $Status -Setting $Setting -RequestedValue $Change[$Setting] -Message $Message

                }

            }

        }

        $RemoteParameters = @{

            ComputerName     = $Batch.Computers.ToArray()
            ScriptBlock      = Get-WinClatSetConfigurationScript
            ArgumentList     = @($Change)
            ThrottleLimit    = $ThrottleLimit
            NewFailureObject = $NewFailure
            Cmdlet           = $Cmdlet

        }

        If ($Credential) {

            $RemoteParameters['Credential'] = $Credential

        }

        ForEach ($Row in @(Invoke-WinClatRemote @RemoteParameters)) {

            $Key = [string]$Row.ComputerName

            If (-not $Results.ContainsKey($Key)) {

                $Results[$Key] = New-Object -TypeName 'System.Collections.Generic.List[object]'

            }

            $Results[$Key].Add($Row)

        }

    }

    ForEach ($Item in $Plan) {

        $Rows = $Results[$Item.ComputerName]
        $Controlled = @($Item.Active.PolicyControlled -split ',\s*')

        ForEach ($Setting in $SettingOrder) {

            If ($Item.Reported -and $Item.Reported.ContainsKey($Setting)) {

                $Reported = @{

                    ComputerName          = $Item.ComputerName
                    Status                = 'Success'
                    Setting               = $Setting
                    PersistentValue       = $Item.Persistent.$Setting
                    EffectiveValue        = $Item.Active.$Setting
                    PolicyControlled      = $Controlled -contains $Setting
                    EffectiveValueChanged = $false
                    Message               = $Item.Reported[$Setting]

                }

                ConvertTo-WinClatSetResult @Reported

            }

            If ($Item.Unchanged.ContainsKey($Setting)) {

                $Unchanged = @{

                    ComputerName          = $Item.ComputerName
                    Status                = 'Success'
                    Setting               = $Setting
                    RequestedValue        = $Item.Unchanged[$Setting]
                    PersistentValue       = $Item.Persistent.$Setting
                    EffectiveValue        = $Item.Active.$Setting
                    PolicyControlled      = $Controlled -contains $Setting
                    EffectiveValueChanged = $false
                    Message               = 'The persistent value already matches, so it was not written.'

                }

                ConvertTo-WinClatSetResult @Unchanged

            }

            ForEach ($Row in @($Rows | Where-Object -FilterScript { $_.Setting -eq $Setting })) {

                $Row

                If ($Row.PolicyControlled -eq $true) {

                    $Cmdlet.WriteWarning("$($Row.ComputerName): Group Policy controls $($Row.Setting). The local value was written, but the policy decides the effective value (currently $($Row.EffectiveValue)).")

                }

            }

        }

    }

}