# internal functions for WaitForX

# Fallback message strings in en-US
DATA localizedData
{
    # culture = "en-US"
    ConvertFrom-StringData @'
        InvalidInputParam = Invalid input parameters!
        InvalidInputThrottle = Invalid input throttle limit!
        CheckRemoteState = Checking remote resource '{0}' state ...
        RemoteResourceNotReady = Remote resource '{0}' is not ready.
        RemoteResourceNotReadyAndRetry = Remote resource '{0}' is not ready. Retrying after {1} second(s)
        RemoteResourceReady = Remote resource '{0}' is ready
        RemoteConfigNotReady = Resource '{0}' on machine(s) '{1}' is not ready.
        RemoteConfigNotReadyWithRetry = Resource '{0}' on machine(s) '{1}' is not ready after '{2}' retries with retry interval of '{3}' second(s).
        NodeNameHasDuplicates = NodeName contains duplicate machine names! Check the NodeName and expected MinimalNumberOfMachineInState parameters.
        RemoteConnectivityFailure = Cannot create remote session to machine '{0}'
'@
}
Import-LocalizedData  LocalizedData -filename PSDSCxMachine.strings.psd1

$script:NodeInDesiredState = @()
$script:NodeNameIsDuplicated = $false

function Get-_InternalPSDscXMachineTR
{
    param
    (
        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $RemoteResourceId,

        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string[]] $RemoteMachine,
        
        [Uint32] $MinimalNumberOfMachineInState = $RemoteMachine.Length,

        [ValidateRange(1,[Uint64]::MaxValue)]
        [Uint64] $RetryIntervalSec = 1, 

        [Uint32] $RetryCount = 0, 
        
        [Uint32] $ThrottleLimit = 32
    )

    return @{
        ResourceName = $RemoteResourceId
        NodeName = $RemoteMachine
        RetryIntervalSec = $RetryIntervalSec
        RetryCount = $RetryCount
        ThrottleLimit = $ThrottleLimit
    }
}

function IsAccessDeniedIssue
{
   param ( $er)
   
   $ci = $er.CategoryInfo
   [xml]$xml = $er.Exception.Message
   return ($ci.TargetName -ilike "*MSFT_DscProxy") -and ($er.Exception.HResult -eq 0x80131509 ) -and ( -not (($xml.ChildNodes.Count -gt 0) -and ($xml.ChildNodes[0].Code -eq 5)))
}

function Set-_InternalPSDscXMachineTR
{
    param
    (
        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $RemoteResourceId,

        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string[]] $RemoteMachine,
        
        [Uint32] $MinimalNumberOfMachineInState = $RemoteMachine.Length,

        [ValidateRange(1,[Uint64]::MaxValue)]
        [Uint64] $RetryIntervalSec = 1, 

        [Uint32] $RetryCount = 0, 
        
        [Uint32] $ThrottleLimit = 32
    )

    if ($MinimalNumberOfMachineInState -gt $remoteMachine.Count)
    {
        throw $localizedData.InvalidInputParam
    }

    if ($ThrottleLimit -eq 0)
    {
        throw $localizedData.InvalidInputThrottle
    }

    $bState = $false
    $retry = 0

    Write-Debug -Message ($localizedData.CheckRemoteState -f $RemoteResourceId)
    for ($retry = 0; $retry -lt $RetryCount; $retry++)
    {
        Write-Verbose -Message ($localizedData.RemoteResourceNotReadyAndRetry -f $RemoteResourceId, $RetryIntervalSec)
        Sleep -Seconds $RetryIntervalSec

        $Iterations = [math]::Ceiling($RemoteMachine.Count / $ThrottleLimit)
        $initialItr = $retry * $Iterations
        for ($itr = $initialItr; $itr -lt $initialItr+$Iterations; $itr++)
        {
            try
            {
                $machinesToConnect = Get-MachinesToEnsureConnection -remoteMachine $remoteMachine -iteration $itr -throttleLimit $ThrottleLimit
                if ($machinesToConnect.Count -eq 0)
                {
                    $bState = $true;
                }
                else
                {
                    $bState = Get-RemoteResourceState -remoteMachine $machinesToConnect -RemoteResourceId $RemoteResourceId -MinimalNumberOfMachineInState $MinimalNumberOfMachineInState
                }
            }
            catch
            {
                if (IsAccessDeniedIssue -er $_) 
                {
                    Write-Debug -Message "Exception: $_"
                }
                else
                {
                    Write-Verbose -Message "Exception: $_"
                }
            }

            if ($bState)
            {
                Write-Verbose -Message ($localizedData.RemoteResourceReady -f $RemoteResourceId)
                return
            }
        }
    }
    if ($script:NodeNameIsDuplicated)
    {
        # reset $NodeNameIsDuplicated
        $script:NodeNameIsDuplicated = $false
        throw $localizedData.NodeNameHasDuplicates
    }
    else
    {
        if ($RetryCount -gt 0)
        {
           throw ($localizedData.RemoteConfigNotReadyWithRetry -f $RemoteResourceId, "$($RemoteMachine)", $RetryCount, $RetryIntervalSec )
        }
        else
        {
           throw ($localizedData.RemoteConfigNotReady -f $RemoteResourceId, "$($RemoteMachine)")
        }
    }
}

