Support/Package/Eigenverft.Manifested.Sandbox.Package.Source.ps1

<#
    Eigenverft.Manifested.Sandbox.Package.Source
#>


function Get-PackagePackageFileIndex {
<#
.SYNOPSIS
Loads the Package package-file index.
 
.DESCRIPTION
Returns the configured package-file index document, or an empty record set when
the index file does not exist yet.
 
.PARAMETER PackageConfig
The resolved Package config object.
 
.EXAMPLE
Get-PackagePackageFileIndex -PackageConfig $config
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$PackageConfig
    )

    $indexPath = $PackageConfig.PackageFileIndexFilePath
    if ([string]::IsNullOrWhiteSpace($indexPath)) {
        throw 'Package package-file index path is not configured.'
    }

    if (-not (Test-Path -LiteralPath $indexPath -PathType Leaf)) {
        return [pscustomobject]@{
            Path    = $indexPath
            Records = @()
        }
    }

    $documentInfo = Read-PackageJsonDocument -Path $indexPath
    $records = if ($documentInfo.Document.PSObject.Properties['records']) { @($documentInfo.Document.records) } else { @() }
    return [pscustomobject]@{
        Path    = $documentInfo.Path
        Records = $records
    }
}

function Save-PackagePackageFileIndex {
<#
.SYNOPSIS
Writes the Package package-file index to disk.
 
.DESCRIPTION
Persists the normalized package-file index document to the configured index path.
 
.PARAMETER IndexPath
The target index file path.
 
.PARAMETER Records
The package-file records to persist.
 
.EXAMPLE
Save-PackagePackageFileIndex -IndexPath $path -Records $records
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$IndexPath,

        [Parameter(Mandatory = $true)]
        [object[]]$Records
    )

    $directoryPath = Split-Path -Parent $IndexPath
    if (-not [string]::IsNullOrWhiteSpace($directoryPath)) {
        $null = New-Item -ItemType Directory -Path $directoryPath -Force
    }

    [ordered]@{
        schemaVersion = 1
        records = @($Records)
    } | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath $IndexPath -Encoding UTF8
}

function Update-PackagePackageFileIndexRecord {
<#
.SYNOPSIS
Updates the Package package-file index for one resolved package file path.
 
.DESCRIPTION
Refreshes the tracked source and package metadata for a package-file path in
the package-file index.
 
.PARAMETER PackageResult
The current Package result object.
 
.PARAMETER PackageFilePath
The package-file path to write into the index.
 
.PARAMETER SourceScope
The source scope that produced the artifact.
 
.PARAMETER SourceId
The source id that produced the artifact.
 
.EXAMPLE
Update-PackagePackageFileIndexRecord -PackageResult $result -PackageFilePath $path -SourceScope environment -SourceId defaultPackageDepot
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$PackageResult,

        [Parameter(Mandatory = $true)]
        [string]$PackageFilePath,

        [AllowNull()]
        [string]$SourceScope,

        [AllowNull()]
        [string]$SourceId
    )

    if ([string]::IsNullOrWhiteSpace($PackageFilePath)) {
        return
    }

    $normalizedPackageFilePath = [System.IO.Path]::GetFullPath($PackageFilePath)
    $index = Get-PackagePackageFileIndex -PackageConfig $PackageResult.PackageConfig
    $records = @(
        foreach ($record in @($index.Records)) {
            if (-not [string]::Equals([string]$record.path, $normalizedPackageFilePath, [System.StringComparison]::OrdinalIgnoreCase)) {
                $record
            }
        }
    )

    $records += [pscustomobject]@{
        path         = $normalizedPackageFilePath
        definitionId = $PackageResult.DefinitionId
        releaseId    = $PackageResult.PackageId
        releaseTrack = $PackageResult.ReleaseTrack
        flavor       = if ($PackageResult.Package -and $PackageResult.Package.PSObject.Properties['flavor']) { [string]$PackageResult.Package.flavor } else { $null }
        version      = $PackageResult.PackageVersion
        sourceScope  = $SourceScope
        sourceId     = $SourceId
        updatedAtUtc = [DateTime]::UtcNow.ToString('o')
    }

    Save-PackagePackageFileIndex -IndexPath $index.Path -Records $records
}

