Runtime/Invoke-OSDAppPreInstall.ps1

[CmdletBinding()]
param(
    [Parameter(Mandatory)][string]$StagedPath,
    [ValidateRange(1,120)][int]$OfficeRefreshTimeoutMinutes = 20
)

$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.Net.Http -ErrorAction Stop
$manifestPath = Join-Path $StagedPath 'DeviceManifest.json'
$logPath = [Environment]::ExpandEnvironmentVariables('%ProgramData%\OSDApps\Logs\Runtime.log')
$logDirectory = Split-Path -Path $logPath -Parent
$preInstallStopwatch = [System.Diagnostics.Stopwatch]::StartNew()

function Write-PreInstallLog {
    param([string]$Event,[ValidateSet('Info','Warning','Error')][string]$Level='Info',[string]$Message,[hashtable]$Data)
    New-Item -ItemType Directory -Path $logDirectory -Force | Out-Null
    $type = if ($Level -eq 'Warning') { 2 } elseif ($Level -eq 'Error') { 3 } else { 1 }
    $details = @()
    if ($Data) { foreach ($key in @($Data.Keys | Sort-Object)) { $value=$Data[$key]; if ($value -is [System.Collections.IEnumerable] -and $value -isnot [string]) { $value=@($value)-join ',' }; $details += ('{0}={1}' -f $key,$value) } }
    $msg = if ($Message) { "[$Event] $Message" } else { "[$Event]" }
    if ($details.Count -gt 0) { $msg += ' | ' + ($details -join '; ') }
    $msg = $msg -replace '\]LOG\]!>', ']LOG]! >'
    $now=(Get-Date).ToUniversalTime(); $time=$now.ToString('HH:mm:ss.fff') + '+000'; $date=$now.ToString('MM-dd-yyyy'); $thread=[System.Threading.Thread]::CurrentThread.ManagedThreadId
    $line='<![LOG[{0}]LOG]!><time="{1}" date="{2}" component="PreInstall" context="" type="{3}" thread="{4}" file="">' -f $msg,$time,$date,$type,$thread
    Add-Content -LiteralPath $logPath -Value $line -Encoding UTF8
}

function Test-InternetEndpoint {
    param([Parameter(Mandatory)][string]$Uri,[int]$TimeoutSec=10)
    try { Invoke-WebRequest -Uri $Uri -Method Head -UseBasicParsing -TimeoutSec $TimeoutSec -ErrorAction Stop | Out-Null; return $true } catch { return $false }
}

function Get-OfficeCacheVersion {
    param([Parameter(Mandatory)][string]$OfficeRoot)
    $dataPath=Join-Path $OfficeRoot 'Office\Data'
    if (-not (Test-Path -LiteralPath $dataPath -PathType Container)) { return $null }
    $versions=@(Get-ChildItem -LiteralPath $dataPath -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '^\d+\.\d+\.\d+\.\d+$' } | ForEach-Object { try { [pscustomobject]@{Name=$_.Name;Version=[version]$_.Name} } catch{} } | Sort-Object Version -Descending)
    if ($versions.Count -gt 0) { return $versions[0].Name }
    return $null
}

function Get-TeamsPackageVersion {
    param([Parameter(Mandatory)][string]$Path)
    Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction SilentlyContinue
    $archive=[System.IO.Compression.ZipFile]::OpenRead($Path)
    try {
        $entry=$archive.Entries | Where-Object { $_.FullName -eq 'AppxManifest.xml' } | Select-Object -First 1
        if (-not $entry) { throw 'AppxManifest.xml was not found in the Teams MSIX.' }
        $reader=New-Object System.IO.StreamReader($entry.Open())
        try { [xml]$m=$reader.ReadToEnd() } finally { $reader.Dispose() }
        return [string]$m.Package.Identity.Version
    } finally { $archive.Dispose() }
}

function Get-RemoteMetadata {
    param([Parameter(Mandatory)][string]$Uri,[int]$TimeoutSec=15)
    $handler=[System.Net.Http.HttpClientHandler]::new(); $handler.AllowAutoRedirect=$true
    $client=[System.Net.Http.HttpClient]::new($handler); $client.Timeout=[TimeSpan]::FromSeconds($TimeoutSec)
    try {
        $request=[System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Head,$Uri)
        $response=$client.SendAsync($request).GetAwaiter().GetResult()
        try {
            $response.EnsureSuccessStatusCode() | Out-Null
            [pscustomobject]@{
                ETag=if($response.Headers.ETag){[string]$response.Headers.ETag.Tag}else{$null}
                LastModified=if($response.Content.Headers.LastModified){$response.Content.Headers.LastModified.UtcDateTime.ToString('o')}else{$null}
                ContentLength=if($response.Content.Headers.ContentLength){[int64]$response.Content.Headers.ContentLength}else{$null}
                FinalUri=[string]$response.RequestMessage.RequestUri.AbsoluteUri
            }
        } finally { $response.Dispose(); $request.Dispose() }
    } finally { $client.Dispose(); $handler.Dispose() }
}

