CleanupMonster.psm1

function ConvertFrom-DistinguishedName { 
    <#
    .SYNOPSIS
    Converts a Distinguished Name to CN, OU, Multiple OUs or DC
 
    .DESCRIPTION
    Converts a Distinguished Name to CN, OU, Multiple OUs or DC
 
    .PARAMETER DistinguishedName
    Distinguished Name to convert
 
    .PARAMETER ToOrganizationalUnit
    Converts DistinguishedName to Organizational Unit
 
    .PARAMETER ToDC
    Converts DistinguishedName to DC
 
    .PARAMETER ToDomainCN
    Converts DistinguishedName to Domain Canonical Name (CN)
 
    .PARAMETER ToCanonicalName
    Converts DistinguishedName to Canonical Name
 
    .EXAMPLE
    $DistinguishedName = 'CN=Przemyslaw Klys,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz'
    ConvertFrom-DistinguishedName -DistinguishedName $DistinguishedName -ToOrganizationalUnit
 
    Output:
    OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz
 
    .EXAMPLE
    $DistinguishedName = 'CN=Przemyslaw Klys,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz'
    ConvertFrom-DistinguishedName -DistinguishedName $DistinguishedName
 
    Output:
    Przemyslaw Klys
 
    .EXAMPLE
    ConvertFrom-DistinguishedName -DistinguishedName 'OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToMultipleOrganizationalUnit -IncludeParent
 
    Output:
    OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz
    OU=Production,DC=ad,DC=evotec,DC=xyz
 
    .EXAMPLE
    ConvertFrom-DistinguishedName -DistinguishedName 'OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToMultipleOrganizationalUnit
 
    Output:
    OU=Production,DC=ad,DC=evotec,DC=xyz
 
    .EXAMPLE
    $Con = @(
        'CN=Windows Authorization Access Group,CN=Builtin,DC=ad,DC=evotec,DC=xyz'
        'CN=Mmm,DC=elo,CN=nee,DC=RootDNSServers,CN=MicrosoftDNS,CN=System,DC=ad,DC=evotec,DC=xyz'
        'CN=e6d5fd00-385d-4e65-b02d-9da3493ed850,CN=Operations,CN=DomainUpdates,CN=System,DC=ad,DC=evotec,DC=xyz'
        'OU=Domain Controllers,DC=ad,DC=evotec,DC=pl'
        'OU=Microsoft Exchange Security Groups,DC=ad,DC=evotec,DC=xyz'
    )
 
    ConvertFrom-DistinguishedName -DistinguishedName $Con -ToLastName
 
    Output:
    Windows Authorization Access Group
    Mmm
    e6d5fd00-385d-4e65-b02d-9da3493ed850
    Domain Controllers
    Microsoft Exchange Security Groups
 
    .EXAMPLEE
    ConvertFrom-DistinguishedName -DistinguishedName 'DC=ad,DC=evotec,DC=xyz' -ToCanonicalName
    ConvertFrom-DistinguishedName -DistinguishedName 'OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToCanonicalName
    ConvertFrom-DistinguishedName -DistinguishedName 'CN=test,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToCanonicalName
 
    Output:
    ad.evotec.xyz
    ad.evotec.xyz\Production\Users
    ad.evotec.xyz\Production\Users\test
 
    .NOTES
    General notes
    #>

    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param(
        [Parameter(ParameterSetName = 'ToOrganizationalUnit')]
        [Parameter(ParameterSetName = 'ToMultipleOrganizationalUnit')]
        [Parameter(ParameterSetName = 'ToDC')]
        [Parameter(ParameterSetName = 'ToDomainCN')]
        [Parameter(ParameterSetName = 'Default')]
        [Parameter(ParameterSetName = 'ToLastName')]
        [Parameter(ParameterSetName = 'ToCanonicalName')]
        [alias('Identity', 'DN')][Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)][string[]] $DistinguishedName,
        [Parameter(ParameterSetName = 'ToOrganizationalUnit')][switch] $ToOrganizationalUnit,
        [Parameter(ParameterSetName = 'ToMultipleOrganizationalUnit')][alias('ToMultipleOU')][switch] $ToMultipleOrganizationalUnit,
        [Parameter(ParameterSetName = 'ToMultipleOrganizationalUnit')][switch] $IncludeParent,
        [Parameter(ParameterSetName = 'ToDC')][switch] $ToDC,
        [Parameter(ParameterSetName = 'ToDomainCN')][switch] $ToDomainCN,
        [Parameter(ParameterSetName = 'ToLastName')][switch] $ToLastName,
        [Parameter(ParameterSetName = 'ToCanonicalName')][switch] $ToCanonicalName
    )
    Process {
        foreach ($Distinguished in $DistinguishedName) {
            if ($ToDomainCN) {
                $DN = $Distinguished -replace '.*?((DC=[^=]+,)+DC=[^=]+)$', '$1'
                $CN = $DN -replace ',DC=', '.' -replace "DC="
                if ($CN) {
                    $CN
                }
            } elseif ($ToOrganizationalUnit) {
                $Value = [Regex]::Match($Distinguished, '(?=OU=)(.*\n?)(?<=.)').Value
                if ($Value) {
                    $Value
                }
            } elseif ($ToMultipleOrganizationalUnit) {
                if ($IncludeParent) {
                    $Distinguished
                }
                while ($true) {

                    $Distinguished = $Distinguished -replace '^.+?,(?=..=)'
                    if ($Distinguished -match '^DC=') {
                        break
                    }
                    $Distinguished
                }
            } elseif ($ToDC) {

                $Value = $Distinguished -replace '.*?((DC=[^=]+,)+DC=[^=]+)$', '$1'
                if ($Value) {
                    $Value
                }
            } elseif ($ToLastName) {

                $NewDN = $Distinguished -split ",DC="
                if ($NewDN[0].Contains(",OU=")) {
                    [Array] $ChangedDN = $NewDN[0] -split ",OU="
                } elseif ($NewDN[0].Contains(",CN=")) {
                    [Array] $ChangedDN = $NewDN[0] -split ",CN="
                } else {
                    [Array] $ChangedDN = $NewDN[0]
                }
                if ($ChangedDN[0].StartsWith('CN=')) {
                    $ChangedDN[0] -replace 'CN=', ''
                } else {
                    $ChangedDN[0] -replace 'OU=', ''
                }
            } elseif ($ToCanonicalName) {
                $Domain = $null
                $Rest = $null
                foreach ($O in $Distinguished -split '(?<!\\),') {
                    if ($O -match '^DC=') {
                        $Domain += $O.Substring(3) + '.'
                    } else {
                        $Rest = $O.Substring(3) + '\' + $Rest
                    }
                }
                if ($Domain -and $Rest) {
                    $Domain.Trim('.') + '\' + ($Rest.TrimEnd('\') -replace '\\,', ',')
                } elseif ($Domain) {
                    $Domain.Trim('.')
                } elseif ($Rest) {
                    $Rest.TrimEnd('\') -replace '\\,', ','
                }
            } else {
                $Regex = '^CN=(?<cn>.+?)(?<!\\),(?<ou>(?:(?:OU|CN).+?(?<!\\),)+(?<dc>DC.+?))$'

                $Found = $Distinguished -match $Regex
                if ($Found) {
                    $Matches.cn
                }
            }
        }
    }
}
function ConvertTo-OperatingSystem { 
    <#
    .SYNOPSIS
    Allows easy conversion of OperatingSystem, Operating System Version to proper Windows 10 naming based on WMI or AD
 
    .DESCRIPTION
    Allows easy conversion of OperatingSystem, Operating System Version to proper Windows 10 naming based on WMI or AD
 
    .PARAMETER OperatingSystem
    Operating System as returned by Active Directory
 
    .PARAMETER OperatingSystemVersion
    Operating System Version as returned by Active Directory
 
    .EXAMPLE
    $Computers = Get-ADComputer -Filter * -Properties OperatingSystem, OperatingSystemVersion | ForEach-Object {
        $OPS = ConvertTo-OperatingSystem -OperatingSystem $_.OperatingSystem -OperatingSystemVersion $_.OperatingSystemVersion
        Add-Member -MemberType NoteProperty -Name 'OperatingSystemTranslated' -Value $OPS -InputObject $_ -Force
        $_
    }
    $Computers | Select-Object DNS*, Name, SamAccountName, Enabled, OperatingSystem*, DistinguishedName | Format-Table
 
    .EXAMPLE
    $Registry = Get-PSRegistry -ComputerName 'AD1' -RegistryPath 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
    ConvertTo-OperatingSystem -OperatingSystem $Registry.ProductName -OperatingSystemVersion $Registry.CurrentBuildNumber
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [string] $OperatingSystem,
        [string] $OperatingSystemVersion
    )

    if ($OperatingSystem -like 'Windows 10*' -or $OperatingSystem -like 'Windows 11*') {
        $Systems = @{

            '10.0 (22621)' = 'Windows 11 22H2'
            '10.0 (22000)' = 'Windows 11 21H2'
            '10.0 (19045)' = 'Windows 10 22H2'
            '10.0 (19044)' = 'Windows 10 21H2'
            '10.0 (19043)' = 'Windows 10 21H1'
            '10.0 (19042)' = 'Windows 10 20H2'
            '10.0 (19041)' = 'Windows 10 2004'
            '10.0 (18898)' = 'Windows 10 Insider Preview'
            '10.0 (18363)' = "Windows 10 1909"
            '10.0 (18362)' = "Windows 10 1903"
            '10.0 (17763)' = "Windows 10 1809"
            '10.0 (17134)' = "Windows 10 1803"
            '10.0 (16299)' = "Windows 10 1709"
            '10.0 (15063)' = "Windows 10 1703"
            '10.0 (14393)' = "Windows 10 1607"
            '10.0 (10586)' = "Windows 10 1511"
            '10.0 (10240)' = "Windows 10 1507"

            '10.0.22621'   = 'Windows 11 22H2'
            '10.0.22000'   = 'Windows 11 21H2'
            '10.0.19045'   = 'Windows 10 22H2'
            '10.0.19044'   = 'Windows 10 21H2'
            '10.0.19043'   = 'Windows 10 21H1'
            '10.0.19042'   = 'Windows 10 20H2'
            '10.0.19041'   = 'Windows 10 2004'
            '10.0.18898'   = 'Windows 10 Insider Preview'
            '10.0.18363'   = "Windows 10 1909"
            '10.0.18362'   = "Windows 10 1903"
            '10.0.17763'   = "Windows 10 1809"
            '10.0.17134'   = "Windows 10 1803"
            '10.0.16299'   = "Windows 10 1709"
            '10.0.15063'   = "Windows 10 1703"
            '10.0.14393'   = "Windows 10 1607"
            '10.0.10586'   = "Windows 10 1511"
            '10.0.10240'   = "Windows 10 1507"

            '22621'        = 'Windows 11 22H2'
            '22000'        = 'Windows 11 21H2'
            '19045'        = 'Windows 10 22H2'
            '19044'        = 'Windows 10 21H2'
            '19043'        = 'Windows 10 21H1'
            '19042'        = 'Windows 10 20H2'
            '19041'        = 'Windows 10 2004'
            '18898'        = 'Windows 10 Insider Preview'
            '18363'        = "Windows 10 1909"
            '18362'        = "Windows 10 1903"
            '17763'        = "Windows 10 1809"
            '17134'        = "Windows 10 1803"
            '16299'        = "Windows 10 1709"
            '15063'        = "Windows 10 1703"
            '14393'        = "Windows 10 1607"
            '10586'        = "Windows 10 1511"
            '10240'        = "Windows 10 1507"
        }
        $System = $Systems[$OperatingSystemVersion]
        if (-not $System) {
            $System = $OperatingSystemVersion
        }
    } elseif ($OperatingSystem -like 'Windows Server*') {

        $Systems = @{

            '10.0 (20348)' = 'Windows Server 2022'
            '10.0 (19042)' = 'Windows Server 2019 20H2'
            '10.0 (19041)' = 'Windows Server 2019 2004'
            '10.0 (18363)' = 'Windows Server 2019 1909'
            '10.0 (18362)' = "Windows Server 2019 1903" 
            '10.0 (17763)' = "Windows Server 2019 1809" 
            '10.0 (17134)' = "Windows Server 2016 1803" 
            '10.0 (14393)' = "Windows Server 2016 1607"
            '6.3 (9600)'   = 'Windows Server 2012 R2'
            '6.1 (7601)'   = 'Windows Server 2008 R2'
            '5.2 (3790)'   = 'Windows Server 2003'

            '10.0.20348'   = 'Windows Server 2022'
            '10.0.19042'   = 'Windows Server 2019 20H2'
            '10.0.19041'   = 'Windows Server 2019 2004'
            '10.0.18363'   = 'Windows Server 2019 1909'
            '10.0.18362'   = "Windows Server 2019 1903" 
            '10.0.17763'   = "Windows Server 2019 1809"  
            '10.0.17134'   = "Windows Server 2016 1803" 
            '10.0.14393'   = "Windows Server 2016 1607"
            '6.3.9600'     = 'Windows Server 2012 R2'
            '6.1.7601'     = 'Windows Server 2008 R2' 
            '5.2.3790'     = 'Windows Server 2003' 

            '20348'        = 'Windows Server 2022'
            '19042'        = 'Windows Server 2019 20H2'
            '19041'        = 'Windows Server 2019 2004'
            '18363'        = 'Windows Server 2019 1909'
            '18362'        = "Windows Server 2019 1903" 
            '17763'        = "Windows Server 2019 1809" 
            '17134'        = "Windows Server 2016 1803" 
            '14393'        = "Windows Server 2016 1607"
            '9600'         = 'Windows Server 2012 R2'
            '7601'         = 'Windows Server 2008 R2'
            '3790'         = 'Windows Server 2003'
        }
        $System = $Systems[$OperatingSystemVersion]
        if (-not $System) {
            $System = $OperatingSystemVersion
        }
    } else {
        $System = $OperatingSystem
    }
    if ($System) {
        $System
    } else {
        'Unknown'
    }
}
function Get-GitHubVersion { 
    <#
    .SYNOPSIS
    Get the latest version of a GitHub repository and compare with local version
 
    .DESCRIPTION
    Get the latest version of a GitHub repository and compare with local version
 
    .PARAMETER Cmdlet
    Cmdlet to find module for
 
    .PARAMETER RepositoryOwner
    Repository owner
 
    .PARAMETER RepositoryName
    Repository name
 
    .EXAMPLE
    Get-GitHubVersion -Cmdlet 'Start-DelegationModel' -RepositoryOwner 'evotecit' -RepositoryName 'DelegationModel'
 
    .NOTES
    General notes
    #>

    [cmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $Cmdlet,
        [Parameter(Mandatory)][string] $RepositoryOwner,
        [Parameter(Mandatory)][string] $RepositoryName
    )
    $App = Get-Command -Name $Cmdlet -ErrorAction SilentlyContinue
    if ($App) {
        [Array] $GitHubReleases = (Get-GitHubLatestRelease -Url "https://api.github.com/repos/$RepositoryOwner/$RepositoryName/releases" -Verbose:$false)
        $LatestVersion = $GitHubReleases[0]
        if (-not $LatestVersion.Errors) {
            if ($App.Version -eq $LatestVersion.Version) {
                "Current/Latest: $($LatestVersion.Version) at $($LatestVersion.PublishDate)"
            } elseif ($App.Version -lt $LatestVersion.Version) {
                "Current: $($App.Version), Published: $($LatestVersion.Version) at $($LatestVersion.PublishDate). Update?"
            } elseif ($App.Version -gt $LatestVersion.Version) {
                "Current: $($App.Version), Published: $($LatestVersion.Version) at $($LatestVersion.PublishDate). Lucky you!"
            }
        } else {
            "Current: $($App.Version)"
        }
    } else {
        "Current: Unknown"
    }
}
function Get-WinADForestDetails { 
    [CmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [string[]] $ExcludeDomainControllers,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [alias('DomainControllers', 'ComputerName')][string[]] $IncludeDomainControllers,
        [switch] $SkipRODC,
        [string] $Filter = '*',
        [switch] $TestAvailability,
        [ValidateSet('All', 'Ping', 'WinRM', 'PortOpen', 'Ping+WinRM', 'Ping+PortOpen', 'WinRM+PortOpen')] $Test = 'All',
        [int[]] $Ports = 135,
        [int] $PortsTimeout = 100,
        [int] $PingCount = 1,
        [switch] $PreferWritable,
        [switch] $Extended,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    if ($Global:ProgressPreference -ne 'SilentlyContinue') {
        $TemporaryProgress = $Global:ProgressPreference
        $Global:ProgressPreference = 'SilentlyContinue'
    }

    if (-not $ExtendedForestInformation) {

        $Findings = [ordered] @{ }
        try {
            if ($Forest) {
                $ForestInformation = Get-ADForest -ErrorAction Stop -Identity $Forest
            } else {
                $ForestInformation = Get-ADForest -ErrorAction Stop
            }
        } catch {
            Write-Warning "Get-WinADForestDetails - Error discovering DC for Forest - $($_.Exception.Message)"
            return
        }
        if (-not $ForestInformation) {
            return
        }
        $Findings['Forest'] = $ForestInformation
        $Findings['ForestDomainControllers'] = @()
        $Findings['QueryServers'] = @{ }
        $Findings['DomainDomainControllers'] = @{ }
        [Array] $Findings['Domains'] = foreach ($Domain in $ForestInformation.Domains) {
            if ($IncludeDomains) {
                if ($Domain -in $IncludeDomains) {
                    $Domain.ToLower()
                }

                continue
            }
            if ($Domain -notin $ExcludeDomains) {
                $Domain.ToLower()
            }
        }

        [Array] $DomainsActive = foreach ($Domain in $Findings['Forest'].Domains) {
            try {
                $DC = Get-ADDomainController -DomainName $Domain -Discover -ErrorAction Stop -Writable:$PreferWritable.IsPresent

                $OrderedDC = [ordered] @{
                    Domain      = $DC.Domain
                    Forest      = $DC.Forest
                    HostName    = [Array] $DC.HostName
                    IPv4Address = $DC.IPv4Address
                    IPv6Address = $DC.IPv6Address
                    Name        = $DC.Name
                    Site        = $DC.Site
                }
            } catch {
                Write-Warning "Get-WinADForestDetails - Error discovering DC for domain $Domain - $($_.Exception.Message)"
                continue
            }
            if ($Domain -eq $Findings['Forest']['Name']) {
                $Findings['QueryServers']['Forest'] = $OrderedDC
            }
            $Findings['QueryServers']["$Domain"] = $OrderedDC

            $Domain
        }

        [Array] $Findings['Domains'] = foreach ($Domain in $Findings['Domains']) {
            if ($Domain -notin $DomainsActive) {
                Write-Warning "Get-WinADForestDetails - Domain $Domain doesn't seem to be active (no DCs). Skipping."
                continue
            }
            $Domain
        }

        [Array] $Findings['ForestDomainControllers'] = foreach ($Domain in $Findings.Domains) {
            $QueryServer = $Findings['QueryServers'][$Domain]['HostName'][0]

            [Array] $AllDC = try {
                try {
                    $DomainControllers = Get-ADDomainController -Filter $Filter -Server $QueryServer -ErrorAction Stop
                } catch {
                    Write-Warning "Get-WinADForestDetails - Error listing DCs for domain $Domain - $($_.Exception.Message)"
                    continue
                }
                foreach ($S in $DomainControllers) {
                    if ($IncludeDomainControllers.Count -gt 0) {
                        If (-not $IncludeDomainControllers[0].Contains('.')) {
                            if ($S.Name -notin $IncludeDomainControllers) {
                                continue
                            }
                        } else {
                            if ($S.HostName -notin $IncludeDomainControllers) {
                                continue
                            }
                        }
                    }
                    if ($ExcludeDomainControllers.Count -gt 0) {
                        If (-not $ExcludeDomainControllers[0].Contains('.')) {
                            if ($S.Name -in $ExcludeDomainControllers) {
                                continue
                            }
                        } else {
                            if ($S.HostName -in $ExcludeDomainControllers) {
                                continue
                            }
                        }
                    }
                    $Server = [ordered] @{
                        Domain                 = $Domain
                        HostName               = $S.HostName
                        Name                   = $S.Name
                        Forest                 = $ForestInformation.RootDomain
                        Site                   = $S.Site
                        IPV4Address            = $S.IPV4Address
                        IPV6Address            = $S.IPV6Address
                        IsGlobalCatalog        = $S.IsGlobalCatalog
                        IsReadOnly             = $S.IsReadOnly
                        IsSchemaMaster         = ($S.OperationMasterRoles -contains 'SchemaMaster')
                        IsDomainNamingMaster   = ($S.OperationMasterRoles -contains 'DomainNamingMaster')
                        IsPDC                  = ($S.OperationMasterRoles -contains 'PDCEmulator')
                        IsRIDMaster            = ($S.OperationMasterRoles -contains 'RIDMaster')
                        IsInfrastructureMaster = ($S.OperationMasterRoles -contains 'InfrastructureMaster')
                        OperatingSystem        = $S.OperatingSystem
                        OperatingSystemVersion = $S.OperatingSystemVersion
                        OperatingSystemLong    = ConvertTo-OperatingSystem -OperatingSystem $S.OperatingSystem -OperatingSystemVersion $S.OperatingSystemVersion
                        LdapPort               = $S.LdapPort
                        SslPort                = $S.SslPort
                        DistinguishedName      = $S.ComputerObjectDN
                        Pingable               = $null
                        WinRM                  = $null
                        PortOpen               = $null
                        Comment                = ''
                    }
                    if ($TestAvailability) {
                        if ($Test -eq 'All' -or $Test -like 'Ping*') {
                            $Server.Pingable = Test-Connection -ComputerName $Server.IPV4Address -Quiet -Count $PingCount
                        }
                        if ($Test -eq 'All' -or $Test -like '*WinRM*') {
                            $Server.WinRM = (Test-WinRM -ComputerName $Server.HostName).Status
                        }
                        if ($Test -eq 'All' -or '*PortOpen*') {
                            $Server.PortOpen = (Test-ComputerPort -Server $Server.HostName -PortTCP $Ports -Timeout $PortsTimeout).Status
                        }
                    }
                    [PSCustomObject] $Server
                }
            } catch {
                [PSCustomObject]@{
                    Domain                   = $Domain
                    HostName                 = ''
                    Name                     = ''
                    Forest                   = $ForestInformation.RootDomain
                    IPV4Address              = ''
                    IPV6Address              = ''
                    IsGlobalCatalog          = ''
                    IsReadOnly               = ''
                    Site                     = ''
                    SchemaMaster             = $false
                    DomainNamingMasterMaster = $false
                    PDCEmulator              = $false
                    RIDMaster                = $false
                    InfrastructureMaster     = $false
                    LdapPort                 = ''
                    SslPort                  = ''
                    DistinguishedName        = ''
                    Pingable                 = $null
                    WinRM                    = $null
                    PortOpen                 = $null
                    Comment                  = $_.Exception.Message -replace "`n", " " -replace "`r", " "
                }
            }
            if ($SkipRODC) {
                [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC | Where-Object { $_.IsReadOnly -eq $false }
            } else {
                [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC
            }

            [Array] $Findings['DomainDomainControllers'][$Domain]
        }
        if ($Extended) {
            $Findings['DomainsExtended'] = @{ }
            $Findings['DomainsExtendedNetBIOS'] = @{ }
            foreach ($DomainEx in $Findings['Domains']) {
                try {

                    $Findings['DomainsExtended'][$DomainEx] = Get-ADDomain -Server $Findings['QueryServers'][$DomainEx].HostName[0] | ForEach-Object {

                        [ordered] @{
                            AllowedDNSSuffixes                 = $_.AllowedDNSSuffixes | ForEach-Object -Process { $_ }                
                            ChildDomains                       = $_.ChildDomains | ForEach-Object -Process { $_ }                      
                            ComputersContainer                 = $_.ComputersContainer                 
                            DeletedObjectsContainer            = $_.DeletedObjectsContainer            
                            DistinguishedName                  = $_.DistinguishedName                  
                            DNSRoot                            = $_.DNSRoot                            
                            DomainControllersContainer         = $_.DomainControllersContainer         
                            DomainMode                         = $_.DomainMode                         
                            DomainSID                          = $_.DomainSID.Value                        
                            ForeignSecurityPrincipalsContainer = $_.ForeignSecurityPrincipalsContainer 
                            Forest                             = $_.Forest                             
                            InfrastructureMaster               = $_.InfrastructureMaster               
                            LastLogonReplicationInterval       = $_.LastLogonReplicationInterval       
                            LinkedGroupPolicyObjects           = $_.LinkedGroupPolicyObjects | ForEach-Object -Process { $_ }           
                            LostAndFoundContainer              = $_.LostAndFoundContainer              
                            ManagedBy                          = $_.ManagedBy                          
                            Name                               = $_.Name                               
                            NetBIOSName                        = $_.NetBIOSName                        
                            ObjectClass                        = $_.ObjectClass                        
                            ObjectGUID                         = $_.ObjectGUID                         
                            ParentDomain                       = $_.ParentDomain                       
                            PDCEmulator                        = $_.PDCEmulator                        
                            PublicKeyRequiredPasswordRolling   = $_.PublicKeyRequiredPasswordRolling | ForEach-Object -Process { $_ }   
                            QuotasContainer                    = $_.QuotasContainer                    
                            ReadOnlyReplicaDirectoryServers    = $_.ReadOnlyReplicaDirectoryServers | ForEach-Object -Process { $_ }    
                            ReplicaDirectoryServers            = $_.ReplicaDirectoryServers | ForEach-Object -Process { $_ }           
                            RIDMaster                          = $_.RIDMaster                          
                            SubordinateReferences              = $_.SubordinateReferences | ForEach-Object -Process { $_ }            
                            SystemsContainer                   = $_.SystemsContainer                   
                            UsersContainer                     = $_.UsersContainer                     
                        }
                    }

                    $NetBios = $Findings['DomainsExtended'][$DomainEx]['NetBIOSName']
                    $Findings['DomainsExtendedNetBIOS'][$NetBios] = $Findings['DomainsExtended'][$DomainEx]
                } catch {
                    Write-Warning "Get-WinADForestDetails - Error gathering Domain Information for domain $DomainEx - $($_.Exception.Message)"
                    continue
                }
            }
        }

        if ($TemporaryProgress) {
            $Global:ProgressPreference = $TemporaryProgress
        }

        $Findings
    } else {

        $Findings = Copy-DictionaryManual -Dictionary $ExtendedForestInformation
        [Array] $Findings['Domains'] = foreach ($_ in $Findings.Domains) {
            if ($IncludeDomains) {
                if ($_ -in $IncludeDomains) {
                    $_.ToLower()
                }

                continue
            }
            if ($_ -notin $ExcludeDomains) {
                $_.ToLower()
            }
        }

        foreach ($_ in [string[]] $Findings.DomainDomainControllers.Keys) {
            if ($_ -notin $Findings.Domains) {
                $Findings.DomainDomainControllers.Remove($_)
            }
        }

        foreach ($_ in [string[]] $Findings.DomainsExtended.Keys) {
            if ($_ -notin $Findings.Domains) {
                $Findings.DomainsExtended.Remove($_)
                $NetBiosName = $Findings.DomainsExtended.$_.'NetBIOSName'
                if ($NetBiosName) {
                    $Findings.DomainsExtendedNetBIOS.Remove($NetBiosName)
                }
            }
        }
        [Array] $Findings['ForestDomainControllers'] = foreach ($Domain in $Findings.Domains) {
            [Array] $AllDC = foreach ($S in $Findings.DomainDomainControllers["$Domain"]) {
                if ($IncludeDomainControllers.Count -gt 0) {
                    If (-not $IncludeDomainControllers[0].Contains('.')) {
                        if ($S.Name -notin $IncludeDomainControllers) {
                            continue
                        }
                    } else {
                        if ($S.HostName -notin $IncludeDomainControllers) {
                            continue
                        }
                    }
                }
                if ($ExcludeDomainControllers.Count -gt 0) {
                    If (-not $ExcludeDomainControllers[0].Contains('.')) {
                        if ($S.Name -in $ExcludeDomainControllers) {
                            continue
                        }
                    } else {
                        if ($S.HostName -in $ExcludeDomainControllers) {
                            continue
                        }
                    }
                }
                $S
            }
            if ($SkipRODC) {
                [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC | Where-Object { $_.IsReadOnly -eq $false }
            } else {
                [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC
            }

            [Array] $Findings['DomainDomainControllers'][$Domain]
        }
        $Findings
    }
}
function Remove-EmptyValue { 
    [alias('Remove-EmptyValues')]
    [CmdletBinding()]
    param(
        [alias('Splat', 'IDictionary')][Parameter(Mandatory)][System.Collections.IDictionary] $Hashtable,
        [string[]] $ExcludeParameter,
        [switch] $Recursive,
        [int] $Rerun,
        [switch] $DoNotRemoveNull,
        [switch] $DoNotRemoveEmpty,
        [switch] $DoNotRemoveEmptyArray,
        [switch] $DoNotRemoveEmptyDictionary
    )
    foreach ($Key in [string[]] $Hashtable.Keys) {
        if ($Key -notin $ExcludeParameter) {
            if ($Recursive) {
                if ($Hashtable[$Key] -is [System.Collections.IDictionary]) {
                    if ($Hashtable[$Key].Count -eq 0) {
                        if (-not $DoNotRemoveEmptyDictionary) {
                            $Hashtable.Remove($Key)
                        }
                    } else {
                        Remove-EmptyValue -Hashtable $Hashtable[$Key] -Recursive:$Recursive
                    }
                } else {
                    if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) {
                        $Hashtable.Remove($Key)
                    } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') {
                        $Hashtable.Remove($Key)
                    } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) {
                        $Hashtable.Remove($Key)
                    }
                }
            } else {
                if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) {
                    $Hashtable.Remove($Key)
                } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') {
                    $Hashtable.Remove($Key)
                } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) {
                    $Hashtable.Remove($Key)
                }
            }
        }
    }
    if ($Rerun) {
        for ($i = 0; $i -lt $Rerun; $i++) {
            Remove-EmptyValue -Hashtable $Hashtable -Recursive:$Recursive
        }
    }
}
function Write-Color { 
    <#
    .SYNOPSIS
    Write-Color is a wrapper around Write-Host delivering a lot of additional features for easier color options.
 
    .DESCRIPTION
    Write-Color is a wrapper around Write-Host delivering a lot of additional features for easier color options.
 
    It provides:
    - Easy manipulation of colors,
    - Logging output to file (log)
    - Nice formatting options out of the box.
    - Ability to use aliases for parameters
 
    .PARAMETER Text
    Text to display on screen and write to log file if specified.
    Accepts an array of strings.
 
    .PARAMETER Color
    Color of the text. Accepts an array of colors. If more than one color is specified it will loop through colors for each string.
    If there are more strings than colors it will start from the beginning.
    Available colors are: Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White
 
    .PARAMETER BackGroundColor
    Color of the background. Accepts an array of colors. If more than one color is specified it will loop through colors for each string.
    If there are more strings than colors it will start from the beginning.
    Available colors are: Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White
 
    .PARAMETER StartTab
    Number of tabs to add before text. Default is 0.
 
    .PARAMETER LinesBefore
    Number of empty lines before text. Default is 0.
 
    .PARAMETER LinesAfter
    Number of empty lines after text. Default is 0.
 
    .PARAMETER StartSpaces
    Number of spaces to add before text. Default is 0.
 
    .PARAMETER LogFile
    Path to log file. If not specified no log file will be created.
 
    .PARAMETER DateTimeFormat
    Custom date and time format string. Default is yyyy-MM-dd HH:mm:ss
 
    .PARAMETER LogTime
    If set to $true it will add time to log file. Default is $true.
 
    .PARAMETER LogRetry
    Number of retries to write to log file, in case it can't write to it for some reason, before skipping. Default is 2.
 
    .PARAMETER Encoding
    Encoding of the log file. Default is Unicode.
 
    .PARAMETER ShowTime
    Switch to add time to console output. Default is not set.
 
    .PARAMETER NoNewLine
    Switch to not add new line at the end of the output. Default is not set.
 
    .PARAMETER NoConsoleOutput
    Switch to not output to console. Default all output goes to console.
 
    .EXAMPLE
    Write-Color -Text "Red ", "Green ", "Yellow " -Color Red,Green,Yellow
 
    .EXAMPLE
    Write-Color -Text "This is text in Green ",
                      "followed by red ",
                      "and then we have Magenta... ",
                      "isn't it fun? ",
                      "Here goes DarkCyan" -Color Green,Red,Magenta,White,DarkCyan
 
    .EXAMPLE
    Write-Color -Text "This is text in Green ",
                      "followed by red ",
                      "and then we have Magenta... ",
                      "isn't it fun? ",
                      "Here goes DarkCyan" -Color Green,Red,Magenta,White,DarkCyan -StartTab 3 -LinesBefore 1 -LinesAfter 1
 
    .EXAMPLE
    Write-Color "1. ", "Option 1" -Color Yellow, Green
    Write-Color "2. ", "Option 2" -Color Yellow, Green
    Write-Color "3. ", "Option 3" -Color Yellow, Green
    Write-Color "4. ", "Option 4" -Color Yellow, Green
    Write-Color "9. ", "Press 9 to exit" -Color Yellow, Gray -LinesBefore 1
 
    .EXAMPLE
    Write-Color -LinesBefore 2 -Text "This little ","message is ", "written to log ", "file as well." `
                -Color Yellow, White, Green, Red, Red -LogFile "C:\testing.txt" -TimeFormat "yyyy-MM-dd HH:mm:ss"
    Write-Color -Text "This can get ","handy if ", "want to display things, and log actions to file ", "at the same time." `
                -Color Yellow, White, Green, Red, Red -LogFile "C:\testing.txt"
 
    .EXAMPLE
    Write-Color -T "My text", " is ", "all colorful" -C Yellow, Red, Green -B Green, Green, Yellow
    Write-Color -t "my text" -c yellow -b green
    Write-Color -text "my text" -c red
 
    .EXAMPLE
    Write-Color -Text "TestujÄ™ czy siÄ™ Å‚adnie zapisze, czy bÄ™dÄ… problemy" -Encoding unicode -LogFile 'C:\temp\testinggg.txt' -Color Red -NoConsoleOutput
 
    .NOTES
    Understanding Custom date and time format strings: https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings
    Project support: https://github.com/EvotecIT/PSWriteColor
    Original idea: Josh (https://stackoverflow.com/users/81769/josh)
 
    #>

    [alias('Write-Colour')]
    [CmdletBinding()]
    param (
        [alias ('T')] [String[]]$Text,
        [alias ('C', 'ForegroundColor', 'FGC')] [ConsoleColor[]]$Color = [ConsoleColor]::White,
        [alias ('B', 'BGC')] [ConsoleColor[]]$BackGroundColor = $null,
        [alias ('Indent')][int] $StartTab = 0,
        [int] $LinesBefore = 0,
        [int] $LinesAfter = 0,
        [int] $StartSpaces = 0,
        [alias ('L')] [string] $LogFile = '',
        [Alias('DateFormat', 'TimeFormat')][string] $DateTimeFormat = 'yyyy-MM-dd HH:mm:ss',
        [alias ('LogTimeStamp')][bool] $LogTime = $true,
        [int] $LogRetry = 2,
        [ValidateSet('unknown', 'string', 'unicode', 'bigendianunicode', 'utf8', 'utf7', 'utf32', 'ascii', 'default', 'oem')][string]$Encoding = 'Unicode',
        [switch] $ShowTime,
        [switch] $NoNewLine,
        [alias('HideConsole')][switch] $NoConsoleOutput
    )
    if (-not $NoConsoleOutput) {
        $DefaultColor = $Color[0]
        if ($null -ne $BackGroundColor -and $BackGroundColor.Count -ne $Color.Count) {
            Write-Error "Colors, BackGroundColors parameters count doesn't match. Terminated."
            return
        }
        if ($LinesBefore -ne 0) {
            for ($i = 0; $i -lt $LinesBefore; $i++) {
                Write-Host -Object "`n" -NoNewline 
            } 
        } # Add empty line before
        if ($StartTab -ne 0) {
            for ($i = 0; $i -lt $StartTab; $i++) {
                Write-Host -Object "`t" -NoNewline 
            } 
        }  # Add TABS before text
        if ($StartSpaces -ne 0) {
            for ($i = 0; $i -lt $StartSpaces; $i++) {
                Write-Host -Object ' ' -NoNewline 
            } 
        }  # Add SPACES before text
        if ($ShowTime) {
            Write-Host -Object "[$([datetime]::Now.ToString($DateTimeFormat))] " -NoNewline 
        } # Add Time before output
        if ($Text.Count -ne 0) {
            if ($Color.Count -ge $Text.Count) {
                # the real deal coloring
                if ($null -eq $BackGroundColor) {
                    for ($i = 0; $i -lt $Text.Length; $i++) {
                        Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -NoNewline 
                    }
                } else {
                    for ($i = 0; $i -lt $Text.Length; $i++) {
                        Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -BackgroundColor $BackGroundColor[$i] -NoNewline 
                    }
                }
            } else {
                if ($null -eq $BackGroundColor) {
                    for ($i = 0; $i -lt $Color.Length ; $i++) {
                        Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -NoNewline 
                    }
                    for ($i = $Color.Length; $i -lt $Text.Length; $i++) {
                        Write-Host -Object $Text[$i] -ForegroundColor $DefaultColor -NoNewline 
                    }
                } else {
                    for ($i = 0; $i -lt $Color.Length ; $i++) {
                        Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -BackgroundColor $BackGroundColor[$i] -NoNewline 
                    }
                    for ($i = $Color.Length; $i -lt $Text.Length; $i++) {
                        Write-Host -Object $Text[$i] -ForegroundColor $DefaultColor -BackgroundColor $BackGroundColor[0] -NoNewline 
                    }
                }
            }
        }
        if ($NoNewLine -eq $true) {
            Write-Host -NoNewline 
        } else {
            Write-Host 
        } # Support for no new line
        if ($LinesAfter -ne 0) {
            for ($i = 0; $i -lt $LinesAfter; $i++) {
                Write-Host -Object "`n" -NoNewline 
            } 
        }  # Add empty line after
    }
    if ($Text.Count -and $LogFile) {
        # Save to file
        $TextToFile = ""
        for ($i = 0; $i -lt $Text.Length; $i++) {
            $TextToFile += $Text[$i]
        }
        $Saved = $false
        $Retry = 0
        Do {
            $Retry++
            try {
                if ($LogTime) {
                    "[$([datetime]::Now.ToString($DateTimeFormat))] $TextToFile" | Out-File -FilePath $LogFile -Encoding $Encoding -Append -ErrorAction Stop -WhatIf:$false
                } else {
                    "$TextToFile" | Out-File -FilePath $LogFile -Encoding $Encoding -Append -ErrorAction Stop -WhatIf:$false
                }
                $Saved = $true
            } catch {
                if ($Saved -eq $false -and $Retry -eq $LogRetry) {
                    Write-Warning "Write-Color - Couldn't write to log file $($_.Exception.Message). Tried ($Retry/$LogRetry))"
                } else {
                    Write-Warning "Write-Color - Couldn't write to log file $($_.Exception.Message). Retrying... ($Retry/$LogRetry)"
                }
            }
        } Until ($Saved -eq $true -or $Retry -ge $LogRetry)
    }
}
function Copy-DictionaryManual { 
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $Dictionary
    )

    $clone = @{}
    foreach ($Key in $Dictionary.Keys) {
        $value = $Dictionary.$Key

        $clonedValue = switch ($Dictionary.$Key) {
            { $null -eq $_ } {
                $null
                continue
            }
            { $_ -is [System.Collections.IDictionary] } {
                Copy-DictionaryManual -Dictionary $_
                continue
            }
            {
                $type = $_.GetType()
                $type.IsPrimitive -or $type.IsValueType -or $_ -is [string]
            } {
                $_
                continue
            }
            default {
                $_ | Select-Object -Property *
            }

        }

        if ($value -is [System.Collections.IList]) {
            $clone[$Key] = @($clonedValue)
        } else {
            $clone[$Key] = $clonedValue
        }
    }

    $clone
}
function Get-GitHubLatestRelease { 
    <#
    .SYNOPSIS
    Gets one or more releases from GitHub repository
 
    .DESCRIPTION
    Gets one or more releases from GitHub repository
 
    .PARAMETER Url
    Url to github repository
 
    .EXAMPLE
    Get-GitHubLatestRelease -Url "https://api.github.com/repos/evotecit/Testimo/releases" | Format-Table
 
    .NOTES
    General notes
    #>

    [CmdLetBinding()]
    param(
        [parameter(Mandatory)][alias('ReleasesUrl')][uri] $Url
    )
    $ProgressPreference = 'SilentlyContinue'

    $Responds = Test-Connection -ComputerName $URl.Host -Quiet -Count 1
    if ($Responds) {
        Try {
            [Array] $JsonOutput = (Invoke-WebRequest -Uri $Url -ErrorAction Stop | ConvertFrom-Json)
            foreach ($JsonContent in $JsonOutput) {
                [PSCustomObject] @{
                    PublishDate = [DateTime]  $JsonContent.published_at
                    CreatedDate = [DateTime] $JsonContent.created_at
                    PreRelease  = [bool] $JsonContent.prerelease
                    Version     = [version] ($JsonContent.name -replace 'v', '')
                    Tag         = $JsonContent.tag_name
                    Branch      = $JsonContent.target_commitish
                    Errors      = ''
                }
            }
        } catch {
            [PSCustomObject] @{
                PublishDate = $null
                CreatedDate = $null
                PreRelease  = $null
                Version     = $null
                Tag         = $null
                Branch      = $null
                Errors      = $_.Exception.Message
            }
        }
    } else {
        [PSCustomObject] @{
            PublishDate = $null
            CreatedDate = $null
            PreRelease  = $null
            Version     = $null
            Tag         = $null
            Branch      = $null
            Errors      = "No connection (ping) to $($Url.Host)"
        }
    }
    $ProgressPreference = 'Continue'
}
function Test-ComputerPort { 
    [CmdletBinding()]
    param (
        [alias('Server')][string[]] $ComputerName,
        [int[]] $PortTCP,
        [int[]] $PortUDP,
        [int]$Timeout = 5000
    )
    begin {
        if ($Global:ProgressPreference -ne 'SilentlyContinue') {
            $TemporaryProgress = $Global:ProgressPreference
            $Global:ProgressPreference = 'SilentlyContinue'
        }
    }
    process {
        foreach ($Computer in $ComputerName) {
            foreach ($P in $PortTCP) {
                $Output = [ordered] @{
                    'ComputerName' = $Computer
                    'Port'         = $P
                    'Protocol'     = 'TCP'
                    'Status'       = $null
                    'Summary'      = $null
                    'Response'     = $null
                }

                $TcpClient = Test-NetConnection -ComputerName $Computer -Port $P -InformationLevel Detailed -WarningAction SilentlyContinue
                if ($TcpClient.TcpTestSucceeded) {
                    $Output['Status'] = $TcpClient.TcpTestSucceeded
                    $Output['Summary'] = "TCP $P Successful"
                } else {
                    $Output['Status'] = $false
                    $Output['Summary'] = "TCP $P Failed"
                    $Output['Response'] = $Warnings
                }
                [PSCustomObject]$Output
            }
            foreach ($P in $PortUDP) {
                $Output = [ordered] @{
                    'ComputerName' = $Computer
                    'Port'         = $P
                    'Protocol'     = 'UDP'
                    'Status'       = $null
                    'Summary'      = $null
                }
                $UdpClient = [System.Net.Sockets.UdpClient]::new($Computer, $P)
                $UdpClient.Client.ReceiveTimeout = $Timeout

                $Encoding = [System.Text.ASCIIEncoding]::new()
                $byte = $Encoding.GetBytes("Evotec")
                [void]$UdpClient.Send($byte, $byte.length)
                $RemoteEndpoint = [System.Net.IPEndPoint]::new([System.Net.IPAddress]::Any, 0)
                try {
                    $Bytes = $UdpClient.Receive([ref]$RemoteEndpoint)
                    [string]$Data = $Encoding.GetString($Bytes)
                    If ($Data) {
                        $Output['Status'] = $true
                        $Output['Summary'] = "UDP $P Successful"
                        $Output['Response'] = $Data
                    }
                } catch {
                    $Output['Status'] = $false
                    $Output['Summary'] = "UDP $P Failed"
                    $Output['Response'] = $_.Exception.Message
                }
                $UdpClient.Close()
                $UdpClient.Dispose()
                [PSCustomObject]$Output
            }
        }
    }
    end {

        if ($TemporaryProgress) {
            $Global:ProgressPreference = $TemporaryProgress
        }
    }
}
function Test-WinRM { 
    [CmdletBinding()]
    param (
        [alias('Server')][string[]] $ComputerName
    )
    $Output = foreach ($Computer in $ComputerName) {
        $Test = [PSCustomObject] @{
            Output       = $null
            Status       = $null
            ComputerName = $Computer
        }
        try {
            $Test.Output = Test-WSMan -ComputerName $Computer -ErrorAction Stop
            $Test.Status = $true
        } catch {
            $Test.Status = $false
        }
        $Test
    }
    $Output
}
function Convert-ListProcessed {
    <#
    .SYNOPSIS
    This function converts old format of writting down pending list with DN to new format with SamAccountName with domain.
 
    .DESCRIPTION
    This function converts old format of writting down pending list with DN to new format with SamAccountName with domain.
    The old way would probably break if someone would move computer after disabling which would cause the computer to be removed from the list.
 
    .PARAMETER FileImport
    Hashtable with PendingDeletion and History keys.
 
    .EXAMPLE
    $FileImport = Import-Clixml -LiteralPath $DataStorePath -ErrorAction Stop
    # convert old format to new format
    $FileImport = Convert-ListProcessed -FileImport $FileImport
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $FileImport
    )
    if (-not $FileImport.Contains('PendingDeletion') -and $FileImport.PendingDeletion.Keys.Count -eq 0) {
        return $FileImport
    }
    if ($FileImport.PendingDeletion.Keys[0] -like "*@*") {
        # Write-Color -Text "[i] ", "List is already converted. Terminating." -Color Yellow, Green
        return $FileImport
    }
    Write-Color -Text "[i] ", "Converting list to new format." -Color Yellow, Green
    foreach ($Key in [string[]] $FileImport.PendingDeletion.Keys) {
        $DomainName = ConvertFrom-DistinguishedName -DistinguishedName $FileImport.PendingDeletion[$Key].DistinguishedName -ToDomainCN
        $NewKey = -join ($FileImport.PendingDeletion[$Key].SamAccountName, "@", $DomainName)
        $FileImport.PendingDeletion[$NewKey] = $FileImport.PendingDeletion[$Key]
        $null = $FileImport.PendingDeletion.Remove($Key)
    }
    Write-Color -Text "[i] ", "List converted." -Color Yellow, Green
    $FileImport
}
function ConvertTo-PreparedComputer {
    [CmdletBinding()]
    param(
        [Microsoft.ActiveDirectory.Management.ADComputer[]] $Computers,
        [System.Collections.IDictionary] $AzureInformationCache,
        [System.Collections.IDictionary] $JamfInformationCache,
        [switch] $IncludeAzureAD,
        [switch] $IncludeIntune,
        [switch] $IncludeJamf
    )

    foreach ($Computer in $Computers) {
        if ($IncludeAzureAD) {
            $AzureADComputer = $AzureInformationCache['AzureAD']["$($Computer.Name)"]
            $DataAzureAD = [ordered] @{
                'AzureLastSeen'     = $AzureADComputer.LastSeen
                'AzureLastSeenDays' = $AzureADComputer.LastSeenDays
                'AzureLastSync'     = $AzureADComputer.LastSynchronized
                'AzureLastSyncDays' = $AzureADComputer.LastSynchronizedDays
                'AzureOwner'        = $AzureADComputer.OwnerDisplayName
                'AzureOwnerStatus'  = $AzureADComputer.OwnerEnabled
                'AzureOwnerUPN'     = $AzureADComputer.OwnerUserPrincipalName
            }
        }
        if ($IncludeIntune) {
            # data was requested from Intune
            $IntuneComputer = $AzureInformationCache['Intune']["$($Computer.Name)"]
            $DataIntune = [ordered] @{
                'IntuneLastSeen'     = $IntuneComputer.LastSeen
                'IntuneLastSeenDays' = $IntuneComputer.LastSeenDays
                'IntuneUser'         = $IntuneComputer.UserDisplayName
                'IntuneUserUPN'      = $IntuneComputer.UserPrincipalName
                'IntuneUserEmail'    = $IntuneComputer.EmailAddress
            }
        }
        if ($IncludeJamf) {
            $JamfComputer = $JamfInformationCache["$($Computer.Name)"]
            $DataJamf = [ordered] @{
                JamfLastContactTime     = $JamfComputer.lastContactTime
                JamfLastContactTimeDays = $JamfComputer.lastContactTimeDays
                JamfCapableUsers        = $JamfComputer.mdmCapableCapableUsers
            }
        }
        $LastLogonDays = if ($null -ne $Computer.LastLogonDate) {
            - $($Computer.LastLogonDate - $Today).Days
        } else {
            $null
        }
        $PasswordLastChangedDays = if ($null -ne $Computer.PasswordLastSet) {
            - $($Computer.PasswordLastSet - $Today).Days
        } else {
            $null
        }


        $DataStart = [ordered] @{
            'DNSHostName'             = $Computer.DNSHostName
            'SamAccountName'          = $Computer.SamAccountName
            'DomainName'              = ConvertFrom-DistinguishedName -DistinguishedName $Computer.DistinguishedName -ToDomainCN
            'Enabled'                 = $Computer.Enabled
            'Action'                  = 'Not required'
            'ActionStatus'            = $null
            'ActionDate'              = $null
            'ActionComment'           = $null
            'OperatingSystem'         = $Computer.OperatingSystem
            'OperatingSystemVersion'  = $Computer.OperatingSystemVersion
            'OperatingSystemLong'     = ConvertTo-OperatingSystem -OperatingSystem $Computer.OperatingSystem -OperatingSystemVersion $Computer.OperatingSystemVersion
            'LastLogonDate'           = $Computer.LastLogonDate
            'LastLogonDays'           = $LastLogonDays
            'PasswordLastSet'         = $Computer.PasswordLastSet
            'PasswordLastChangedDays' = $PasswordLastChangedDays
        }
        $DataEnd = [ordered] @{
            'PasswordExpired'      = $Computer.PasswordExpired
            'LogonCount'           = $Computer.logonCount
            'ManagedBy'            = $Computer.ManagedBy
            'DistinguishedName'    = $Computer.DistinguishedName
            'OrganizationalUnit'   = ConvertFrom-DistinguishedName -DistinguishedName $Computer.DistinguishedName -ToOrganizationalUnit
            'Description'          = $Computer.Description
            'WhenCreated'          = $Computer.WhenCreated
            'WhenChanged'          = $Computer.WhenChanged
            'ServicePrincipalName' = $Computer.servicePrincipalName #-join [System.Environment]::NewLine
        }
        if ($IncludeAzureAD -and $IncludeIntune -and $IncludeJamf) {
            $Data = $DataStart + $DataAzureAD + $DataIntune + $DataJamf + $DataEnd
        } elseif ($IncludeAzureAD -and $IncludeIntune) {
            $Data = $DataStart + $DataAzureAD + $DataIntune + $DataEnd
        } elseif ($IncludeAzureAD -and $IncludeJamf) {
            $Data = $DataStart + $DataAzureAD + $DataJamf + $DataEnd
        } elseif ($IncludeIntune -and $IncludeJamf) {
            $Data = $DataStart + $DataIntune + $DataJamf + $DataEnd
        } elseif ($IncludeAzureAD) {
            $Data = $DataStart + $DataAzureAD + $DataEnd
        } elseif ($IncludeIntune) {
            $Data = $DataStart + $DataIntune + $DataEnd
        } elseif ($IncludeJamf) {
            $Data = $DataStart + $DataJamf + $DataEnd
        } else {
            $Data = $DataStart + $DataEnd
        }
        [PSCustomObject] $Data
    }
}
function Get-ADComputersToProcess {
    [CmdletBinding()]
    param(
        [parameter(Mandatory)][ValidateSet('Disable', 'Move', 'Delete')][string] $Type,
        [Array] $Computers,
        [alias('DeleteOnlyIf', 'DisableOnlyIf', 'MoveOnlyIf')][System.Collections.IDictionary] $ActionIf,
        [Array] $Exclusions,
        [System.Collections.IDictionary] $DomainInformation,
        [System.Collections.IDictionary] $ProcessedComputers,
        [System.Collections.IDictionary] $AzureInformationCache,
        [System.Collections.IDictionary] $JamfInformationCache,
        [switch] $IncludeAzureAD,
        [switch] $IncludeIntune,
        [switch] $IncludeJamf
    )
    Write-Color -Text "[i] ", "Applying following rules to $Type action: " -Color Yellow, Cyan, Green
    foreach ($Key in $ActionIf.Keys) {
        if ($null -eq $ActionIf[$Key] -or $ActionIf[$Key].Count -eq 0) {
            Write-Color -Text " [>] ", $($Key), " is ", 'Not Set' -Color Yellow, Cyan, Yellow
        } else {
            if ($Key -in 'LastLogonDateMoreThan', 'LastLogonDateOlderThan') {
                Write-Color -Text " [>] ", $($Key), " is ", $($ActionIf[$Key]), " or ", "Never logged on" -Color Yellow, Cyan, Green
            } elseif ($Key -in 'PasswordLastSetMoreThan', 'PasswordLastSetOlderThan') {
                Write-Color -Text " [>] ", $($Key), " is ", $($ActionIf[$Key]), " or ", "Never changed" -Color Yellow, Cyan, Green
            } elseif ($Key -in 'LastSeenAzureMoreThan', 'LastSeenIntuneMoreThan', 'LastSyncAzureMoreThan', 'LastContactJamfMoreThan') {
                Write-Color -Text " [>] ", $($Key), " is ", $($ActionIf[$Key]), " or ", "Never synced/seen" -Color Yellow, Cyan, Green
            } else {
                Write-Color -Text " [>] ", $($Key), " is ", $($ActionIf[$Key]) -Color Yellow, Cyan, Green
            }
        }
    }
    $Count = 0
    $Today = Get-Date

    Write-Color -Text "[i] ", "Looking for computers to $Type" -Color Yellow, Cyan, Green
    :SkipComputer foreach ($Computer in $Computers) {
        if ($Type -eq 'Delete') {
            # actions to happen only if we are deleting computers
            if ($null -ne $ActionIf.ListProcessedMoreThan) {
                # if more then 0 this means computer has to be on list of disabled computers for that number of days.
                if ($ProcessedComputers.Count -gt 0) {
                    $FullComputerName = "$($Computer.SamAccountName)@$($Computer.DomainName)"
                    $FoundComputer = $ProcessedComputers[$FullComputerName]
                    if ($FoundComputer) {
                        if ($FoundComputer.ActionDate -is [DateTime]) {
                            $TimeSpan = New-TimeSpan -Start $FoundComputer.ActionDate -End $Today
                            if ($TimeSpan.Days -gt $ActionIf.ListProcessedMoreThan) {

                            } else {
                                continue SkipComputer
                            }
                        } else {
                            continue SkipComputer
                        }
                    } else {
                        continue SkipComputer
                    }
                } else {
                    # ListProcessed doesn't have members, and it's part of requirement
                    break
                }
            }
        }
        if ($Type -eq 'Disable') {
            # actions to happen only if we are disabling computers
            if ($ProcessedComputers.Count -gt 0) {
                $FullComputerName = "$($Computer.SamAccountName)@$($Computer.DomainName)"
                $FoundComputer = $ProcessedComputers[$FullComputerName]
                if ($FoundComputer) {
                    if ($Computer.Enabled -eq $true) {
                        # We checked and it seems the computer has been enabled since it was added to list, we remove it from the list and reprocess
                        Write-Color -Text "[*] Removing computer from pending list (computer is enabled) ", $FoundComputer.SamAccountName, " ($($FoundComputer.DistinguishedName))" -Color DarkYellow, Green, DarkYellow
                        $ProcessedComputers.Remove($FullComputerName)
                    } else {
                        # we skip adding to disabled because it's already on the list for removing
                        continue SkipComputer
                    }
                }
            }
        }
        # rest of actions are same for all types
        foreach ($PartialExclusion in $Exclusions) {
            if ($Computer.DistinguishedName -like "$PartialExclusion") {
                continue SkipComputer
            }
            if ($Computer.SamAccountName -like "$PartialExclusion") {
                continue SkipComputer
            }
            if ($Computer.DNSHostName -like "$PartialExclusion") {
                continue SkipComputer
            }
        }
        if ($ActionIf.ExcludeServicePrincipalName.Count -gt 0) {
            foreach ($ExcludeSPN in $ActionIf.ExcludeServicePrincipalName) {
                if ($Computer.servicePrincipalName -like "$ExcludeSPN") {
                    continue SkipComputer
                }
            }
        }
        if ($ActionIf.IncludeServicePrincipalName.Count -gt 0) {
            $FoundInclude = $false
            foreach ($IncludeSPN in $ActionIf.IncludeServicePrincipalName) {
                if ($Computer.servicePrincipalName -like "$IncludeSPN") {
                    $FoundInclude = $true
                    break
                }
            }
            # If not found in includes we need to skip the computer
            if (-not $FoundInclude) {
                continue SkipComputer
            }
        }
        if ($ActionIf.ExcludeSystems.Count -gt 0) {
            foreach ($Exclude in $ActionIf.ExcludeSystems) {
                if ($Computer.OperatingSystem -like $Exclude) {
                    continue SkipComputer
                }
            }
        }
        if ($ActionIf.IncludeSystems.Count -gt 0) {
            $FoundInclude = $false
            foreach ($Include in $ActionIf.IncludeSystems) {
                if ($Computer.OperatingSystem -like $Include) {
                    $FoundInclude = $true
                    break
                }
            }
            # If not found in includes we need to skip the computer
            if (-not $FoundInclude) {
                continue SkipComputer
            }
        }
        if ($ActionIf.IsEnabled -eq $true) {
            # action computer only if it's Enabled
            if ($Computer.Enabled -eq $false) {
                continue SkipComputer
            }
        } elseif ($ActionIf.IsEnabled -eq $false) {
            # action computer only if it's Disabled
            if ($Computer.Enabled -eq $true) {
                continue SkipComputer
            }
        }

        if ($ActionIf.NoServicePrincipalName -eq $true) {
            # action computer only if it has no service principal names defined
            if ($Computer.servicePrincipalName.Count -gt 0) {
                continue SkipComputer
            }
        } elseif ($ActionIf.NoServicePrincipalName -eq $false) {
            # action computer only if it has service principal names defined
            if ($Computer.servicePrincipalName.Count -eq 0) {
                continue SkipComputer
            }
        }

        if ($ActionIf.LastLogonDateMoreThan) {
            # This runs only if more than 0
            if ($Computer.LastLogonDate) {
                # We ignore empty

                $TimeToCompare = ($Computer.LastLogonDate).AddDays($ActionIf.LastLogonDateMoreThan)
                if ($TimeToCompare -gt $Today) {
                    continue SkipComputer
                }
            }
        }
        if ($ActionIf.PasswordLastSetMoreThan) {
            # This runs only if more than 0
            if ($Computer.PasswordLastSet) {
                # We ignore empty

                $TimeToCompare = ($Computer.PasswordLastSet).AddDays($ActionIf.PasswordLastSetMoreThan)
                if ($TimeToCompare -gt $Today) {
                    continue SkipComputer
                }
            }
        }

        if ($ActionIf.PasswordLastSetOlderThan) {
            # This runs only if not null
            if ($Computer.PasswordLastSet) {
                # We ignore empty

                if ($ActionIf.PasswordLastSetOlderThan -le $Computer.PasswordLastSet) {
                    continue SkipComputer
                }
            }
        }
        if ($ActionIf.LastLogonDateOlderThan) {
            # This runs only if not null
            if ($Computer.LastLogonDate) {
                # We ignore empty

                if ($ActionIf.LastLogonDateOlderThan -le $Computer.LastLogonDate) {
                    continue SkipComputer
                }
            }
        }

        if ($IncludeAzureAD) {
            if ($null -ne $ActionIf.LastSeenAzureMoreThan -and $null -ne $Computer.AzureLastSeenDays) {
                if ($Computer.AzureLastSeenDays -le $ActionIf.LastSeenAzureMoreThan) {
                    continue SkipComputer
                }

            }
            if ($null -ne $ActionIf.LastSyncAzureMoreThan -and $null -ne $Computer.AzureLastSyncDays) {
                if ($Computer.AzureLastSyncDays -le $ActionIf.LastSyncAzureMoreThan) {
                    continue SkipComputer
                }
            }
        }
        if ($IncludeIntune) {
            if ($null -ne $ActionIf.LastSeenIntuneMoreThan -and $null -ne $Computer.IntuneLastSeenDays) {
                if ($Computer.IntuneLastSeenDays -le $ActionIf.LastSeenIntuneMoreThan) {
                    continue SkipComputer
                }
            }
        }
        if ($IncludeJamf) {
            if ($null -ne $ActionIf.LastContactJamfMoreThan -and $null -ne $Computer.JamfLastContactTimeDays) {
                if ($Computer.JamfLastContactTimeDays -le $ActionIf.LastContactJamfMoreThan) {
                    continue SkipComputer
                }
            }
        }
        $Computer.'Action' = $Type
        $Count++
    }
    $Count
}
function Get-InitialADComputers {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $Report,
        [System.Collections.IDictionary] $ForestInformation,
        [string] $Filter,
        [string[]] $Properties,
        [bool] $Disable,
        [bool] $Delete,
        [bool] $Move,
        [System.Collections.IDictionary] $DisableOnlyIf,
        [System.Collections.IDictionary] $DeleteOnlyIf,
        [System.Collections.IDictionary] $MoveOnlyIf,
        [Array] $Exclusions,
        [System.Collections.IDictionary] $ProcessedComputers,
        [nullable[int]] $SafetyADLimit,
        [System.Collections.IDictionary] $AzureInformationCache,
        [System.Collections.IDictionary] $JamfInformationCache
    )
    $AllComputers = [ordered] @{}

    $AzureRequired = $false
    $IntuneRequired = $false
    $JamfRequired = $false

    if ($DisableOnlyIf) {
        if ($null -ne $DisableOnlyIf.LastSyncAzureMoreThan -or $null -ne $DisableOnlyIf.LastSeenAzureMoreThan) {
            $AzureRequired = $true
        }
        if ($null -ne $DisableOnlyIf.LastContactJamfMoreThan) {
            $JamfRequired = $true
        }
        if ($null -ne $DisableOnlyIf.LastSeenIntuneMoreThan) {
            $IntuneRequired = $true
        }
    }
    if ($MoveOnlyIf) {
        if ($null -ne $MoveOnlyIf.LastSyncAzureMoreThan -or $null -ne $MoveOnlyIf.LastSeenAzureMoreThan) {
            $AzureRequired = $true
        }
        if ($null -ne $MoveOnlyIf.LastContactJamfMoreThan) {
            $JamfRequired = $true
        }
        if ($null -ne $MoveOnlyIf.LastSeenIntuneMoreThan) {
            $IntuneRequired = $true
        }
    }
    if ($DeleteOnlyIf) {
        if ($null -ne $DeleteOnlyIf.LastSyncAzureMoreThan -or $null -ne $DeleteOnlyIf.LastSeenAzureMoreThan) {
            $AzureRequired = $true
        }
        if ($null -ne $DeleteOnlyIf.LastContactJamfMoreThan) {
            $JamfRequired = $true
        }
        if ($null -ne $DeleteOnlyIf.LastSeenIntuneMoreThan) {
            $IntuneRequired = $true
        }
    }

    foreach ($Domain in $ForestInformation.Domains) {
        $Report["$Domain"] = [ordered] @{ }
        $Server = $ForestInformation['QueryServers'][$Domain].HostName[0]
        if (-not $Server) {
            Write-Color "[e] ", "No server found for domain $Domain" -Color Yellow, Red
            continue
        }
        $DomainInformation = $ForestInformation.DomainsExtended[$Domain]
        $Report["$Domain"]['Server'] = $Server
        Write-Color "[i] Getting all computers for domain ", $Domain -Color Yellow, Magenta, Yellow
        [Array] $Computers = Get-ADComputer -Filter $Filter -Server $Server -Properties $Properties #| Where-Object { $_.SamAccountName -like 'Windows2012*' }
        foreach ($Computer in $Computers) {
            # we will be using it later to just check if computer exists in AD
            $DomainName = ConvertFrom-DistinguishedName -DistinguishedName $Computer.DistinguishedName -ToDomainCN
            $ComputerFullName = -join ($Computer.SamAccountName, "@", $DomainName)
            # initially we used DN, but DN changes when moving so it wouldn't work
            $AllComputers[$ComputerFullName] = $Computer
        }
        $Report["$Domain"]['Computers'] = @(
            $convertToPreparedComputerSplat = @{
                Computers             = $Computers
                AzureInformationCache = $AzureInformationCache
                JamfInformationCache  = $JamfInformationCache
                IncludeAzureAD        = $AzureRequired
                IncludeJamf           = $JamfRequired
                IncludeIntune         = $IntuneRequired
            }

            ConvertTo-PreparedComputer @convertToPreparedComputerSplat
        )
        Write-Color "[i] ", "Computers found for domain $Domain`: ", $($Computers.Count) -Color Yellow, Cyan, Green
        if ($Disable) {
            Write-Color "[i] ", "Processing computers to disable for domain $Domain" -Color Yellow, Cyan, Green
            $getADComputersToDisableSplat = @{
                Computers             = $Report["$Domain"]['Computers']
                DisableOnlyIf         = $DisableOnlyIf
                Exclusions            = $Exclusions
                DomainInformation     = $DomainInformation
                ProcessedComputers    = $ProcessedComputers
                AzureInformationCache = $AzureInformationCache
                JamfInformationCache  = $JamfInformationCache
                IncludeAzureAD        = $AzureRequired
                IncludeJamf           = $JamfRequired
                IncludeIntune         = $IntuneRequired
                Type                  = 'Disable'
            }
            $Report["$Domain"]['ComputersToBeDisabled'] = Get-ADComputersToProcess @getADComputersToDisableSplat
        }
        if ($Move) {
            Write-Color "[i] ", "Processing computers to move for domain $Domain" -Color Yellow, Cyan, Green
            $getADComputersToDeleteSplat = @{
                Computers             = $Report["$Domain"]['Computers']
                MoveOnlyIf            = $MoveOnlyIf
                Exclusions            = $Exclusions
                DomainInformation     = $DomainInformation
                ProcessedComputers    = $ProcessedComputers
                AzureInformationCache = $AzureInformationCache
                JamfInformationCache  = $JamfInformationCache
                IncludeAzureAD        = $AzureRequired
                IncludeJamf           = $JamfRequired
                IncludeIntune         = $IntuneRequired
                Type                  = 'Move'
            }
            $Report["$Domain"]['ComputersToBeMoved'] = Get-ADComputersToProcess @getADComputersToDeleteSplat
        }
        if ($Delete) {
            Write-Color "[i] ", "Processing computers to delete for domain $Domain" -Color Yellow, Cyan, Green
            $getADComputersToDeleteSplat = @{
                Computers             = $Report["$Domain"]['Computers']
                DeleteOnlyIf          = $DeleteOnlyIf
                Exclusions            = $Exclusions
                DomainInformation     = $DomainInformation
                ProcessedComputers    = $ProcessedComputers
                AzureInformationCache = $AzureInformationCache
                JamfInformationCache  = $JamfInformationCache
                IncludeAzureAD        = $AzureRequired
                IncludeJamf           = $JamfRequired
                IncludeIntune         = $IntuneRequired
                Type                  = 'Delete'
            }
            $Report["$Domain"]['ComputersToBeDeleted'] = Get-ADComputersToProcess @getADComputersToDeleteSplat
        }
    }
    if ($null -ne $SafetyADLimit -and $AllComputers.Count -lt $SafetyADLimit) {
        Write-Color "[e] ", "Only ", $($AllComputers.Count), " computers found in AD, this is less than the safety limit of ", $SafetyADLimit, ". Terminating!" -Color Yellow, Cyan, Red, Cyan
        return $false
    }
    $AllComputers
}
function Get-InitialGraphComputers {
    [CmdletBinding()]
    param(
        [nullable[int]] $SafetyAzureADLimit,
        [nullable[int]] $SafetyIntuneLimit,
        [nullable[int]] $DeleteLastSeenAzureMoreThan,
        [nullable[int]] $DeleteLastSeenIntuneMoreThan,
        [nullable[int]] $DeleteLastSyncAzureMoreThan,
        [nullable[int]] $DisableLastSeenAzureMoreThan,
        [nullable[int]] $DisableLastSeenIntuneMoreThan,
        [nullable[int]] $DisableLastSyncAzureMoreThan,
        [nullable[int]] $MoveLastSeenAzureMoreThan,
        [nullable[int]] $MoveLastSeenIntuneMoreThan,
        [nullable[int]] $MoveLastSyncAzureMoreThan
    )

    $AzureInformationCache = [ordered] @{
        AzureAD = [ordered] @{}
        Intune  = [ordered] @{}
    }

    if ($PSBoundParameters.ContainsKey('DisableLastSeenAzureMoreThan') -or
        $PSBoundParameters.ContainsKey('DisableLastSyncAzureMoreThan') -or
        $PSBoundParameters.ContainsKey('DeleteLastSeenAzureMoreThan') -or
        $PSBoundParameters.ContainsKey('DeleteLastSyncAzureMoreThan') -or
        $PSBoundParameters.ContainsKey('MoveLastSeenAzureMoreThan') -or
        $PSBoundParameters.ContainsKey('MoveLastSyncAzureMoreThan')) {
        Write-Color "[i] ", "Getting all computers from AzureAD" -Color Yellow, Cyan, Green

        [Array] $Devices = Get-MyDevice -Synchronized -WarningAction SilentlyContinue -WarningVariable WarningVar
        if ($WarningVar) {
            Write-Color "[e] ", "Error getting computers from AzureAD: ", $WarningVar, " Terminating!" -Color Yellow, Red, Yellow, Red
            return $false
        }
        if ($Devices.Count -eq 0) {
            Write-Color "[e] ", "No computers found in AzureAD, terminating! Please disable Azure AD integration or fix connectivity." -Color Yellow, Red
            return $false
        }
        foreach ($Device in $Devices) {
            $AzureInformationCache.AzureAD[$Device.Name] = $Device
        }

        if ($null -ne $SafetyAzureADLimit -and $Devices.Count -lt $SafetyAzureADLimit) {
            Write-Color "[e] ", "Only ", $($Devices.Count), " computers found in AzureAD, this is less than the safety limit of ", $SafetyAzureADLimit, ". Terminating!" -Color Yellow, Cyan, Red, Cyan
            return $false
        }
        Write-Color "[i] ", "Synchronized Computers found in AzureAD`: ", $($Devices.Count) -Color Yellow, Cyan, Green
    }
    if ($PSBoundParameters.ContainsKey('DisableLastSeenIntuneMoreThan') -or
        $PSBoundParameters.ContainsKey('DeleteLastSeenIntuneMoreThan') -or
        $PSBoundParameters.ContainsKey('MoveLastSeenIntuneMoreThan')) {
        Write-Color "[i] ", "Getting all computers from Intune" -Color Yellow, Cyan, Green

        [Array] $DevicesIntune = Get-MyDeviceIntune -WarningAction SilentlyContinue -WarningVariable WarningVar
        if ($WarningVar) {
            Write-Color "[e] ", "Error getting computers from Intune: ", $WarningVar, " Terminating!" -Color Yellow, Red, Yellow, Red
            return $false
        }
        if ($DevicesIntune.Count -eq 0) {
            Write-Color "[e] ", "No computers found in Intune, terminating! Please disable Intune integration or fix connectivity." -Color Yellow, Red
            return $false
        }

        foreach ($device in $DevicesIntune) {
            $AzureInformationCache.Intune[$Device.Name] = $device
        }

        if ($null -ne $SafetyIntuneLimit -and $DevicesIntune.Count -lt $SafetyIntuneLimit) {
            Write-Color "[e] ", "Only ", $($DevicesIntune.Count), " computers found in Intune, this is less than the safety limit of ", $SafetyIntuneLimit, ". Terminating!" -Color Yellow, Cyan, Red, Cyan
            return $false
        }

        Write-Color "[i] ", "Synchronized Computers found in Intune`: ", $($DevicesIntune.Count) -Color Yellow, Cyan, Green
    }

    $AzureInformationCache
}
function Get-InitialJamfComputers {
    [CmdletBinding()]
    param(
        [bool] $DisableLastContactJamfMoreThan,
        [bool] $MoveLastContactJamfMoreThan,
        [bool] $DeleteLastContactJamfMoreThan,
        [nullable[int]] $SafetyJamfLimit
    )
    $JamfCache = [ordered] @{}
    if ($PSBoundParameters.ContainsKey('DisableLastContactJamfMoreThan') -or
        $PSBoundParameters.ContainsKey('DeleteLastContactJamfMoreThan') -or
        $PSBoundParameters.ContainsKey('MoveLastContactJamfMoreThan')
    ) {
        Write-Color "[i] ", "Getting all computers from Jamf" -Color Yellow, Cyan, Green
        [Array] $Jamf = Get-JamfDevice -WarningAction SilentlyContinue -WarningVariable WarningVar
        if ($WarningVar) {
            Write-Color "[e] ", "Error getting computers from Jamf: ", $WarningVar, " Terminating!" -Color Yellow, Red, Yellow, Red
            return $false
        }
        if ($Jamf.Count -eq 0) {
            Write-Color "[e] ", "No computers found in Jamf, terminating! Please disable Jamf integration or fix connectivity." -Color Yellow, Red
            return $false
        } else {
            Write-Color "[i] ", "Computers found in Jamf`: ", $($Jamf.Count) -Color Yellow, Cyan, Green
        }

        if ($null -ne $SafetyJamfLimit -and $Jamf.Count -lt $SafetyJamfLimit) {
            Write-Color "[e] ", "Only ", $($Jamf.Count), " computers found in Jamf, this is less than the safety limit of ", $SafetyJamfLimit, ". Terminating!" -Color Yellow, Cyan, Red, Cyan
            return $false
        }

        foreach ($device in $Jamf) {
            $JamfCache[$Device.Name] = $device
        }
    }
    $JamfCache
}
function Import-ComputersData {
    [CmdletBinding()]
    param(
        [string] $DataStorePath,
        [System.Collections.IDictionary] $Export
    )

    $ProcessedComputers = [ordered] @{ }

    try {
        if ($DataStorePath -and (Test-Path -LiteralPath $DataStorePath)) {
            $FileImport = Import-Clixml -LiteralPath $DataStorePath -ErrorAction Stop
            # convert old format to new format
            $FileImport = Convert-ListProcessed -FileImport $FileImport

            if ($FileImport.PendingDeletion) {
                if ($FileImport.PendingDeletion.GetType().Name -notin 'Hashtable', 'OrderedDictionary') {
                    Write-Color -Text "[e] ", "Incorrect XML format. PendingDeletion is not a hashtable/ordereddictionary. Terminating." -Color Yellow, Red
                    return $false
                }
            }
            if ($FileImport.History) {
                if ($FileImport.History.GetType().Name -ne 'ArrayList') {
                    Write-Color -Text "[e] ", "Incorrect XML format. History is not a ArrayList. Terminating." -Color Yellow, Red
                    return $False
                }
            }
            $ProcessedComputers = $FileImport.PendingDeletion
            $Export['History'] = $FileImport.History
        }
        if (-not $ProcessedComputers) {
            $ProcessedComputers = [ordered] @{ }
        }
    } catch {
        Write-Color -Text "[e] ", "Couldn't read the list or wrong format. Error: $($_.Exception.Message)" -Color Yellow, Red
        return $false
    }

    $ProcessedComputers
}
function New-ADComputersStatistics {
    [CmdletBinding()]
    param(
        [Array] $ComputersToProcess
    )
    $Statistics = [ordered] @{
        All                          = $ComputersToProcess.Count

        ToMove                       = 0
        ToMoveComputerWorkstation    = 0
        ToMoveComputerServer         = 0
        ToMoveComputerUnknown        = 0

        ToDisable                    = 0
        ToDisableComputerUnknown     = 0
        ToDisableComputerWorkstation = 0
        ToDisableComputerServer      = 0

        ToDelete                     = 0
        ToDeleteComputerWorkstation  = 0
        ToDeleteComputerServer       = 0
        ToDeleteComputerUnknown      = 0

        TotalWindowsServers          = 0
        TotalWindowsWorkstations     = 0
        TotalMacOS                   = 0
        TotalLinux                   = 0
        TotalUnknown                 = 0

        Delete                       = [ordered] @{
            LastLogonDays           = [ordered ]@{}
            PasswordLastChangedDays = [ordered] @{}
            Systems                 = [ordered] @{}
        }
        Move                         = [ordered] @{
            LastLogonDays           = [ordered] @{}
            PasswordLastChangedDays = [ordered] @{}
            Systems                 = [ordered] @{}
        }
        Disable                      = [ordered] @{
            LastLogonDays           = [ordered] @{}
            PasswordLastChangedDays = [ordered] @{}
            Systems                 = [ordered] @{}
        }
        'Not required'               = [ordered] @{
            LastLogonDays           = [ordered] @{}
            PasswordLastChangedDays = [ordered] @{}
            Systems                 = [ordered] @{}
        }
    }
    foreach ($Computer in $ComputersToProcess) {
        if ($Computer.OperatingSystem -like "Windows Server*") {
            $Statistics.TotalWindowsServers++
        } elseif ($Computer.OperatingSystem -notlike "Windows Server*" -and $Computer.OperatingSystem -like "Windows*") {
            $Statistics.TotalWindowsWorkstations++
        } elseif ($Computer.OperatingSystem -like "Mac*") {
            $Statistics.TotalMacOS++
        } elseif ($Computer.OperatingSystem -like "Linux*") {
            $Statistics.TotalLinux++
        } else {
            $Statistics.TotalUnknown++
        }
        if ($Computer.Action -eq 'Disable') {
            $Statistics.ToDisable++
            if ($Computer.OperatingSystem -like "Windows Server*") {
                $Statistics.ToDisableComputerServer++
            } elseif ($Computer.OperatingSystem -notlike "Windows Server*" -and $Computer.OperatingSystem -like "Windows*") {
                $Statistics.ToDisableComputerWorkstation++
            } else {
                $Statistics.ToDisableComputerUnknown++
            }
        } elseif ($Computer.Action -eq 'Move') {
            $Statistics.ToMove++
            if ($Computer.OperatingSystem -like "Windows Server*") {
                $Statistics.ToMoveComputerServer++
            } elseif ($Computer.OperatingSystem -notlike "Windows Server*" -and $Computer.OperatingSystem -like "Windows*") {
                $Statistics.ToMoveComputerWorkstation++
            } else {
                $Statistics.ToMoveComputerUnknown++
            }
        } elseif ($Computer.Action -eq 'Delete') {
            $Statistics.ToDelete++
            if ($Computer.OperatingSystem -like "Windows Server*") {
                $Statistics.ToDeleteComputerServer++
            } elseif ($Computer.OperatingSystem -notlike "Windows Server*" -and $Computer.OperatingSystem -like "Windows*") {
                $Statistics.ToDeleteComputerWorkstation++
            } else {
                $Statistics.ToDeleteComputerUnknown++
            }
        }
        if ($Computer.OperatingSystem) {
            $Statistics[$Computer.Action]['Systems'][$Computer.OperatingSystem]++
        } else {
            $Statistics[$Computer.Action]['Systems']['Unknown']++
        }
        if ($Computer.LastLogonDays -gt 720) {
            $Statistics[$Computer.Action]['LastLogonDays']['Over 720 days']++
        } elseif ($Computer.LastLogonDays -gt 360) {
            $Statistics[$Computer.Action]['LastLogonDays']['Over 360 days']++
        } elseif ($Computer.LastLogonDays -gt 180) {
            $Statistics[$Computer.Action]['LastLogonDays']['Over 180 days']++
        } elseif ($Computer.LastLogonDays -gt 90) {
            $Statistics[$Computer.Action]['LastLogonDays']['Over 90 days']++
        } elseif ($Computer.LastLogonDays -gt 30) {
            $Statistics[$Computer.Action]['LastLogonDays']['Over 30 days']++
        } else {
            $Statistics[$Computer.Action]['LastLogonDays']['Under 30 days']++
        }
        if ($Computer.PasswordLastChangedDays -gt 720) {
            $Statistics[$Computer.Action]['PasswordLastChangedDays']['Over 720 days']++
        } elseif ($Computer.PasswordLastChangedDays -gt 360) {
            $Statistics[$Computer.Action]['PasswordLastChangedDays']['Over 360 days']++
        } elseif ($Computer.PasswordLastChangedDays -gt 180) {
            $Statistics[$Computer.Action]['PasswordLastChangedDays']['Over 180 days']++
        } elseif ($Computer.PasswordLastChangedDays -gt 90) {
            $Statistics[$Computer.Action]['PasswordLastChangedDays']['Over 90 days']++
        } elseif ($Computer.PasswordLastChangedDays -gt 30) {
            $Statistics[$Computer.Action]['PasswordLastChangedDays']['Over 30 days']++
        } else {
            $Statistics[$Computer.Action]['PasswordLastChangedDays']['Under 30 days']++
        }
    }
    $Statistics
}
function New-HTMLProcessedComputers {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $Export,
        [System.Collections.IDictionary] $DisableOnlyIf,
        [System.Collections.IDictionary] $DeleteOnlyIf,
        [System.Collections.IDictionary] $MoveOnlyIf,
        [Array] $ComputersToProcess,
        [string] $FilePath,
        [switch] $Online,
        [switch] $ShowHTML,
        [string] $LogFile,
        [switch] $Disable,
        [switch] $Delete,
        [switch] $ReportOnly
    )

    New-HTML {
        New-HTMLTabStyle -BorderRadius 0px -TextTransform capitalize -BackgroundColorActive SlateGrey -BackgroundColor BlizzardBlue
        New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow
        New-HTMLPanelStyle -BorderRadius 0px
        New-HTMLTableOption -DataStore JavaScript -BoolAsString -ArrayJoinString ', ' -ArrayJoin

        New-HTMLHeader {
            New-HTMLSection -Invisible {
                New-HTMLSection {
                    New-HTMLText -Text "Report generated on $(Get-Date)" -Color Blue
                } -JustifyContent flex-start -Invisible
                New-HTMLSection {
                    New-HTMLText -Text "Cleanup Monster - $($Export['Version'])" -Color Blue
                } -JustifyContent flex-end -Invisible
            }
        }
        if (-not $ReportOnly) {
            New-HTMLTab -Name 'Devices Current Run' {
                New-HTMLSection {
                    [Array] $ListAll = $($Export.CurrentRun)
                    New-HTMLPanel {
                        New-HTMLToast -TextHeader 'Total in this run' -Text "Actions (disable & delete): $($ListAll.Count)" -BarColorLeft MintGreen -IconSolid info-circle -IconColor MintGreen
                    } -Invisible

                    [Array] $ListDisabled = $($($Export.CurrentRun | Where-Object { $_.Action -eq 'Disable' }))
                    New-HTMLPanel {
                        New-HTMLToast -TextHeader 'Disable' -Text "Computers disabled: $($ListDisabled.Count)" -BarColorLeft OrangePeel -IconSolid info-circle -IconColor OrangePeel
                    } -Invisible

                    [Array] $ListMoved = $($($Export.CurrentRun | Where-Object { $_.Action -eq 'Move' }))
                    New-HTMLPanel {
                        New-HTMLToast -TextHeader 'Move' -Text "Computers moved: $($ListMoved.Count)" -BarColorLeft OrangePeel -IconSolid info-circle -IconColor OrangePeel
                    } -Invisible

                    [Array] $ListDeleted = $($($Export.CurrentRun | Where-Object { $_.Action -eq 'Delete' }))
                    New-HTMLPanel {
                        New-HTMLToast -TextHeader 'Delete' -Text "Computers deleted: $($ListDeleted.Count)" -BarColorLeft OrangeRed -IconSolid info-circle -IconColor OrangeRed
                    } -Invisible
                } -Invisible
                New-HTMLTable -DataTable $Export.CurrentRun -Filtering -ScrollX {
                    New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Delete' -BackgroundColor PinkLace
                    New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Disable' -BackgroundColor EnergyYellow
                    New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'True' -BackgroundColor LightGreen
                    New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'False' -BackgroundColor Salmon
                    New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Whatif' -BackgroundColor LightBlue
                } -WarningAction SilentlyContinue
            }
            New-HTMLTab -Name 'Devices History' {
                New-HTMLSection {
                    [Array] $ListAll = $($Export.History)
                    New-HTMLPanel {
                        New-HTMLToast -TextHeader 'Total History' -Text "Actions (disable & move & delete): $($ListAll.Count)" -BarColorLeft MintGreen -IconSolid info-circle -IconColor MintGreen
                    } -Invisible

                    [Array] $ListDisabled = $($($Export.History | Where-Object { $_.Action -eq 'Disable' }))
                    New-HTMLPanel {
                        New-HTMLToast -TextHeader 'Disabled History' -Text "Computers disabled so far: $($ListDisabled.Count)" -BarColorLeft OrangePeel -IconSolid info-circle -IconColor OrangePeel
                    } -Invisible

                    [Array] $ListMoved = $($($Export.History | Where-Object { $_.Action -eq 'Move' }))
                    New-HTMLPanel {
                        New-HTMLToast -TextHeader 'Moved History' -Text "Computers moved so far: $($ListMoved.Count)" -BarColorLeft OrangePeel -IconSolid info-circle -IconColor OrangePeel
                    } -Invisible

                    [Array] $ListDeleted = $($($Export.History | Where-Object { $_.Action -eq 'Delete' }))
                    New-HTMLPanel {
                        New-HTMLToast -TextHeader 'Deleted History' -Text "Computers deleted so far: $($ListDeleted.Count)" -BarColorLeft OrangeRed -IconSolid info-circle -IconColor OrangeRed
                    } -Invisible

                } -Invisible
                New-HTMLTable -DataTable $Export.History -Filtering -ScrollX {
                    New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Delete' -BackgroundColor PinkLace
                    New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Move' -BackgroundColor Yellow
                    New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Disable' -BackgroundColor EnergyYellow
                    New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'True' -BackgroundColor LightGreen
                    New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'False' -BackgroundColor Salmon
                    New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Whatif' -BackgroundColor LightBlue
                } -WarningAction SilentlyContinue -AllProperties
            }
            New-HTMLTab -Name 'Devices Pending' {
                New-HTMLTable -DataTable $Export.PendingDeletion.Values -Filtering -ScrollX {
                    New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Delete' -BackgroundColor PinkLace
                    New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Move' -BackgroundColor Yellow
                    New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Disable' -BackgroundColor EnergyYellow
                    New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'True' -BackgroundColor LightGreen
                    New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'False' -BackgroundColor Salmon
                    New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Whatif' -BackgroundColor LightBlue
                } -WarningAction SilentlyContinue -AllProperties
            }
        }
        New-HTMLTab -Name 'Devices' {
            New-HTMLSection {
                New-HTMLPanel {
                    New-HTMLToast -TextHeader 'Total' -Text "Computers Total: $($ComputersToProcess.Count)" -BarColorLeft MintGreen -IconSolid info-circle -IconColor MintGreen
                } -Invisible
                New-HTMLPanel {
                    New-HTMLToast -TextHeader 'To disable' -Text "Computers to be disabled: $($Export.Statistics.ToDisable)" -BarColorLeft OrangePeel -IconSolid info-circle -IconColor OrangePeel
                } -Invisible
                New-HTMLPanel {
                    New-HTMLToast -TextHeader 'To move' -Text "Computers to be moved: $($Export.Statistics.ToMove)" -BarColorLeft OrangePeel -IconSolid info-circle -IconColor OrangePeel
                } -Invisible
                New-HTMLPanel {
                    New-HTMLToast -TextHeader 'To delete' -Text "Computers to be deleted: $($Export.Statistics.ToDelete)" -BarColorLeft OrangeRed -IconSolid info-circle -IconColor OrangeRed
                } -Invisible
            } -Invisible
            New-HTMLSection -HeaderText 'General statistics' -CanCollapse {
                New-HTMLPanel {
                    New-HTMLChart {
                        New-ChartPie -Name 'To be disabled' -Value $Export.Statistics.ToDisable
                        New-ChartPie -Name 'To be deleted' -Value $Export.Statistics.ToMove
                        New-ChartPie -Name 'To be deleted' -Value $Export.Statistics.ToDelete
                    } -Title "Computers to be disabled or deleted"
                }
                if ($Export.Statistics.ToDisableComputerWorkstation -or $Export.Statistics.ToDisableComputerServer -or $Export.Statistics.ToDisableComputerUnknown) {
                    New-HTMLPanel {
                        New-HTMLChart {
                            New-ChartPie -Name "Disable workstations" -Value $Export.Statistics.ToDisableComputerWorkstation
                            New-ChartPie -Name "Disable servers" -Value $Export.Statistics.ToDisableComputerServer
                            New-ChartPie -Name "Disable unknown" -Value $Export.Statistics.ToDisableComputerUnknown
                        } -Title "Computers to be disabled by type"
                    }
                }
                if ($Export.Statistics.ToDeleteComputerWorkstation -or $Export.Statistics.ToDeleteComputerServer -or $Export.Statistics.ToDeleteComputerUnknown) {
                    New-HTMLPanel {
                        New-HTMLChart {
                            New-ChartPie -Name "Delete workstations" -Value $Export.Statistics.ToDeleteComputerWorkstation
                            New-ChartPie -Name "Delete servers" -Value $Export.Statistics.ToDeleteComputerServer
                            New-ChartPie -Name "Delete unknown" -Value $Export.Statistics.ToDeleteComputerUnknown
                        } -Title "Computers to be deleted by type"
                    }
                }
            }
            New-HTMLText -LineBreak
            New-HTMLHeading -Heading h3 -HeadingText "Full list of computers that will be processed if there are no limits to processing. "
            New-HTMLText -LineBreak
            New-HTMLSection -Invisible {
                New-HTMLPanel {
                    if ($Disable) {
                        New-HTMLText -Text "Computers will be disabled only if: " -FontWeight bold
                        New-HTMLList {
                            foreach ($Key in $DisableOnlyIf.Keys) {
                                New-HTMLListItem -Text @(
                                    if ($null -eq $DisableOnlyIf[$Key] -or $DisableOnlyIf[$Key].Count -eq 0) {
                                        $($Key), " is ", 'Not Set'
                                        $ColorInUse = 'Cinnabar'
                                    } else {
                                        if ($Key -in 'LastLogonDateMoreThan', 'LastLogonDateOlderThan') {
                                            $($Key), " is ", $($DisableOnlyIf[$Key]), " or ", "Never logged on"
                                        } elseif ($Key -in 'PasswordLastSetMoreThan', 'PasswordLastSetOlderThan') {
                                            $($Key), " is ", $($DisableOnlyIf[$Key]), " or ", "Never changed"
                                        } elseif ($Key -in 'LastSeenAzureMoreThan', 'LastSeenIntuneMoreThan', 'LastSyncAzureMoreThan', 'LastContactJamfMoreThan') {
                                            $($Key), " is ", $($DisableOnlyIf[$Key]), " or ", "Never synced/seen"
                                        } else {
                                            $($Key), " is ", $($DisableOnlyIf[$Key])
                                        }
                                        $ColorInUse = 'Apple'
                                    }
                                ) -FontWeight bold, normal, bold, normal, bold -Color $ColorInUse, None, CornflowerBlue, None, CornflowerBlue
                            }
                        }
                    } else {
                        New-HTMLText -Text "Computers will not be disabled, as the disable functionality was not enabled. " -FontWeight bold
                    }
                }
                New-HTMLPanel {
                    if ($Disable) {
                        New-HTMLText -Text "Computers will be moved only if: " -FontWeight bold
                        New-HTMLList {
                            foreach ($Key in $MoveOnlyIf.Keys) {
                                New-HTMLListItem -Text @(
                                    if ($null -eq $MoveOnlyIf[$Key] -or $MoveOnlyIf[$Key].Count -eq 0) {
                                        $($Key), " is ", 'Not Set'
                                        $ColorInUse = 'Cinnabar'
                                    } else {
                                        if ($Key -in 'LastLogonDateMoreThan', 'LastLogonDateOlderThan') {
                                            $($Key), " is ", $($MoveOnlyIf[$Key]), " or ", "Never logged on"
                                        } elseif ($Key -in 'PasswordLastSetMoreThan', 'PasswordLastSetOlderThan') {
                                            $($Key), " is ", $($MoveOnlyIf[$Key]), " or ", "Never changed"
                                        } elseif ($Key -in 'LastSeenAzureMoreThan', 'LastSeenIntuneMoreThan', 'LastSyncAzureMoreThan', 'LastContactJamfMoreThan') {
                                            $($Key), " is ", $($MoveOnlyIf[$Key]), " or ", "Never synced/seen"
                                        } else {
                                            $($Key), " is ", $($MoveOnlyIf[$Key])
                                        }
                                        $ColorInUse = 'Apple'
                                    }
                                ) -FontWeight bold, normal, bold, normal, bold -Color $ColorInUse, None, CornflowerBlue, None, CornflowerBlue
                            }
                        }
                    } else {
                        New-HTMLText -Text "Computers will not be moved, as the move functionality was not enabled. " -FontWeight bold
                    }
                }
                New-HTMLPanel {
                    if ($Delete) {
                        New-HTMLText -Text "Computers will be deleted only if: " -FontWeight bold
                        New-HTMLList {
                            foreach ($Key in $DeleteOnlyIf.Keys) {
                                New-HTMLListItem -Text @(
                                    if ($null -eq $DeleteOnlyIf[$Key] -or $DeleteOnlyIf[$Key].Count -eq 0) {
                                        $($Key), " is ", 'Not Set'
                                        $ColorInUse = 'Cinnabar'
                                    } else {
                                        if ($Key -in 'LastLogonDateMoreThan', 'LastLogonDateOlderThan') {
                                            $($Key), " is ", $($DeleteOnlyIf[$Key]), " or ", "Never logged on"
                                        } elseif ($Key -in 'PasswordLastSetMoreThan', 'PasswordLastSetOlderThan') {
                                            $($Key), " is ", $($DeleteOnlyIf[$Key]), " or ", "Never changed"
                                        } elseif ($Key -in 'LastSeenAzureMoreThan', 'LastSeenIntuneMoreThan', 'LastSyncAzureMoreThan', 'LastContactJamfMoreThan') {
                                            $($Key), " is ", $($DeleteOnlyIf[$Key]), " or ", "Never synced/seen"
                                        } else {
                                            $($Key), " is ", $($DeleteOnlyIf[$Key])
                                        }
                                        $ColorInUse = 'Apple'
                                    }
                                ) -FontWeight bold, normal, bold, normal, bold -Color $ColorInUse, None, CornflowerBlue, None, CornflowerBlue
                            }
                        }
                    } else {
                        New-HTMLText -Text "Computers will not be deleted, as the delete functionality was not enabled. " -FontWeight bold
                    }
                }
            }

            New-HTMLTable -DataTable $ComputersToProcess -Filtering -ScrollX {
                New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Delete' -BackgroundColor PinkLace
                New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Disable' -BackgroundColor EnergyYellow
                New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'True' -BackgroundColor LightGreen
                New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'False' -BackgroundColor Salmon
                New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Whatif' -BackgroundColor LightBlue
            } -WarningAction SilentlyContinue
        }
        if ($LogFile -and (Test-Path -LiteralPath $LogFile)) {
            $LogContent = Get-Content -Raw -LiteralPath $LogFile
            New-HTMLTab -Name 'Log' {
                New-HTMLCodeBlock -Code $LogContent -Style generic
            }
        }
    } -FilePath $FilePath -Online:$Online.IsPresent -ShowHTML:$ShowHTML.IsPresent
}
function Request-ADComputersDelete {
    [cmdletBinding(SupportsShouldProcess)]
    param(
        [System.Collections.IDictionary] $Report,
        [switch] $ReportOnly,
        [switch] $WhatIfDelete,
        [int] $DeleteLimit,
        [System.Collections.IDictionary] $ProcessedComputers,
        [DateTime] $Today,
        [switch] $DontWriteToEventLog
    )

    $CountDeleteLimit = 0
    # :top means name of the loop, so we can break it
    :topLoop foreach ($Domain in $Report.Keys) {
        foreach ($Computer in $Report["$Domain"]['Computers']) {
            $Server = $Report["$Domain"]['Server']
            if ($Computer.Action -ne 'Delete') {
                continue
            }
            if ($ReportOnly) {
                $Computer
            } else {
                Write-Color -Text "[i] Deleting computer ", $Computer.SamAccountName, ' DN: ', $Computer.DistinguishedName, ' Enabled: ', $Computer.Enabled, ' Operating System: ', $Computer.OperatingSystem, ' LastLogon: ', $Computer.LastLogonDate, " / " , $Computer.LastLogonDays , ' days, PasswordLastSet: ', $Computer.PasswordLastSet, " / ", $Computer.PasswordLastChangedDays, " days" -Color Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green
                try {
                    $Success = $true
                    Remove-ADObject -Identity $Computer.DistinguishedName -Recursive -WhatIf:$WhatIfDelete -Server $Server -ErrorAction Stop -Confirm:$false
                    Write-Color -Text "[+] Deleting computer ", $Computer.DistinguishedName, " (WhatIf: $($WhatIfDelete.IsPresent)) successful." -Color Yellow, Green, Yellow
                    if (-not $DontWriteToEventLog) {
                        Write-Event -ID 12 -LogName 'Application' -EntryType Warning -Category 1000 -Source 'CleanupComputers' -Message "Deleting computer $($Computer.SamAccountName) successful." -AdditionalFields @('Delete', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfDelete) -WarningAction SilentlyContinue -WarningVariable warnings
                    }
                    foreach ($W in $Warnings) {
                        Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red
                    }

                } catch {
                    $Success = $false
                    Write-Color -Text "[-] Deleting computer ", $Computer.DistinguishedName, " (WhatIf: $($WhatIfDelete.IsPresent)) failed. Error: $($_.Exception.Message)" -Color Yellow, Red, Yellow
                    if (-not $DontWriteToEventLog) {
                        Write-Event -ID 12 -LogName 'Application' -EntryType Error -Category 1000 -Source 'CleanupComputers' -Message "Deleting computer $($Computer.SamAccountName) failed." -AdditionalFields @('Delete', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfDelete, $($_.Exception.Message)) -WarningAction SilentlyContinue -WarningVariable warnings
                    }
                    foreach ($W in $Warnings) {
                        Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red
                    }
                    $Computer.ActionComment = $_.Exception.Message
                }
                $Computer.ActionDate = $Today
                if ($WhatIfDelete.IsPresent) {
                    $Computer.ActionStatus = 'WhatIf'
                } else {
                    if ($Success) {
                        # lets remove computer from $ProcessedComputers
                        # but only if it's not WhatIf and only if it's successful
                        $ComputerOnTheList = -join ($Computer.SamAccountName, "@", $Domain)
                        $ProcessedComputers.Remove("$ComputerOnTheList")
                    }
                    $Computer.ActionStatus = $Success
                }
                # return computer to $ReportDeleted so we can see summary just in case
                $Computer
                $CountDeleteLimit++
                if ($DeleteLimit) {
                    if ($DeleteLimit -eq $CountDeleteLimit) {
                        break topLoop # this breaks top loop
                    }
                }
            }
        }
    }
}
function Request-ADComputersDisable {
    [cmdletbinding(SupportsShouldProcess)]
    param(
        [nullable[bool]] $Delete,
        [nullable[bool]] $Move,
        [nullable[bool]] $DisableAndMove,
        [System.Collections.IDictionary] $Report,
        [switch] $WhatIfDisable,
        [switch] $DisableModifyDescription,
        [switch] $DisableModifyAdminDescription,
        [int] $DisableLimit,
        [switch] $ReportOnly,
        [DateTime] $Today,
        [switch] $DontWriteToEventLog,
        [Object] $DisableMoveTargetOrganizationalUnit,
        [switch] $DoNotAddToPendingList
    )

    if ($DisableAndMove -and $DisableMoveTargetOrganizationalUnit) {
        if ($DisableMoveTargetOrganizationalUnit -is [System.Collections.IDictionary]) {
            $OrganizationalUnit = $DisableMoveTargetOrganizationalUnit
        } elseif ($TargetOrganizationalUnit -is [string]) {
            $DomainCN = ConvertFrom-DistinguishedName -DistinguishedName $TargetOrganizationalUnit -ToDomainCN
            $OrganizationalUnit = [ordered] @{
                $DomainCN = $DisableMoveTargetOrganizationalUnit
            }
        } else {
            Write-Color -Text "[-] DisableMoveTargetOrganizationalUnit is not a string or hashtable. Skipping moving to proper OU." -Color Yellow, Red
            return
        }
    }

    $CountDisable = 0
    # :top means name of the loop, so we can break it
    :topLoop foreach ($Domain in $Report.Keys) {
        Write-Color "[i] ", "Starting process of disabling computers for domain $Domain" -Color Yellow, Green
        foreach ($Computer in $Report["$Domain"]['Computers']) {
            $Server = $Report["$Domain"]['Server']
            if ($Computer.Action -ne 'Disable') {
                continue
            }
            if ($ReportOnly) {
                $Computer
            } else {
                Write-Color -Text "[i] Disabling computer ", $Computer.SamAccountName, ' DN: ', $Computer.DistinguishedName, ' Enabled: ', $Computer.Enabled, ' Operating System: ', $Computer.OperatingSystem, ' LastLogon: ', $Computer.LastLogonDate, " / " , $Computer.LastLogonDays , ' days, PasswordLastSet: ', $Computer.PasswordLastSet, " / ", $Computer.PasswordLastChangedDays, " days" -Color Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green
                try {
                    Disable-ADAccount -Identity $Computer.DistinguishedName -Server $Server -WhatIf:$WhatIfDisable -ErrorAction Stop
                    Write-Color -Text "[+] Disabling computer ", $Computer.DistinguishedName, " (WhatIf: $WhatIfDisable) successful." -Color Yellow, Green, Yellow
                    if (-not $DontWriteToEventLog) {
                        Write-Event -ID 10 -LogName 'Application' -EntryType Information -Category 1000 -Source 'CleanupComputers' -Message "Disabling computer $($Computer.SamAccountName) successful." -AdditionalFields @('Disable', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfDisable) -WarningAction SilentlyContinue -WarningVariable warnings
                    }
                    foreach ($W in $Warnings) {
                        Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red
                    }
                    $Success = $true
                } catch {
                    $Computer.ActionComment = $_.Exception.Message
                    $Success = $false
                    Write-Color -Text "[-] Disabling computer ", $Computer.DistinguishedName, " (WhatIf: $WhatIfDisable) failed. Error: $($_.Exception.Message)" -Color Yellow, Red, Yellow
                    if (-not $DontWriteToEventLog) {
                        Write-Event -ID 10 -LogName 'Application' -EntryType Error -Category 1001 -Source 'CleanupComputers' -Message "Disabling computer $($Computer.SamAccountName) failed. Error: $($_.Exception.Message)" -AdditionalFields @('Disable', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfDisable, $($_.Exception.Message)) -WarningAction SilentlyContinue -WarningVariable warnings
                    }
                    foreach ($W in $Warnings) {
                        Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red
                    }
                }
                if ($Success -and $DisableAndMove) {
                    # we only move if we successfully disabled the computer
                    if ($OrganizationalUnit[$Domain]) {
                        if ($Computer.OrganizationalUnit -eq $OrganizationalUnit[$Domain]) {

                        } else {
                            try {
                                $Success = $WhatIfDisable
                                Move-ADObject -Identity $Computer.DistinguishedName -WhatIf:$WhatIfDisable -Server $Server -ErrorAction Stop -Confirm:$false -TargetPath $OrganizationalUnit[$Domain]
                                Write-Color -Text "[+] Moving computer ", $Computer.DistinguishedName, " (WhatIf: $($WhatIfDisable.IsPresent)) successful." -Color Yellow, Green, Yellow
                                if (-not $DontWriteToEventLog) {
                                    Write-Event -ID 11 -LogName 'Application' -EntryType Warning -Category 1000 -Source 'CleanupComputers' -Message "Moving computer $($Computer.SamAccountName) successful." -AdditionalFields @('Move', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfDisable) -WarningAction SilentlyContinue -WarningVariable warnings
                                }
                                foreach ($W in $Warnings) {
                                    Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red
                                }
                            } catch {
                                $Success = $false
                                Write-Color -Text "[-] Moving computer ", $Computer.DistinguishedName, " (WhatIf: $($WhatIfDisable.IsPresent)) failed. Error: $($_.Exception.Message)" -Color Yellow, Red, Yellow
                                if (-not $DontWriteToEventLog) {
                                    Write-Event -ID 11 -LogName 'Application' -EntryType Error -Category 1000 -Source 'CleanupComputers' -Message "Moving computer $($Computer.SamAccountName) failed." -AdditionalFields @('Move', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfDisable, $($_.Exception.Message)) -WarningAction SilentlyContinue -WarningVariable warnings
                                }
                                foreach ($W in $Warnings) {
                                    Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red
                                }
                                $Computer.ActionComment = $Computer.ActionComment + [System.Environment]::NewLine + $_.Exception.Message
                            }
                        }
                    }
                }
                if ($Success) {
                    if ($DisableModifyDescription -eq $true) {
                        $DisableModifyDescriptionText = "Disabled by a script, LastLogon $($Computer.LastLogonDate) ($($DisableOnlyIf.LastLogonDateMoreThan)), PasswordLastSet $($Computer.PasswordLastSet) ($($DisableOnlyIf.PasswordLastSetMoreThan))"
                        try {
                            Set-ADComputer -Identity $Computer.DistinguishedName -Description $DisableModifyDescriptionText -WhatIf:$WhatIfDisable -ErrorAction Stop -Server $Server
                            Write-Color -Text "[+] ", "Setting description on disabled computer ", $Computer.DistinguishedName, " (WhatIf: $WhatIfDisable) successful. Set to: ", $DisableModifyDescriptionText -Color Yellow, Green, Yellow, Green, Yellow
                        } catch {
                            $Computer.ActionComment = $Computer.ActionComment + [System.Environment]::NewLine + $_.Exception.Message
                            Write-Color -Text "[-] ", "Setting description on disabled computer ", $Computer.DistinguishedName, " (WhatIf: $WhatIfDisable) failed. Error: $($_.Exception.Message)" -Color Yellow, Red, Yellow
                        }
                    }
                    if ($DisableModifyAdminDescription) {
                        $DisableModifyAdminDescriptionText = "Disabled by a script, LastLogon $($Computer.LastLogonDate) ($($DisableOnlyIf.LastLogonDateMoreThan)), PasswordLastSet $($Computer.PasswordLastSet) ($($DisableOnlyIf.PasswordLastSetMoreThan))"
                        try {
                            Set-ADObject -Identity $Computer.DistinguishedName -Replace @{ AdminDescription = $DisableModifyAdminDescriptionText } -WhatIf:$WhatIfDisable -ErrorAction Stop -Server $Server
                            Write-Color -Text "[+] ", "Setting admin description on disabled computer ", $Computer.DistinguishedName, " (WhatIf: $WhatIfDisable) successful. Set to: ", $DisableModifyAdminDescriptionText -Color Yellow, Green, Yellow, Green, Yellow
                        } catch {
                            $Computer.ActionComment + [System.Environment]::NewLine + $_.Exception.Message
                            Write-Color -Text "[-] ", "Setting admin description on disabled computer ", $Computer.DistinguishedName, " (WhatIf: $WhatIfDisable) failed. Error: $($_.Exception.Message)" -Color Yellow, Red, Yellow
                        }
                    }
                }

                # this is to store actual disabling time - we can't trust WhenChanged date
                $Computer.ActionDate = $Today
                if ($WhatIfDisable.IsPresent) {
                    $Computer.ActionStatus = 'WhatIf'
                } else {
                    $Computer.ActionStatus = $Success
                }
                if ($Move -or $Delete) {
                    # we only add to processed list if Move or Delete are also enabled
                    # otherwise it makes no sense to add computers to processed list
                    if (-not $DoNotAddToPendingList) {
                        $FullComputerName = -join ($Computer.SamAccountName, '@', $Domain)
                        $ProcessedComputers[$FullComputerName] = $Computer
                    }
                }
                # return computer to $ReportDisabled so we can see summary just in case
                $Computer
                $CountDisable++
                if ($DisableLimit) {
                    if ($DisableLimit -eq $CountDisable) {
                        break topLoop # this breaks top loop
                    }
                }
            }
        }
    }
}
function Request-ADComputersMove {
    [cmdletBinding(SupportsShouldProcess)]
    param(
        [nullable[bool]] $Delete,
        [System.Collections.IDictionary] $Report,
        [switch] $ReportOnly,
        [switch] $WhatIfMove,
        [int] $MoveLimit,
        [System.Collections.IDictionary] $ProcessedComputers,
        [DateTime] $Today,
        [Object] $TargetOrganizationalUnit,
        [switch] $DontWriteToEventLog,
        [switch] $DoNotAddToPendingList
    )

    if ($TargetOrganizationalUnit -is [System.Collections.IDictionary]) {
        $OrganizationalUnit = $TargetOrganizationalUnit
    } elseif ($TargetOrganizationalUnit -is [string]) {
        $DomainCN = ConvertFrom-DistinguishedName -DistinguishedName $TargetOrganizationalUnit -ToDomainCN
        $OrganizationalUnit = [ordered] @{
            $DomainCN = $TargetOrganizationalUnit
        }
    } else {
        Write-Color -Text "[-] TargetOrganizationalUnit is not a string or hashtable. Skipping moving to proper OU." -Color Yellow, Red
        return
    }
    $CountMoveLimit = 0
    # :top means name of the loop, so we can break it
    :topLoop foreach ($Domain in $Report.Keys) {
        foreach ($Computer in $Report["$Domain"]['Computers']) {
            $Server = $Report["$Domain"]['Server']
            if ($Computer.Action -ne 'Move') {
                continue
            }
            if ($ReportOnly) {
                $Computer
            } else {
                if ($OrganizationalUnit[$Domain]) {
                    # we check if the computer is already in the correct OU
                    if ($Computer.OrganizationalUnit -eq $OrganizationalUnit[$Domain]) {
                        # this shouldn't really happen as we should have filtered it out earlier
                    } else {
                        Write-Color -Text "[i] Moving computer ", $Computer.SamAccountName, ' DN: ', $Computer.DistinguishedName, ' Enabled: ', $Computer.Enabled, ' Operating System: ', $Computer.OperatingSystem, ' LastLogon: ', $Computer.LastLogonDate, " / " , $Computer.LastLogonDays , ' days, PasswordLastSet: ', $Computer.PasswordLastSet, " / ", $Computer.PasswordLastChangedDays, " days" -Color Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green
                        try {
                            $Success = $true
                            Move-ADObject -Identity $Computer.DistinguishedName -WhatIf:$WhatIfMove -Server $Server -ErrorAction Stop -Confirm:$false -TargetPath $OrganizationalUnit[$Domain]
                            Write-Color -Text "[+] Moving computer ", $Computer.DistinguishedName, " (WhatIf: $($WhatIfMove.IsPresent)) successful." -Color Yellow, Green, Yellow
                            if (-not $DontWriteToEventLog) {
                                Write-Event -ID 11 -LogName 'Application' -EntryType Warning -Category 1000 -Source 'CleanupComputers' -Message "Moving computer $($Computer.SamAccountName) successful." -AdditionalFields @('Move', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfMove) -WarningAction SilentlyContinue -WarningVariable warnings
                            }
                            foreach ($W in $Warnings) {
                                Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red
                            }
                            if (-not $Delete) {
                                # lets remove computer from $ProcessedComputers
                                # we only remove it if Delete is not part of the removal process and move is the last step
                                if (-not $DoNotAddToPendingList) {
                                    $ComputerOnTheList = -join ($Computer.SamAccountName, "@", $Domain)
                                    $ProcessedComputers.Remove("$ComputerOnTheList")
                                }
                            }
                        } catch {
                            $Success = $false
                            Write-Color -Text "[-] Moving computer ", $Computer.DistinguishedName, " (WhatIf: $($WhatIfMove.IsPresent)) failed. Error: $($_.Exception.Message)" -Color Yellow, Red, Yellow
                            if (-not $DontWriteToEventLog) {
                                Write-Event -ID 11 -LogName 'Application' -EntryType Error -Category 1000 -Source 'CleanupComputers' -Message "Moving computer $($Computer.SamAccountName) failed." -AdditionalFields @('Move', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfMove, $($_.Exception.Message)) -WarningAction SilentlyContinue -WarningVariable warnings
                            }
                            foreach ($W in $Warnings) {
                                Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red
                            }
                            $Computer.ActionComment = $_.Exception.Message
                        }
                        $Computer.ActionDate = $Today
                        if ($WhatIfMove.IsPresent) {
                            $Computer.ActionStatus = 'WhatIf'
                        } else {
                            $Computer.ActionStatus = $Success
                        }
                        # return computer to $ReportMoved so we can see summary just in case
                        $Computer
                        $CountMoveLimit++
                        if ($MoveLimit) {
                            if ($MoveLimit -eq $CountMoveLimit) {
                                break topLoop # this breaks top loop
                            }
                        }
                    }
                } else {
                    Write-Color -Text "[-] Moving computer ", $Computer.SamAccountName, " failed. TargetOrganizationalUnit for domain $Domain not found." -Color Yellow, Red, Yellow
                    if (-not $DontWriteToEventLog) {
                        Write-Event -ID 11 -LogName 'Application' -EntryType Error -Category 1000 -Source 'CleanupComputers' -Message "Moving computer $($Computer.SamAccountName) failed." -AdditionalFields @('Move', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfMove, "TargetOrganizationalUnit for domain $Domain not found.") -WarningAction SilentlyContinue -WarningVariable warnings
                    }
                    foreach ($W in $Warnings) {
                        Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red
                    }
                    $Computer.ActionComment = "TargetOrganizationalUnit for domain $Domain not found."
                    $Computer.ActionDate = $Today
                    $Computer.ActionStatus = $false
                    # return computer to $ReportMoved so we can see summary just in case
                    $Computer
                    $CountMoveLimit++
                    if ($MoveLimit) {
                        if ($MoveLimit -eq $CountMoveLimit) {
                            break topLoop # this breaks top loop
                        }
                    }
                }
            }
        }
    }
}
function Set-LoggingCapabilities {
    [CmdletBinding()]
    param(
        [string] $LogPath,
        [int] $LogMaximum,
        [switch] $ShowTime,
        [string] $TimeFormat
    )

    $Script:PSDefaultParameterValues = @{
        "Write-Color:LogFile"    = $LogPath
        "Write-Color:ShowTime"   = if ($PSBoundParameters.ContainsKey('ShowTime')) { $ShowTime.IsPresent } else { $null }
        "Write-Color:TimeFormat" = $TimeFormat
    }
    Remove-EmptyValue -Hashtable $Script:PSDefaultParameterValues

    if ($LogPath) {
        $FolderPath = [io.path]::GetDirectoryName($LogPath)
        if (-not (Test-Path -LiteralPath $FolderPath)) {
            $null = New-Item -Path $FolderPath -ItemType Directory -Force -WhatIf:$false
        }
        if ($LogMaximum -gt 0) {
            $CurrentLogs = Get-ChildItem -LiteralPath $FolderPath | Sort-Object -Property CreationTime -Descending | Select-Object -Skip $LogMaximum
            if ($CurrentLogs) {
                Write-Color -Text '[i] ', "Logs directory has more than ", $LogMaximum, " log files. Cleanup required..." -Color Yellow, DarkCyan, Red, DarkCyan
                foreach ($Log in $CurrentLogs) {
                    try {
                        Remove-Item -LiteralPath $Log.FullName -Confirm:$false -WhatIf:$false
                        Write-Color -Text '[+] ', "Deleted ", "$($Log.FullName)" -Color Yellow, White, Green
                    } catch {
                        Write-Color -Text '[-] ', "Couldn't delete log file $($Log.FullName). Error: ', "$($_.Exception.Message) -Color Yellow, White, Red
                    }
                }
            }
        } else {
            Write-Color -Text '[i] ', "LogMaximum is set to 0 (Unlimited). No log files will be deleted." -Color Yellow, DarkCyan
        }
    }
}
function Set-ReportingCapabilities {
    [CmdletBinding()]
    param(
        [string] $ReportPath,
        [int] $ReportMaximum
    )
    if ($ReportPath) {
        $FolderPath = [io.path]::GetDirectoryName($ReportPath)
        if (-not (Test-Path -LiteralPath $FolderPath)) {
            $null = New-Item -Path $FolderPath -ItemType Directory -Force -WhatIf:$false
        }
        if ($ReportMaximum -gt 0) {
            $CurrentLogs = Get-ChildItem -LiteralPath $FolderPath | Sort-Object -Property CreationTime -Descending | Select-Object -Skip $ReportMaximum
            if ($CurrentLogs) {
                Write-Color -Text '[i] ', "Reporting directory has more than ", $ReportMaximum, " report files. Cleanup required..." -Color Yellow, DarkCyan, Red, DarkCyan
                foreach ($Report in $CurrentLogs) {
                    try {
                        Remove-Item -LiteralPath $Report.FullName -Confirm:$false -WhatIf:$false
                        Write-Color -Text '[+] ', "Deleted ", "$($Report.FullName)" -Color Yellow, White, Green
                    } catch {
                        Write-Color -Text '[-] ', "Couldn't delete report file $($Report.FullName). Error: ', "$($_.Exception.Message) -Color Yellow, White, Red
                    }
                }
            }
        } else {
            Write-Color -Text '[i] ', "ReportMaximum is set to 0 (Unlimited). No report files will be deleted." -Color Yellow, DarkCyan
        }
    }
}
function Invoke-ADComputersCleanup {
    <#
    .SYNOPSIS
    Active Directory Cleanup function that can disable or delete computers
    that have not been logged on for a certain amount of time.
 
    .DESCRIPTION
    Active Directory Cleanup function that can disable or delete computers
    that have not been logged on for a certain amount of time.
    It has many options to customize the cleanup process.
 
    .PARAMETER Disable
    Enable the disable process, meaning the computers that meet the criteria will be disabled.
 
    .PARAMETER DisableAndMove
    Enable the disable and move process, meaning the computers that meet the criteria will be disabled and moved (in that order).
    This is useful if you want to disable computers first and then move them to a different OU right after.
    It's integral part of disabling process.
    If you want Move as a separate process, use Move settings.
 
    .PARAMETER DisableIsEnabled
    Disable computer only if it's Enabled or only if it's Disabled.
    By default it will try to disable all computers that are either disabled or enabled.
    While counter-intuitive for already disabled computers,
    this is useful if you want preproceess computers for deletion and need to get them on the list.
 
    .PARAMETER DisableNoServicePrincipalName
    Disable computer only if it has a ServicePrincipalName or only if it doesn't have a ServicePrincipalName.
    By default it doesn't care if it has a ServicePrincipalName or not.
 
    .PARAMETER DisableLastLogonDateMoreThan
    Disable computer only if it has a LastLogonDate that is more than the specified number of days.
 
    .PARAMETER DisablePasswordLastSetMoreThan
    Disable computer only if it has a PasswordLastSet that is more than the specified number of days.
 
    .PARAMETER DisablePasswordLastSetOlderThan
    Disable computer only if it has a PasswordLastSet that is older than the specified date.
 
    .PARAMETER DisableLastLogonDateOlderThan
    Disable computer only if it has a LastLogonDate that is older than the specified date.
 
    .PARAMETER DisableLastSeenAzureMoreThan
    Disable computer only if it Last Seen in Azure is more than the specified number of days.
    Please note that you need to make connection to Azure using Connect-MgGraph with proper permissions first.
    Additionally you will need GraphEssentials PowerShell Module installed.
 
    .PARAMETER DisableLastSeenIntuneMoreThan
    Disable computer only if it Last Seen in Intune is more than the specified number of days.
    Please note that you need to make connection to Intune using Connect-MgGraph with proper permissions first.
    Additionally you will need GraphEssentials PowerShell Module installed.
 
    .PARAMETER DisableLastSyncAzureMoreThan
    Disable computer only if it Last Synced in Azure is more than the specified number of days.
    Please note that you need to make connection to Azure AD using Connect-MgGraph with proper permissions first.
    Additionally you will need GraphEssentials PowerShell Module installed.
 
    .PARAMETER DisableLastContactJamfMoreThan
    Disable computer only if it Last Contacted in Jamf is more than the specified number of days.
    Please note that you need to make connection to Jamf using PowerJamf PowerShell Module first.
    Additionally you will need PowerJamf PowerShell Module installed.
 
    .PARAMETER DisableExcludeSystems
    Disable computer only if it's not on the list of excluded operating systems.
    If you want to exclude Windows 10, you can specify 'Windows 10' or 'Windows 10*' or 'Windows 10*' or '*Windows 10*' or '*Windows 10*'.
    You can also specify multiple operating systems by separating them with a comma.
    It's using the -like operator, so you can use wildcards.
    It's using OperatingSystem property of the computer object for comparison.
 
    .PARAMETER DisableIncludeSystems
    Disable computer only if it's on the list of included operating systems.
    If you want to include Windows 10, you can specify 'Windows 10' or 'Windows 10*'
    or 'Windows 10*' or '*Windows 10*' or '*Windows 10*'.
    You can also specify multiple operating systems by separating them with a comma.
    It's using the -like operator, so you can use wildcards.
 
    .PARAMETER DisableExcludeServicePrincipalName
    Disable computer only if it's not on the list of excluded ServicePrincipalNames.
    You can also specify multiple ServicePrincipalNames by providing an array of entries.
    It's using the -like operator, so you can use wildcards.
 
    .PARAMETER DisableIncludeServicePrincipalName
    Disable computer only if it's on the list of included ServicePrincipalNames.
    You can also specify multiple ServicePrincipalNames by providing an array of entries.
    It's using the -like operator, so you can use wildcards.
 
    .PARAMETER DisableMoveTargetOrganizationalUnit
    Move computer to the specified OU after it's disabled.
    It can take a string with DistinguishedName, or hashtable with key being the domain, and value being the DistinguishedName.
    If you have a forest with multiple domains and want to move computers to different OUs based on their domain, you can use hashtable.
 
    .PARAMETER DisableDoNotAddToPendingList
    By default, computers that are disabled are added to the list of computers that will be actioned later (moved/deleted).
    If you want to disable computers, but not add them to the list of computers that will be actioned later (aka pending list), use this switch.
 
    .PARAMETER Delete
    Enable the delete process, meaning the computers that meet the criteria will be deleted.
 
    .PARAMETER DeleteIsEnabled
    Delete computer only if it's Enabled or only if it's Disabled.
    By default it will try to delete all computers that are either disabled or enabled.
 
    .PARAMETER DeleteNoServicePrincipalName
    Delete computer only if it has a ServicePrincipalName or only if it doesn't have a ServicePrincipalName.
    By default it doesn't care if it has a ServicePrincipalName or not.
 
    .PARAMETER DeleteLastLogonDateMoreThan
    Delete computer only if it has a LastLogonDate that is more than the specified number of days.
 
    .PARAMETER DeletePasswordLastSetMoreThan
    Delete computer only if it has a PasswordLastSet that is more than the specified number of days.
 
    .PARAMETER DeleteListProcessedMoreThan
    Delete computer only if it has been processed by this script more than the specified number of days ago.
    This is useful if you want to delete computers that have been disabled for a certain amount of time.
    It uses XML file to store the list of processed computers, so please make sure to not remove it or it will start over.
 
    .PARAMETER DeletePasswordLastSetOlderThan
    Delete computer only if it has a PasswordLastSet that is older than the specified date.
 
    .PARAMETER DeleteLastLogonDateOlderThan
    Delete computer only if it has a LastLogonDate that is older than the specified date.
 
    .PARAMETER DeleteLastSeenAzureMoreThan
    Delete computer only if it Last Seen in Azure is more than the specified number of days.
    Please note that you need to make connection to Azure using Connect-MgGraph with proper permissions first.
    Additionally yopu will need GraphEssentials PowerShell Module installed.
 
    .PARAMETER DeleteLastSeenIntuneMoreThan
    Delete computer only if it Last Seen in Intune is more than the specified number of days.
    Please note that you need to make connection to Intune using Connect-MgGraph with proper permissions first.
    Additionally you will need GraphEssentials PowerShell Module installed.
 
    .PARAMETER DeleteLastSyncAzureMoreThan
    Delete computer only if it Last Synced in Azure is more than the specified number of days.
    Please note that you need to make connection to Azure AD using Connect-MgGraph with proper permissions first.
    Additionally you will need GraphEssentials PowerShell Module installed.
 
    .PARAMETER DeleteLastContactJamfMoreThan
    Delete computer only if it Last Contacted in Jamf is more than the specified number of days.
    Please note that you need to make connection to Jamf using PowerJamf PowerShell Module first.
    Additionally you will need PowerJamf PowerShell Module installed.
 
    .PARAMETER DeleteExcludeSystems
    Delete computer only if it's not on the list of excluded operating systems.
    If you want to exclude Windows 10, you can specify 'Windows 10' or 'Windows 10*'
    or 'Windows 10*' or '*Windows 10*' or '*Windows 10*'.
    You can also specify multiple operating systems by separating them with a comma.
    It's using the -like operator, so you can use wildcards.
    It's using OperatingSystem property of the computer object for comparison.
 
    .PARAMETER DeleteIncludeSystems
    Delete computer only if it's on the list of included operating systems.
    If you want to include Windows 10, you can specify 'Windows 10' or 'Windows 10*'
    or 'Windows 10*' or '*Windows 10*' or '*Windows 10*'.
    You can also specify multiple operating systems by separating them with a comma.
    It's using the -like operator, so you can use wildcards.
 
    .PARAMETER DeleteExcludeServicePrincipalName
    Delete computer only if it's not on the list of excluded ServicePrincipalNames.
    You can also specify multiple ServicePrincipalNames by providing an array of entries.
    It's using the -like operator, so you can use wildcards.
 
    .PARAMETER DeleteIncludeServicePrincipalName
    Delete computer only if it's on the list of included ServicePrincipalNames.
    You can also specify multiple ServicePrincipalNames by providing an array of entries.
    It's using the -like operator, so you can use wildcards.
 
    .PARAMETER DeleteLimit
    Limit the number of computers that will be deleted. 0 = unlimited. Default is 1.
    This is to prevent accidental deletion of all computers that meet the criteria.
    Adjust the limit to your needs.
 
    .PARAMETER MoveIsEnabled
    Move computer only if it's Enabled or only if it's Disabled.
    By default it will try to Move all computers that are either disabled or enabled.
 
    .PARAMETER MoveNoServicePrincipalName
    Move computer only if it has a ServicePrincipalName or only if it doesn't have a ServicePrincipalName.
    By default it doesn't care if it has a ServicePrincipalName or not.
 
    .PARAMETER MoveLastLogonDateMoreThan
    Move computer only if it has a LastLogonDate that is more than the specified number of days.
 
    .PARAMETER MovePasswordLastSetMoreThan
    Move computer only if it has a PasswordLastSet that is more than the specified number of days.
 
    .PARAMETER MoveListProcessedMoreThan
    Move computer only if it has been processed by this script more than the specified number of days ago.
    This is useful if you want to Move computers that have been disabled for a certain amount of time.
    It uses XML file to store the list of processed computers, so please make sure to not remove it or it will start over.
 
    .PARAMETER MovePasswordLastSetOlderThan
    Move computer only if it has a PasswordLastSet that is older than the specified date.
 
    .PARAMETER MoveLastLogonDateOlderThan
    Move computer only if it has a LastLogonDate that is older than the specified date.
 
    .PARAMETER MoveLastSeenAzureMoreThan
    Move computer only if it Last Seen in Azure is more than the specified number of days.
    Please note that you need to make connection to Azure using Connect-MgGraph with proper permissions first.
    Additionally yopu will need GraphEssentials PowerShell Module installed.
 
    .PARAMETER MoveLastSeenIntuneMoreThan
    Move computer only if it Last Seen in Intune is more than the specified number of days.
    Please note that you need to make connection to Intune using Connect-MgGraph with proper permissions first.
    Additionally you will need GraphEssentials PowerShell Module installed.
 
    .PARAMETER MoveLastSyncAzureMoreThan
    Move computer only if it Last Synced in Azure is more than the specified number of days.
    Please note that you need to make connection to Azure AD using Connect-MgGraph with proper permissions first.
    Additionally you will need GraphEssentials PowerShell Module installed.
 
    .PARAMETER MoveLastContactJamfMoreThan
    Move computer only if it Last Contacted in Jamf is more than the specified number of days.
    Please note that you need to make connection to Jamf using PowerJamf PowerShell Module first.
    Additionally you will need PowerJamf PowerShell Module installed.
 
    .PARAMETER MoveExcludeSystems
    Move computer only if it's not on the list of excluded operating systems.
    If you want to exclude Windows 10, you can specify 'Windows 10' or 'Windows 10*'
    or 'Windows 10*' or '*Windows 10*' or '*Windows 10*'.
    You can also specify multiple operating systems by separating them with a comma.
    It's using the -like operator, so you can use wildcards.
    It's using OperatingSystem property of the computer object for comparison.
 
    .PARAMETER MoveIncludeSystems
    Move computer only if it's on the list of included operating systems.
    If you want to include Windows 10, you can specify 'Windows 10' or 'Windows 10*'
    or 'Windows 10*' or '*Windows 10*' or '*Windows 10*'.
    You can also specify multiple operating systems by separating them with a comma.
    It's using the -like operator, so you can use wildcards.
 
    .PARAMETER MoveExcludeServicePrincipalName
    Move computer only if it's not on the list of excluded ServicePrincipalNames.
    You can also specify multiple ServicePrincipalNames by providing an array of entries.
    It's using the -like operator, so you can use wildcards.
 
    .PARAMETER MoveIncludeServicePrincipalName
    Move computer only if it's on the list of included ServicePrincipalNames.
    You can also specify multiple ServicePrincipalNames by providing an array of entries.
    It's using the -like operator, so you can use wildcards.
 
    .PARAMETER MoveTargetOrganizationalUnit
    Target Organizational Unit where the computer will be moved as part of Move action.
    It can take a string with DistinguishedName, or hashtable with key being the domain, and value being the DistinguishedName.
    If you have a forest with multiple domains and want to move computers to different OUs based on their domain, you can use hashtable.
 
    .PARAMETER MoveLimit
    Limit the number of computers that will be moved. 0 = unlimited. Default is 1.
    This is to prevent accidental move of all computers that meet the criteria.
    Adjust the limit to your needs.
 
    .PARAMETER MoveDoNotAddToPendingList
    By default the script will add computers that are moved to a list of computers that will be actioned later (deleted).
    If you want to move computers, but not add them to the list of computers that will be action later (aka pending list), use this switch.
 
    .PARAMETER DeleteLimit
    Limit the number of computers that will be deleted. 0 = unlimited. Default is 1.
    This is to prevent accidental deletion of all computers that meet the criteria.
    Adjust the limit to your needs.
 
    .PARAMETER DisableLimit
    Limit the number of computers that will be disabled. 0 = unlimited. Default is 1.
    This is to prevent accidental disabling of all computers that meet the criteria.
    Adjust the limit to your needs.
 
    .PARAMETER Exclusions
    List of computers to exclude from the process.
    You can specify multiple computers by separating them with a comma.
    It's using the -like operator, so you can use wildcards.
    You can use SamAccoutName (remember about ending $), DistinguishedName,
    or DNSHostName property of the computer object for comparison.
 
    .PARAMETER DisableModifyDescription
    Modify the description of the computer object to include the date and time when it was disabled.
    By default it will not modify the description.
 
    .PARAMETER DisableModifyAdminDescription
    Modify the admin description of the computer object to include the date and time when it was disabled.
    By default it will not modify the admin description.
 
    .PARAMETER Filter
    Filter to use when searching for computers in Get-ADComputer cmdlet.
    Default is '*'
 
    .PARAMETER DataStorePath
    Path to the XML file that will be used to store the list of processed computers, current run, and history data.
    Default is $PSScriptRoot\ProcessedComputers.xml
 
    .PARAMETER ReportOnly
    Only generate the report, don't disable or delete computers.
 
    .PARAMETER ReportMaximum
    Maximum number of reports to keep. Default is Unlimited (0).
 
    .PARAMETER WhatIfDelete
    WhatIf parameter for the Delete process.
    It's not nessessary to specify this parameter if you use WhatIf parameter which applies to all processes.
 
    .PARAMETER WhatIfDisable
    WhatIf parameter for the Disable process.
    It's not nessessary to specify this parameter if you use WhatIf parameter which applies to all processes.
 
    .PARAMETER WhatIfMove
    WhatIf parameter for the Move process.
    It's not nessessary to specify this parameter if you use WhatIf parameter which applies to all processes.
 
    .PARAMETER LogPath
    Path to the log file. Default is no logging to file.
 
    .PARAMETER LogMaximum
    Maximum number of log files to keep. Default is 5.
 
    .PARAMETER Suppress
    Suppress output of the object and only display to console
 
    .PARAMETER ShowHTML
    Show HTML report in the browser once the function is complete
 
    .PARAMETER Online
    Online parameter causes HTML report to use CDN for CSS and JS files.
    This can be useful to minimize the size of the HTML report.
    Otherwise the report will start with at least 2MB in size.
 
    .PARAMETER ReportPath
    Path to the HTML report file. Default is $PSScriptRoot\ProcessedComputers.html
 
    .PARAMETER SafetyADLimit
    Minimum number of computers that must be returned by AD cmdlets to proceed with the process.
    Default is not to check.
    This is there to prevent accidental deletion of all computers if there is a problem with AD.
 
    .PARAMETER SafetyAzureADLimit
    Minimum number of computers that must be returned by AzureAD cmdlets to proceed with the process.
    Default is not to check.
    This is there to prevent accidental deletion of all computers if there is a problem with AzureAD.
    It only applies if Azure AD parameters are used.
 
    .PARAMETER SafetyIntuneLimit
    Minimum number of computers that must be returned by Intune cmdlets to proceed with the process.
    Default is not to check.
    This is there to prevent accidental deletion of all computers if there is a problem with Intune.
    It only applies if Intune parameters are used.
 
    .PARAMETER SafetyJamfLimit
    Minimum number of computers that must be returned by Jamf cmdlets to proceed with the process.
    Default is not to check.
    This is there to prevent accidental deletion of all computers if there is a problem with Jamf.
    It only applies if Jamf parameters are used.
 
    .PARAMETER DontWriteToEventLog
    By default the function will write to the event log making sure the cleanup process is logged.
    This parameter will prevent the function from writing to the event log.
 
    .EXAMPLE
    $Output = Invoke-ADComputersCleanup -DeleteIsEnabled $false -Delete -WhatIfDelete -ShowHTML -ReportOnly -LogPath $PSScriptRoot\Logs\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).log -ReportPath $PSScriptRoot\Reports\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).html
    $Output
 
    .EXAMPLE
    $Output = Invoke-ADComputersCleanup -DeleteListProcessedMoreThan 100 -Disable -DeleteIsEnabled $false -Delete -WhatIfDelete -ShowHTML -ReportOnly -LogPath $PSScriptRoot\Logs\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).log -ReportPath $PSScriptRoot\Reports\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).html
    $Output
 
    .EXAMPLE
    # this is a fresh run and it will provide report only according to it's defaults
    $Output = Invoke-ADComputersCleanup -WhatIf -ReportOnly -Disable -Delete -ShowHTML
    $Output
 
    .EXAMPLE
    # this is a fresh run and it will try to disable computers according to it's defaults
    # read documentation to understand what it does
    $Output = Invoke-ADComputersCleanup -Disable -ShowHTML -WhatIfDisable -WhatIfDelete -Delete
    $Output
 
    .EXAMPLE
    # this is a fresh run and it will try to delete computers according to it's defaults
    # read documentation to understand what it does
    $Output = Invoke-ADComputersCleanup -Delete -WhatIfDelete -ShowHTML -LogPath $PSScriptRoot\Logs\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).log -ReportPath $PSScriptRoot\Reports\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).html
    $Output
 
    .EXAMPLE
    # Run the script
    $Configuration = @{
        Disable = $true
        DisableNoServicePrincipalName = $null
        DisableIsEnabled = $true
        DisableLastLogonDateMoreThan = 90
        DisablePasswordLastSetMoreThan = 90
        DisableExcludeSystems = @(
            # 'Windows Server*'
        )
        DisableIncludeSystems = @()
        DisableLimit = 2 # 0 means unlimited, ignored for reports
        DisableModifyDescription = $false
        DisableAdminModifyDescription = $true
 
        Delete = $true
        DeleteIsEnabled = $false
        DeleteNoServicePrincipalName = $null
        DeleteLastLogonDateMoreThan = 180
        DeletePasswordLastSetMoreThan = 180
        DeleteListProcessedMoreThan = 90 # 90 days since computer was added to list
        DeleteExcludeSystems = @(
            # 'Windows Server*'
        )
        DeleteIncludeSystems = @(
 
        )
        DeleteLimit = 2 # 0 means unlimited, ignored for reports
 
        Exclusions = @(
            '*OU=Domain Controllers*'
            '*OU=Servers,OU=Production*'
            'EVOMONSTER$'
            'EVOMONSTER.AD.EVOTEC.XYZ'
        )
 
        Filter = '*'
        WhatIfDisable = $true
        WhatIfDelete = $true
        LogPath = "$PSScriptRoot\Logs\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).log"
        DataStorePath = "$PSScriptRoot\DeleteComputers_ListProcessed.xml"
        ReportPath = "$PSScriptRoot\Reports\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).html"
        ShowHTML = $true
    }
 
    # Run one time as admin: Write-Event -ID 10 -LogName 'Application' -EntryType Information -Category 0 -Message 'Initialize' -Source 'CleanupComputers'
    $Output = Invoke-ADComputersCleanup @Configuration
    $Output
 
    .NOTES
    General notes
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [string] $Forest,
        [alias('Domain')][string[]] $IncludeDomains,
        [string[]] $ExcludeDomains,
        # Disable options
        [switch] $Disable,
        [switch] $DisableAndMove,
        [nullable[bool]] $DisableIsEnabled,
        [nullable[bool]] $DisableNoServicePrincipalName,
        [nullable[int]] $DisableLastLogonDateMoreThan = 180,
        [nullable[int]] $DisablePasswordLastSetMoreThan = 180,
        [nullable[DateTime]] $DisablePasswordLastSetOlderThan,
        [nullable[DateTime]] $DisableLastLogonDateOlderThan,
        [nullable[int]] $DisableLastSeenAzureMoreThan,
        [nullable[int]] $DisableLastSeenIntuneMoreThan,
        [nullable[int]] $DisableLastSyncAzureMoreThan,
        [nullable[int]] $DisableLastContactJamfMoreThan,
        [Array] $DisableExcludeSystems = @(),
        [Array] $DisableIncludeSystems = @(),
        [Array] $DisableExcludeServicePrincipalName = @(),
        [Array] $DisableIncludeServicePrincipalName = @(),
        [int] $DisableLimit = 1, # 0 = unlimited
        [Object] $DisableMoveTargetOrganizationalUnit,
        [switch] $DisableDoNotAddToPendingList,
        # Move options
        [switch] $Move,
        [nullable[bool]] $MoveIsEnabled,
        [nullable[bool]] $MoveNoServicePrincipalName,
        [nullable[int]] $MoveLastLogonDateMoreThan,
        [nullable[int]] $MovePasswordLastSetMoreThan,
        [nullable[int]] $MoveListProcessedMoreThan,
        [nullable[DateTime]] $MovePasswordLastSetOlderThan,
        [nullable[DateTime]] $MoveLastLogonDateOlderThan,
        [nullable[int]] $MoveLastSeenAzureMoreThan,
        [nullable[int]] $MoveLastSeenIntuneMoreThan,
        [nullable[int]] $MoveLastSyncAzureMoreThan,
        [nullable[int]] $MoveLastContactJamfMoreThan,
        [Array] $MoveExcludeSystems = @(),
        [Array] $MoveIncludeSystems = @(),
        [Array] $MoveExcludeServicePrincipalName = @(),
        [Array] $MoveIncludeServicePrincipalName = @(),
        [int] $MoveLimit = 1, # 0 = unlimited
        [Object] $MoveTargetOrganizationalUnit,
        # Delete options
        [switch] $Delete,
        [nullable[bool]] $DeleteIsEnabled,
        [nullable[bool]] $DeleteNoServicePrincipalName,
        [nullable[int]] $DeleteLastLogonDateMoreThan = 180,
        [nullable[int]] $DeletePasswordLastSetMoreThan = 180,
        [nullable[int]] $DeleteListProcessedMoreThan,
        [nullable[DateTime]] $DeletePasswordLastSetOlderThan,
        [nullable[DateTime]] $DeleteLastLogonDateOlderThan,
        [nullable[int]] $DeleteLastSeenAzureMoreThan,
        [nullable[int]] $DeleteLastSeenIntuneMoreThan,
        [nullable[int]] $DeleteLastSyncAzureMoreThan,
        [nullable[int]] $DeleteLastContactJamfMoreThan,
        [Array] $DeleteExcludeSystems = @(),
        [Array] $DeleteIncludeSystems = @(),
        [Array] $DeleteExcludeServicePrincipalName = @(),
        [Array] $DeleteIncludeServicePrincipalName = @(),
        [int] $DeleteLimit = 1, # 0 = unlimited
        # General options
        [Array] $Exclusions = @(
            # default globalexclusions
            '*OU=Domain Controllers*'
        ),
        [switch] $DisableModifyDescription,
        [alias('DisableAdminModifyDescription')][switch] $DisableModifyAdminDescription,
        [string] $Filter = '*',
        [string] $DataStorePath,
        [switch] $ReportOnly,
        [int] $ReportMaximum,
        [switch] $WhatIfDelete,
        [switch] $WhatIfDisable,
        [switch] $WhatIfMove,
        [string] $LogPath,
        [int] $LogMaximum = 5,
        [switch] $Suppress,
        [switch] $ShowHTML,
        [switch] $Online,
        [string] $ReportPath,
        [nullable[int]] $SafetyADLimit,
        [nullable[int]] $SafetyAzureADLimit,
        [nullable[int]] $SafetyIntuneLimit,
        [nullable[int]] $SafetyJamfLimit,
        [switch] $DontWriteToEventLog
    )
    # we will use it to check for intune/azuread/jamf functionality
    $Script:CleanupOptions = [ordered] @{}

    # just in case user wants to use -WhatIf instead of -WhatIfDelete and -WhatIfDisable
    if (-not $WhatIfDelete -and -not $WhatIfDisable) {
        $WhatIfDelete = $WhatIfDisable = $WhatIfPreference
    }

    # lets enable global logging
    Set-LoggingCapabilities -LogPath $LogPath -LogMaximum $LogMaximum

    # prepare configuration
    $DisableOnlyIf = [ordered] @{
        # Active directory
        IsEnabled                   = $DisableIsEnabled
        NoServicePrincipalName      = $DisableNoServicePrincipalName
        LastLogonDateMoreThan       = $DisableLastLogonDateMoreThan
        PasswordLastSetMoreThan     = $DisablePasswordLastSetMoreThan
        ExcludeSystems              = $DisableExcludeSystems
        IncludeSystems              = $DisableIncludeSystems
        ExcludeServicePrincipalName = $DisableExcludeServicePrincipalName
        IncludeServicePrincipalName = $DisableIncludeServicePrincipalName
        PasswordLastSetOlderThan    = $DisablePasswordLastSetOlderThan
        LastLogonDateOlderThan      = $DisableLastLogonDateOlderThan
        # Intune
        LastSeenIntuneMoreThan      = $DisableLastSeenIntuneMoreThan
        # Azure
        LastSyncAzureMoreThan       = $DisableLastSyncAzureMoreThan
        LastSeenAzureMoreThan       = $DisableLastSeenAzureMoreThan
        # Jamf
        LastContactJamfMoreThan     = $DisableLastContactJamfMoreThan

        DoNotAddToPendingList       = $DisableDoNotAddToPendingList
    }

    $MoveOnlyIf = [ordered] @{
        # Active directory
        IsEnabled                   = $MoveIsEnabled
        NoServicePrincipalName      = $MoveNoServicePrincipalName
        LastLogonDateMoreThan       = $MoveLastLogonDateMoreThan
        PasswordLastSetMoreThan     = $MovePasswordLastSetMoreThan
        ListProcessedMoreThan       = $MoveListProcessedMoreThan
        ExcludeSystems              = $MoveExcludeSystems
        IncludeSystems              = $MoveIncludeSystems
        ExcludeServicePrincipalName = $MoveExcludeServicePrincipalName
        IncludeServicePrincipalName = $MoveIncludeServicePrincipalName
        PasswordLastSetOlderThan    = $MovePasswordLastSetOlderThan
        LastLogonDateOlderThan      = $MoveLastLogonDateOlderThan
        # Intune
        LastSeenIntuneMoreThan      = $MoveLastSeenIntuneMoreThan
        # Azure
        LastSeenAzureMoreThan       = $MoveLastSeenAzureMoreThan
        LastSyncAzureMoreThan       = $MoveLastSyncAzureMoreThan
        # Jamf
        LastContactJamfMoreThan     = $MoveLastContactJamfMoreThan
        # special option for move only
        TargetOrganizationalUnit    = $MoveTargetOrganizationalUnit

        DoNotAddToPendingList       = $MoveDoNotAddToPendingList
    }

    $DeleteOnlyIf = [ordered] @{
        # Active directory
        IsEnabled                   = $DeleteIsEnabled
        NoServicePrincipalName      = $DeleteNoServicePrincipalName
        LastLogonDateMoreThan       = $DeleteLastLogonDateMoreThan
        PasswordLastSetMoreThan     = $DeletePasswordLastSetMoreThan
        ListProcessedMoreThan       = $DeleteListProcessedMoreThan
        ExcludeSystems              = $DeleteExcludeSystems
        IncludeSystems              = $DeleteIncludeSystems
        ExcludeServicePrincipalName = $DeleteExcludeServicePrincipalName
        IncludeServicePrincipalName = $DeleteIncludeServicePrincipalName
        PasswordLastSetOlderThan    = $DeletePasswordLastSetOlderThan
        LastLogonDateOlderThan      = $DeleteLastLogonDateOlderThan
        # Intune
        LastSeenIntuneMoreThan      = $DeleteLastSeenIntuneMoreThan
        # Azure
        LastSeenAzureMoreThan       = $DeleteLastSeenAzureMoreThan
        LastSyncAzureMoreThan       = $DeleteLastSyncAzureMoreThan
        # Jamf
        LastContactJamfMoreThan     = $DeleteLastContactJamfMoreThan
    }

    if (-not $DataStorePath) {
        $DataStorePath = $($MyInvocation.PSScriptRoot) + '\ProcessedComputers.xml'
    }
    if (-not $ReportPath) {
        $ReportPath = $($MyInvocation.PSScriptRoot) + '\ProcessedComputers.html'
    }

    # lets create report path, reporting is enabled by default
    Set-ReportingCapabilities -ReportPath $ReportPath -ReportMaximum $ReportMaximum

    $Today = Get-Date
    $Properties = 'DistinguishedName', 'DNSHostName', 'SamAccountName', 'Enabled', 'OperatingSystem', 'OperatingSystemVersion', 'LastLogonDate', 'PasswordLastSet', 'PasswordExpired', 'servicePrincipalName', 'logonCount', 'ManagedBy', 'Description', 'WhenCreated', 'WhenChanged'

    $Export = [ordered] @{
        Version         = Get-GitHubVersion -Cmdlet 'Invoke-ADComputersCleanup' -RepositoryOwner 'evotecit' -RepositoryName 'CleanupMonster'
        CurrentRun      = $null
        History         = $null
        PendingDeletion = $null
    }

    Write-Color '[i] ', "[CleanupMonster] ", 'Version', ' [Informative] ', $Export['Version'] -Color Yellow, DarkGray, Yellow, DarkGray, Magenta
    Write-Color -Text "[i] Started process of cleaning up stale computers" -Color Green
    Write-Color -Text "[i] Executed by: ", $Env:USERNAME, ' from domain ', $Env:USERDNSDOMAIN -Color Green

    try {
        $ForestInformation = Get-WinADForestDetails -PreferWritable -Forest $Forest -Extended -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains
    } catch {
        Write-Color -Text "[i] ", "Couldn't get forest. Terminating. Lack of domain contact? Error: $($_.Exception.Message)." -Color Yellow, Red
        return
    }

    if (-not $ReportOnly) {
        $ProcessedComputers = Import-ComputersData -Export $Export -DataStorePath $DataStorePath
        if ($ProcessedComputers -eq $false) {
            return
        }
        Write-Color -Text "[i] ", "Loaded ", $($ProcessedComputers.Count), " computers from $($DataStorePath) and added to pending list of computers." -Color Yellow, White, Green, White
    }

    if (-not $Disable -and -not $Delete) {
        Write-Color -Text "[i] ", "No action can be taken. You need to enable Disable or/and Delete feature to have any action." -Color Yellow, Red
        return
    }

    $Report = [ordered] @{}

    $getInitialGraphComputersSplat = [ordered] @{
        SafetyAzureADLimit            = $SafetyAzureADLimit
        SafetyIntuneLimit             = $SafetyIntuneLimit
        DeleteLastSeenAzureMoreThan   = $DeleteLastSeenAzureMoreThan
        DeleteLastSeenIntuneMoreThan  = $DeleteLastSeenIntuneMoreThan
        DeleteLastSyncAzureMoreThan   = $DeleteLastSyncAzureMoreThan
        DisableLastSeenAzureMoreThan  = $DisableLastSeenAzureMoreThan
        DisableLastSeenIntuneMoreThan = $DisableLastSeenIntuneMoreThan
        DisableLastSyncAzureMoreThan  = $DisableLastSyncAzureMoreThan
        MoveLastSeenAzureMoreThan     = $MoveLastSeenAzureMoreThan
        MoveLastSeenIntuneMoreThan    = $MoveLastSeenIntuneMoreThan
        MoveLastSyncAzureMoreThan     = $MoveLastSyncAzureMoreThan
    }
    Remove-EmptyValue -Hashtable $getInitialGraphComputersSplat
    $AzureInformationCache = Get-InitialGraphComputers @getInitialGraphComputersSplat
    if ($AzureInformationCache -eq $false) {
        return
    }

    $getInitialJamf = @{
        DisableLastContactJamfMoreThan = $DisableLastContactJamfMoreThan
        DeleteLastContactJamfMoreThan  = $DeleteLastContactJamfMoreThan
        MoveLastContactJamfMoreThan    = $MoveLastContactJamfMoreThan
        SafetyJamfLimit                = $SafetyJamfLimit
    }
    Remove-EmptyValue -Hashtable $getInitialJamf
    $JamfInformationCache = Get-InitialJamfComputers @getInitialJamf
    if ($JamfInformationCache -eq $false) {
        return
    }

    $SplatADComputers = [ordered] @{
        Report                = $Report
        ForestInformation     = $ForestInformation
        Filter                = $Filter
        Properties            = $Properties
        Disable               = $Disable
        Delete                = $Delete
        Move                  = $Move
        DisableOnlyIf         = $DisableOnlyIf
        DeleteOnlyIf          = $DeleteOnlyIf
        MoveOnlyIf            = $MoveOnlyIf
        Exclusions            = $Exclusions
        ProcessedComputers    = $ProcessedComputers
        SafetyADLimit         = $SafetyADLimit
        AzureInformationCache = $AzureInformationCache
        JamfInformationCache  = $JamfInformationCache
    }

    $AllComputers = Get-InitialADComputers @SplatADComputers
    if ($AllComputers -eq $false) {
        return
    }

    foreach ($Domain in $Report.Keys) {
        if ($Disable -or $DisableAndMove) {
            if ($DisableLimit -eq 0) {
                $DisableLimitText = 'Unlimited'
            } else {
                $DisableLimitText = $DisableLimit
            }
            Write-Color "[i] ", "Computers to be disabled for domain $Domain`: ", $Report["$Domain"]['ComputersToBeDisabled'], ". Current disable limit: ", $DisableLimitText -Color Yellow, Cyan, Green, Cyan, Yellow
        }

        if ($Move) {
            if ($MoveLimit -eq 0) {
                $MoveLimitText = 'Unlimited'
            } else {
                $MoveLimitText = $MoveLimit
            }
            Write-Color "[i] ", "Computers to be moved for domain $Domain`: ", $Report["$Domain"]['ComputersToBeMoved'], ". Current move limit: ", $MoveLimitText -Color Yellow, Cyan, Green, Cyan, Yellow
        }

        if ($Delete) {
            if ($DeleteLimit -eq 0) {
                $DeleteLimitText = 'Unlimited'
            } else {
                $DeleteLimitText = $DeleteLimit
            }
            Write-Color "[i] ", "Computers to be deleted for domain $Domain`: ", $Report["$Domain"]['ComputersToBeDeleted'], ". Current delete limit: ", $DeleteLimitText -Color Yellow, Cyan, Green, Cyan, Yellow
        }
    }


    if ($Disable -or $DisableAndMove) {
        $requestADComputersDisableSplat = @{
            # those 2 are added only to make sure we don't add to processing list
            # if there is no process later on
            Delete                              = $Delete
            Move                                = $Move
            # we can disable and move on one go
            DisableAndMove                      = $DisableAndMove
            Report                              = $Report
            WhatIfDisable                       = $WhatIfDisable
            WhatIf                              = $WhatIfPreference
            DisableModifyDescription            = $DisableModifyDescription.IsPresent
            DisableModifyAdminDescription       = $DisableModifyAdminDescription.IsPresent
            DisableLimit                        = $DisableLimit
            ReportOnly                          = $ReportOnly
            Today                               = $Today
            DontWriteToEventLog                 = $DontWriteToEventLog
            DisableMoveTargetOrganizationalUnit = $DisableMoveTargetOrganizationalUnit

            DoNotAddToPendingList               = $DisableDoNotAddToPendingList
        }
        [Array] $ReportDisabled = Request-ADComputersDisable @requestADComputersDisableSplat
    }

    if ($Move) {
        $requestADComputersMoveSplat = @{
            Report                   = $Report
            WhatIfMove               = $WhatIfMove
            WhatIf                   = $WhatIfPreference
            MoveLimit                = $MoveLimit
            ReportOnly               = $ReportOnly
            Today                    = $Today
            ProcessedComputers       = $ProcessedComputers
            TargetOrganizationalUnit = $MoveTargetOrganizationalUnit
            DontWriteToEventLog      = $DontWriteToEventLog
            Delete                   = $Delete

            DoNotAddToPendingList    = $MoveDoNotAddToPendingList
        }
        [Array] $ReportMoved = Request-ADComputersMove @requestADComputersMoveSplat
    }

    if ($Delete) {
        $requestADComputersDeleteSplat = @{
            Report              = $Report
            WhatIfDelete        = $WhatIfDelete
            WhatIf              = $WhatIfPreference
            DeleteLimit         = $DeleteLimit
            ReportOnly          = $ReportOnly
            Today               = $Today
            ProcessedComputers  = $ProcessedComputers
            DontWriteToEventLog = $DontWriteToEventLog
        }
        [Array] $ReportDeleted = Request-ADComputersDelete @requestADComputersDeleteSplat
    }

    Write-Color "[i] ", "Cleanup process for processed computers that no longer exists in AD" -Color Yellow, Green
    foreach ($FullName in [string[]] $ProcessedComputers.Keys) {
        if (-not $AllComputers["$($FullName)"]) {
            Write-Color -Text "[*] Removing computer from pending list ", $ProcessedComputers[$FullName].SamAccountName, " ($($ProcessedComputers[$FullName].DistinguishedName))" -Color Yellow, Green, Yellow
            $ProcessedComputers.Remove("$($FullName)")
        }
    }

    # Building up summary
    $Export.PendingDeletion = $ProcessedComputers
    $Export.CurrentRun = @(
        if ($ReportDisabled.Count -gt 0) {
            $ReportDisabled
        }
        if ($ReportMoved.Count -gt 0) {
            $ReportMoved
        }
        if ($ReportDeleted.Count -gt 0) {
            $ReportDeleted
        }
    )
    $Export.History = @(
        if ($Export.History) {
            $Export.History
        }
        if ($ReportDisabled.Count -gt 0) {
            $ReportDisabled
        }
        if ($ReportMoved.Count -gt 0) {
            $ReportMoved
        }
        if ($ReportDeleted.Count -gt 0) {
            $ReportDeleted
        }
    )

    Write-Color "[i] ", "Exporting Processed List" -Color Yellow, Magenta
    if (-not $ReportOnly) {
        try {
            $Export | Export-Clixml -LiteralPath $DataStorePath -Encoding Unicode -WhatIf:$false -ErrorAction Stop
        } catch {
            Write-Color -Text "[-] Exporting Processed List failed. Error: $($_.Exception.Message)" -Color Yellow, Red
        }
    }
    Write-Color -Text "[i] ", "Summary of cleaning up stale computers" -Color Yellow, Cyan
    foreach ($Domain in $Report.Keys) {
        if ($Disable -or $DisableAndMove) {
            Write-Color -Text "[i] ", "Computers to be disabled for domain $Domain`: ", $Report["$Domain"]['ComputersToBeDisabled'] -Color Yellow, Cyan, Green
        }
        if ($Move) {
            Write-Color -Text "[i] ", "Computers to be moved for domain $Domain`: ", $Report["$Domain"]['ComputersToBeMoved'] -Color Yellow, Cyan, Green
        }
        if ($Delete) {
            Write-Color -Text "[i] ", "Computers to be deleted for domain $Domain`: ", $Report["$Domain"]['ComputersToBeDeleted'] -Color Yellow, Cyan, Green
        }
    }
    if (-not $ReportOnly) {
        Write-Color -Text "[i] ", "Computers on pending list`: ", $Export['PendingDeletion'].Count -Color Yellow, Cyan, Green
    }
    if (($Disable -or $DisableAndMove) -and -not $ReportOnly) {
        Write-Color -Text "[i] ", "Computers disabled in this run`: ", $ReportDisabled.Count -Color Yellow, Cyan, Green
    }
    if ($Move -and -not $ReportOnly) {
        Write-Color -Text "[i] ", "Computers moved in this run`: ", $ReportMoved.Count -Color Yellow, Cyan, Green
    }
    if ($Delete -and -not $ReportOnly) {
        Write-Color -Text "[i] ", "Computers deleted in this run`: ", $ReportDeleted.Count -Color Yellow, Cyan, Green
    }

    if ($Export -and $ReportPath) {
        [Array] $ComputersToProcess = foreach ($Domain in $Report.Keys) {
            if ($Report["$Domain"]['Computers'].Count -gt 0) {
                $Report["$Domain"]['Computers']
            }
        }
        Write-Color -Text "[i] ", "Computers to be processed for HTML report`: ", $ComputersToProcess.Count -Color Yellow, Cyan, Green
        $Export.Statistics = New-ADComputersStatistics -ComputersToProcess $ComputersToProcess

        $newHTMLProcessedComputersSplat = @{
            Export             = $Export
            FilePath           = $ReportPath
            Online             = $Online.IsPresent
            ShowHTML           = $ShowHTML.IsPresent
            LogFile            = $LogPath
            ComputersToProcess = $ComputersToProcess
            DisableOnlyIf      = $DisableOnlyIf
            DeleteOnlyIf       = $DeleteOnlyIf
            MoveOnlyIf         = $MoveOnlyIf
            Delete             = $Delete
            Disable            = $Disable
            ReportOnly         = $ReportOnly
        }
        Write-Color "[i] ", "Generating HTML report ($ReportPath)" -Color Yellow, Magenta
        New-HTMLProcessedComputers @newHTMLProcessedComputersSplat
    }

    Write-Color -Text "[i] Finished process of cleaning up stale computers" -Color Green

    if (-not $Suppress) {
        $Export
    }
}


