src/PowerShell/Core.ps1

# cdp PowerShell domain: Core.ps1
# Loaded by src/cdp.psm1; do not dot-source peer files.

function New-CdpActionResult {
    param(
        [Parameter(Mandatory = $true)][string]$Action,
        [Parameter(Mandatory = $true)][string]$Target,
        [Parameter(Mandatory = $true)]
        [ValidateSet('preview', 'succeeded', 'skipped', 'canceled', 'failed')]
        [string]$Status,
        [Parameter(Mandatory = $true)][bool]$Changed,
        [Parameter(Mandatory = $false)][AllowEmptyString()][string]$Error = '',
        [Parameter(Mandatory = $false)][object]$Details
    )

    $result = [PSCustomObject][ordered]@{
        Action = $Action
        Target = $Target
        Status = $Status
        Changed = $Changed
        Error = $Error
    }
    if ($null -ne $Details) {
        $result | Add-Member -NotePropertyName Details -NotePropertyValue $Details
        foreach ($property in $Details.PSObject.Properties) {
            if (-not $result.PSObject.Properties[$property.Name]) {
                $result | Add-Member -NotePropertyName $property.Name -NotePropertyValue $property.Value
            }
        }
    }
    $result
}

function ConvertTo-CdpJsonArrayValue {
    param([Parameter(Mandatory = $false)][AllowNull()][object]$Value)

    $items = [object[]]::new(0)
    if ($Value -is [System.Array]) {
        $items = [object[]]::new($Value.Count)
        [Array]::Copy($Value, $items, $Value.Count)
    } elseif ($null -ne $Value) {
        $items = [object[]]::new(1)
        $items[0] = $Value
    }
    [PSCustomObject]@{ Value = $items }
}

function Read-CdpJsonArrayMutationDocument {
    param([Parameter(Mandatory = $true)][string]$LiteralPath)

    if (Test-Path -LiteralPath $LiteralPath -PathType Leaf) {
        return Read-CdpJsonDocument -LiteralPath $LiteralPath
    }
    [PSCustomObject]@{
        Path = $LiteralPath
        Fingerprint = 'missing'
        Value = @()
    }
}

function Get-CdpUserHome {
    if (-not [string]::IsNullOrWhiteSpace($env:USERPROFILE)) { return $env:USERPROFILE }
    if (-not [string]::IsNullOrWhiteSpace($env:HOME)) { return $env:HOME }
    (Get-Location).Path
}

function Get-CdpFileFingerprint {
    param([Parameter(Mandatory = $true)][string]$LiteralPath)

    if (-not (Test-Path -LiteralPath $LiteralPath -PathType Leaf)) {
        return 'missing'
    }

    $stream = [System.IO.File]::Open(
        $LiteralPath,
        [System.IO.FileMode]::Open,
        [System.IO.FileAccess]::Read,
        [System.IO.FileShare]::ReadWrite
    )
    try {
        $algorithm = [System.Security.Cryptography.SHA256]::Create()
        try {
            $hash = $algorithm.ComputeHash($stream)
            return ([System.BitConverter]::ToString($hash)).Replace('-', '').ToLowerInvariant()
        } finally {
            $algorithm.Dispose()
        }
    } finally {
        $stream.Dispose()
    }
}

function Read-CdpJsonDocument {
    param([Parameter(Mandatory = $true)][string]$LiteralPath)

    if (-not (Test-Path -LiteralPath $LiteralPath -PathType Leaf)) {
        throw "JSON document not found: $LiteralPath"
    }

    $fingerprint = Get-CdpFileFingerprint -LiteralPath $LiteralPath
    $content = Get-Content -LiteralPath $LiteralPath -Raw -Encoding UTF8
    $parsedValue = ConvertFrom-Json -InputObject $content
    $value = if ($content.TrimStart().StartsWith('[')) {
        (ConvertTo-CdpJsonArrayValue -Value $parsedValue).Value
    } else { $parsedValue }
    [PSCustomObject]@{
        Path = $LiteralPath
        Fingerprint = $fingerprint
        Value = $value
    }
}

function Remove-CdpOldJsonBackups {
    param(
        [Parameter(Mandatory = $true)][string]$LiteralPath,
        [Parameter(Mandatory = $true)][int]$Keep
    )

    $directory = Split-Path -Parent $LiteralPath
    $leaf = Split-Path -Leaf $LiteralPath
    $backups = @(Get-ChildItem -LiteralPath $directory -Filter "$leaf.cdp-backup.*" -File -ErrorAction SilentlyContinue |
        Sort-Object -Property Name -Descending)
    if ($backups.Count -le $Keep) { return }
    $backups | Select-Object -Skip $Keep | Remove-Item -Force -ErrorAction SilentlyContinue
}

function Write-CdpJsonTemporaryFile {
    param(
        [Parameter(Mandatory = $true)][string]$LiteralPath,
        [Parameter(Mandatory = $true)][AllowNull()][object]$Value
    )

    $json = ConvertTo-Json -InputObject $Value -Depth 20
    [void](ConvertFrom-Json -InputObject $json)
    $encoding = New-Object System.Text.UTF8Encoding($false)
    $writer = New-Object System.IO.StreamWriter($LiteralPath, $false, $encoding)
    try {
        $writer.WriteLine($json)
        $writer.Flush()
        $writer.BaseStream.Flush($true)
    } finally {
        $writer.Dispose()
    }
}