function Get-PackageSourceDefinition {
<#
.SYNOPSIS
Returns a resolved Package source definition by sourceRef.
 
.DESCRIPTION
Looks up an acquisition source from the effective acquisition environment or
from definition-local upstream sources and returns the normalized source
definition with scope and id metadata.
 
.PARAMETER PackageConfig
The resolved Package config object.
 
.PARAMETER SourceRef
The acquisition-candidate sourceRef object.
 
.EXAMPLE
Get-PackageSourceDefinition -PackageConfig $config -SourceRef $candidate.sourceRef
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$PackageConfig,

        [Parameter(Mandatory = $true)]
        [psobject]$SourceRef
    )

    $scope = [string]$SourceRef.scope
    $id = [string]$SourceRef.id
    $sourceObject = $null

    switch -Exact ($scope) {
        'environment' {
            foreach ($property in @($PackageConfig.EnvironmentSources.PSObject.Properties)) {
                if ([string]::Equals([string]$property.Name, $id, [System.StringComparison]::OrdinalIgnoreCase)) {
                    $sourceObject = $property.Value
                    $id = $property.Name
                    break
                }
            }
            if (-not $sourceObject) {
                throw "Package environment source '$($SourceRef.id)' was not found in the effective acquisition environment."
            }
        }
        'definition' {
            foreach ($property in @($PackageConfig.DefinitionUpstreamSources.PSObject.Properties)) {
                if ([string]::Equals([string]$property.Name, $id, [System.StringComparison]::OrdinalIgnoreCase)) {
                    $sourceObject = $property.Value
                    $id = $property.Name
                    break
                }
            }
            if (-not $sourceObject) {
                throw "Package definition source '$($SourceRef.id)' was not found in definition '$($PackageConfig.DefinitionId)'."
            }
        }
        default {
            throw "Unsupported Package sourceRef.scope '$scope'."
        }
    }

    return [pscustomobject]@{
        Scope           = $scope
        Id              = $id
        Kind            = if ($sourceObject.PSObject.Properties['kind']) { [string]$sourceObject.kind } else { $null }
        BaseUri         = if ($sourceObject.PSObject.Properties['baseUri']) { [string]$sourceObject.baseUri } else { $null }
        BasePath        = if ($sourceObject.PSObject.Properties['basePath']) { [string]$sourceObject.basePath } else { $null }
        RepositoryOwner = if ($sourceObject.PSObject.Properties['repositoryOwner']) { [string]$sourceObject.repositoryOwner } else { $null }
        RepositoryName  = if ($sourceObject.PSObject.Properties['repositoryName']) { [string]$sourceObject.repositoryName } else { $null }
    }
}

function Resolve-PackageSource {
<#
.SYNOPSIS
Resolves a concrete source location from a source definition and acquisition candidate.
 
.DESCRIPTION
Combines a resolved source definition with one release acquisition candidate
and returns the concrete URI or filesystem path that should be used for the
package-file save.
 
.PARAMETER SourceDefinition
The resolved source definition for the acquisition candidate.
 
.PARAMETER AcquisitionCandidate
The release acquisition candidate.
 
.PARAMETER Package
The selected effective release object. Required for source kinds that resolve
through release metadata, such as GitHub release lookup by tag.
 
.EXAMPLE
Resolve-PackageSource -SourceDefinition $source -AcquisitionCandidate $candidate
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$SourceDefinition,

        [Parameter(Mandatory = $true)]
        [psobject]$AcquisitionCandidate,

        [AllowNull()]
        [psobject]$Package
    )

    switch -Exact ([string]$SourceDefinition.Kind) {
        'download' {
            if ([string]::IsNullOrWhiteSpace([string]$SourceDefinition.BaseUri)) {
                throw "Package download source '$($SourceDefinition.Id)' does not define baseUri."
            }
            if (-not $AcquisitionCandidate.PSObject.Properties['sourcePath'] -or [string]::IsNullOrWhiteSpace([string]$AcquisitionCandidate.sourcePath)) {
                throw "Package acquisition candidate for '$($SourceDefinition.Id)' does not define sourcePath."
            }

            $baseUriText = ([string]$SourceDefinition.BaseUri).TrimEnd('/') + '/'
            $resolvedUri = [System.Uri]::new([System.Uri]$baseUriText, [string]$AcquisitionCandidate.sourcePath)
            return [pscustomobject]@{
                Kind           = 'download'
                ResolvedSource = $resolvedUri.AbsoluteUri
            }
        }
        'githubRelease' {
            if (-not $Package) {
                throw "Package GitHub release source '$($SourceDefinition.Id)' requires the selected package release context."
            }
            if ([string]::IsNullOrWhiteSpace([string]$SourceDefinition.RepositoryOwner)) {
                throw "Package GitHub release source '$($SourceDefinition.Id)' does not define repositoryOwner."
            }
            if ([string]::IsNullOrWhiteSpace([string]$SourceDefinition.RepositoryName)) {
                throw "Package GitHub release source '$($SourceDefinition.Id)' does not define repositoryName."
            }
            if (-not $Package.PSObject.Properties['releaseTag'] -or [string]::IsNullOrWhiteSpace([string]$Package.releaseTag)) {
                throw "Package release '$($Package.id)' requires releaseTag when acquisition uses GitHub release source '$($SourceDefinition.Id)'."
            }
            if (-not $Package.PSObject.Properties['packageFile'] -or
                $null -eq $Package.packageFile -or
                -not $Package.packageFile.PSObject.Properties['fileName'] -or
                [string]::IsNullOrWhiteSpace([string]$Package.packageFile.fileName)) {
                throw "Package release '$($Package.id)' requires packageFile.fileName when acquisition uses GitHub release source '$($SourceDefinition.Id)'."
            }

            $release = Get-GitHubRelease -RepositoryOwner $SourceDefinition.RepositoryOwner -RepositoryName $SourceDefinition.RepositoryName -ReleaseTag ([string]$Package.releaseTag)
            $assetName = [string]$Package.packageFile.fileName
            $matchedAsset = @(
                $release.Assets | Where-Object {
                    [string]::Equals([string]$_.Name, $assetName, [System.StringComparison]::OrdinalIgnoreCase)
                }
            ) | Select-Object -First 1

            if (-not $matchedAsset) {
                throw "GitHub release '$($Package.releaseTag)' for '$($SourceDefinition.RepositoryOwner)/$($SourceDefinition.RepositoryName)' does not contain asset '$assetName'."
            }
            if ([string]::IsNullOrWhiteSpace([string]$matchedAsset.DownloadUrl)) {
                throw "GitHub release '$($Package.releaseTag)' asset '$assetName' for '$($SourceDefinition.RepositoryOwner)/$($SourceDefinition.RepositoryName)' does not expose a download URL."
            }

            return [pscustomobject]@{
                Kind           = 'download'
                ResolvedSource = [string]$matchedAsset.DownloadUrl
            }
        }
        'filesystem' {
            if (-not $AcquisitionCandidate.PSObject.Properties['sourcePath'] -or [string]::IsNullOrWhiteSpace([string]$AcquisitionCandidate.sourcePath)) {
                throw "Package acquisition candidate for '$($SourceDefinition.Id)' does not define sourcePath."
            }

            $sourcePath = ([string]$AcquisitionCandidate.sourcePath).Trim() -replace '/', '\'
            if ([System.IO.Path]::IsPathRooted($sourcePath)) {
                $resolvedPath = Resolve-PackagePathValue -PathValue $sourcePath
            }
            else {
                if ([string]::IsNullOrWhiteSpace([string]$SourceDefinition.BasePath)) {
                    throw "Package filesystem source '$($SourceDefinition.Id)' does not define basePath."
                }

                $resolvedPath = [System.IO.Path]::GetFullPath((Join-Path $SourceDefinition.BasePath $sourcePath))
            }

            return [pscustomobject]@{
                Kind           = 'filesystem'
                ResolvedSource = $resolvedPath
            }
        }
        default {
            throw "Unsupported Package source kind '$($SourceDefinition.Kind)'."
        }
    }
}