function Test-_InternalPSDscXMachineTR
{
    param
    (
        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $RemoteResourceId,

        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string[]] $RemoteMachine,
        
        [Uint32] $MinimalNumberOfMachineInState = $RemoteMachine.Length,

        [ValidateRange(1,[Uint64]::MaxValue)]
        [Uint64] $RetryIntervalSec = 1, 

        [Uint32] $RetryCount = 0, 
        
        [Uint32] $ThrottleLimit = 32
    )

    if ($MinimalNumberOfMachineInState -gt $remoteMachine.Count)
    {
        throw $localizedData.InvalidInputParam
    }

    if ($ThrottleLimit -eq 0)
    {
        throw $localizedData.InvalidInputThrottle
    }

    $bState = $false
    
    Write-Debug -Message ($localizedData.CheckRemoteState -f $RemoteResourceId)

    $script:NodeInDesiredState = @()
    
    $TotalRetryCount = 1
    $MaxRetryCount = 5
    for ($retry = 0; $retry -lt $TotalRetryCount; $retry++)   
    {
        if ($retry -gt 0) { Sleep -Milliseconds 200 }

        # according to algorithm described in Get-MachinesToEnusreConnection, the number of iterations to cover all machines at least once is calculated as stoping condition in following for loop
        for ($iteration = 0; $iteration -lt [math]::Ceiling($RemoteMachine.Count / $ThrottleLimit); $iteration++)
        {
            try
            {
                $machinesToConnect = Get-MachinesToEnsureConnection -remoteMachine $remoteMachine -iteration $iteration -throttleLimit $ThrottleLimit
                $bState = Get-RemoteResourceState -remoteMachine $machinesToConnect -RemoteResourceId $RemoteResourceId -MinimalNumberOfMachineInState $MinimalNumberOfMachineInState
            }
            catch
            {
                if (IsAccessDeniedIssue -er $_) 
                {
                    Write-Debug -Message "Exception: $_"
                    $TotalRetryCount = $MaxRetryCount
                }
                else
                {
                    Write-Verbose -Message "Exception: $_"
                    $TotalRetryCount = $retry   # stop TotalRetryCount as non-connectivity issue.
                }
            }

            if ($bState)
            {
                Write-Verbose -Message ($localizedData.RemoteResourceReady -f $RemoteResourceId)
                return $true
            }
        }
    }

    Write-Verbose -Message ($localizedData.RemoteResourceNotReady -f $RemoteResourceId)

    return $false
}

