Public/Restore-TaxEditorBlob.ps1

# Copyright (c) 2026 Jeffrey Snover. All rights reserved.
# Licensed under the MIT License. See LICENSE file in the project root.

function Restore-TaxEditorBlob {
    <#
    .SYNOPSIS
        Undeletes a soft-deleted blob from the taxonomy editor storage.
    .DESCRIPTION
        Restores a specific soft-deleted blob in Azure Blob Storage.
        Requires Azure Blob soft-delete to be enabled (30-day retention).
        Supports -WhatIf/-Confirm.
    .PARAMETER Container
        Container name where the blob resides (e.g., 'user-content').
    .PARAMETER Path
        Blob path/name to restore.
    .PARAMETER StorageAccount
        Storage account name. Default: auto-detected from resource group.
    .PARAMETER ResourceGroup
        Azure resource group name. Default: ai-triad.
    .EXAMPLE
        Restore-TaxEditorBlob -Container 'user-content' -Path 'debates/abc123.json'
    .EXAMPLE
        Get-TaxEditorBlob -Deleted | Select-Object -First 1 | Restore-TaxEditorBlob
    .LINK
        Show-AITriadHelp
    .LINK
        Get-TaxEditorBlob
    .LINK
        Get-TaxEditorDataCommit
    .LINK
        Restore-TaxEditorKnownGood
    .LINK
        Set-TaxEditorKnownGood
    .LINK
        Sync-TaxEditorData
    .LINK
        Undo-TaxEditorDataCommit
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [string]$Container,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [Alias('Name')]
        [string]$Path,

        [Parameter()]
        [string]$StorageAccount,

        [Parameter()]
        [string]$ResourceGroup = 'ai-triad'
    )

    begin {
        Set-StrictMode -Version Latest
        $CallerName = 'Restore-TaxEditorBlob'

        # ── Validate az CLI ──────────────────────────────────────────────
        $AzCmd = Get-Command az -ErrorAction SilentlyContinue
        if (-not $AzCmd) {
            throw (New-ActionableError `
                -Goal 'Restore soft-deleted blob' `
                -Problem 'Azure CLI (az) not found on PATH' `
                -Location $CallerName `
                -NextSteps @('Install Azure CLI: https://aka.ms/installazurecli'))
        }

        $AccountJson = & az account show --output json 2>$null
        if ($LASTEXITCODE -ne 0 -or -not $AccountJson) {
            throw (New-ActionableError `
                -Goal 'Restore soft-deleted blob' `
                -Problem 'Azure CLI is not logged in' `
                -Location $CallerName `
                -NextSteps @('Run: az login', 'Verify subscription: az account show'))
        }

        # ── Resolve storage account ──────────────────────────────────────
        if (-not $StorageAccount) {
            $AccountListJson = & az storage account list -g $ResourceGroup --query "[?starts_with(name,'staitriad')].name" -o json 2>$null
            if ($LASTEXITCODE -ne 0 -or -not $AccountListJson) {
                throw (New-ActionableError `
                    -Goal 'Resolve storage account' `
                    -Problem "No storage accounts found in resource group '$ResourceGroup'" `
                    -Location $CallerName `
                    -NextSteps @('Pass -StorageAccount explicitly'))
            }
            $Accounts = $AccountListJson | ConvertFrom-Json
            if (@($Accounts).Count -eq 0) {
                throw (New-ActionableError `
                    -Goal 'Resolve storage account' `
                    -Problem "No 'staitriad*' storage accounts found in '$ResourceGroup'" `
                    -Location $CallerName `
                    -NextSteps @('Pass -StorageAccount explicitly'))
            }
            $StorageAccount = $Accounts[0]
        }

        # ── Check soft-delete is enabled ─────────────────────────────────
        $PropsJson = & az storage account blob-service-properties show --account-name $StorageAccount --query "deleteRetentionPolicy" -o json 2>$null
        if ($LASTEXITCODE -eq 0 -and $PropsJson) {
            $Props = $PropsJson | ConvertFrom-Json
            if ($Props.PSObject.Properties['enabled'] -and -not $Props.enabled) {
                throw (New-ActionableError `
                    -Goal 'Restore soft-deleted blob' `
                    -Problem "Blob soft-delete is not enabled on storage account '$StorageAccount'" `
                    -Location $CallerName `
                    -NextSteps @('Enable soft-delete in main.bicep (deleteRetentionPolicy.enabled = true)',
                                 'Redeploy infrastructure'))
            }
        }
    }

    process {
        # ── WhatIf / Confirm gate ────────────────────────────────────────
        if (-not $PSCmdlet.ShouldProcess("$Container/$Path", "Undelete soft-deleted blob on '$StorageAccount'")) {
            return
        }

        # ── Restore the blob ─────────────────────────────────────────────
        Write-Verbose "Restoring blob '$Path' in container '$Container'..."
        $RestoreOutput = & az storage blob undelete `
            --account-name $StorageAccount `
            --container-name $Container `
            --name $Path `
            --auth-mode login `
            --output json 2>$null

        if ($LASTEXITCODE -ne 0) {
            throw (New-ActionableError `
                -Goal "Restore blob '$Path'" `
                -Problem "az storage blob undelete failed (exit code $LASTEXITCODE)" `
                -Location $CallerName `
                -NextSteps @("Verify blob exists and is soft-deleted: Get-TaxEditorBlob -Container '$Container' -Deleted",
                             "Check storage account access permissions"))
        }

        Write-Verbose "Restored '$Container/$Path' successfully."

        [PSCustomObject]@{
            Action         = 'BlobRestore'
            Container      = $Container
            Path           = $Path
            StorageAccount = $StorageAccount
            Timestamp      = (Get-Date).ToString('o')
        }
    }
}