function Test-PackageSavedFile {
<#
.SYNOPSIS
Evaluates a package file against a save-time verification policy.
 
.DESCRIPTION
Applies the acquisition candidate verification policy to a local file and
returns the verification status, whether the file is accepted, and the expected
and actual SHA256 values when hashing is performed.
 
.PARAMETER Path
The local file path to verify.
 
.PARAMETER Verification
The verification policy object from the acquisition candidate.
 
.EXAMPLE
Test-PackageSavedFile -Path .\package.zip -Verification $verification
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,

        [AllowNull()]
        [psobject]$Verification
    )

    if ($Verification -is [System.Collections.IDictionary]) {
        $Verification = [pscustomobject]$Verification
    }

    $authenticode = if ($Verification -and $Verification.PSObject.Properties['authenticode'] -and $null -ne $Verification.authenticode) {
        if ($Verification.authenticode -is [System.Collections.IDictionary]) {
            [pscustomobject]$Verification.authenticode
        }
        else {
            $Verification.authenticode
        }
    }
    else {
        $null
    }

    $mode = if ($Verification -and $Verification.PSObject.Properties['mode'] -and -not [string]::IsNullOrWhiteSpace([string]$Verification.mode)) {
        ([string]$Verification.mode).ToLowerInvariant()
    }
    else {
        'none'
    }

    if (-not (Test-Path -LiteralPath $Path)) {
        return [pscustomobject]@{
            Status       = 'FileMissing'
            Accepted     = $false
            Verified     = $false
            Mode         = $mode
            Algorithm    = $null
            ExpectedHash = $null
            ActualHash   = $null
        }
    }

    if ($mode -eq 'none' -and -not $authenticode) {
        return [pscustomobject]@{
            Status       = 'VerificationSkipped'
            Accepted     = $true
            Verified     = $false
            Mode         = $mode
            Algorithm    = $null
            ExpectedHash = $null
            ActualHash   = $null
            SignatureStatus = $null
            SignerSubject = $null
        }
    }

    $algorithm = if ($Verification -and $Verification.PSObject.Properties['algorithm'] -and -not [string]::IsNullOrWhiteSpace([string]$Verification.algorithm)) {
        ([string]$Verification.algorithm).ToLowerInvariant()
    }
    else {
        'sha256'
    }
    if ($algorithm -ne 'sha256') {
        return [pscustomobject]@{
            Status       = 'VerificationAlgorithmUnsupported'
            Accepted     = $false
            Verified     = $false
            Mode         = $mode
            Algorithm    = $algorithm
            ExpectedHash = $null
            ActualHash   = $null
            SignatureStatus = $null
            SignerSubject = $null
        }
    }

    $expectedHash = if ($Verification -and $Verification.PSObject.Properties['sha256'] -and -not [string]::IsNullOrWhiteSpace([string]$Verification.sha256)) {
        ([string]$Verification.sha256).Trim().ToLowerInvariant()
    }
    else {
        $null
    }

    if ([string]::IsNullOrWhiteSpace($expectedHash) -and -not $authenticode) {
        return [pscustomobject]@{
            Status       = if ($mode -eq 'required') { 'VerificationHashMissing' } else { 'VerificationHashMissingOptional' }
            Accepted     = ($mode -ne 'required')
            Verified     = $false
            Mode         = $mode
            Algorithm    = $algorithm
            ExpectedHash = $null
            ActualHash   = $null
            SignatureStatus = $null
            SignerSubject = $null
        }
    }

    $actualHash = $null
    $hashAccepted = $true
    if (-not [string]::IsNullOrWhiteSpace($expectedHash)) {
        $actualHash = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
        $hashAccepted = ($actualHash -eq $expectedHash)
    }

    $signatureStatus = $null
    $signerSubject = $null
    $authenticodeAccepted = $true
    if ($authenticode) {
        $authenticodeAccepted = $false
        try {
            $signature = Get-AuthenticodeSignature -FilePath $Path
            $signatureStatus = $signature.Status.ToString()
            $signerSubject = if ($signature.SignerCertificate) { $signature.SignerCertificate.Subject } else { $null }
            $requiresValid = $true
            if ($authenticode.PSObject.Properties['requireValid']) {
                $requiresValid = [bool]$authenticode.requireValid
            }

            $authenticodeAccepted = (-not $requiresValid) -or ($signature.Status -eq [System.Management.Automation.SignatureStatus]::Valid)
            if ($authenticodeAccepted -and $authenticode.PSObject.Properties['subjectContains'] -and
                -not [string]::IsNullOrWhiteSpace([string]$authenticode.subjectContains)) {
                $authenticodeAccepted = ($null -ne $signerSubject -and $signerSubject -match [regex]::Escape([string]$authenticode.subjectContains))
            }
        }
        catch {
            $signatureStatus = 'Failed'
            $authenticodeAccepted = $false
        }
    }

    $accepted = $hashAccepted -and $authenticodeAccepted
    $status = if (-not $hashAccepted) {
        'VerificationFailed'
    }
    elseif ($authenticode -and -not $authenticodeAccepted) {
        'AuthenticodeFailed'
    }
    elseif ($authenticode -and [string]::IsNullOrWhiteSpace($expectedHash)) {
        'AuthenticodePassed'
    }
    else {
        'VerificationPassed'
    }

    return [pscustomobject]@{
        Status       = $status
        Accepted     = $accepted
        Verified     = $true
        Mode         = $mode
        Algorithm    = $algorithm
        ExpectedHash = $expectedHash
        ActualHash   = $actualHash
        SignatureStatus = $signatureStatus
        SignerSubject = $signerSubject
    }
}