function Get-MachinesToEnsureConnection
{
    param(
        [ValidateNotNullOrEmpty()][string[]] $remoteMachine,
        [int] $iteration,
        [int] $throttleLimit
    )

    $allMachines = @()
    foreach ($machine in $remoteMachine)
    {
        if ($machine -notin $script:NodeInDesiredState)
        {
            $allMachines += $machine
        }
    }

    <# the algorithm works like this:
        suppose we have 10 machines (0-9) and throttle limit is 3
        following is the machines alively connected in each iteration
        iteration    machines
        0            0,1,2
        1            3,4,5
        2            6,7,8
        3            9,0,1
        4            2,3,4
        ...
    #>

    if ($throttleLimit -ge $allMachines.Count)
    {
        return $allMachines
    }
    else
    {
        $machinesToConnect = @()
        $start = ($iteration * $throttleLimit) % ($allMachines.Count)
        
        $count = 0
        for ($i = $start; $i -lt $allMachines.Count -and $count -lt $throttleLimit; $i++)
        {
            $machinesToConnect += $allMachines[$i]
            $count++
        }

        if ($count -lt $throttleLimit)
        {
            for ($i = 0; $i -lt ($throttleLimit - $count); $i++)
            {
                $machinesToConnect += $allMachines[$i]
            }
        }
        return $machinesToConnect
    }
}

function Get-RemoteResourceState
{
    param (
        
        [ValidateNotNullOrEmpty()][string[]] $remoteMachine,
        [ValidateNotNullOrEmpty()][string] $RemoteResourceId,
        [Uint32] $MinimalNumberOfMachineInState = $remoteMachine.Count
    )

    Write-Debug "Get-RemoteResourceState ..."

    if ($script:NodeInDesiredState.Count -eq $MinimalNumberOfMachineInState)
    {
        return $true
    }

    $remoteMachineException = $null

    foreach ($machine in $remoteMachine)
    {
        Write-Debug "Get resource state on remoteMachine: $machine"
        #Remove duplicate machine names from $remoteMachine list
        if ($script:NodeInDesiredState -contains $machine)
        {
            $script:NodeNameIsDuplicated = $true
            continue
        }
        try
        {
            if (Get-RemoteResourceStateOnMachine -RemoteResourceId $RemoteResourceId -MachineName $machine)
            {
                $script:NodeInDesiredState += $machine

                if ($script:NodeInDesiredState.Count -eq $MinimalNumberOfMachineInState)
                {
                    return $true
                }
            }
            else
            {
                Write-Debug "Resource $RemoteResourceId on machine $machine is not ready"
            }
        }
        catch
        {
               # continue here to iterate every machine from $remoteMachine list
              $remoteMachineException = $_
        }
    }
    # re-throw the original exception once browsing  all the machines is complete
    if($remoteMachineException -ne $null)
    {
        throw $remoteMachineException
    }

    $false
}

function Get-RemoteResourceStateOnMachine
{
    param (
       [ValidateNotNullOrEmpty()][string] $RemoteResourceId, 
       [ValidateNotNullOrEmpty()][string] $MachineName
    )

    Write-Debug "Get-RemoteResourceStateOnMachine on machine: $MachineName and resource: $RemoteRecourceId..."

    $buf = [Text.Encoding]::Unicode.GetBytes($RemoteResourceId)
    $data = [System.Convert]::ToBase64String($buf)

    $result = Invoke-WSManAction -ResourceURI http://schemas.microsoft.com/wbem/wsman/1/wmi/root/microsoft/windows/DesiredStateConfigurationProxy/MSFT_DscProxy `
                    -action GetResourceState -ComputerName $MachineName -valueset @{ConfigurationData = "$data" } `
                    -Authentication None
    $retV = $result.State

    Write-Debug "Get state ($retV) on machine: $MachineName and resource: $RemoteRecourceId"

    return $retV -ieq 'true'
}

Export-ModuleMember -Function *-_InternalPSDscXMachineTR