function Move-CdpJsonTemporaryFile {
    param(
        [Parameter(Mandatory = $true)][string]$TempPath,
        [Parameter(Mandatory = $true)][string]$LiteralPath,
        [Parameter(Mandatory = $true)][string]$Directory,
        [Parameter(Mandatory = $true)][string]$Leaf
    )

    if (Test-Path -LiteralPath $LiteralPath -PathType Leaf) {
        $stamp = [DateTime]::UtcNow.ToString('yyyyMMddHHmmssfffffff')
        [System.IO.File]::Replace($TempPath, $LiteralPath, (Join-Path $Directory "$Leaf.cdp-backup.$stamp"))
        return
    }
    [System.IO.File]::Move($TempPath, $LiteralPath)
}

function Write-CdpJsonFile {
    param(
        [Parameter(Mandatory = $true)][string]$LiteralPath,
        [Parameter(Mandatory = $true)][AllowNull()][object]$Value,
        [Parameter(Mandatory = $false)][string]$ExpectedFingerprint,
        [Parameter(Mandatory = $false)][int]$BackupCount = 3
    )

    $directory = Split-Path -Parent $LiteralPath
    if ([string]::IsNullOrWhiteSpace($directory)) { $directory = (Get-Location).Path }
    if (-not (Test-Path -LiteralPath $directory)) {
        New-Item -ItemType Directory -Path $directory -Force | Out-Null
    }

    $leaf = Split-Path -Leaf $LiteralPath
    $lockPath = Join-Path $directory "$leaf.cdp.lock"
    $tempPath = Join-Path $directory "$leaf.cdp-tmp.$([Guid]::NewGuid().ToString('N'))"
    $lockStream = $null
    $ownsLock = $false
    try {
        try {
            $lockStream = [System.IO.File]::Open(
                $lockPath,
                [System.IO.FileMode]::CreateNew,
                [System.IO.FileAccess]::Write,
                [System.IO.FileShare]::None
            )
            $ownsLock = $true
        } catch {
            throw "JSON document is locked by another cdp process: $LiteralPath"
        }

        $currentFingerprint = Get-CdpFileFingerprint -LiteralPath $LiteralPath
        if (-not [string]::IsNullOrWhiteSpace($ExpectedFingerprint) -and
            $ExpectedFingerprint -ne $currentFingerprint) {
            throw "JSON document changed since it was read: $LiteralPath"
        }

        Write-CdpJsonTemporaryFile -LiteralPath $tempPath -Value $Value
        Move-CdpJsonTemporaryFile -TempPath $tempPath -LiteralPath $LiteralPath -Directory $directory -Leaf $leaf
        Remove-CdpOldJsonBackups -LiteralPath $LiteralPath -Keep $BackupCount
        Clear-CdpProjectConfigCache -ConfigPath $LiteralPath
        return Get-CdpFileFingerprint -LiteralPath $LiteralPath
    } finally {
        if ($lockStream) { $lockStream.Dispose() }
        if (Test-Path -LiteralPath $tempPath) { Remove-Item -LiteralPath $tempPath -Force -ErrorAction SilentlyContinue }
        if ($ownsLock -and (Test-Path -LiteralPath $lockPath)) {
            Remove-Item -LiteralPath $lockPath -Force -ErrorAction SilentlyContinue
        }
    }
}

function Get-CdpValidJsonBackups {
    param([Parameter(Mandatory = $true)][string]$LiteralPath)

    $directory = Split-Path -Parent $LiteralPath
    $leaf = Split-Path -Leaf $LiteralPath
    @(Get-ChildItem -LiteralPath $directory -Filter "$leaf.cdp-backup.*" -File -ErrorAction SilentlyContinue |
        Sort-Object -Property Name -Descending |
        Where-Object {
            try {
                [void](ConvertFrom-Json -InputObject (Get-Content -LiteralPath $_.FullName -Raw -Encoding UTF8))
                $true
            } catch {
                $false
            }
        })
}

function Restore-CdpJsonBackup {
    param(
        [Parameter(Mandatory = $true)][string]$LiteralPath,
        [Parameter(Mandatory = $true)][string]$BackupPath
    )

    $validBackups = @(Get-CdpValidJsonBackups -LiteralPath $LiteralPath)
    $backup = @($validBackups | Where-Object { $_.FullName -eq $BackupPath } | Select-Object -First 1)
    if ($backup.Count -ne 1) {
        throw "Backup is missing or invalid: $BackupPath"
    }

    $content = Get-Content -LiteralPath $backup[0].FullName -Raw -Encoding UTF8
    $parsedValue = ConvertFrom-Json -InputObject $content
    $value = if ($content.TrimStart().StartsWith('[')) {
        (ConvertTo-CdpJsonArrayValue -Value $parsedValue).Value
    } else { $parsedValue }
    Write-CdpJsonFile `
        -LiteralPath $LiteralPath `
        -Value $value `
        -ExpectedFingerprint (Get-CdpFileFingerprint -LiteralPath $LiteralPath)
}