param(
  [Parameter(Mandatory = $true)]
  [string]$ConfigPath
)

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
Add-Type -AssemblyName System.Net.Http

function Read-Config {
  if (-not (Test-Path -LiteralPath $ConfigPath)) {
    throw "config-not-found: $ConfigPath"
  }
  return Get-Content -Raw -LiteralPath $ConfigPath | ConvertFrom-Json
}

$Config = Read-Config
$StateDir = [string]$Config.host.stateDir
$StatusPath = Join-Path $StateDir 'status.json'
$LogPath = Join-Path $StateDir 'install.log'
$LockPath = Join-Path $StateDir 'bootstrap.lock'
$script:LockStream = $null
$StartedAt = [DateTimeOffset]::UtcNow
$CurrentStage = 'initializing'

New-Item -ItemType Directory -Force -Path $StateDir | Out-Null

function Acquire-BootstrapLock {
  try {
    $script:LockStream = [System.IO.File]::Open($LockPath, [System.IO.FileMode]::CreateNew, [System.IO.FileAccess]::Write, [System.IO.FileShare]::None)
    $bytes = [Text.Encoding]::UTF8.GetBytes("pid=$PID`nstartedAt=$($StartedAt.ToString('o'))`n")
    $script:LockStream.Write($bytes, 0, $bytes.Length)
    $script:LockStream.Flush($true)
  } catch [System.IO.IOException] {
    $owner = if (Test-Path -LiteralPath $LockPath) { Get-Content -Raw -LiteralPath $LockPath -ErrorAction SilentlyContinue } else { 'unknown' }
    if ($owner -match 'pid=([0-9]+)' -and (Get-Process -Id ([int]$Matches[1]) -ErrorAction SilentlyContinue)) {
      throw "bootstrap-already-running: $owner"
    }
    Remove-Item -Force -LiteralPath $LockPath -ErrorAction SilentlyContinue
    $script:LockStream = [System.IO.File]::Open($LockPath, [System.IO.FileMode]::CreateNew, [System.IO.FileAccess]::Write, [System.IO.FileShare]::None)
    $bytes = [Text.Encoding]::UTF8.GetBytes("pid=$PID`nstartedAt=$($StartedAt.ToString('o'))`n")
    $script:LockStream.Write($bytes, 0, $bytes.Length)
    $script:LockStream.Flush($true)
  }
}

function Rotate-Log {
  if (-not (Test-Path -LiteralPath $LogPath)) { return }
  $maxBytes = [int64]$Config.status.logMaxBytes
  if ((Get-Item -LiteralPath $LogPath).Length -lt $maxBytes) { return }
  $backups = [int]$Config.status.logBackups
  for ($index = $backups - 1; $index -ge 1; $index--) {
    $source = "$LogPath.$index"
    $destination = "$LogPath.$($index + 1)"
    if (Test-Path -LiteralPath $source) { Move-Item -Force -LiteralPath $source -Destination $destination }
  }
  Move-Item -Force -LiteralPath $LogPath -Destination "$LogPath.1"
}

function Write-Log([string]$Message) {
  Rotate-Log
  $line = '{0} {1}' -f ([DateTimeOffset]::Now.ToString('yyyy-MM-dd HH:mm:ss zzz')), $Message
  Add-Content -LiteralPath $LogPath -Value $line -Encoding UTF8
}

function Write-State {
  param(
    [string]$Stage,
    [string]$State = 'running',
    [string]$Message = '',
    [hashtable]$Extra = @{}
  )
  $script:CurrentStage = $Stage
  $now = [DateTimeOffset]::UtcNow
  $payload = [ordered]@{
    ok = $State -eq 'completed'
    state = $State
    stage = $Stage
    message = $Message
    targetId = [string]$Config.targetId
    vmName = [string]$Config.vm.name
    startedAt = $StartedAt.ToString('o')
    updatedAt = $now.ToString('o')
    elapsedSeconds = [math]::Round(($now - $StartedAt).TotalSeconds, 1)
    logPath = $LogPath
    errorCode = $null
    recoveryHint = $null
  }
  foreach ($key in $Extra.Keys) { $payload[$key] = $Extra[$key] }
  $temporary = "$StatusPath.tmp"
  [System.IO.File]::WriteAllText($temporary, ($payload | ConvertTo-Json -Depth 8), [System.Text.UTF8Encoding]::new($false))
  Move-Item -Force -LiteralPath $temporary -Destination $StatusPath
  Write-Log "stage=$Stage state=$State message=$Message"
}