# Export functions and aliases as required
Export-ModuleMember -Function @('Invoke-ADComputersCleanup') -Alias @()
# SIG # Begin signature block
# MIItsQYJKoZIhvcNAQcCoIItojCCLZ4CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCBGQtuczq+WlQP
# nPVRDwCGwJ4l6hZSgeC0s29ChTMTl6CCJrQwggWNMIIEdaADAgECAhAOmxiO+dAt
# 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV
# BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa
# Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD
# ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
# ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E
# MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy
# unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF
# xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1
# 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB
# MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR
# WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6
# nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB
# YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S
# UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x
# q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB
# NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP
# TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC
# AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp
# Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv
# bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0
# aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB
# LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc
# Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov
# Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy
# oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW
# juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF
# mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z
# twGpn1eqXijiuZQwggWQMIIDeKADAgECAhAFmxtXno4hMuI5B72nd3VcMA0GCSqG
# SIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx
# GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy
# dXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL/mkHNo3rvkXUo8MCIw
# aTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/zG6Q4FutWxpdtHauyefLK
# EdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZanMylNEQRBAu34LzB4Tm
# dDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7sWxq868nPzaw0QF+xembu
# d8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL2pNe3I6PgNq2kZhAkHnD
# eMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfbBHMqbpEBfCFM1LyuGwN1
# XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3JFxGj2T3wWmIdph2PVld
# QnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3cAORFJYm2mkQZK37AlLTS
# YW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqxYxhElRp2Yn72gLD76GSm
# M9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0viastkF13nqsX40/ybzT
# QRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aLT8LWRV+dIPyhHsXAj6Kx
# fgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD
# VR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwPTzANBgkq
# hkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNkaA9Wz3eucPn9mkqZucl4
# XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjSPMFDQK4dUPVS/JA7u5iZ
# aWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK7VB6fWIhCoDIc2bRoAVg
# X+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eBcg3AFDLvMFkuruBx8lbk
# apdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp5aPNoiBB19GcZNnqJqGL
# FNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msgdDDS4Dk0EIUhFQEI6FUy
# 3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vriRbgjU2wGb2dVf0a1TD9u
# KFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ79ARj6e/CVABRoIoqyc54
# zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5nLGbsQAe79APT0JsyQq8
# 7kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3i0objwG2J5VT6LaJbVu8
# aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0HEEcRrYc9B9F1vM/zZn4w
# ggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqGSIb3DQEBCwUAMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1
# c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwggIiMA0GCSqG
# SIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXHJQPE8pE3qZdRodbS
# g9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMfUBMLJnOWbfhXqAJ9
# /UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w1lbU5ygt69OxtXXn
# HwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRktFLydkf3YYMZ3V+0
# VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYbqMFkdECnwHLFuk4f
# sbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052FVUmcJgmf6AaRyBD40Nj
# gHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP65x9abJTyUpURK1h0
# QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzKQtwYSH8UNM/STKvv
# mz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo80VgvCONWPfcYd6T
# /jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjBJgj5FBASA31fI7tk
# 42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXcheMBK9Rp6103a50g5r
# mQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E
# FgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5n
# P+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMHcG
# CCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu
# Y29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln
# aUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8v
# Y3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNybDAgBgNV
# HSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIB
# AH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd4ksp+3CKDaopafxp
# wc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiCqBa9qVbPFXONASIl
# zpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl/Yy8ZCaHbJK9nXzQ
# cAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeCRK6ZJxurJB4mwbfe
# Kuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYTgAnEtp/Nh4cku0+j
# Sbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/a6fxZsNBzU+2QJsh
# IUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37xJV77QpfMzmHQXh6
# OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmLNriT1ObyF5lZynDw
# N7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0YgkPCr2B2RP+v6TR
# 81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJRyvmfxqkhQ/8mJb2
# VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIGsDCCBJigAwIBAgIQ
# CK1AsmDSnEyfXs2pvZOu2TANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQGEwJVUzEV
# MBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29t
# MSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjEwNDI5MDAw
# MDAwWhcNMzYwNDI4MjM1OTU5WjBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln
# aUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBT
# aWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMIICIjANBgkqhkiG9w0BAQEF
# AAOCAg8AMIICCgKCAgEA1bQvQtAorXi3XdU5WRuxiEL1M4zrPYGXcMW7xIUmMJ+k
# jmjYXPXrNCQH4UtP03hD9BfXHtr50tVnGlJPDqFX/IiZwZHMgQM+TXAkZLON4gh9
# NH1MgFcSa0OamfLFOx/y78tHWhOmTLMBICXzENOLsvsI8IrgnQnAZaf6mIBJNYc9
# URnokCF4RS6hnyzhGMIazMXuk0lwQjKP+8bqHPNlaJGiTUyCEUhSaN4QvRRXXegY
# E2XFf7JPhSxIpFaENdb5LpyqABXRN/4aBpTCfMjqGzLmysL0p6MDDnSlrzm2q2AS
# 4+jWufcx4dyt5Big2MEjR0ezoQ9uo6ttmAaDG7dqZy3SvUQakhCBj7A7CdfHmzJa
# wv9qYFSLScGT7eG0XOBv6yb5jNWy+TgQ5urOkfW+0/tvk2E0XLyTRSiDNipmKF+w
# c86LJiUGsoPUXPYVGUztYuBeM/Lo6OwKp7ADK5GyNnm+960IHnWmZcy740hQ83eR
# Gv7bUKJGyGFYmPV8AhY8gyitOYbs1LcNU9D4R+Z1MI3sMJN2FKZbS110YU0/EpF2
# 3r9Yy3IQKUHw1cVtJnZoEUETWJrcJisB9IlNWdt4z4FKPkBHX8mBUHOFECMhWWCK
# ZFTBzCEa6DgZfGYczXg4RTCZT/9jT0y7qg0IU0F8WD1Hs/q27IwyCQLMbDwMVhEC
# AwEAAaOCAVkwggFVMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFGg34Ou2
# O/hfEYb7/mF7CIhl9E5CMB8GA1UdIwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9P
# MA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDAzB3BggrBgEFBQcB
# AQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggr
# BgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1
# c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGln
# aWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmwwHAYDVR0gBBUwEzAH
# BgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcNAQEMBQADggIBADojRD2NCHbuj7w6
# mdNW4AIapfhINPMstuZ0ZveUcrEAyq9sMCcTEp6QRJ9L/Z6jfCbVN7w6XUhtldU/
# SfQnuxaBRVD9nL22heB2fjdxyyL3WqqQz/WTauPrINHVUHmImoqKwba9oUgYftzY
# gBoRGRjNYZmBVvbJ43bnxOQbX0P4PpT/djk9ntSZz0rdKOtfJqGVWEjVGv7XJz/9
# kNF2ht0csGBc8w2o7uCJob054ThO2m67Np375SFTWsPK6Wrxoj7bQ7gzyE84FJKZ
# 9d3OVG3ZXQIUH0AzfAPilbLCIXVzUstG2MQ0HKKlS43Nb3Y3LIU/Gs4m6Ri+kAew
# Q3+ViCCCcPDMyu/9KTVcH4k4Vfc3iosJocsL6TEa/y4ZXDlx4b6cpwoG1iZnt5Lm
# Tl/eeqxJzy6kdJKt2zyknIYf48FWGysj/4+16oh7cGvmoLr9Oj9FpsToFpFSi0HA
# SIRLlk2rREDjjfAVKM7t8RhWByovEMQMCGQ8M4+uKIw8y4+ICw2/O/TOHnuO77Xr
# y7fwdxPm5yg/rBKupS8ibEH5glwVZsxsDsrFhsP2JjMMB0ug0wcCampAMEhLNKhR
# ILutG4UI4lkNbcoFUCvqShyepf2gpx8GdOfy1lKQ/a+FSCH5Vzu0nAPthkX0tGFu
# v2jiJmCG6sivqf6UHedjGzqGVnhOMIIGwjCCBKqgAwIBAgIQBUSv85SdCDmmv9s/
# X+VhFjANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln
# aUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5
# NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTIzMDcxNDAwMDAwMFoXDTM0MTAx
# MzIzNTk1OVowSDELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMu
# MSAwHgYDVQQDExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyMzCCAiIwDQYJKoZIhvcN
# AQEBBQADggIPADCCAgoCggIBAKNTRYcdg45brD5UsyPgz5/X5dLnXaEOCdwvSKOX
# ejsqnGfcYhVYwamTEafNqrJq3RApih5iY2nTWJw1cb86l+uUUI8cIOrHmjsvlmbj
# aedp/lvD1isgHMGXlLSlUIHyz8sHpjBoyoNC2vx/CSSUpIIa2mq62DvKXd4ZGIX7
# ReoNYWyd/nFexAaaPPDFLnkPG2ZS48jWPl/aQ9OE9dDH9kgtXkV1lnX+3RChG4PB
# uOZSlbVH13gpOWvgeFmX40QrStWVzu8IF+qCZE3/I+PKhu60pCFkcOvV5aDaY7Mu
# 6QXuqvYk9R28mxyyt1/f8O52fTGZZUdVnUokL6wrl76f5P17cz4y7lI0+9S769Sg
# LDSb495uZBkHNwGRDxy1Uc2qTGaDiGhiu7xBG3gZbeTZD+BYQfvYsSzhUa+0rRUG
# FOpiCBPTaR58ZE2dD9/O0V6MqqtQFcmzyrzXxDtoRKOlO0L9c33u3Qr/eTQQfqZc
# ClhMAD6FaXXHg2TWdc2PEnZWpST618RrIbroHzSYLzrqawGw9/sqhux7UjipmAmh
# cbJsca8+uG+W1eEQE/5hRwqM/vC2x9XH3mwk8L9CgsqgcT2ckpMEtGlwJw1Pt7U2
# 0clfCKRwo+wK8REuZODLIivK8SgTIUlRfgZm0zu++uuRONhRB8qUt+JQofM604qD
# y0B7AgMBAAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAW
# BgNVHSUBAf8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglg
# hkgBhv1sBwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8wHQYDVR0O
# BBYEFKW27xPn783QZKHVVqllMaPe1eNJMFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6
# Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEy
# NTZUaW1lU3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQGCCsGAQUF
# BzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKGTGh0dHA6
# Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZT
# SEEyNTZUaW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIBAIEa1t6g
# qbWYF7xwjU+KPGic2CX/yyzkzepdIpLsjCICqbjPgKjZ5+PF7SaCinEvGN1Ott5s
# 1+FgnCvt7T1IjrhrunxdvcJhN2hJd6PrkKoS1yeF844ektrCQDifXcigLiV4JZ0q
# BXqEKZi2V3mP2yZWK7Dzp703DNiYdk9WuVLCtp04qYHnbUFcjGnRuSvExnvPnPp4
# 4pMadqJpddNQ5EQSviANnqlE0PjlSXcIWiHFtM+YlRpUurm8wWkZus8W8oM3NG6w
# QSbd3lqXTzON1I13fXVFoaVYJmoDRd7ZULVQjK9WvUzF4UbFKNOt50MAcN7MmJ4Z
# iQPq1JE3701S88lgIcRWR+3aEUuMMsOI5ljitts++V+wQtaP4xeR0arAVeOGv6wn
# LEHQmjNKqDbUuXKWfpd5OEhfysLcPTLfddY2Z1qJ+Panx+VPNTwAvb6cKmx5Adza
# ROY63jg7B145WPR8czFVoIARyxQMfq68/qTreWWqaNYiyjvrmoI1VygWy2nyMpqy
# 0tg6uLFGhmu6F/3Ed2wVbK6rr3M66ElGt9V/zLY4wNjsHPW2obhDLN9OTH0eaHDA
# dwrUAuBcYLso/zjlUlrWrBciI0707NMX+1Br/wd3H3GXREHJuEbTbDJ8WC9nR2Xl
# G3O2mflrLAZG70Ee8PBf4NvZrZCARK+AEEGKMIIHXzCCBUegAwIBAgIQB8JSdCgU
# otar/iTqF+XdLjANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQGEwJVUzEXMBUGA1UE
# ChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQg
# Q29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMB4XDTIzMDQxNjAw
# MDAwMFoXDTI2MDcwNjIzNTk1OVowZzELMAkGA1UEBhMCUEwxEjAQBgNVBAcMCU1p
# a2/FgsOzdzEhMB8GA1UECgwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMSEwHwYD
# VQQDDBhQcnplbXlzxYJhdyBLxYJ5cyBFVk9URUMwggIiMA0GCSqGSIb3DQEBAQUA
# A4ICDwAwggIKAoICAQCUmgeXMQtIaKaSkKvbAt8GFZJ1ywOH8SwxlTus4McyrWmV
# OrRBVRQA8ApF9FaeobwmkZxvkxQTFLHKm+8knwomEUslca8CqSOI0YwELv5EwTVE
# h0C/Daehvxo6tkmNPF9/SP1KC3c0l1vO+M7vdNVGKQIQrhxq7EG0iezBZOAiukNd
# GVXRYOLn47V3qL5PwG/ou2alJ/vifIDad81qFb+QkUh02Jo24SMjWdKDytdrMXi0
# 235CN4RrW+8gjfRJ+fKKjgMImbuceCsi9Iv1a66bUc9anAemObT4mF5U/yQBgAuA
# o3+jVB8wiUd87kUQO0zJCF8vq2YrVOz8OJmMX8ggIsEEUZ3CZKD0hVc3dm7cWSAw
# 8/FNzGNPlAaIxzXX9qeD0EgaCLRkItA3t3eQW+IAXyS/9ZnnpFUoDvQGbK+Q4/bP
# 0ib98XLfQpxVGRu0cCV0Ng77DIkRF+IyR1PcwVAq+OzVU3vKeo25v/rntiXCmCxi
# W4oHYO28eSQ/eIAcnii+3uKDNZrI15P7VxDrkUIc6FtiSvOhwc3AzY+vEfivUkFK
# RqwvSSr4fCrrkk7z2Qe72Zwlw2EDRVHyy0fUVGO9QMuh6E3RwnJL96ip0alcmhKA
# BGoIqSW05nXdCUbkXmhPCTT5naQDuZ1UkAXbZPShKjbPwzdXP2b8I9nQ89VSgQID
# AQABo4ICAzCCAf8wHwYDVR0jBBgwFoAUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHQYD
# VR0OBBYEFHrxaiVZuDJxxEk15bLoMuFI5233MA4GA1UdDwEB/wQEAwIHgDATBgNV
# HSUEDDAKBggrBgEFBQcDAzCBtQYDVR0fBIGtMIGqMFOgUaBPhk1odHRwOi8vY3Js
# My5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQw
# OTZTSEEzODQyMDIxQ0ExLmNybDBToFGgT4ZNaHR0cDovL2NybDQuZGlnaWNlcnQu
# Y29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAy
# MUNBMS5jcmwwPgYDVR0gBDcwNTAzBgZngQwBBAEwKTAnBggrBgEFBQcCARYbaHR0
# cDovL3d3dy5kaWdpY2VydC5jb20vQ1BTMIGUBggrBgEFBQcBAQSBhzCBhDAkBggr
# BgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBo
# dHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2Rl
# U2lnbmluZ1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqG
# SIb3DQEBCwUAA4ICAQC3EeHXUPhpe31K2DL43Hfh6qkvBHyR1RlD9lVIklcRCR50
# ZHzoWs6EBlTFyohvkpclVCuRdQW33tS6vtKPOucpDDv4wsA+6zkJYI8fHouW6Tqa
# 1W47YSrc5AOShIcJ9+NpNbKNGih3doSlcio2mUKCX5I/ZrzJBkQpJ0kYha/pUST2
# CbE3JroJf2vQWGUiI+J3LdiPNHmhO1l+zaQkSxv0cVDETMfQGZKKRVESZ6Fg61b0
# djvQSx510MdbxtKMjvS3ZtAytqnQHk1ipP+Rg+M5lFHrSkUlnpGa+f3nuQhxDb7N
# 9E8hUVevxALTrFifg8zhslVRH5/Df/CxlMKXC7op30/AyQsOQxHW1uNx3tG1DMgi
# zpwBasrxh6wa7iaA+Lp07q1I92eLhrYbtw3xC2vNIGdMdN7nd76yMIjdYnAn7r38
# wwtaJ3KYD0QTl77EB8u/5cCs3ShZdDdyg4K7NoJl8iEHrbqtooAHOMLiJpiL2i9Y
# n8kQMB6/Q6RMO3IUPLuycB9o6DNiwQHf6Jt5oW7P09k5NxxBEmksxwNbmZvNQ65Z
# n3exUAKqG+x31Egz5IZ4U/jPzRalElEIpS0rgrVg8R8pEOhd95mEzp5WERKFyXhe
# 6nB6bSYHv8clLAV0iMku308rpfjMiQkqS3LLzfUJ5OHqtKKQNMLxz9z185UCszGC
# BlMwggZPAgEBMH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ
# bmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBS
# U0E0MDk2IFNIQTM4NCAyMDIxIENBMQIQB8JSdCgUotar/iTqF+XdLjANBglghkgB
# ZQMEAgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJ
# AzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8G
# CSqGSIb3DQEJBDEiBCDhKAx2kcRJTNimjgpqnSJu1tNo4xN+gXs2iqJtwVsp2TAN
# BgkqhkiG9w0BAQEFAASCAgApQdmyO8Ey54LI3pztuho/+iN2DnZkvI8SwlVExcX0
# HAPorZXxURDgeyHqXIZi7dD5WO/tll/+Uauk8Y6Oegy8OBL2P9p6g6M/FZLPyRwb
# TqyLbGQKR9gy9X/DdTF1GIAYVX2uyBPwCgqIbTOZ3Ydk5JwckJ9TxkoaoAp3LO6Z
# JsAB4/KhMa99O6NNL6SSpz9kjfEKoh8ix2YYN7GGpGgcxsTgVKgc0eMQKTDT68GO
# yCjLqRXKGR3Z0NJHatbRw1k2REkwNKBkTDrj1+s7H3VliHnGoakzJ7nH0f7s4tyQ
# uAKsfsGGGYW18LT2y9GfU6Fc7uinBP3RfTLm022fqFQKFk7QVuB0mj0ebnKkC7IJ
# 80vlOhA0zBKhsghEZsb1S0Ld+oUpDgMlMpBsgom84XmTOezFNIG1Fc6oO96DIVYr
# XqU1NdZLIPaDHNUQL/BpkrlmTJcb1UNBCFHQv9D0bdKabnpFogKE1pWYu8bD+x4W
# I6h7awKxPuLK8Tptfx0eCw33bgT+yHARGeYQ6ckSEsxTbS7blVbOgufjTOjaB1pu
# GxmaaxKcVhnNn9yIl6Vq3NZxjghehAxhte2I7IOIgSqaAagfKXPSZd0aK6KpN4ZG
# /DMmCm+Edtm5H0C1uN/MydRBaTRGy6s+26Ff16GZOC5Fp7tWGT050QS/VI/l0Y5I
# wKGCAyAwggMcBgkqhkiG9w0BCQYxggMNMIIDCQIBATB3MGMxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1
# c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0ECEAVEr/OUnQg5
# pr/bP1/lYRYwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcN
# AQcBMBwGCSqGSIb3DQEJBTEPFw0yMzA5MDYxMTI2MDZaMC8GCSqGSIb3DQEJBDEi
# BCBmg0a9d5goYTvUOaJedcvgC6l+7xBIUZJHdzMlzyKU+zANBgkqhkiG9w0BAQEF
# AASCAgCUHSgtSGg7Il/+PLTvYema/EHUNGU0HbQpX7C3uJE1keHXGX9ud+WjWTdC
# 2JG996KE+rTOf6eoLJuka4bxLH6xfAZMe4H0Pairjuv+7VDEaZSmMfJ+opq18beS
# /+KR+bsj5QVl9lAMglgxqJWIYwnyUWeusthiGkqfkjsiseUDy1u4LSMkeJ+8bTRj
# 66X/8rjYFUHOrAI40CfOUQLHPpPaDISjvStxOiz7/ZPhASpKZ2rj8DZr3vprl6oA
# oDUsqas6cEz/nmTOIp+R3MiGucP90fCHdBuUfz6nRyTJzq/VwopOtMawSx6RekNd
# p7PgZppunt4sPth16a/lzqb4NaavfQWLwS1XhPWYr4GzJ8KIaudoBiI2LJHmvniH
# KR7LYKtC2j/jvVQDzd95e7dWIk0IwK8/SopZ7PrQDX8FAiv1+fEqMbqeCJl/NzBQ
# uljXS6TP6fqMwGgIthbBCPL4spU87XY+5uMrcNAkKmTvxy2tXexlEmyAB/vS5SKg
# 6rnHbQweTpV+/TcB6DUprOBng7CZxRR6rBZIlSd4q7PkpGuuhjoB57jdg8pP6DlF
# HLv9nZbraclQPftyLEqy9ApxQm96lcpRk1vB2hsyHcw3MsjB4dKRH4kK3OsP/G9N
# ZYGTW7Tlij/LBOQ5SeciHsmwAZPgNdkoW/jKcWzvAFWnHzfRiA==
# SIG # End signature block