function Save-PackageDownloadFile {
<#
.SYNOPSIS
Downloads a package file to a local path.
 
.DESCRIPTION
Uses the module's download helper to fetch a package file from an HTTP or HTTPS
source into a staging path for later verification and promotion.
 
.PARAMETER Uri
The package download URI.
 
.PARAMETER TargetPath
The local staging path that should receive the file.
 
.EXAMPLE
Save-PackageDownloadFile -Uri https://example.org/package.zip -TargetPath C:\Temp\package.zip
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Uri,

        [Parameter(Mandatory = $true)]
        [string]$TargetPath
    )

    Invoke-WebRequestEx -Uri $Uri -OutFile $TargetPath -UseBasicParsing
    return (Resolve-Path -LiteralPath $TargetPath -ErrorAction Stop).Path
}

function Save-PackageFilesystemFile {
<#
.SYNOPSIS
Copies a package file from a filesystem source.
 
.DESCRIPTION
Copies a package file from a local or network filesystem path into a staging
path for later verification and promotion.
 
.PARAMETER SourcePath
The local or network path that contains the package file.
 
.PARAMETER TargetPath
The local staging path that should receive the copy.
 
.EXAMPLE
Save-PackageFilesystemFile -SourcePath \\server\share\package.zip -TargetPath C:\Temp\package.zip
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$SourcePath,

        [Parameter(Mandatory = $true)]
        [string]$TargetPath
    )

    if (-not (Test-Path -LiteralPath $SourcePath)) {
        throw "Package filesystem source '$SourcePath' does not exist."
    }

    return (Copy-FileToPath -SourcePath $SourcePath -TargetPath $TargetPath -Overwrite)
}

function Test-PackagePackageFileAcquisitionRequired {
<#
.SYNOPSIS
Determines whether the selected release needs an acquired package file.
 
.DESCRIPTION
Interprets the current install kind so acquisition is skipped for install flows
that do not consume a saved package file.
 
.PARAMETER Package
The selected release object.
 
.EXAMPLE
Test-PackagePackageFileAcquisitionRequired -Package $package
#>

    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$Package
    )

    $installKind = if ($Package.install -and $Package.install.PSObject.Properties['kind']) {
        [string]$Package.install.kind
    }
    else {
        $null
    }

    switch -Exact ($installKind) {
        'expandArchive' { return $true }
        'placePackageFile' { return $true }
        'runInstaller' {
            return (-not $Package.install.PSObject.Properties['commandPath'] -or [string]::IsNullOrWhiteSpace([string]$Package.install.commandPath))
        }
        default { return $false }
    }
}

function Get-PackagePreferredVerification {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [object[]]$AcquisitionCandidates
    )

    foreach ($candidate in @($AcquisitionCandidates)) {
        if ($candidate.PSObject.Properties['verification'] -and $null -ne $candidate.verification) {
            return $candidate.verification
        }
    }

    return [pscustomobject]@{ mode = 'none' }
}