function Assert-Administrator {
  $principal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
  if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
    throw 'administrator-required: approve the Windows UAC prompt'
  }
}

function Select-ImageSource {
  Write-State -Stage 'selecting-image-source' -Message 'Testing YAML-declared Ubuntu image sources'
  foreach ($source in @($Config.image.sources)) {
    try {
      $request = [System.Net.HttpWebRequest]::Create([string]$source.url)
      $request.Method = 'GET'
      $request.UserAgent = 'Mozilla/5.0 UniDesk-HyperV/1.0'
      $request.AddRange(0, 0)
      $request.Timeout = 15000
      $response = $request.GetResponse()
      $contentRange = [string]$response.Headers['Content-Range']
      $length = if ($contentRange -match '/([0-9]+)$') { [int64]$Matches[1] } else { [int64]$response.ContentLength }
      $response.Close()
      if ($length -gt 0) {
        Write-Log "selected_image_source=$($source.id) region=$($source.region) bytes=$length"
        return [ordered]@{ id = [string]$source.id; url = [string]$source.url; bytes = $length }
      }
    } catch {
      Write-Log "image_source_unavailable=$($source.id) error=$($_.Exception.Message)"
    }
  }
  throw 'image-source-unavailable: no YAML-declared Ubuntu image source is reachable'
}

