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

function Get-AvdWorkshopPaths {
    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-virtual-desktop-deep-dive'

    [pscustomobject]@{
        BundleRoot    = $bundleRoot
        OutputsRoot   = $outputsRoot
        WorkspaceRoot = $workspaceRoot
        RuntimeRoot   = $runtimeRoot
        StatePath     = Join-Path $runtimeRoot 'runtime.json'
        EvidenceRoot  = Join-Path $bundleRoot 'evidence'
        ScreenshotRoot = Join-Path $bundleRoot 'screenshots'
    }
}

function Initialize-AvdWorkshopDirectory {
    param([Parameter(Mandatory)]$Paths)

    foreach ($directory in @($Paths.RuntimeRoot, $Paths.EvidenceRoot, $Paths.ScreenshotRoot)) {
        if (-not (Test-Path -LiteralPath $directory)) {
            New-Item -ItemType Directory -Path $directory -Force | Out-Null
        }
    }
}

function Read-AvdWorkshopState {
    param(
        [Parameter(Mandatory)][string]$Path,
        [switch]$AllowMissing
    )

    if (-not (Test-Path -LiteralPath $Path)) {
        if ($AllowMissing) { return $null }
        throw "AVD workshop runtime state was not found at $Path. Run Stage Preflight first."
    }

    Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json -Depth 60
}

function Write-AvdWorkshopState {
    param(
        [Parameter(Mandatory)]$State,
        [Parameter(Mandatory)][string]$Path
    )

    $State.UpdatedUtc = [DateTime]::UtcNow.ToString('o')
    $json = $State | ConvertTo-Json -Depth 60
    [System.IO.File]::WriteAllText($Path, $json, [System.Text.UTF8Encoding]::new($false))
}

function Invoke-AvdAzCli {
    param(
        [Parameter(Mandatory)][string[]]$Arguments,
        [switch]$AllowFailure,
        [switch]$Quiet
    )

    if (-not $Quiet) { Write-Host ("az " + ($Arguments -join ' ')) -ForegroundColor DarkGray }
    $previousPythonWarnings = $env:PYTHONWARNINGS
    try {
        $env:PYTHONWARNINGS = 'ignore'
        $azCommand = Get-Command az -ErrorAction Stop
        $powerShellWrapper = Join-Path (Split-Path -Parent $azCommand.Source) 'azps.ps1'
        if (Test-Path -LiteralPath $powerShellWrapper) {
            $output = & $powerShellWrapper @Arguments 2>&1
        }
        else {
            $output = & az @Arguments 2>&1
        }
        $exitCode = $LASTEXITCODE
    }
    finally {
        if ($null -eq $previousPythonWarnings) { Remove-Item Env:PYTHONWARNINGS -ErrorAction SilentlyContinue }
        else { $env:PYTHONWARNINGS = $previousPythonWarnings }
    }
    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-AvdAzJson {
    param(
        [Parameter(Mandatory)][string[]]$Arguments,
        [switch]$AllowFailure,
        [switch]$Quiet
    )

    $argumentsWithOutput = @($Arguments)
    if ($argumentsWithOutput -notcontains '--output' -and $argumentsWithOutput -notcontains '-o') {
        $argumentsWithOutput += @('--output', 'json')
    }

    $result = Invoke-AvdAzCli -Arguments $argumentsWithOutput -AllowFailure:$AllowFailure -Quiet:$Quiet
    if ($result.ExitCode -ne 0 -or [string]::IsNullOrWhiteSpace($result.Output)) { return $null }
    try {
        $result.Output | ConvertFrom-Json -Depth 80
    }
    catch {
        $lines = @($result.Output -split "`r?`n")
        $start = -1
        for ($index = 0; $index -lt $lines.Count; $index++) {
            if ($lines[$index] -match '^\s*(\{|\[|"|-?\d|true\b|false\b|null\b)') {
                $start = $index
                break
            }
        }
        if ($start -lt 0) { throw }
        ($lines[$start..($lines.Count - 1)] -join [Environment]::NewLine) | ConvertFrom-Json -Depth 80
    }
}

function Invoke-AvdAzRestJson {
    param(
        [Parameter(Mandatory)][ValidateSet('GET','PUT','POST','PATCH','DELETE')][string]$Method,
        [Parameter(Mandatory)][string]$Uri,
        [object]$Body,
        [switch]$AllowFailure,
        [string]$Resource,
        [switch]$Quiet
    )

    $arguments = @('rest', '--method', $Method, '--uri', $Uri)
    if ($Resource) { $arguments += @('--resource', $Resource) }
    $bodyPath = $null
    try {
        if ($PSBoundParameters.ContainsKey('Body')) {
            $tempRoot = [System.IO.Path]::GetTempPath()
            $bodyPath = Join-Path $tempRoot ("avd-{0}.json" -f [guid]::NewGuid().ToString('N'))
            $json = if ($Body -is [string]) { $Body } else { $Body | ConvertTo-Json -Depth 80 -Compress }
            [System.IO.File]::WriteAllText($bodyPath, $json, [System.Text.UTF8Encoding]::new($false))
            $arguments += @('--body', "@$bodyPath", '--headers', 'Content-Type=application/json')
        }
        Invoke-AvdAzJson -Arguments $arguments -AllowFailure:$AllowFailure -Quiet:$Quiet
    }
    finally {
        if ($bodyPath -and (Test-Path -LiteralPath $bodyPath)) {
            Remove-Item -LiteralPath $bodyPath -Force
        }
    }
}

function Invoke-AvdGraphJson {
    param(
        [Parameter(Mandatory)][ValidateSet('GET','POST','PATCH','DELETE')][string]$Method,
        [Parameter(Mandatory)][string]$Path,
        [object]$Body,
        [switch]$AllowFailure
    )

    $uri = if ($Path -match '^https://') { $Path } else { "https://graph.microsoft.com/v1.0/$($Path.TrimStart('/'))" }
    if ($PSBoundParameters.ContainsKey('Body')) {
        Invoke-AvdAzRestJson -Method $Method -Uri $uri -Body $Body -AllowFailure:$AllowFailure -Resource 'https://graph.microsoft.com/' -Quiet
    }
    else {
        Invoke-AvdAzRestJson -Method $Method -Uri $uri -AllowFailure:$AllowFailure -Resource 'https://graph.microsoft.com/' -Quiet
    }
}

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

function New-AvdWorkshopPassword {
    $bytes = [System.Security.Cryptography.RandomNumberGenerator]::GetBytes(18)
    $core = [Convert]::ToBase64String($bytes).TrimEnd('=').Replace('/','x').Replace('+','Y')
    "Avd!$core`9z"
}

function Get-AvdVmPowerState {
    param(
        [Parameter(Mandatory)][string]$ResourceGroup,
        [Parameter(Mandatory)][string]$Name
    )

    $value = Invoke-AvdAzJson -Arguments @(
        'vm','get-instance-view','--resource-group',$ResourceGroup,'--name',$Name,
        '--query',"instanceView.statuses[?starts_with(code,'PowerState/')].displayStatus | [0]"
    ) -Quiet
    [string]$value
}

function Wait-AvdVmPowerState {
    param(
        [Parameter(Mandatory)][string]$ResourceGroup,
        [Parameter(Mandatory)][string]$Name,
        [Parameter(Mandatory)][string[]]$Expected,
        [int]$TimeoutSeconds = 1800,
        [int]$PollSeconds = 20
    )

    $deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds)
    do {
        $current = Get-AvdVmPowerState -ResourceGroup $ResourceGroup -Name $Name
        if ($Expected -contains $current) { return $current }
        Start-Sleep -Seconds $PollSeconds
    } while ([DateTime]::UtcNow -lt $deadline)

    throw "VM $Name did not reach [$($Expected -join ', ')] within $TimeoutSeconds seconds. Last state: $current"
}