function Resolve-PackageAcquisitionCandidateVerification {
<#
.SYNOPSIS
Builds the effective verification policy for one acquisition candidate.
 
.DESCRIPTION
Combines acquisition-candidate verification mode with canonical package-file
integrity metadata when present, while remaining compatible with older
candidate-local hash definitions.
 
.PARAMETER Package
The selected effective release.
 
.PARAMETER AcquisitionCandidate
The raw acquisition candidate.
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$Package,

        [AllowNull()]
        [psobject]$AcquisitionCandidate
    )

    $candidateVerification = if ($AcquisitionCandidate -and $AcquisitionCandidate.PSObject.Properties['verification']) {
        $AcquisitionCandidate.verification
    }
    else {
        $null
    }
    if ($candidateVerification -is [System.Collections.IDictionary]) {
        $candidateVerification = [pscustomobject]$candidateVerification
    }

    $packageIntegrity = if ($Package -and
        $Package.PSObject.Properties['packageFile'] -and
        $Package.packageFile -and
        $Package.packageFile.PSObject.Properties['integrity']) {
        $Package.packageFile.integrity
    }
    else {
        $null
    }
    if ($packageIntegrity -is [System.Collections.IDictionary]) {
        $packageIntegrity = [pscustomobject]$packageIntegrity
    }

    $packageAuthenticode = if ($Package -and
        $Package.PSObject.Properties['packageFile'] -and
        $Package.packageFile -and
        $Package.packageFile.PSObject.Properties['authenticode']) {
        $Package.packageFile.authenticode
    }
    else {
        $null
    }
    if ($packageAuthenticode -is [System.Collections.IDictionary]) {
        $packageAuthenticode = [pscustomobject]$packageAuthenticode
    }

    $mode = if ($candidateVerification -and $candidateVerification.PSObject.Properties['mode'] -and -not [string]::IsNullOrWhiteSpace([string]$candidateVerification.mode)) {
        [string]$candidateVerification.mode
    }
    else {
        'none'
    }

    $algorithm = if ($packageIntegrity -and $packageIntegrity.PSObject.Properties['algorithm'] -and -not [string]::IsNullOrWhiteSpace([string]$packageIntegrity.algorithm)) {
        [string]$packageIntegrity.algorithm
    }
    elseif ($candidateVerification -and $candidateVerification.PSObject.Properties['algorithm'] -and -not [string]::IsNullOrWhiteSpace([string]$candidateVerification.algorithm)) {
        [string]$candidateVerification.algorithm
    }
    else {
        'sha256'
    }

    $sha256 = if ($packageIntegrity -and $packageIntegrity.PSObject.Properties['sha256'] -and -not [string]::IsNullOrWhiteSpace([string]$packageIntegrity.sha256)) {
        [string]$packageIntegrity.sha256
    }
    elseif ($candidateVerification -and $candidateVerification.PSObject.Properties['sha256'] -and -not [string]::IsNullOrWhiteSpace([string]$candidateVerification.sha256)) {
        [string]$candidateVerification.sha256
    }
    else {
        $null
    }

    $verification = [ordered]@{
        mode = $mode
    }
    if (-not [string]::IsNullOrWhiteSpace($algorithm)) {
        $verification.algorithm = $algorithm
    }
    if (-not [string]::IsNullOrWhiteSpace($sha256)) {
        $verification.sha256 = $sha256
    }
    if ($packageAuthenticode) {
        $verification.authenticode = $packageAuthenticode
    }

    return [pscustomobject]$verification
}

function Get-PackagePackageDepotSources {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$PackageConfig
    )

    $orderedSources = New-Object System.Collections.Generic.List[object]

    foreach ($property in @($PackageConfig.EnvironmentSources.PSObject.Properties)) {
        $source = $property.Value
        if (-not [string]::Equals([string]$source.kind, 'filesystem', [System.StringComparison]::OrdinalIgnoreCase)) {
            continue
        }

        $orderedSources.Add([pscustomobject]@{
            id       = $property.Name
            priority = if ($source.PSObject.Properties['priority']) { [int]$source.priority } else { 1000 }
        }) | Out-Null
    }

    return @(
        $orderedSources.ToArray() |
            Sort-Object -Property priority, id
    )
}