function Download-FileWithMetrics {
  param(
    [string]$Url,
    [string]$Destination,
    [int64]$ExpectedBytes,
    [string]$ExpectedHash,
    [string]$HashAlgorithm,
    [string]$Stage,
    [string]$Message
  )
  if (Test-Path -LiteralPath $Destination) {
    $existingHash = (Get-FileHash -Algorithm $HashAlgorithm -LiteralPath $Destination).Hash.ToLowerInvariant()
    if ($existingHash -eq $ExpectedHash.ToLowerInvariant()) {
      Write-Log 'download_cache_hit=true'
      return
    }
    Remove-Item -Force -LiteralPath $Destination
  }
  Write-State -Stage $Stage -Message $Message -Extra @{ sourceUrl = $Url; downloadedBytes = 0; totalBytes = $ExpectedBytes; bytesPerSecond = 0; etaSeconds = $null }
  $client = [System.Net.Http.HttpClient]::new()
  $client.Timeout = [TimeSpan]::FromSeconds([int]$Config.status.downloadTimeoutSeconds)
  $client.DefaultRequestHeaders.UserAgent.ParseAdd('Mozilla/5.0 UniDesk-HyperV/1.0')
  $response = $client.GetAsync($Url, [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead).GetAwaiter().GetResult()
  $response.EnsureSuccessStatusCode()
  $total = if ($response.Content.Headers.ContentLength) { [int64]$response.Content.Headers.ContentLength } else { $ExpectedBytes }
  $input = $response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()
  $output = [System.IO.File]::Open($Destination, [System.IO.FileMode]::CreateNew, [System.IO.FileAccess]::Write, [System.IO.FileShare]::None)
  try {
    $buffer = New-Object byte[] (4MB)
    $downloaded = [int64]0
    $watch = [Diagnostics.Stopwatch]::StartNew()
    $lastReport = 0.0
    while (($read = $input.Read($buffer, 0, $buffer.Length)) -gt 0) {
      $output.Write($buffer, 0, $read)
      $downloaded += $read
      if (($watch.Elapsed.TotalSeconds - $lastReport) -ge 1) {
        $seconds = [math]::Max($watch.Elapsed.TotalSeconds, 0.001)
        $speed = [int64]($downloaded / $seconds)
        $eta = if ($speed -gt 0 -and $total -gt $downloaded) { [int][math]::Ceiling(($total - $downloaded) / $speed) } else { 0 }
        Write-State -Stage $Stage -Message $Message -Extra @{ sourceUrl = $Url; downloadedBytes = $downloaded; totalBytes = $total; bytesPerSecond = $speed; etaSeconds = $eta; downloadElapsedSeconds = [math]::Round($seconds, 1) }
        $lastReport = $watch.Elapsed.TotalSeconds
      }
    }
    $output.Flush($true)
  } finally {
    $output.Dispose()
    $input.Dispose()
    $response.Dispose()
    $client.Dispose()
  }
  $hash = (Get-FileHash -Algorithm $HashAlgorithm -LiteralPath $Destination).Hash.ToLowerInvariant()
  if ($hash -ne $ExpectedHash.ToLowerInvariant()) {
    throw "download-hash-mismatch: algorithm=$HashAlgorithm expected=$ExpectedHash actual=$hash"
  }
}

function Ensure-HostKey {
  Write-State -Stage 'ensuring-host-key' -Message 'Ensuring the dedicated D601-VM SSH key'
  $keyPath = Join-Path $StateDir 'id_ed25519'
  if (-not (Test-Path -LiteralPath $keyPath)) {
    & ssh-keygen.exe -q -t ed25519 -N '""' -C 'unidesk-d601-vm' -f $keyPath
    if ($LASTEXITCODE -ne 0) { throw "ssh-keygen-failed: exit=$LASTEXITCODE" }
  }
  return [ordered]@{ private = $keyPath; public = (Get-Content -Raw -LiteralPath "$keyPath.pub").Trim() }
}

function Ensure-QemuImg {
  $binary = [string]$Config.image.conversion.binary
  if (Test-Path -LiteralPath $binary) { return $binary }
  $installer = $Config.image.conversion.installer
  $installerPath = Join-Path $StateDir ([string]$installer.fileName)
  Download-FileWithMetrics -Url ([string]$installer.url) -Destination $installerPath -ExpectedBytes 0 -ExpectedHash ([string]$installer.sha512) -HashAlgorithm SHA512 -Stage 'downloading-qemu' -Message 'Downloading the private Windows QEMU converter'
  Write-State -Stage 'installing-qemu' -Message 'Installing the private Windows QEMU converter'
  New-Item -ItemType Directory -Force -Path ([string]$Config.image.conversion.installDir) | Out-Null
  $installerProcess = Start-Process -FilePath $installerPath -ArgumentList '/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART', "/DIR=$($Config.image.conversion.installDir)" -Wait -PassThru
  if ($installerProcess.ExitCode -ne 0) { throw "qemu-installer-failed: exit=$($installerProcess.ExitCode)" }
  if (-not (Test-Path -LiteralPath $binary)) { throw "qemu-img-missing-after-install: $binary" }
  return $binary
}

function Ensure-Network {
  Write-State -Stage 'configuring-network' -Message 'Configuring isolated Hyper-V NAT network'
  $switchName = [string]$Config.network.switchName
  if (-not (Get-VMSwitch -Name $switchName -ErrorAction SilentlyContinue)) {
    New-VMSwitch -Name $switchName -SwitchType Internal | Out-Null
  }
  $adapter = Get-NetAdapter | Where-Object Name -eq "vEthernet ($switchName)"
  if (-not $adapter) { throw "hyperv-nat-adapter-missing: vEthernet ($switchName)" }
  $gateway = [string]$Config.network.gateway
  $prefixLength = [int]$Config.network.prefixLength
  if (-not (Get-NetIPAddress -InterfaceIndex $adapter.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue | Where-Object IPAddress -eq $gateway)) {
    Get-NetIPAddress -InterfaceIndex $adapter.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue | Remove-NetIPAddress -Confirm:$false -ErrorAction SilentlyContinue
    New-NetIPAddress -InterfaceIndex $adapter.ifIndex -IPAddress $gateway -PrefixLength $prefixLength | Out-Null
  }
  $natName = [string]$Config.network.natName
  $nat = Get-NetNat -Name $natName -ErrorAction SilentlyContinue
  if ($nat -and $nat.InternalIPInterfaceAddressPrefix -ne [string]$Config.network.prefix) {
    throw "hyperv-nat-prefix-drift: actual=$($nat.InternalIPInterfaceAddressPrefix) desired=$($Config.network.prefix)"
  }
  if (-not $nat) { New-NetNat -Name $natName -InternalIPInterfaceAddressPrefix ([string]$Config.network.prefix) | Out-Null }
}

function New-SeedDisk {
  param([string]$SeedPath, [string]$AuthorizedKey)
  if (Test-Path -LiteralPath $SeedPath) { Remove-Item -Force -LiteralPath $SeedPath }
  New-VHD -Path $SeedPath -Dynamic -SizeBytes 128MB | Out-Null
  $mounted = Mount-VHD -Path $SeedPath -PassThru
  try {
    $disk = $mounted | Get-Disk
    Initialize-Disk -Number $disk.Number -PartitionStyle MBR | Out-Null
    $partition = New-Partition -DiskNumber $disk.Number -UseMaximumSize -AssignDriveLetter
    Format-Volume -Partition $partition -FileSystem FAT32 -NewFileSystemLabel CIDATA -Confirm:$false | Out-Null
    $drive = "$($partition.DriveLetter):\"
    $dns = @($Config.network.dns) | ForEach-Object { "        - $_" }
    $userData = @"
#cloud-config
hostname: $($Config.ubuntu.hostname)
manage_etc_hosts: true
timezone: $($Config.ubuntu.timezone)
locale: $($Config.ubuntu.locale)
users:
  - default
  - name: $($Config.ubuntu.user)
    groups: [adm, sudo, docker]
    shell: /bin/bash
    sudo: ALL=(ALL) NOPASSWD:ALL
    ssh_authorized_keys:
      - $AuthorizedKey
ssh_pwauth: false
disable_root: true
package_update: true
apt:
  preserve_sources_list: false
  primary:
    - arches: [default]
      uri: $($Config.ubuntu.aptMirror)
  security:
    - arches: [default]
      uri: $($Config.ubuntu.aptMirror)
packages:
  - openssh-server
  - qemu-guest-agent
  - curl
  - ca-certificates
  - docker.io
  - docker-compose-v2
write_files:
  - path: /etc/docker/daemon.json
    permissions: '0644'
    content: |
      {"registry-mirrors":["$($Config.docker.registryMirrors[0])"],"log-driver":"local","log-opts":{"max-size":"20m","max-file":"5"}}
runcmd:
  - [systemctl, enable, --now, ssh]
  - [systemctl, enable, --now, docker]
  - [systemctl, enable, --now, qemu-guest-agent]
  - [bash, -lc, "mkdir -p /home/$($Config.ubuntu.user)/.unidesk/host-ssh /home/$($Config.ubuntu.user)/unidesk-provider && chown -R $($Config.ubuntu.user):$($Config.ubuntu.user) /home/$($Config.ubuntu.user)/.unidesk /home/$($Config.ubuntu.user)/unidesk-provider"]
final_message: "UNIDESK_CLOUD_INIT_COMPLETE"
"@
    $metaData = @"
instance-id: $($Config.targetId.ToLowerInvariant())
local-hostname: $($Config.ubuntu.hostname)
"@
    $networkConfig = @"
version: 2
ethernets:
  eth0:
    match:
      driver: hv_netvsc
    set-name: eth0
    dhcp4: false
    addresses:
      - $($Config.network.address)/$($Config.network.prefixLength)
    routes:
      - to: default
        via: $($Config.network.gateway)
    nameservers:
      addresses:
$($dns -join "`n")
"@
    [System.IO.File]::WriteAllText((Join-Path $drive 'user-data'), $userData, [Text.UTF8Encoding]::new($false))
    [System.IO.File]::WriteAllText((Join-Path $drive 'meta-data'), $metaData, [Text.UTF8Encoding]::new($false))
    [System.IO.File]::WriteAllText((Join-Path $drive 'network-config'), $networkConfig, [Text.UTF8Encoding]::new($false))
  } finally {
    Dismount-VHD -Path $SeedPath -ErrorAction SilentlyContinue
  }
}

function Repair-BrokenVmDiskChain {
  $name = [string]$Config.vm.name
  $vm = Get-VM -Name $name -ErrorAction SilentlyContinue
  if (-not $vm) { return }
  $snapshots = @(Get-VMSnapshot -VMName $name -ErrorAction SilentlyContinue)
  $differencingDisks = @(Get-VMHardDiskDrive -VMName $name -ErrorAction SilentlyContinue | Where-Object { $_.Path -and $_.Path.EndsWith('.avhdx', [StringComparison]::OrdinalIgnoreCase) })
  if ($snapshots.Count -eq 0 -and $differencingDisks.Count -eq 0) { return }
  Write-State -Stage 'repairing-vm-disk-chain' -Message 'Removing failed automatic checkpoints before recreating the VM configuration'
  if ($vm.State -ne 'Off') { Stop-VM -Name $name -TurnOff -Force -ErrorAction SilentlyContinue }
  Remove-VM -Name $name -Force
  Get-ChildItem -Path ([string]$Config.host.vmRoot) -Recurse -Filter '*.avhdx' -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue
}

function Ensure-VirtualMachine {
  param([string]$DiskPath, [string]$SeedPath)
  Write-State -Stage 'creating-vm' -Message 'Creating or reconciling Hyper-V virtual machine'
  $name = [string]$Config.vm.name
  $vm = Get-VM -Name $name -ErrorAction SilentlyContinue
  if ($vm -and (Test-VirtualMachineNeedsRecreate -DiskPath $DiskPath -SeedPath $SeedPath)) {
    if ($vm.State -ne 'Off') { Stop-VM -Name $name -TurnOff -Force }
    Remove-VM -Name $name -Force
    $vm = $null
  }
  if (-not $vm) {
    $vm = New-VM -Name $name -Generation ([int]$Config.vm.generation) -MemoryStartupBytes ([int64]$Config.vm.memoryStartupBytes) -VHDPath $DiskPath -SwitchName ([string]$Config.network.switchName) -Path ([string]$Config.host.vmRoot)
    if ([int]$Config.vm.generation -eq 2) {
      Add-VMHardDiskDrive -VMName $name -ControllerType SCSI -ControllerNumber 0 -ControllerLocation 1 -Path $SeedPath
      Set-VMFirmware -VMName $name -EnableSecureBoot On -SecureBootTemplate MicrosoftUEFICertificateAuthority
    } else {
      Add-VMHardDiskDrive -VMName $name -ControllerType IDE -ControllerNumber 0 -ControllerLocation 1 -Path $SeedPath
    }
  } elseif ($vm.State -ne 'Off') {
    Stop-VM -Name $name -TurnOff -Force
  }
  Set-VMMemory -VMName $name -DynamicMemoryEnabled ([bool]$Config.vm.dynamicMemoryEnabled) -StartupBytes ([int64]$Config.vm.memoryStartupBytes) -MinimumBytes ([int64]$Config.vm.memoryMinimumBytes) -MaximumBytes ([int64]$Config.vm.memoryMaximumBytes) -Buffer ([int]$Config.vm.memoryBufferPercent)
  Set-VMProcessor -VMName $name -Count ([int]$Config.vm.processorCount)
  Set-VM -Name $name -AutomaticStartAction ([string]$Config.vm.automaticStartAction) -AutomaticStopAction ([string]$Config.vm.automaticStopAction) -AutomaticStartDelay ([int]$Config.vm.automaticStartDelaySeconds) -AutomaticCheckpointsEnabled $false -CheckpointType Disabled
  Set-VMComPort -VMName $name -Number 1 -Path $null -ErrorAction SilentlyContinue
  Start-VM -Name $name | Out-Null
}

function Test-VirtualMachineNeedsRecreate {
  param([string]$DiskPath, [string]$SeedPath)
  $name = [string]$Config.vm.name
  $vm = Get-VM -Name $name -ErrorAction SilentlyContinue
  if (-not $vm) { return $true }
  if ([int]$vm.Generation -ne [int]$Config.vm.generation) { return $true }
  $adapter = Get-VMNetworkAdapter -VMName $name | Select-Object -First 1
  if (-not $adapter -or [string]$adapter.SwitchName -ne [string]$Config.network.switchName) { return $true }
  $disks = @(Get-VMHardDiskDrive -VMName $name -ErrorAction SilentlyContinue)
  $systemDisk = $disks | Where-Object { [string]$_.Path -ieq $DiskPath }
  $seedDisk = $disks | Where-Object { [string]$_.Path -ieq $SeedPath }
  if (-not $systemDisk -or -not $seedDisk) { return $true }
  if ([int]$Config.vm.generation -eq 2 -and ([string]$seedDisk.ControllerType -ne 'SCSI' -or [int]$seedDisk.ControllerNumber -ne 0 -or [int]$seedDisk.ControllerLocation -ne 1)) { return $true }
  return $false
}

function Wait-For-Ssh {
  param([string]$KeyPath)
  Write-State -Stage 'waiting-for-cloud-init' -Message 'Waiting for Ubuntu SSH and cloud-init'
  $deadline = [DateTimeOffset]::UtcNow.AddSeconds([int]$Config.status.sshTimeoutSeconds)
  while ([DateTimeOffset]::UtcNow -lt $deadline) {
    $previousErrorPreference = $ErrorActionPreference
    $ErrorActionPreference = 'Continue'
    & ssh.exe -i $KeyPath -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=5 "$($Config.ubuntu.user)@$($Config.network.address)" 'test -f /var/lib/cloud/instance/boot-finished' 2>$null
    $sshExitCode = $LASTEXITCODE
    $ErrorActionPreference = $previousErrorPreference
    if ($sshExitCode -eq 0) { return }
    Start-Sleep -Seconds ([int]$Config.status.heartbeatSeconds)
    Write-State -Stage 'waiting-for-cloud-init' -Message 'Ubuntu is booting or cloud-init is still running'
  }
  throw 'cloud-init-timeout: inspect the Hyper-V console and install.log'
}

function Install-Provider {
  param([string]$KeyPath)
  Write-State -Stage 'installing-provider' -Message 'Installing provider-gateway inside Ubuntu'
  $bundle = [string]$Config.artifacts.providerBundlePath
  if (-not (Test-Path -LiteralPath $bundle)) { throw "provider-bundle-missing: $bundle" }
  $target = "$($Config.ubuntu.user)@$($Config.network.address)"
  & scp.exe -i $KeyPath -o BatchMode=yes -o StrictHostKeyChecking=accept-new $bundle "${target}:/tmp/unidesk-provider-bundle.tar.gz"
  if ($LASTEXITCODE -ne 0) { throw "provider-bundle-scp-failed: exit=$LASTEXITCODE" }
  $command = "rm -rf ~/unidesk-provider/* && tar -xzf /tmp/unidesk-provider-bundle.tar.gz -C ~/unidesk-provider && chmod +x ~/unidesk-provider/install-provider.sh && sudo ~/unidesk-provider/install-provider.sh"
  & ssh.exe -i $KeyPath -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ServerAliveInterval=15 $target $command
  if ($LASTEXITCODE -ne 0) { throw "provider-install-failed: exit=$LASTEXITCODE" }
}

function Install-K3s {
  param([string]$KeyPath)
  Write-State -Stage 'installing-k3s' -Message 'Installing the YAML-pinned k3s artifacts copied from the control plane'
  $binary = [string]$Config.artifacts.k3sBinaryPath
  $installer = [string]$Config.artifacts.k3sInstallerPath
  if (-not (Test-Path -LiteralPath $binary)) { throw "k3s-binary-missing: $binary" }
  if (-not (Test-Path -LiteralPath $installer)) { throw "k3s-installer-missing: $installer" }
  $target = "$($Config.ubuntu.user)@$($Config.network.address)"
  & scp.exe -i $KeyPath -o BatchMode=yes -o StrictHostKeyChecking=accept-new $binary "${target}:/tmp/k3s"
  if ($LASTEXITCODE -ne 0) { throw "k3s-binary-scp-failed: exit=$LASTEXITCODE" }
  & scp.exe -i $KeyPath -o BatchMode=yes -o StrictHostKeyChecking=accept-new $installer "${target}:/tmp/install-k3s.sh"
  if ($LASTEXITCODE -ne 0) { throw "k3s-installer-scp-failed: exit=$LASTEXITCODE" }
  $exec = "server --disable traefik --disable servicelb --node-name $($Config.k3s.nodeName) --node-label unidesk.ai/node-id=$($Config.targetId) --node-label unidesk.ai/provider-id=$($Config.provider.id) --write-kubeconfig-mode 644 --kubelet-arg max-pods=$($Config.k3s.maxPods)"
  $command = "sudo install -m 0755 /tmp/k3s /usr/local/bin/k3s && sudo chmod 0755 /tmp/install-k3s.sh && sudo env INSTALL_K3S_SKIP_DOWNLOAD=true INSTALL_K3S_VERSION='$($Config.k3s.version)' INSTALL_K3S_EXEC='$exec' sh /tmp/install-k3s.sh"
  & ssh.exe -i $KeyPath -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ServerAliveInterval=15 $target $command
  if ($LASTEXITCODE -ne 0) { throw "k3s-install-failed: exit=$LASTEXITCODE" }
}

try {
  Acquire-BootstrapLock
  Write-State -Stage 'preflight' -Message 'Checking administrator, Hyper-V and host capacity'
  Assert-Administrator
  Import-Module Hyper-V
  if (-not (Get-Service vmms -ErrorAction SilentlyContinue)) { throw 'hyperv-service-missing: enable Microsoft Hyper-V first' }
  $totalMemory = [int64](Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory
  if (($totalMemory - [int64]$Config.vm.memoryMaximumBytes) -lt [int64]$Config.vm.hostReservedMemoryBytes) {
    throw "host-memory-reserve-insufficient: total=$totalMemory vmMax=$($Config.vm.memoryMaximumBytes) reserve=$($Config.vm.hostReservedMemoryBytes)"
  }
  $driveName = ([System.IO.Path]::GetPathRoot([string]$Config.host.vmRoot)).TrimEnd('\')
  $drive = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='$driveName'"
  if (-not $drive -or [int64]$drive.FreeSpace -lt [int64]$Config.host.minimumFreeBytesBeforeInstall) {
    throw "host-disk-space-insufficient: drive=$driveName free=$($drive.FreeSpace) required=$($Config.host.minimumFreeBytesBeforeInstall)"
  }

  $source = Select-ImageSource
  $key = Ensure-HostKey
  $imagePath = Join-Path $StateDir ([string]$Config.image.fileName)
  Download-FileWithMetrics -Url $source.url -Destination $imagePath -ExpectedBytes $source.bytes -ExpectedHash ([string]$Config.image.sha256) -HashAlgorithm SHA256 -Stage 'downloading-image' -Message 'Downloading the Canonical generic Ubuntu image'
  $qemuImg = Ensure-QemuImg

  Write-State -Stage 'preparing-disk' -Message 'Converting the Canonical generic cloud image to VHDX'
  $vmRoot = [string]$Config.host.vmRoot
  New-Item -ItemType Directory -Force -Path $vmRoot | Out-Null
  $diskPath = Join-Path $vmRoot "$($Config.vm.name).vhdx"
  $diskMarkerPath = "$diskPath.source-sha256"
  $installedImageSha = if (Test-Path -LiteralPath $diskMarkerPath) { (Get-Content -Raw -LiteralPath $diskMarkerPath).Trim() } else { '' }
  if ((Test-Path -LiteralPath $diskPath) -and $installedImageSha -ne [string]$Config.image.sha256) {
    Write-State -Stage 'replacing-base-image' -Message 'Replacing the Azure-specific disk with the Canonical generic cloud image'
    $staleVm = Get-VM -Name ([string]$Config.vm.name) -ErrorAction SilentlyContinue
    if ($staleVm) {
      if ($staleVm.State -ne 'Off') { Stop-VM -Name $staleVm.Name -TurnOff -Force }
      Remove-VM -Name $staleVm.Name -Force
    }
    Get-ChildItem -Path $vmRoot -Recurse -Filter '*.avhdx' -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue
    Remove-Item -Force -LiteralPath $diskPath -ErrorAction SilentlyContinue
    Remove-Item -Force -LiteralPath (Join-Path $vmRoot "$($Config.vm.name)-cidata.vhdx") -ErrorAction SilentlyContinue
    Remove-Item -Force -LiteralPath $diskMarkerPath -ErrorAction SilentlyContinue
  }
  $diskReady = (Test-Path -LiteralPath $diskPath) -and ($installedImageSha -eq [string]$Config.image.sha256)
  if (-not $diskReady) {
    if (Test-Path -LiteralPath $diskPath) { Remove-Item -Force -LiteralPath $diskPath }
    $conversionArguments = @('convert', '-f', [string]$Config.image.format, '-O', [string]$Config.image.conversion.outputFormat, '-o', 'subformat=dynamic', '-S', '0', $imagePath, $diskPath)
    $previousErrorPreference = $ErrorActionPreference
    $ErrorActionPreference = 'Continue'
    $conversionOutput = (& $qemuImg @conversionArguments 2>&1 | Out-String).Trim()
    $conversionExitCode = $LASTEXITCODE
    $ErrorActionPreference = $previousErrorPreference
    if ($conversionExitCode -ne 0) { throw "image-conversion-failed: exit=$conversionExitCode output=$conversionOutput" }
    Write-State -Stage 'desparsifying-disk' -Message 'Removing the NTFS sparse-file flag for Hyper-V compatibility'
    $previousErrorPreference = $ErrorActionPreference
    $ErrorActionPreference = 'Continue'
    $sparseOutput = (& fsutil.exe sparse setflag $diskPath 0 2>&1 | Out-String).Trim()
    $sparseExitCode = $LASTEXITCODE
    $ErrorActionPreference = $previousErrorPreference
    if ($sparseExitCode -ne 0) { throw "disk-desparse-failed: exit=$sparseExitCode output=$sparseOutput" }
    Write-Log "disk_sparse_flag_cleared=true output=$sparseOutput"
    Resize-VHD -Path $diskPath -SizeBytes ([int64]$Config.vm.diskBytes)
    [IO.File]::WriteAllText($diskMarkerPath, "$($Config.image.sha256)`n", [Text.UTF8Encoding]::new($false))
  }

  Ensure-Network
  $seedPath = Join-Path $vmRoot "$($Config.vm.name)-cidata.vhdx"
  $needsRecreate = Test-VirtualMachineNeedsRecreate -DiskPath $diskPath -SeedPath $seedPath
  if (-not $needsRecreate) {
    Write-State -Stage 'resuming-running-vm' -Message 'The VM is already running; preserving guest state and resuming readiness checks'
  } else {
    Write-State -Stage 'reconciling-vm-shape' -Message 'Existing VM shape differs from YAML; recreating the VM definition'
    Repair-BrokenVmDiskChain
    New-SeedDisk -SeedPath $seedPath -AuthorizedKey $key.public
    Ensure-VirtualMachine -DiskPath $diskPath -SeedPath $seedPath
  }
  Wait-For-Ssh -KeyPath $key.private
  if ([string]$Config.k3s.management.mode -eq 'external-cluster') {
    Write-State -Stage 'staging-k3s-agent' -Message 'Pinned k3s artifacts are staged; the YAML-controlled cluster manager owns the agent role'
  } elseif ([string]$Config.k3s.management.mode -eq 'standalone') {
    Write-State -Stage 'staging-k3s-standalone' -Message 'Pinned k3s artifacts are staged; the YAML-controlled cluster manager owns the standalone role'
  } else {
    throw "unsupported-k3s-management-mode: $($Config.k3s.management.mode)"
  }
  Install-Provider -KeyPath $key.private

  Write-State -Stage 'completed' -State 'completed' -Message 'Ubuntu VM, Docker, YAML-managed k3s and provider-gateway are ready' -Extra @{
    address = [string]$Config.network.address
    providerId = [string]$Config.provider.id
    processorCount = [int]$Config.vm.processorCount
    memoryStartupBytes = [int64]$Config.vm.memoryStartupBytes
    memoryMaximumBytes = [int64]$Config.vm.memoryMaximumBytes
    diskBytes = [int64]$Config.vm.diskBytes
  }
} catch {
  $errorCode = if ($_.Exception.Message -match '^([^:]+):') { $Matches[1] } else { 'hyperv-vm-bootstrap-failed' }
  $extra = @{ errorCode = $errorCode; recoveryHint = 'Read install.log, correct the reported stage, then rerun the same YAML-controlled apply command.' }
  try {
    $failedVm = Get-VM -Name ([string]$Config.vm.name) -ErrorAction SilentlyContinue
    if ($failedVm) {
      $extra.vmState = [string]$failedVm.State
      $extra.vmStatus = [string]$failedVm.Status
      $extra.memoryAssignedBytes = [int64]$failedVm.MemoryAssigned
      $extra.memoryDemandBytes = [int64]$failedVm.MemoryDemand
      $extra.vmOperationalStatus = @($failedVm.OperationalStatus | ForEach-Object { [string]$_ })
      $failedMemory = Get-VMMemory -VMName ([string]$Config.vm.name)
      $extra.vmMemory = [ordered]@{
        dynamic = [bool]$failedMemory.DynamicMemoryEnabled
        minimumBytes = [int64]$failedMemory.Minimum
        startupBytes = [int64]$failedMemory.Startup
        maximumBytes = [int64]$failedMemory.Maximum
      }
      $extra.vmDisks = @(Get-VMHardDiskDrive -VMName ([string]$Config.vm.name) | ForEach-Object { [ordered]@{ path = $_.Path; controllerType = [string]$_.ControllerType; controllerNumber = $_.ControllerNumber; controllerLocation = $_.ControllerLocation } })
    }
  } catch {}
  try {
    $since = [DateTime]::Now.AddMinutes(-10)
    $events = @()
    foreach ($eventLog in @('Microsoft-Windows-Hyper-V-Worker-Admin', 'Microsoft-Windows-Hyper-V-VMMS-Admin')) {
      $events += @(Get-WinEvent -FilterHashtable @{ LogName = $eventLog; StartTime = $since; Level = 2 } -ErrorAction SilentlyContinue | Select-Object -First 4 | ForEach-Object { $message = [string]$_.Message; [ordered]@{ log = $eventLog; id = $_.Id; level = $_.LevelDisplayName; message = $message.Substring(0, [Math]::Min(1000, $message.Length)) } })
    }
    $extra.hyperVEvents = $events
  } catch {}
  Write-State -Stage $CurrentStage -State 'failed' -Message $_.Exception.Message -Extra $extra
  Write-Log ($_ | Out-String)
  exit 1
} finally {
  if ($script:LockStream) {
    $script:LockStream.Dispose()
    Remove-Item -Force -LiteralPath $LockPath -ErrorAction SilentlyContinue
  }
}
