Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

function Get-GoldenImageWorkshopPaths {
    param([Parameter(Mandatory)][string]$ScriptRoot)

    $bundleRoot = [System.IO.Path]::GetFullPath($ScriptRoot)
    $outputsRoot = Split-Path -Parent $bundleRoot
    $workspaceRoot = Split-Path -Parent $outputsRoot
    $runtimeRoot = Join-Path $workspaceRoot 'work\azure-golden-images-deep-dive'
    [pscustomobject]@{
        BundleRoot      = $bundleRoot
        OutputsRoot     = $outputsRoot
        WorkspaceRoot   = $workspaceRoot
        RuntimeRoot     = $runtimeRoot
        StatePath       = Join-Path $runtimeRoot 'runtime.json'
        RawEvidenceRoot = Join-Path $runtimeRoot 'raw-evidence'
        TemplateRoot    = Join-Path $runtimeRoot 'templates'
        EvidenceRoot    = Join-Path $bundleRoot 'evidence'
        ScreenshotRoot  = Join-Path $bundleRoot 'screenshots'
    }
}

function Initialize-GoldenImageWorkshopDirectory {
    param([Parameter(Mandatory)]$Paths)
    foreach ($directory in @($Paths.RuntimeRoot,$Paths.RawEvidenceRoot,$Paths.TemplateRoot,$Paths.EvidenceRoot,$Paths.ScreenshotRoot)) {
        if (-not (Test-Path -LiteralPath $directory)) { New-Item -ItemType Directory -Path $directory -Force | Out-Null }
    }
}

function Read-GoldenImageWorkshopState {
    param([Parameter(Mandatory)][string]$Path,[switch]$AllowMissing)
    if (-not (Test-Path -LiteralPath $Path)) {
        if ($AllowMissing) { return $null }
        throw "Golden-image workshop runtime state wasn't found at $Path. Run Stage Preflight first."
    }
    Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json -Depth 100
}

function Write-GoldenImageWorkshopState {
    param([Parameter(Mandatory)]$State,[Parameter(Mandatory)][string]$Path)
    $State.UpdatedUtc = [DateTime]::UtcNow.ToString('o')
    [System.IO.File]::WriteAllText($Path,($State | ConvertTo-Json -Depth 100),[System.Text.UTF8Encoding]::new($false))
}

function Write-GoldenImageJson {
    param([Parameter(Mandatory)]$InputObject,[Parameter(Mandatory)][string]$Path)
    [System.IO.File]::WriteAllText($Path,($InputObject | ConvertTo-Json -Depth 100),[System.Text.UTF8Encoding]::new($false))
}

function Invoke-GoldenImageAzCli {
    param([Parameter(Mandatory)][string[]]$Arguments,[switch]$AllowFailure,[switch]$Quiet)
    if (-not $Quiet) { Write-Host ('az ' + ($Arguments -join ' ')) -ForegroundColor DarkGray }
    $previousWarnings = $env:PYTHONWARNINGS
    try {
        $env:PYTHONWARNINGS = 'ignore'
        $az = Get-Command az -ErrorAction Stop
        $output = & $az.Source @Arguments 2>&1
        $exitCode = $LASTEXITCODE
    }
    finally {
        if ($null -eq $previousWarnings) { Remove-Item Env:PYTHONWARNINGS -ErrorAction SilentlyContinue }
        else { $env:PYTHONWARNINGS = $previousWarnings }
    }
    if ($exitCode -ne 0 -and -not $AllowFailure) { throw "Azure CLI failed ($exitCode): $($output -join [Environment]::NewLine)" }
    [pscustomobject]@{ ExitCode=$exitCode; Output=($output -join [Environment]::NewLine) }
}

function Invoke-GoldenImageAzJson {
    param([Parameter(Mandatory)][string[]]$Arguments,[switch]$AllowFailure,[switch]$Quiet)
    $args = @($Arguments)
    if ($args -notcontains '--output' -and $args -notcontains '-o') { $args += @('--output','json') }
    $result = Invoke-GoldenImageAzCli -Arguments $args -AllowFailure:$AllowFailure -Quiet:$Quiet
    if ($result.ExitCode -ne 0 -or [string]::IsNullOrWhiteSpace($result.Output)) { return $null }
    try { return ($result.Output | ConvertFrom-Json -Depth 100) }
    catch {
        $lines = @($result.Output -split "`r?`n")
        $start = -1
        for ($i=0; $i -lt $lines.Count; $i++) { if ($lines[$i] -match '^\s*(\{|\[|"|-?\d|true\b|false\b|null\b)') { $start=$i; break } }
        if ($start -lt 0) { throw }
        return (($lines[$start..($lines.Count-1)] -join [Environment]::NewLine) | ConvertFrom-Json -Depth 100)
    }
}