function Build-PackageAcquisitionPlan {
<#
.SYNOPSIS
Builds the internal Package acquisition plan for the selected release.
 
.DESCRIPTION
Normalizes the ordered acquisition candidates and captures the install-workspace
and default-depot targets so later package-file save steps can execute linearly.
 
.PARAMETER PackageResult
The Package result object to enrich.
 
.EXAMPLE
Build-PackageAcquisitionPlan -PackageResult $result
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$PackageResult
    )

    $package = $PackageResult.Package
    if (-not $package) {
        throw 'Build-PackageAcquisitionPlan requires a selected release.'
    }

    if ([string]::Equals([string]$PackageResult.InstallOrigin, 'AlreadySatisfied', [System.StringComparison]::OrdinalIgnoreCase)) {
        $PackageResult.AcquisitionPlan = [pscustomobject]@{
            PackageFileRequired      = $false
            InstallWorkspaceFilePath = $PackageResult.PackageFilePath
            Candidates               = @()
        }
        Write-PackageExecutionMessage -Message '[STATE] Acquisition skipped because package target is already satisfied.'
        return $PackageResult
    }

    $requiresPackageFile = Test-PackagePackageFileAcquisitionRequired -Package $package
    $orderedCandidates = New-Object System.Collections.Generic.List[object]
    if ($requiresPackageFile -and $package.PSObject.Properties['acquisitionCandidates']) {
        foreach ($candidate in @($package.acquisitionCandidates | Sort-Object -Property @{
                    Expression = { if ($_.PSObject.Properties['priority']) { [int]$_.priority } else { [int]::MaxValue } }
                })) {
            $resolvedVerification = Resolve-PackageAcquisitionCandidateVerification -Package $package -AcquisitionCandidate $candidate
            switch -Exact ([string]$candidate.kind) {
                'packageDepot' {
                    $resolvedDepotSourcePath = Join-Path $PackageResult.PackageFileRelativeDirectory ([string]$package.packageFile.fileName)
                    foreach ($depotSource in @(Get-PackagePackageDepotSources -PackageConfig $PackageResult.PackageConfig)) {
                        $orderedCandidates.Add([pscustomobject]@{
                            kind         = 'packageDepot'
                            priority     = if ($candidate.PSObject.Properties['priority']) { [int]$candidate.priority } else { [int]::MaxValue }
                            sourcePriority = [int]$depotSource.priority
                            sourceRef    = [pscustomobject]@{
                                scope = 'environment'
                                id    = $depotSource.id
                            }
                            sourcePath   = $resolvedDepotSourcePath
                            verification = $resolvedVerification
                        }) | Out-Null
                    }
                }
                'download' {
                    $orderedCandidates.Add([pscustomobject]@{
                        kind         = 'download'
                        priority     = if ($candidate.PSObject.Properties['priority']) { [int]$candidate.priority } else { [int]::MaxValue }
                        sourcePriority = 1000
                        sourceRef    = [pscustomobject]@{
                            scope = 'definition'
                            id    = [string]$candidate.sourceId
                        }
                        sourcePath   = [string]$candidate.sourcePath
                        verification = $resolvedVerification
                    }) | Out-Null
                }
                'filesystem' {
                    $orderedCandidates.Add([pscustomobject]@{
                        kind         = 'filesystem'
                        priority     = if ($candidate.PSObject.Properties['priority']) { [int]$candidate.priority } else { [int]::MaxValue }
                        sourcePriority = 1000
                        sourceRef    = if ($candidate.PSObject.Properties['sourceId'] -and -not [string]::IsNullOrWhiteSpace([string]$candidate.sourceId)) {
                            [pscustomobject]@{
                                scope = 'environment'
                                id    = [string]$candidate.sourceId
                            }
                        }
                        else {
                            $null
                        }
                        sourcePath   = [string]$candidate.sourcePath
                        verification = $resolvedVerification
                    }) | Out-Null
                }
            }
        }
    }

    $PackageResult.AcquisitionPlan = [pscustomobject]@{
        PackageFileRequired    = $requiresPackageFile
        InstallWorkspaceFilePath = $PackageResult.PackageFilePath
        DefaultPackageDepotFilePath = $PackageResult.DefaultPackageDepotFilePath
        Candidates             = @(
            $orderedCandidates.ToArray() |
                Sort-Object -Property priority, sourcePriority, @{
                    Expression = {
                        if ($_.sourceRef) { [string]$_.sourceRef.id } else { [string]::Empty }
                    }
                }
        )
    }

    $candidateSummary = @(
        foreach ($candidate in @($PackageResult.AcquisitionPlan.Candidates)) {
            $sourceSummary = if ($candidate.sourceRef) {
                '{0}:{1}' -f [string]$candidate.sourceRef.scope, [string]$candidate.sourceRef.id
            }
            else {
                'direct'
            }
            '{0}@{1}->{2}' -f [string]$candidate.kind, [string]$candidate.priority, $sourceSummary
        }
    ) -join ', '
    if ([string]::IsNullOrWhiteSpace($candidateSummary)) {
        $candidateSummary = '<none>'
    }
    Write-PackageExecutionMessage -Message ("[STATE] Acquisition plan packageFileRequired='{0}' with {1} candidate(s): {2}." -f $requiresPackageFile, @($PackageResult.AcquisitionPlan.Candidates).Count, $candidateSummary)

    return $PackageResult
}