function Save-PreInstallDownload {
    param(
        [Parameter(Mandatory)][string]$Uri,
        [Parameter(Mandatory)][string]$DestinationPath,
        [Parameter(Mandatory)][string]$Id,
        [Parameter(Mandatory)][string]$Description,
        [int]$TimeoutMinutes = 30
    )

    $directory = Split-Path -Path $DestinationPath -Parent
    if ($directory) { New-Item -ItemType Directory -Path $directory -Force | Out-Null }

    $tempPath = "$DestinationPath.download"
    if (Test-Path -LiteralPath $tempPath) { Remove-Item -LiteralPath $tempPath -Force }

    $handler = [System.Net.Http.HttpClientHandler]::new()
    $handler.AllowAutoRedirect = $true
    $client = [System.Net.Http.HttpClient]::new($handler)
    $client.Timeout = [TimeSpan]::FromMinutes($TimeoutMinutes)
    $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()

    Write-PreInstallLog -Event 'DownloadStart' -Message "Downloading $Description." -Data @{ Id=$Id; Uri=$Uri; Destination=$DestinationPath }

    try {
        $response = $client.GetAsync($Uri, [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead).GetAwaiter().GetResult()
        try {
            $response.EnsureSuccessStatusCode() | Out-Null
            $totalBytes = $response.Content.Headers.ContentLength
            $source = $response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()

            $target = [System.IO.FileStream]::new(
                $tempPath,
                [System.IO.FileMode]::Create,
                [System.IO.FileAccess]::Write,
                [System.IO.FileShare]::None
            )

            try {
                $buffer = New-Object byte[] (1024 * 1024)
                [int64]$downloaded = 0

                while (($read = $source.Read($buffer, 0, $buffer.Length)) -gt 0) {
                    $target.Write($buffer, 0, $read)
                    $downloaded += $read
                }
            }
            finally {
                $target.Dispose()
                $source.Dispose()
            }
        }
        finally {
            $response.Dispose()
        }

        if (Test-Path -LiteralPath $DestinationPath) {
            Remove-Item -LiteralPath $DestinationPath -Force
        }
        Move-Item -LiteralPath $tempPath -Destination $DestinationPath -Force

        $stopwatch.Stop()
        $bytes = (Get-Item -LiteralPath $DestinationPath).Length
        $seconds = [math]::Max($stopwatch.Elapsed.TotalSeconds, 0.001)
        $averageMBps = [math]::Round(($bytes / 1MB) / $seconds, 1)

        Write-PreInstallLog -Event 'DownloadComplete' -Message "$Description download completed." -Data @{
            Id=$Id
            Bytes=$bytes
            SizeMB=[math]::Round($bytes / 1MB, 1)
            DurationSeconds=[math]::Round($seconds, 1)
            AverageMBps=$averageMBps
            Destination=$DestinationPath
        }
    }
    catch {
        if (Test-Path -LiteralPath $tempPath) {
            Remove-Item -LiteralPath $tempPath -Force -ErrorAction SilentlyContinue
        }
        throw
    }
    finally {
        if ($stopwatch.IsRunning) { $stopwatch.Stop() }
        $client.Dispose()
        $handler.Dispose()
    }
}


function Copy-DirectoryReplace {
    param([Parameter(Mandatory)][string]$Source,[Parameter(Mandatory)][string]$Destination)
    New-Item -ItemType Directory -Path (Split-Path $Destination -Parent) -Force | Out-Null
    $temp="$Destination.refresh"
    if(Test-Path -LiteralPath $temp){Remove-Item -LiteralPath $temp -Recurse -Force}
    Copy-Item -LiteralPath $Source -Destination $temp -Recurse -Force
    if(Test-Path -LiteralPath $Destination){Remove-Item -LiteralPath $Destination -Recurse -Force}
    Move-Item -LiteralPath $temp -Destination $Destination -Force
}

function Test-LocalOfficeSource {
    param([string]$Root)
    return ((Test-Path -LiteralPath (Join-Path $Root 'setup.exe') -PathType Leaf) -and (Test-Path -LiteralPath (Join-Path $Root 'configuration.xml') -PathType Leaf) -and (Test-Path -LiteralPath (Join-Path $Root 'Office\Data') -PathType Container))
}

function Test-LocalTeamsSource {
    param([string]$Root)
    return ((Test-Path -LiteralPath (Join-Path $Root 'teamsbootstrapper.exe') -PathType Leaf) -and (Test-Path -LiteralPath (Join-Path $Root 'teams.msix') -PathType Leaf))
}

function Test-LocalAdobeAcrobatUnifiedSource {
    param([string]$Root)
    return (Test-Path -LiteralPath (Join-Path $Root 'Package.zip') -PathType Leaf)
}

function Test-LocalMsiSource {
    param([string]$Root)
    return (Test-Path -LiteralPath (Join-Path $Root 'Package.msi') -PathType Leaf)
}

function Sync-PreInstallVendorMsi {
    param(
        [Parameter(Mandatory)][psobject]$App,
        [Parameter(Mandatory)][string]$RelativeRoot,
        [Parameter(Mandatory)][string]$PackageUri,
        [Parameter(Mandatory)][string]$DisplayName
    )

    $id = [string]$App.Id
    $localRoot = Join-Path $StagedPath $RelativeRoot
    New-Item -ItemType Directory -Path $localRoot -Force | Out-Null

    $acquireRoot = if ($usbRoot) { Join-Path $usbRoot $RelativeRoot } else { $localRoot }
    New-Item -ItemType Directory -Path $acquireRoot -Force | Out-Null

    if ($usbRoot -and -not (Test-LocalMsiSource -Root $localRoot) -and (Test-LocalMsiSource -Root $acquireRoot)) {
        Copy-Item -LiteralPath (Join-Path $acquireRoot 'Package.msi') -Destination (Join-Path $localRoot 'Package.msi') -Force
        if (Test-Path -LiteralPath (Join-Path $acquireRoot 'CacheInfo.json') -PathType Leaf) {
            Copy-Item -LiteralPath (Join-Path $acquireRoot 'CacheInfo.json') -Destination (Join-Path $localRoot 'CacheInfo.json') -Force
        }
        Write-PreInstallLog -Event 'BuiltInCacheRestaged' -Message "Existing $DisplayName USB cache was staged locally." -Data @{ Id=$id }
    }

    if (-not $networkAvailable) {
        if (Test-LocalMsiSource -Root $localRoot) {
            Write-PreInstallLog -Event 'BuiltInRefreshSkipped' -Level 'Warning' -Message "No network is available. Existing staged $DisplayName payload will be used." -Data @{ Id=$id }
            return
        }
        throw "$DisplayName has no staged or USB-cached payload and no network connection is available to acquire one."
    }

    try {
        $cacheInfoPath = Join-Path $acquireRoot 'CacheInfo.json'
        $cacheInfo = $null
        if (Test-Path -LiteralPath $cacheInfoPath -PathType Leaf) {
            try { $cacheInfo = Get-Content -LiteralPath $cacheInfoPath -Raw -Encoding UTF8 | ConvertFrom-Json } catch { }
        }

        $packagePath = Join-Path $acquireRoot 'Package.msi'
        Write-PreInstallLog -Event 'BuiltInRefreshStart' -Message "Checking $DisplayName cache for updates." -Data @{ Id=$id; Target=if($usbRoot){'USB'}else{'Local'} }

        $remote = Get-RemoteMetadata -Uri $PackageUri
        $matches = $false

        if ((Test-Path -LiteralPath $packagePath -PathType Leaf) -and $cacheInfo) {
            if ($remote.ETag -and $cacheInfo.RemoteETag) {
                $matches = $remote.ETag -eq [string]$cacheInfo.RemoteETag
            }
            elseif ($remote.LastModified -and $cacheInfo.RemoteLastModified -and $remote.ContentLength -and $cacheInfo.RemoteContentLength) {
                $matches = (
                    $remote.LastModified -eq [string]$cacheInfo.RemoteLastModified -and
                    [int64]$remote.ContentLength -eq [int64]$cacheInfo.RemoteContentLength
                )
            }
        }

        $updated = $false
        if (-not $matches) {
            $tempRoot = Join-Path $StagedPath (Join-Path 'Work\Refresh' $id)
            New-Item -ItemType Directory -Path $tempRoot -Force | Out-Null
            $tempPackage = Join-Path $tempRoot 'Package.msi'
            Save-PreInstallDownload -Uri $PackageUri -DestinationPath $tempPackage -Id $id -Description "$DisplayName MSI" -TimeoutMinutes 15
            Copy-Item -LiteralPath $tempPackage -Destination $packagePath -Force
            $updated = $true
        }

        $cacheRecord = [ordered]@{
            Id                  = $id
            Cached              = [bool]$usbRoot
            Version             = 'Current'
            Architecture        = $App.Architecture
            SourcePolicy        = 'Evergreen'
            SyncMethod          = 'HttpMetadata'
            Updated             = $updated
            PackageUri          = $PackageUri
            RemoteETag          = $remote.ETag
            RemoteLastModified  = $remote.LastModified
            RemoteContentLength = $remote.ContentLength
            RemoteFinalUri      = $remote.FinalUri
            SyncedAt            = (Get-Date).ToUniversalTime().ToString('o')
        }
        if ($App.PSObject.Properties.Name -contains 'Channel' -and $App.Channel) { $cacheRecord.Channel = [string]$App.Channel }
        if ($App.PSObject.Properties.Name -contains 'Language' -and $App.Language) { $cacheRecord.Language = [string]$App.Language }

        $cacheRecord | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $cacheInfoPath -Encoding UTF8

        if ($usbRoot) {
            Copy-Item -LiteralPath $packagePath -Destination (Join-Path $localRoot 'Package.msi') -Force
            Copy-Item -LiteralPath $cacheInfoPath -Destination (Join-Path $localRoot 'CacheInfo.json') -Force
        }

        Write-PreInstallLog -Event 'BuiltInRefreshComplete' -Message "$DisplayName content is ready for installation." -Data @{ Id=$id; PackageUpdated=$updated; CacheSynchronized=[bool]$usbRoot; SourcePolicy='Evergreen'; SyncMethod='HttpMetadata'; DurationSeconds=[math]::Round($builtInStopwatch.Elapsed.TotalSeconds,1) }
    }
    catch {
        if (Test-LocalMsiSource -Root $localRoot) {
            Write-PreInstallLog -Event 'BuiltInRefreshFailed' -Level 'Warning' -Message $_.Exception.Message -Data @{ Id=$id; Fallback='ExistingStagedPayload' }
            return
        }
        throw "$DisplayName acquisition failed and no staged fallback is available. $($_.Exception.Message)"
    }
}

try {
    if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { throw "Device manifest not found: $manifestPath" }
    $manifest=Get-Content -LiteralPath $manifestPath -Raw -Encoding UTF8 | ConvertFrom-Json
    if (-not $manifest.Runtime) { throw 'Device manifest Runtime configuration is missing.' }
    if (-not $manifest.Runtime.CacheVolumeLabel) { throw 'Device manifest Runtime.CacheVolumeLabel is missing.' }
    if (-not $manifest.Runtime.LogPath) { throw 'Device manifest Runtime.LogPath is missing.' }

    $cacheVolumeLabel = [string]$manifest.Runtime.CacheVolumeLabel
    $logPath = [Environment]::ExpandEnvironmentVariables([string]$manifest.Runtime.LogPath)
    $logDirectory = Split-Path -Path $logPath -Parent

    $apps=if($manifest.PSObject.Properties.Name -contains 'Apps'){@($manifest.Apps)}else{@()}
    $builtInApps=@($apps | Where-Object { [string]$_.Source -eq 'BuiltIn' })
    if($builtInApps.Count -eq 0){Write-PreInstallLog -Event 'RefreshSkipped' -Message 'No built-in applications are staged. Nothing to refresh.'; exit 0}

    $cacheVolume=Get-Volume -ErrorAction SilentlyContinue | Where-Object { $_.FileSystemLabel -eq $cacheVolumeLabel -and $_.DriveLetter } | Select-Object -First 1
    $usbRoot=if($cacheVolume){"$($cacheVolume.DriveLetter):\OSDApps"}else{$null}
    Write-PreInstallLog -Event 'RefreshStart' -Message 'Starting built-in acquisition and refresh before installation.' -Data @{StagedPath=$StagedPath;BuiltInApps=@($builtInApps.Id);CacheAvailable=[bool]$usbRoot;CacheRoot=$usbRoot}
    if($usbRoot){New-Item -ItemType Directory -Path $usbRoot -Force | Out-Null; Write-PreInstallLog -Event 'CacheVolumeFound' -Message 'OSDCloud USB cache is available and will be used automatically.' -Data @{CacheRoot=$usbRoot}}
    else{Write-PreInstallLog -Event 'CacheVolumeNotFound' -Message 'No OSDCloud USB cache is connected. Built-ins will be acquired directly to the local Windows runtime.'}

    $networkAvailable=[System.Net.NetworkInformation.NetworkInterface]::GetIsNetworkAvailable()

    foreach($app in $builtInApps){
        $builtInStopwatch = [System.Diagnostics.Stopwatch]::StartNew()
        switch($app.Id){
            'Microsoft365Apps' {
                $localRoot=Join-Path $StagedPath 'BuiltIn\Microsoft365Apps'
                New-Item -ItemType Directory -Path $localRoot -Force | Out-Null
                $acquireRoot=if($usbRoot){Join-Path $usbRoot 'BuiltIn\Microsoft365Apps'}else{$localRoot}
                New-Item -ItemType Directory -Path $acquireRoot -Force | Out-Null
                $localConfig=Join-Path $localRoot 'configuration.xml'
                if(-not (Test-Path -LiteralPath $localConfig -PathType Leaf)){throw 'Microsoft 365 Apps configuration.xml is missing from the staged deployment intent.'}

                if($usbRoot -and -not (Test-LocalOfficeSource -Root $localRoot) -and (Test-LocalOfficeSource -Root $acquireRoot)){
                    Copy-Item -LiteralPath (Join-Path $acquireRoot 'setup.exe') -Destination (Join-Path $localRoot 'setup.exe') -Force
                    Copy-Item -LiteralPath (Join-Path $acquireRoot 'configuration.xml') -Destination $localConfig -Force
                    if(Test-Path -LiteralPath (Join-Path $acquireRoot 'CacheInfo.json') -PathType Leaf){Copy-Item -LiteralPath (Join-Path $acquireRoot 'CacheInfo.json') -Destination (Join-Path $localRoot 'CacheInfo.json') -Force}
                    Copy-DirectoryReplace -Source (Join-Path $acquireRoot 'Office') -Destination (Join-Path $localRoot 'Office')
                    Write-PreInstallLog -Event 'BuiltInCacheRestaged' -Message 'Existing Microsoft 365 Apps USB cache was staged locally.' -Data @{Id='Microsoft365Apps'}
                }

                if(-not $networkAvailable){
                    if(Test-LocalOfficeSource -Root $localRoot){Write-PreInstallLog -Event 'BuiltInRefreshSkipped' -Level 'Warning' -Message 'No network is available. Existing staged Office payload will be used.' -Data @{Id='Microsoft365Apps'}; continue}
                    throw 'Microsoft 365 Apps has no staged or USB-cached payload and no network connection is available to acquire one.'
                }

                try {
                    $acquireConfig=Join-Path $acquireRoot 'configuration.xml'
                    Copy-Item -LiteralPath $localConfig -Destination $acquireConfig -Force
                    $setupPath=Join-Path $acquireRoot 'setup.exe'
                    $odtUri=if($app.OfficeDeploymentToolUri){[string]$app.OfficeDeploymentToolUri}else{'https://officecdn.microsoft.com/pr/wsus/setup.exe'}
                    if(-not (Test-Path -LiteralPath $setupPath -PathType Leaf)){Save-PreInstallDownload -Uri $odtUri -DestinationPath $setupPath -Id 'Microsoft365Apps' -Description 'Office Deployment Tool' -TimeoutMinutes 5}
                    if(-not (Test-InternetEndpoint -Uri $odtUri)){throw 'Microsoft 365 Apps CDN is not reachable.'}
                    $previous=Get-OfficeCacheVersion -OfficeRoot $acquireRoot
                    Write-PreInstallLog -Event 'BuiltInRefreshStart' -Message 'Synchronizing Microsoft 365 Apps content.' -Data @{Id='Microsoft365Apps';Target=if($usbRoot){'USB'}else{'Local'};PreviousVersion=$previous;TimeoutMinutes=$OfficeRefreshTimeoutMinutes}
                    $process=Start-Process -FilePath $setupPath -ArgumentList @('/download',$acquireConfig) -WorkingDirectory $acquireRoot -PassThru
                    $sw=[System.Diagnostics.Stopwatch]::StartNew()
                    while(-not $process.HasExited){if($sw.Elapsed.TotalMinutes -ge $OfficeRefreshTimeoutMinutes){try{$process.Kill();$process.WaitForExit()}catch{};throw "Office Deployment Tool cache refresh exceeded the $OfficeRefreshTimeoutMinutes minute timeout."};Start-Sleep -Seconds 2;$process.Refresh()}
                    $sw.Stop()
                    if($process.ExitCode -ne 0){throw "Office Deployment Tool exited with code $($process.ExitCode)."}
                    $version=Get-OfficeCacheVersion -OfficeRoot $acquireRoot
                    if(-not $version){throw 'Office acquisition completed but Office\Data does not contain a resolvable version.'}
                    $cacheInfo=[ordered]@{Id='Microsoft365Apps';Cached=[bool]$usbRoot;Version=$version;Architecture=$app.Architecture;Channel=$app.Channel;ProductId=$app.ProductId;Language=@($app.Language);SourcePolicy='Evergreen';SyncMethod='OfficeDeploymentTool';Updated=($previous -ne $version);SyncedAt=(Get-Date).ToUniversalTime().ToString('o')}
                    $cacheInfoPath=Join-Path $acquireRoot 'CacheInfo.json'; $cacheInfo | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $cacheInfoPath -Encoding UTF8
                    if($usbRoot){
                        Copy-Item -LiteralPath $setupPath -Destination (Join-Path $localRoot 'setup.exe') -Force
                        Copy-Item -LiteralPath $acquireConfig -Destination $localConfig -Force
                        Copy-Item -LiteralPath $cacheInfoPath -Destination (Join-Path $localRoot 'CacheInfo.json') -Force
                        Copy-DirectoryReplace -Source (Join-Path $acquireRoot 'Office') -Destination (Join-Path $localRoot 'Office')
                    }
                    $versionChanged = ($previous -ne $version)
                    Write-PreInstallLog -Event 'BuiltInRefreshComplete' -Message 'Microsoft 365 Apps content is ready for installation.' -Data @{Id='Microsoft365Apps';Version=$version;CacheSynchronized=[bool]$usbRoot;VersionChanged=$versionChanged;SourcePolicy='Evergreen';SyncMethod='OfficeDeploymentTool';DurationSeconds=[math]::Round($builtInStopwatch.Elapsed.TotalSeconds,1)}
                } catch {
                    if(Test-LocalOfficeSource -Root $localRoot){Write-PreInstallLog -Event 'BuiltInRefreshFailed' -Level 'Warning' -Message $_.Exception.Message -Data @{Id='Microsoft365Apps';Fallback='ExistingStagedPayload'};continue}
                    throw "Microsoft 365 Apps acquisition failed and no staged fallback is available. $($_.Exception.Message)"
                }
            }
            'Teams' {
                $localRoot=Join-Path $StagedPath 'BuiltIn\Teams'; New-Item -ItemType Directory -Path $localRoot -Force | Out-Null
                $acquireRoot=if($usbRoot){Join-Path $usbRoot 'BuiltIn\Teams'}else{$localRoot}; New-Item -ItemType Directory -Path $acquireRoot -Force | Out-Null
                if($usbRoot -and -not (Test-LocalTeamsSource -Root $localRoot) -and (Test-LocalTeamsSource -Root $acquireRoot)){
                    Copy-Item -LiteralPath (Join-Path $acquireRoot 'teamsbootstrapper.exe') -Destination (Join-Path $localRoot 'teamsbootstrapper.exe') -Force
                    Copy-Item -LiteralPath (Join-Path $acquireRoot 'teams.msix') -Destination (Join-Path $localRoot 'teams.msix') -Force
                    if(Test-Path -LiteralPath (Join-Path $acquireRoot 'CacheInfo.json') -PathType Leaf){Copy-Item -LiteralPath (Join-Path $acquireRoot 'CacheInfo.json') -Destination (Join-Path $localRoot 'CacheInfo.json') -Force}
                    Write-PreInstallLog -Event 'BuiltInCacheRestaged' -Message 'Existing Microsoft Teams USB cache was staged locally.' -Data @{Id='Teams'}
                }

                if(-not $networkAvailable){
                    if(Test-LocalTeamsSource -Root $localRoot){Write-PreInstallLog -Event 'BuiltInRefreshSkipped' -Level 'Warning' -Message 'No network is available. Existing staged Teams payload will be used.' -Data @{Id='Teams'};continue}
                    throw 'Microsoft Teams has no staged or USB-cached payload and no network connection is available to acquire one.'
                }
                try {
                    $architecture=if($app.Architecture){[string]$app.Architecture}elseif($env:PROCESSOR_ARCHITECTURE -eq 'ARM64'){'arm64'}else{'x64'}
                    $msixUri=if($app.MsixUri){[string]$app.MsixUri}else{switch($architecture){'x86'{'https://go.microsoft.com/fwlink/?clcid=0x409&linkid=2196060'}'x64'{'https://go.microsoft.com/fwlink/?linkid=2196106'}'arm64'{'https://go.microsoft.com/fwlink/?clcid=0x409&linkid=2196207'}}}
                    $bootstrapperUri=if($app.BootstrapperUri){[string]$app.BootstrapperUri}else{'https://go.microsoft.com/fwlink/?clcid=0x409&linkid=2243204'}
                    $cacheInfoPath=Join-Path $acquireRoot 'CacheInfo.json'; $cacheInfo=$null
                    if(Test-Path -LiteralPath $cacheInfoPath -PathType Leaf){try{$cacheInfo=Get-Content -LiteralPath $cacheInfoPath -Raw -Encoding UTF8 | ConvertFrom-Json}catch{}}
                    $msixPath=Join-Path $acquireRoot 'teams.msix'; $bootstrapperPath=Join-Path $acquireRoot 'teamsbootstrapper.exe'
                    $remote=Get-RemoteMetadata -Uri $msixUri; $matches=$false
                    if((Test-Path -LiteralPath $msixPath -PathType Leaf) -and $cacheInfo){if($remote.ETag -and $cacheInfo.RemoteETag){$matches=$remote.ETag -eq [string]$cacheInfo.RemoteETag}elseif($remote.LastModified -and $cacheInfo.RemoteLastModified -and $remote.ContentLength -and $cacheInfo.RemoteContentLength){$matches=($remote.LastModified -eq [string]$cacheInfo.RemoteLastModified -and [int64]$remote.ContentLength -eq [int64]$cacheInfo.RemoteContentLength)}}
                    $tempRoot=Join-Path $StagedPath 'Work\Refresh\Teams'; New-Item -ItemType Directory -Path $tempRoot -Force | Out-Null
                    $tempBootstrapper=Join-Path $tempRoot 'teamsbootstrapper.exe'; Save-PreInstallDownload -Uri $bootstrapperUri -DestinationPath $tempBootstrapper -Id 'Teams' -Description 'Microsoft Teams bootstrapper' -TimeoutMinutes 5; Copy-Item -LiteralPath $tempBootstrapper -Destination $bootstrapperPath -Force
                    $updated=$false
                    if(-not $matches){$tempMsix=Join-Path $tempRoot 'teams.msix';Save-PreInstallDownload -Uri $msixUri -DestinationPath $tempMsix -Id 'Teams' -Description "Microsoft Teams $architecture MSIX" -TimeoutMinutes 15;$version=Get-TeamsPackageVersion -Path $tempMsix;Copy-Item -LiteralPath $tempMsix -Destination $msixPath -Force;$updated=$true}else{$version=if($cacheInfo -and $cacheInfo.Version){[string]$cacheInfo.Version}else{Get-TeamsPackageVersion -Path $msixPath}}
                    [ordered]@{Id='Teams';Cached=[bool]$usbRoot;Version=$version;Architecture=$architecture;SourcePolicy='Evergreen';SyncMethod='HttpMetadata';Updated=$updated;BootstrapperUri=$bootstrapperUri;RemoteETag=$remote.ETag;RemoteLastModified=$remote.LastModified;RemoteContentLength=$remote.ContentLength;RemoteFinalUri=$remote.FinalUri;SyncedAt=(Get-Date).ToUniversalTime().ToString('o')} | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $cacheInfoPath -Encoding UTF8
                    if($usbRoot){Copy-Item -LiteralPath $bootstrapperPath -Destination (Join-Path $localRoot 'teamsbootstrapper.exe') -Force;Copy-Item -LiteralPath $msixPath -Destination (Join-Path $localRoot 'teams.msix') -Force;Copy-Item -LiteralPath $cacheInfoPath -Destination (Join-Path $localRoot 'CacheInfo.json') -Force}
                    Write-PreInstallLog -Event 'BuiltInRefreshComplete' -Message 'Microsoft Teams content is ready for installation.' -Data @{Id='Teams';Version=$version;PackageUpdated=$updated;CacheSynchronized=[bool]$usbRoot;SourcePolicy='Evergreen';SyncMethod='HttpMetadata';DurationSeconds=[math]::Round($builtInStopwatch.Elapsed.TotalSeconds,1)}
                } catch {
                    if(Test-LocalTeamsSource -Root $localRoot){Write-PreInstallLog -Event 'BuiltInRefreshFailed' -Level 'Warning' -Message $_.Exception.Message -Data @{Id='Teams';Fallback='ExistingStagedPayload'};continue}
                    throw "Microsoft Teams acquisition failed and no staged fallback is available. $($_.Exception.Message)"
                }
            }
            'AdobeAcrobatUnified' {
                $architecture=if($app.Architecture){[string]$app.Architecture}else{'x64'}
                $localRoot=Join-Path $StagedPath (Join-Path 'BuiltIn\AdobeAcrobatUnified' $architecture)
                New-Item -ItemType Directory -Path $localRoot -Force | Out-Null
                $acquireRoot=if($usbRoot){Join-Path $usbRoot (Join-Path 'BuiltIn\AdobeAcrobatUnified' $architecture)}else{$localRoot}
                New-Item -ItemType Directory -Path $acquireRoot -Force | Out-Null

                if($usbRoot -and -not (Test-LocalAdobeAcrobatUnifiedSource -Root $localRoot) -and (Test-LocalAdobeAcrobatUnifiedSource -Root $acquireRoot)){
                    Copy-Item -LiteralPath (Join-Path $acquireRoot 'Package.zip') -Destination (Join-Path $localRoot 'Package.zip') -Force
                    if(Test-Path -LiteralPath (Join-Path $acquireRoot 'CacheInfo.json') -PathType Leaf){Copy-Item -LiteralPath (Join-Path $acquireRoot 'CacheInfo.json') -Destination (Join-Path $localRoot 'CacheInfo.json') -Force}
                    Write-PreInstallLog -Event 'BuiltInCacheRestaged' -Message 'Existing Adobe Acrobat Unified USB cache was staged locally.' -Data @{Id='AdobeAcrobatUnified'}
                }

                if(-not $networkAvailable){
                    if(Test-LocalAdobeAcrobatUnifiedSource -Root $localRoot){
                        Write-PreInstallLog -Event 'BuiltInRefreshSkipped' -Level 'Warning' -Message 'No network is available. Existing staged Adobe Acrobat Unified payload will be used.' -Data @{Id='AdobeAcrobatUnified'}
                        continue
                    }
                    throw 'Adobe Acrobat Unified has no staged or USB-cached payload and no network connection is available to acquire one.'
                }

                try {
                    $packageUri=if($app.PackageUri){[string]$app.PackageUri}else{switch($architecture){'x86'{'https://trials.adobe.com/AdobeProducts/APRO/Acrobat_HelpX/win32/Acrobat_DC_Web_WWMUI.zip'}default{'https://trials.adobe.com/AdobeProducts/APRO/Acrobat_HelpX/win32/Acrobat_DC_Web_x64_WWMUI.zip'}}}
                    $cacheInfoPath=Join-Path $acquireRoot 'CacheInfo.json'
                    $cacheInfo=$null
                    if(Test-Path -LiteralPath $cacheInfoPath -PathType Leaf){try{$cacheInfo=Get-Content -LiteralPath $cacheInfoPath -Raw -Encoding UTF8 | ConvertFrom-Json}catch{}}
                    $packagePath=Join-Path $acquireRoot 'Package.zip'

                    Write-PreInstallLog -Event 'BuiltInRefreshStart' -Message 'Checking Adobe Acrobat Unified cache for updates.' -Data @{Id='AdobeAcrobatUnified';Target=if($usbRoot){'USB'}else{'Local'}}

                    $remote=Get-RemoteMetadata -Uri $packageUri
                    $matches=$false
                    if((Test-Path -LiteralPath $packagePath -PathType Leaf) -and $cacheInfo){
                        if($remote.ETag -and $cacheInfo.RemoteETag){
                            $matches=$remote.ETag -eq [string]$cacheInfo.RemoteETag
                        }
                        elseif($remote.LastModified -and $cacheInfo.RemoteLastModified -and $remote.ContentLength -and $cacheInfo.RemoteContentLength){
                            $matches=($remote.LastModified -eq [string]$cacheInfo.RemoteLastModified -and [int64]$remote.ContentLength -eq [int64]$cacheInfo.RemoteContentLength)
                        }
                    }

                    $updated=$false
                    if(-not $matches){
                        $tempRoot=Join-Path $StagedPath 'Work\Refresh\AdobeAcrobatUnified'
                        New-Item -ItemType Directory -Path $tempRoot -Force | Out-Null
                        $tempPackage=Join-Path $tempRoot 'Package.zip'
                        Save-PreInstallDownload -Uri $packageUri -DestinationPath $tempPackage -Id 'AdobeAcrobatUnified' -Description "Adobe Acrobat Unified $architecture package" -TimeoutMinutes 30
                        Copy-Item -LiteralPath $tempPackage -Destination $packagePath -Force
                        $updated=$true
                    }

                    [ordered]@{
                        Id='AdobeAcrobatUnified'
                        Cached=[bool]$usbRoot
                        Version='Current'
                        Architecture=$architecture
                        SourcePolicy='Evergreen'
                        SyncMethod='HttpMetadata'
                        Updated=$updated
                        PackageUri=$packageUri
                        RemoteETag=$remote.ETag
                        RemoteLastModified=$remote.LastModified
                        RemoteContentLength=$remote.ContentLength
                        RemoteFinalUri=$remote.FinalUri
                        SyncedAt=(Get-Date).ToUniversalTime().ToString('o')
                    } | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $cacheInfoPath -Encoding UTF8

                    if($usbRoot){
                        Copy-Item -LiteralPath $packagePath -Destination (Join-Path $localRoot 'Package.zip') -Force
                        Copy-Item -LiteralPath $cacheInfoPath -Destination (Join-Path $localRoot 'CacheInfo.json') -Force
                    }

                    Write-PreInstallLog -Event 'BuiltInRefreshComplete' -Message 'Adobe Acrobat Unified content is ready for installation.' -Data @{Id='AdobeAcrobatUnified';PackageUpdated=$updated;CacheSynchronized=[bool]$usbRoot;SourcePolicy='Evergreen';SyncMethod='HttpMetadata';DurationSeconds=[math]::Round($builtInStopwatch.Elapsed.TotalSeconds,1)}
                }
                catch {
                    if(Test-LocalAdobeAcrobatUnifiedSource -Root $localRoot){
                        Write-PreInstallLog -Event 'BuiltInRefreshFailed' -Level 'Warning' -Message $_.Exception.Message -Data @{Id='AdobeAcrobatUnified';Fallback='ExistingStagedPayload'}
                        continue
                    }
                    throw "Adobe Acrobat Unified acquisition failed and no staged fallback is available. $($_.Exception.Message)"
                }
            }
            'GoogleChromeEnterprise' {
                $architecture = if ($app.Architecture) { [string]$app.Architecture } else { 'x64' }
                $packageUri = if ($app.PackageUri) {
                    [string]$app.PackageUri
                }
                else {
                    switch ($architecture) {
                        'x86' { 'https://dl.google.com/dl/chrome/install/googlechromestandaloneenterprise.msi' }
                        default { 'https://dl.google.com/dl/chrome/install/googlechromestandaloneenterprise64.msi' }
                    }
                }

                $relativeRoot = Join-Path 'BuiltIn\GoogleChromeEnterprise' $architecture
                Sync-PreInstallVendorMsi -App $app -RelativeRoot $relativeRoot -PackageUri $packageUri -DisplayName 'Google Chrome Enterprise'
            }
            'MozillaFirefoxEnterprise' {
                $channel = if ($app.Channel) { [string]$app.Channel } else { 'Rapid' }
                $architecture = if ($app.Architecture) { [string]$app.Architecture } else { 'x64' }
                $language = if ($app.Language) { [string]$app.Language } else { 'en-US' }

                $packageUri = if ($app.PackageUri) {
                    [string]$app.PackageUri
                }
                else {
                    $product = if ($channel -eq 'ESR') { 'firefox-esr-msi-latest-ssl' } else { 'firefox-msi-latest-ssl' }
                    $os = if ($architecture -eq 'x64') { 'win64' } else { 'win' }
                    "https://download.mozilla.org/?product=$product&os=$os&lang=$language"
                }

                $relativeRoot = Join-Path 'BuiltIn\MozillaFirefoxEnterprise' (Join-Path $channel (Join-Path $architecture $language))
                Sync-PreInstallVendorMsi -App $app -RelativeRoot $relativeRoot -PackageUri $packageUri -DisplayName 'Mozilla Firefox Enterprise'
            }
        }
    }

    $preInstallStopwatch.Stop()
    Write-PreInstallLog -Event 'RefreshComplete' -Message 'Built-in acquisition and refresh completed. Installation will continue.' -Data @{CacheUsed=[bool]$usbRoot;DurationSeconds=[math]::Round($preInstallStopwatch.Elapsed.TotalSeconds,1)}
    Write-PreInstallLog -Event 'PreInstallComplete' -Message 'PreInstall completed successfully.' -Data @{BuiltInAppCount=@($builtInApps).Count;DurationSeconds=[math]::Round($preInstallStopwatch.Elapsed.TotalSeconds,1)}
    exit 0
} catch {
    if ($preInstallStopwatch.IsRunning) { $preInstallStopwatch.Stop() }
    $failureMessage = $_.Exception.Message
    $failureType = if ($failureMessage -match 'no network|not reachable') {
        'NetworkUnavailable'
    }
    elseif ($failureMessage -match 'timeout|exceeded') {
        'Timeout'
    }
    elseif ($failureMessage -match 'no staged|no .*cached|missing') {
        'CacheOrSourceMissing'
    }
    elseif ($failureMessage -match 'metadata|HTTP|response|download') {
        'RemoteAcquisition'
    }
    else {
        'PreInstallError'
    }

    Write-PreInstallLog -Event 'RefreshFailed' -Level 'Error' -Message $failureMessage -Data @{FailureType=$failureType;DurationSeconds=[math]::Round($preInstallStopwatch.Elapsed.TotalSeconds,1)}
    exit 1
}