function Wait-AvdSessionHosts {
    param(
        [Parameter(Mandatory)]$State,
        [int]$TimeoutSeconds = 1800,
        [int]$PollSeconds = 30
    )

    $uri = "https://management.azure.com$($State.HostPoolId)/sessionHosts?api-version=$($State.AvdApiVersion)"
    $deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds)
    do {
        $response = Invoke-AvdAzRestJson -Method GET -Uri $uri -AllowFailure -Quiet
        $hosts = @($response.value)
        if ($hosts.Count -eq [int]$State.HostCount -and @($hosts | Where-Object { $_.properties.status -ne 'Available' }).Count -eq 0) {
            return $hosts
        }
        Start-Sleep -Seconds $PollSeconds
    } while ([DateTime]::UtcNow -lt $deadline)

    $summary = @($hosts | ForEach-Object { "$($_.name):$($_.properties.status)" }) -join ', '
    throw "Session hosts did not all become Available within $TimeoutSeconds seconds. Last state: $summary"
}

function Set-AvdSessionHostDrainMode {
    param(
        [Parameter(Mandatory)]$State,
        [Parameter(Mandatory)][string]$SessionHostName,
        [Parameter(Mandatory)][bool]$AllowNewSession
    )

    $uri = "https://management.azure.com$($State.HostPoolId)/sessionHosts/$SessionHostName`?api-version=$($State.AvdApiVersion)"
    Invoke-AvdAzRestJson -Method PATCH -Uri $uri -Body @{ properties = @{ allowNewSession = $AllowNewSession } } -Quiet | Out-Null
}

function Invoke-AvdLogQuery {
    param(
        [Parameter(Mandatory)][string]$WorkspaceId,
        [Parameter(Mandatory)][string]$Query,
        [string]$Timespan = 'P1D',
        [switch]$AllowFailure
    )

    Invoke-AvdAzJson -Arguments @(
        'monitor','log-analytics','query','--workspace',$WorkspaceId,
        '--analytics-query',$Query,'--timespan',$Timespan
    ) -AllowFailure:$AllowFailure -Quiet
}

function Test-AvdSensitiveText {
    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'
        UserPrincipalName = '(?i)\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b'
        RegistrationToken = '(?i)(registration(?:info)?token|password|secret|sig=|token=)\s*["'':=]+\s*[^\s"'']{8,}'
        IPv4              = '(?<!\d)(?:\d{1,3}\.){3}\d{1,3}(?!\d)'
        AzureId           = '(?i)/subscriptions/|/tenants/|/servicePrincipals/|/applications/'
    }

    foreach ($entry in $patterns.GetEnumerator()) {
        if ($Text -match $entry.Value) { return $entry.Key }
    }
    $null
}

function Assert-AvdSanitizedFile {
    param([Parameter(Mandatory)][string]$Path)

    $content = Get-Content -LiteralPath $Path -Raw
    $match = Test-AvdSensitiveText -Text $content
    if ($match) { throw "Sanitization failed for $Path ($match detected)." }
}

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