function Save-PackagePackageFile {
<#
.SYNOPSIS
Ensures the selected package file is present in the install workspace.
 
.DESCRIPTION
Reuses an already-present verified package file when possible, then checks the
default package depot, and otherwise attempts each configured acquisition
candidate in priority order until one succeeds or all candidates fail.
 
.PARAMETER PackageResult
The Package result object to enrich.
 
.EXAMPLE
Save-PackagePackageFile -PackageResult $result
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$PackageResult
    )

    $package = $PackageResult.Package
    $packageConfig = $PackageResult.PackageConfig

    if (-not $package -or -not $package.PSObject.Properties['install'] -or -not $package.install) {
        throw 'Save-PackagePackageFile requires a selected release with install settings.'
    }

    if ($PackageResult.ExistingPackage -and
        $PackageResult.ExistingPackage.PSObject.Properties['Decision'] -and
        $PackageResult.ExistingPackage.Decision -in @('ReusePackageOwned', 'AdoptExternal')) {
        $PackageResult.PackageFileSave = [pscustomobject]@{
            Success         = $true
            Status          = 'Skipped'
            PackageFilePath = $PackageResult.PackageFilePath
            SelectedSource  = $null
            Verification    = $null
            Attempts        = @()
            FailureReason   = $null
            ErrorMessage    = $null
        }
        Write-PackageExecutionMessage -Message ("[STATE] Package file step skipped because existing install decision is '{0}'." -f [string]$PackageResult.ExistingPackage.Decision)
        return $PackageResult
    }

    if (-not $PackageResult.AcquisitionPlan) {
        $PackageResult = Build-PackageAcquisitionPlan -PackageResult $PackageResult
    }

    if (-not $PackageResult.AcquisitionPlan.PackageFileRequired) {
        $PackageResult.PackageFileSave = [pscustomobject]@{
            Success         = $true
            Status          = 'Skipped'
            PackageFilePath = $PackageResult.PackageFilePath
            SelectedSource  = $null
            Verification    = $null
            Attempts        = @()
            FailureReason   = $null
            ErrorMessage    = $null
        }
        Write-PackageExecutionMessage -Message "[STATE] Package file step skipped because the selected install kind does not require a saved package file."
        return $PackageResult
    }

    if ([string]::IsNullOrWhiteSpace($PackageResult.PackageFilePath)) {
        throw "Package release '$($package.id)' does not define packageFile.fileName."
    }

    $orderedCandidates = @($PackageResult.AcquisitionPlan.Candidates)
    if (-not $orderedCandidates) {
        throw "Package release '$($package.id)' does not define any acquisition candidates."
    }

    $attempts = New-Object System.Collections.Generic.List[object]
    $preferredVerification = Get-PackagePreferredVerification -AcquisitionCandidates $orderedCandidates

    if (Test-Path -LiteralPath $PackageResult.PackageFilePath) {
        $verification = Test-PackageSavedFile -Path $PackageResult.PackageFilePath -Verification $preferredVerification
        $attempts.Add([pscustomobject]@{
            AttemptType        = 'ReuseCheck'
            Status             = if ($verification.Accepted) { 'ReusedPackageFile' } else { 'ReuseRejected' }
            SourceScope        = 'installWorkspace'
            SourceId           = 'installWorkspace'
            SourceKind         = 'filesystem'
            ResolvedSource     = $PackageResult.PackageFilePath
            VerificationStatus = $verification.Status
            ErrorMessage       = if ($verification.Accepted) { $null } else { 'Existing install-workspace file did not satisfy verification.' }
        }) | Out-Null

        if ($verification.Accepted) {
            Update-PackagePackageFileIndexRecord -PackageResult $PackageResult -PackageFilePath $PackageResult.PackageFilePath -SourceScope 'installWorkspace' -SourceId 'installWorkspace'
            $PackageResult.PackageFileSave = [pscustomobject]@{
                Success         = $true
                Status          = 'ReusedPackageFile'
                PackageFilePath = $PackageResult.PackageFilePath
                SelectedSource  = [pscustomobject]@{
                    SourceScope = 'installWorkspace'
                    SourceId    = 'installWorkspace'
                    SourceKind  = 'filesystem'
                    ResolvedSource = $PackageResult.PackageFilePath
                }
                Verification    = $verification
                Attempts        = @($attempts.ToArray())
                FailureReason   = $null
                ErrorMessage    = $null
            }
            Write-PackageExecutionMessage -Message ("[ACTION] Reused install workspace package file '{0}'." -f $PackageResult.PackageFilePath)
            return $PackageResult
        }
    }

    $null = New-Item -ItemType Directory -Path $PackageResult.InstallWorkspaceDirectory -Force

    foreach ($candidate in $orderedCandidates) {
        $sourceDefinition = $null
        $resolvedSource = $null
        $verification = $null
        $stagingPath = '{0}.{1}.partial' -f $PackageResult.PackageFilePath, ([guid]::NewGuid().ToString('N'))

        try {
            if ($candidate.sourceRef) {
                $sourceDefinition = Get-PackageSourceDefinition -PackageConfig $packageConfig -SourceRef $candidate.sourceRef
            }
            elseif ([string]::Equals([string]$candidate.kind, 'filesystem', [System.StringComparison]::OrdinalIgnoreCase)) {
                $sourceDefinition = [pscustomobject]@{
                    Scope   = 'direct'
                    Id      = 'directFilesystem'
                    Kind    = 'filesystem'
                    BaseUri = $null
                    BasePath = $null
                }
            }
            else {
                throw "Package acquisition candidate kind '$($candidate.kind)' could not be resolved to a source definition."
            }
            $resolvedSource = Resolve-PackageSource -SourceDefinition $sourceDefinition -AcquisitionCandidate $candidate -Package $package

            switch -Exact ([string]$resolvedSource.Kind) {
                'download' {
                    $null = Save-PackageDownloadFile -Uri $resolvedSource.ResolvedSource -TargetPath $stagingPath
                }
                'filesystem' {
                    $null = Save-PackageFilesystemFile -SourcePath $resolvedSource.ResolvedSource -TargetPath $stagingPath
                }
                default {
                    throw "Unsupported package-file source kind '$($resolvedSource.Kind)'."
                }
            }

            $verification = Test-PackageSavedFile -Path $stagingPath -Verification $candidate.verification
            if (-not $verification.Accepted) {
                if (Test-Path -LiteralPath $stagingPath) {
                    Remove-Item -LiteralPath $stagingPath -Force -ErrorAction SilentlyContinue
                }

                $attempts.Add([pscustomobject]@{
                    AttemptType        = 'Save'
                    Status             = 'Failed'
                    SourceScope        = $sourceDefinition.Scope
                    SourceId           = $sourceDefinition.Id
                    SourceKind         = $resolvedSource.Kind
                    ResolvedSource     = $resolvedSource.ResolvedSource
                    VerificationStatus = $verification.Status
                    ErrorMessage       = 'Saved package file did not satisfy verification.'
                }) | Out-Null

                if (-not $packageConfig.AllowAcquisitionFallback) {
                    break
                }

                continue
            }

            if (Test-Path -LiteralPath $PackageResult.PackageFilePath) {
                Remove-Item -LiteralPath $PackageResult.PackageFilePath -Force
            }
            Move-Item -LiteralPath $stagingPath -Destination $PackageResult.PackageFilePath -Force
            Update-PackagePackageFileIndexRecord -PackageResult $PackageResult -PackageFilePath $PackageResult.PackageFilePath -SourceScope $sourceDefinition.Scope -SourceId $sourceDefinition.Id

            if ([string]::Equals([string]$resolvedSource.Kind, 'download', [System.StringComparison]::OrdinalIgnoreCase) -and
                $packageConfig.MirrorDownloadedArtifactsToDefaultPackageDepot -and
                -not [string]::IsNullOrWhiteSpace($PackageResult.DefaultPackageDepotFilePath)) {
                $null = New-Item -ItemType Directory -Path (Split-Path -Parent $PackageResult.DefaultPackageDepotFilePath) -Force
                $null = Copy-FileToPath -SourcePath $PackageResult.PackageFilePath -TargetPath $PackageResult.DefaultPackageDepotFilePath -Overwrite
                Update-PackagePackageFileIndexRecord -PackageResult $PackageResult -PackageFilePath $PackageResult.DefaultPackageDepotFilePath -SourceScope $sourceDefinition.Scope -SourceId $sourceDefinition.Id
            }
            elseif ([string]::Equals([string]$sourceDefinition.Scope, 'environment', [System.StringComparison]::OrdinalIgnoreCase) -and
                [string]::Equals([string]$sourceDefinition.Id, 'defaultPackageDepot', [System.StringComparison]::OrdinalIgnoreCase) -and
                -not [string]::IsNullOrWhiteSpace($PackageResult.DefaultPackageDepotFilePath)) {
                Update-PackagePackageFileIndexRecord -PackageResult $PackageResult -PackageFilePath $PackageResult.DefaultPackageDepotFilePath -SourceScope 'environment' -SourceId 'defaultPackageDepot'
            }

            $saveStatus = if ([string]::Equals([string]$sourceDefinition.Scope, 'environment', [System.StringComparison]::OrdinalIgnoreCase) -and
                [string]::Equals([string]$sourceDefinition.Id, 'defaultPackageDepot', [System.StringComparison]::OrdinalIgnoreCase)) {
                'HydratedFromDefaultPackageDepot'
            }
            else {
                'SavedPackageFile'
            }

            $attempts.Add([pscustomobject]@{
                AttemptType        = 'Save'
                Status             = $saveStatus
                SourceScope        = $sourceDefinition.Scope
                SourceId           = $sourceDefinition.Id
                SourceKind         = $resolvedSource.Kind
                ResolvedSource     = $resolvedSource.ResolvedSource
                VerificationStatus = $verification.Status
                ErrorMessage       = $null
            }) | Out-Null

            $PackageResult.PackageFileSave = [pscustomobject]@{
                Success         = $true
                Status          = $saveStatus
                PackageFilePath = $PackageResult.PackageFilePath
                SelectedSource  = [pscustomobject]@{
                    SourceScope    = $sourceDefinition.Scope
                    SourceId       = $sourceDefinition.Id
                    SourceKind     = $resolvedSource.Kind
                    ResolvedSource = $resolvedSource.ResolvedSource
                }
                Verification    = $verification
                Attempts        = @($attempts.ToArray())
                FailureReason   = $null
                ErrorMessage    = $null
            }
            Write-PackageExecutionMessage -Message ("[ACTION] Saved package file from '{0}:{1}'." -f $sourceDefinition.Scope, $sourceDefinition.Id)
            return $PackageResult
        }
        catch {
            if (Test-Path -LiteralPath $stagingPath) {
                Remove-Item -LiteralPath $stagingPath -Force -ErrorAction SilentlyContinue
            }

            $attempts.Add([pscustomobject]@{
                AttemptType        = 'Save'
                Status             = 'Failed'
                SourceScope        = if ($sourceDefinition) { $sourceDefinition.Scope } elseif ($candidate.sourceRef) { [string]$candidate.sourceRef.scope } else { $null }
                SourceId           = if ($sourceDefinition) { $sourceDefinition.Id } elseif ($candidate.sourceRef) { [string]$candidate.sourceRef.id } else { $null }
                SourceKind         = if ($resolvedSource) { $resolvedSource.Kind } else { $null }
                ResolvedSource     = if ($resolvedSource) { $resolvedSource.ResolvedSource } else { $null }
                VerificationStatus = if ($verification) { $verification.Status } else { $null }
                ErrorMessage       = $_.Exception.Message
            }) | Out-Null

            if (-not $packageConfig.AllowAcquisitionFallback) {
                break
            }
        }
    }

    $PackageResult.PackageFileSave = [pscustomobject]@{
        Success         = $false
        Status          = 'Failed'
        PackageFilePath = $PackageResult.PackageFilePath
        SelectedSource  = $null
        Verification    = $null
        Attempts        = @($attempts.ToArray())
        FailureReason   = 'AllSourcesFailed'
        ErrorMessage    = "All acquisition candidates failed for Package release '$($package.id)'."
    }

    Write-PackageExecutionMessage -Level 'ERR' -Message ("[ACTION] All acquisition candidates failed for release '{0}'." -f $package.id)

    return $PackageResult
}