function Invoke-GoldenImageAzRestJson {
    param(
        [Parameter(Mandatory)][ValidateSet('GET','PUT','POST','PATCH','DELETE')][string]$Method,
        [Parameter(Mandatory)][string]$Uri,
        [object]$Body,
        [switch]$AllowFailure,
        [switch]$Quiet
    )
    $arguments = @('rest','--method',$Method,'--uri',$Uri)
    $bodyPath = $null
    try {
        if ($PSBoundParameters.ContainsKey('Body')) {
            $bodyPath = Join-Path ([System.IO.Path]::GetTempPath()) ("golden-image-{0}.json" -f [guid]::NewGuid().ToString('N'))
            $json = if ($Body -is [string]) { $Body } else { $Body | ConvertTo-Json -Depth 100 -Compress }
            [System.IO.File]::WriteAllText($bodyPath,$json,[System.Text.UTF8Encoding]::new($false))
            $arguments += @('--body',"@$bodyPath",'--headers','Content-Type=application/json')
        }
        Invoke-GoldenImageAzJson -Arguments $arguments -AllowFailure:$AllowFailure -Quiet:$Quiet
    }
    finally { if ($bodyPath -and (Test-Path -LiteralPath $bodyPath)) { Remove-Item -LiteralPath $bodyPath -Force } }
}

function New-GoldenImageSuffix {
    ([Convert]::ToHexString([System.Security.Cryptography.RandomNumberGenerator]::GetBytes(4))).ToLowerInvariant()
}

function New-GoldenImagePassword {
    $bytes = [System.Security.Cryptography.RandomNumberGenerator]::GetBytes(24)
    'Aib!' + [Convert]::ToBase64String($bytes).Replace('/','x').Replace('+','Y').Substring(0,28) + '9z'
}

function ConvertTo-GoldenImageEncodedCommand {
    param([Parameter(Mandatory)][string]$ScriptText)
    [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($ScriptText))
}

function Wait-GoldenImageCondition {
    param(
        [Parameter(Mandatory)][scriptblock]$Probe,
        [Parameter(Mandatory)][scriptblock]$Success,
        [int]$TimeoutSeconds = 5400,
        [int]$PollSeconds = 20,
        [string]$Description = 'Azure operation'
    )
    $deadline=[DateTime]::UtcNow.AddSeconds($TimeoutSeconds)
    do {
        $result=& $Probe
        if (& $Success $result) { return $result }
        Start-Sleep -Seconds $PollSeconds
    } while ([DateTime]::UtcNow -lt $deadline)
    throw "$Description didn't reach the expected state within $TimeoutSeconds seconds."
}

function Get-GoldenImageRunCommandMessage {
    param([Parameter(Mandatory)]$Response)
    (@($Response.value | ForEach-Object { [string]$_.message }) -join [Environment]::NewLine)
}

function ConvertFrom-GoldenImageMarkedJson {
    param([Parameter(Mandatory)][AllowEmptyString()][string]$Text)
    $match=[regex]::Match($Text,'(?s)WORKSHOP_JSON_BEGIN\s*(\{.*?\})\s*WORKSHOP_JSON_END')
    if (-not $match.Success) { throw 'Run Command did not return a workshop JSON payload.' }
    $match.Groups[1].Value | ConvertFrom-Json -Depth 100
}

function Test-GoldenImageSensitiveText {
    param([Parameter(Mandatory)][AllowEmptyString()][string]$Text)
    $patterns=[ordered]@{
        Guid='(?i)\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b'
        IPv4='(?<!\d)(?:\d{1,3}\.){3}\d{1,3}(?!\d)'
        AzureId='(?i)/subscriptions/|/resourceGroups/|/tenants/'
        Secret='(?i)(password|privatekey|secret|token|sig=)\s*["'':=]+\s*[^\s"'']{8,}'
    }
    foreach ($entry in $patterns.GetEnumerator()) { if ($Text -match $entry.Value) { return $entry.Key } }
    $null
}

function Assert-GoldenImageSanitizedFile {
    param([Parameter(Mandatory)][string]$Path)
    $match=Test-GoldenImageSensitiveText -Text (Get-Content -LiteralPath $Path -Raw)
    if ($match) { throw "Sanitization failed for $Path ($match detected)." }
}

function Get-GoldenImageSha256 {
    param([Parameter(Mandatory)][string]$Path)
    (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
}
