Public/Show-WindowsDeviceLink.ps1

function Show-WindowsDeviceLink {
    <#
    .SYNOPSIS
    Opens the WindowsDeviceLink operator dashboard.
 
    .DESCRIPTION
    Opens a compact Windows 11 Settings-inspired WinForms dashboard on Windows 11 and supported Windows PE environments.
 
    The GUI focuses on inspecting Device Association state, onboarding, and offboarding.
    Lifecycle actions delegate to existing WindowsDeviceLink public cmdlets.
 
    Interactive authentication is the default on full Windows. Windows PE defaults to DeviceCode because Interactive browser authentication is unavailable there. Use -Method and the corresponding authentication parameters to select another supported authentication flow.
 
    Use -Tenants to provide friendly tenant names for the tenant selector:
 
    @{'Management'='11111111-1111-1111-1111-111111111111'; 'Customer A'='22222222-2222-2222-2222-222222222222'}
 
    Use -TenantsUri to load the same friendly-name-to-tenant-ID mapping from a trusted HTTPS JSON endpoint.
    Use -TenantsPath to load the same JSON format from a local file.
    Precedence is: TenantsUri, then TenantsPath, then explicit -Tenants values.
 
    .EXAMPLE
    Show-WindowsDeviceLink
 
    .EXAMPLE
    Show-WindowsDeviceLink -Method DeviceCode
 
    .EXAMPLE
    Show-WindowsDeviceLink -Tenants @{
        'Management' = '11111111-1111-1111-1111-111111111111'
        'Customer A' = '22222222-2222-2222-2222-222222222222'
    }
 
    .EXAMPLE
    Show-WindowsDeviceLink -TenantsUri 'https://config.example.com/windowsdevicelink/tenants.json'
 
    .EXAMPLE
    Show-WindowsDeviceLink -TenantsPath 'E:\Config\tenants.json'
    #>

    [CmdletBinding()]
    param(
        [ValidateSet(
            'DeviceCode','Interactive','ClientSecret','AccessToken','Certificate',
            'CertificateThumbprint','CertificateSubjectName','EnvironmentVariable','ManagedIdentity'
        )]
        [string]$Method,

        [ValidateNotNullOrEmpty()]
        [string]$TenantId,

        [hashtable]$Tenants,

        [ValidateNotNull()]
        [uri]$TenantsUri,

        [ValidateNotNullOrEmpty()]
        [string]$TenantsPath,

        [ValidateNotNullOrEmpty()]
        [string]$ClientId,

        [securestring]$AccessToken,

        [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,

        [ValidateNotNullOrEmpty()]
        [string]$CertificateThumbprint,

        [ValidateNotNullOrEmpty()]
        [string]$CertificateSubjectName,

        [bool]$SendCertificateChain = $false,

        [securestring]$ClientSecret,

        [ValidateNotNullOrEmpty()]
        [string]$Environment = 'Global',

        [ValidateRange(1,600)]
        [double]$ClientTimeout = 100,

        [ValidateNotNullOrEmpty()]
        [string]$WindowsManagementServicePath
    )

    $outerBoundParameters = @{}
    foreach ($key in $PSBoundParameters.Keys) {
        $outerBoundParameters[$key] = $PSBoundParameters[$key]
    }

    $isWinPE = Test-Path -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\MiniNT'

    if (-not $outerBoundParameters.ContainsKey('Method')) {
        $Method = if ($isWinPE) { 'DeviceCode' } else { 'Interactive' }
    }

    if ($isWinPE -and $Method -eq 'Interactive') {
        throw 'Interactive authentication is not available in Windows PE. Use -Method DeviceCode or a supported app-only authentication method.'
    }

    Add-Type -AssemblyName System.Windows.Forms -ErrorAction Stop
    Add-Type -AssemblyName System.Drawing -ErrorAction Stop
    [System.Windows.Forms.Application]::EnableVisualStyles()

    $loadedModule = Get-Module WindowsDeviceLink | Select-Object -First 1
    $baseVersion = if ($loadedModule) { $loadedModule.Version.ToString() } else { 'unknown' }
    $prerelease = $null
    if ($loadedModule -and $loadedModule.PrivateData -and $loadedModule.PrivateData.PSData) {
        $prerelease = [string]$loadedModule.PrivateData.PSData.Prerelease
    }
    $displayVersion = if ([string]::IsNullOrWhiteSpace($prerelease)) { $baseVersion } else { "$baseVersion-$prerelease" }

    $workingArea = [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea
    $targetWidth = [Math]::Max(820,[Math]::Min(1080,$workingArea.Width - 32))
    $targetHeight = [Math]::Max(620,[Math]::Min(760,$workingArea.Height - 40))

    $form = New-Object System.Windows.Forms.Form
    $form.Text = "WindowsDeviceLink $displayVersion (Preview)"
    $form.StartPosition = 'CenterScreen'
    $form.Size = [System.Drawing.Size]::new($targetWidth,$targetHeight)
    $form.MinimumSize = [System.Drawing.Size]::new(820,620)
    $form.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::Dpi
    $form.BackColor = [System.Drawing.Color]::FromArgb(243,243,243)

    $content = New-Object System.Windows.Forms.Panel
    $content.Dock = [System.Windows.Forms.DockStyle]::Fill
    $content.AutoScroll = $false
    $content.BackColor = $form.BackColor
    $form.Controls.Add($content)

    $toolTip = New-Object System.Windows.Forms.ToolTip
    $toolTip.AutoPopDelay = 10000
    $toolTip.InitialDelay = 400
    $toolTip.ReshowDelay = 200
    $toolTip.ShowAlways = $true


    function New-Card {
        param(
            [string]$Title,
            [int]$X,
            [int]$Y,
            [int]$Width,
            [int]$Height
        )

        $panel = New-Object System.Windows.Forms.Panel
        $panel.Location = [System.Drawing.Point]::new($X,$Y)
        $panel.Size = [System.Drawing.Size]::new($Width,$Height)
        $panel.BackColor = [System.Drawing.Color]::White
        $panel.BorderStyle = [System.Windows.Forms.BorderStyle]::None
        $content.Controls.Add($panel)

        if ($Title) {
            $label = New-Object System.Windows.Forms.Label
            $label.Text = $Title
            $label.Font = New-Object System.Drawing.Font('Segoe UI',10,[System.Drawing.FontStyle]::Bold)
            $label.Location = [System.Drawing.Point]::new(14,9)
            $label.AutoSize = $true
            $panel.Controls.Add($label)
        }

        $panel
    }

    function New-ValuePair {
        param(
            [System.Windows.Forms.Control]$Parent,
            [string]$Caption,
            [int]$Y,
            [int]$CaptionWidth = 112
        )

        $captionLabel = New-Object System.Windows.Forms.Label
        $captionLabel.Text = $Caption
        $captionLabel.Font = New-Object System.Drawing.Font('Segoe UI',8.5)
        $captionLabel.ForeColor = [System.Drawing.Color]::FromArgb(102,102,102)
        $captionLabel.Location = [System.Drawing.Point]::new(14,$Y)
        $captionLabel.Size = [System.Drawing.Size]::new($CaptionWidth,20)
        $Parent.Controls.Add($captionLabel)

        $valueLabel = New-Object System.Windows.Forms.Label
        $valueLabel.Text = '-'
        $valueLabel.Font = New-Object System.Drawing.Font('Segoe UI',9,[System.Drawing.FontStyle]::Bold)
        $valueLabel.Location = [System.Drawing.Point]::new(($CaptionWidth + 20),($Y - 1))
        $valueLabel.Size = [System.Drawing.Size]::new(350,22)
        $valueLabel.AutoEllipsis = $true
        $Parent.Controls.Add($valueLabel)

        $valueLabel
    }

    function New-ActionRow {
        param(
            [System.Windows.Forms.Control]$Parent,
            [string]$Title,
            [string]$Description,
            [int]$Y,
            [string[]]$Buttons
        )

        $row = New-Object System.Windows.Forms.Panel
        $row.Location = [System.Drawing.Point]::new(0,$Y)
        $row.Size = [System.Drawing.Size]::new(900,46)
        $row.BackColor = [System.Drawing.Color]::White
        $Parent.Controls.Add($row)

        $titleLabel = New-Object System.Windows.Forms.Label
        $titleLabel.Text = $Title
        $titleLabel.Font = New-Object System.Drawing.Font('Segoe UI',8.3,[System.Drawing.FontStyle]::Bold)
        $titleLabel.Location = [System.Drawing.Point]::new(14,5)
        $titleLabel.AutoSize = $true
        $row.Controls.Add($titleLabel)

        $descriptionLabel = New-Object System.Windows.Forms.Label
        $descriptionLabel.Text = $Description
        $descriptionLabel.Font = New-Object System.Drawing.Font('Segoe UI',8.2)
        $descriptionLabel.ForeColor = [System.Drawing.Color]::FromArgb(108,108,108)
        $descriptionLabel.Location = [System.Drawing.Point]::new(14,23)
        $descriptionLabel.AutoSize = $true
        $row.Controls.Add($descriptionLabel)

        $buttonList = New-Object System.Collections.Generic.List[object]
        $count = $Buttons.Count
        $buttonWidth = if ($count -eq 1) { 118 } elseif ($count -eq 2) { 112 } else { 88 }
        $gap = 7
        $right = 884

        for ($i = $count - 1; $i -ge 0; $i--) {
            $button = New-Object System.Windows.Forms.Button
            $button.Text = $Buttons[$i]
            $button.Font = New-Object System.Drawing.Font('Segoe UI',8.6)
            $button.Size = [System.Drawing.Size]::new($buttonWidth,28)
            $right -= $buttonWidth
            $button.Location = [System.Drawing.Point]::new($right,9)
            $button.FlatStyle = [System.Windows.Forms.FlatStyle]::System
            $button.UseVisualStyleBackColor = $true
            $row.Controls.Add($button)
            $buttonList.Insert(0,$button)
            $right -= $gap
        }

        $separator = New-Object System.Windows.Forms.Panel
        $separator.BackColor = [System.Drawing.Color]::FromArgb(232,232,232)
        $separator.Location = [System.Drawing.Point]::new(14,45)
        $separator.Size = [System.Drawing.Size]::new(860,1)
        $row.Controls.Add($separator)

        [pscustomobject]@{
            Panel = $row
            Buttons = $buttonList.ToArray()
        }
    }

    $effectiveTenants = @{}

    if ($outerBoundParameters.ContainsKey('TenantsUri')) {
        if (-not $TenantsUri.IsAbsoluteUri -or $TenantsUri.Scheme -ne 'https') {
            throw '-TenantsUri must be an absolute HTTPS URI.'
        }

        try {
            $remoteTenantObject = Invoke-RestMethod -Uri $TenantsUri.AbsoluteUri -Method Get -TimeoutSec 15 -ErrorAction Stop
        }
        catch {
            throw "Unable to load tenant JSON from '$($TenantsUri.AbsoluteUri)': $($_.Exception.Message)"
        }

        if ($null -eq $remoteTenantObject -or $remoteTenantObject -is [System.Array]) {
            throw 'The tenant JSON must be a JSON object that maps friendly tenant names to tenant GUIDs.'
        }

        foreach ($property in @($remoteTenantObject.PSObject.Properties)) {
            $name = [string]$property.Name
            $id = [string]$property.Value
            $parsedTenantId = [guid]::Empty

            if ([string]::IsNullOrWhiteSpace($name) -or
                [string]::IsNullOrWhiteSpace($id) -or
                -not [guid]::TryParse($id.Trim(),[ref]$parsedTenantId)) {
                throw "Invalid tenant entry '$name' in '$($TenantsUri.AbsoluteUri)'. Each value must be a tenant GUID."
            }

            $effectiveTenants[$name.Trim()] = $parsedTenantId.ToString()
        }

        if ($effectiveTenants.Count -eq 0) {
            throw "The tenant JSON at '$($TenantsUri.AbsoluteUri)' did not contain any tenant entries."
        }
    }

    if ($outerBoundParameters.ContainsKey('TenantsPath')) {
        if (-not (Test-Path -LiteralPath $TenantsPath -PathType Leaf)) {
            throw "Tenant JSON file '$TenantsPath' was not found."
        }

        try {
            $localTenantObject = Get-Content -LiteralPath $TenantsPath -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
        }
        catch {
            throw "Unable to load tenant JSON from '$TenantsPath': $($_.Exception.Message)"
        }

        if ($null -eq $localTenantObject -or $localTenantObject -is [System.Array]) {
            throw 'The tenant JSON must be a JSON object that maps friendly tenant names to tenant GUIDs.'
        }

        foreach ($property in @($localTenantObject.PSObject.Properties)) {
            $name = [string]$property.Name
            $id = [string]$property.Value
            $parsedTenantId = [guid]::Empty

            if ([string]::IsNullOrWhiteSpace($name) -or
                [string]::IsNullOrWhiteSpace($id) -or
                -not [guid]::TryParse($id.Trim(),[ref]$parsedTenantId)) {
                throw "Invalid tenant entry '$name' in '$TenantsPath'. Each value must be a tenant GUID."
            }

            $effectiveTenants[$name.Trim()] = $parsedTenantId.ToString()
        }

        if ($localTenantObject.PSObject.Properties.Count -eq 0) {
            throw "The tenant JSON file '$TenantsPath' did not contain any tenant entries."
        }
    }

    if ($Tenants) {
        foreach ($name in @($Tenants.Keys)) {
            $id = [string]$Tenants[$name]
            $parsedTenantId = [guid]::Empty

            if ([string]::IsNullOrWhiteSpace([string]$name) -or
                [string]::IsNullOrWhiteSpace($id) -or
                -not [guid]::TryParse($id.Trim(),[ref]$parsedTenantId)) {
                throw "Invalid -Tenants entry '$name'. Each value must be a tenant GUID."
            }

            $effectiveTenants[[string]$name] = $parsedTenantId.ToString()
        }
    }

    $tenantChoiceLookup = @{}
    $tenantChoices = New-Object System.Collections.Generic.List[string]
    $autoLabel = if ($outerBoundParameters.ContainsKey('TenantId')) { 'Default tenant parameter' } else { 'Automatic / detected tenant' }
    $tenantChoiceLookup[$autoLabel] = $null
    $tenantChoices.Add($autoLabel)

    if ($effectiveTenants.Count -gt 0) {
        foreach ($name in @($effectiveTenants.Keys | Sort-Object)) {
            $id = [string]$effectiveTenants[$name]
            $label = [string]$name
            if ($tenantChoiceLookup.ContainsKey($label)) {
                $label = "$label ($id)"
            }
            $tenantChoiceLookup[$label] = $id
            $tenantChoices.Add($label)
        }
    }

    function Get-SelectedTenantId {
        if ($tenantSelector.SelectedItem) {
            $selected = [string]$tenantSelector.SelectedItem
            if ($tenantChoiceLookup.ContainsKey($selected) -and $tenantChoiceLookup[$selected]) {
                return [string]$tenantChoiceLookup[$selected]
            }
        }

        if ($outerBoundParameters.ContainsKey('TenantId')) {
            return [string]$TenantId
        }

        $null
    }

    function Get-GuiAuthParameters {
        $parameters = @{
            Method = $Method
            Environment = $Environment
            ClientTimeout = $ClientTimeout
        }

        foreach ($name in @(
            'ClientId','AccessToken','Certificate','CertificateThumbprint',
            'CertificateSubjectName','SendCertificateChain','ClientSecret'
        )) {
            if ($outerBoundParameters.ContainsKey($name)) {
                $parameters[$name] = $outerBoundParameters[$name]
            }
        }

        $selectedTenant = Get-SelectedTenantId
        if ($selectedTenant) {
            $parameters.TenantId = $selectedTenant
        }

        $parameters
    }


    function Get-GuiRuntimeParameters {
        $parameters = @{}
        if ($outerBoundParameters.ContainsKey('WindowsManagementServicePath')) {
            $parameters.WindowsManagementServicePath = $WindowsManagementServicePath
        }
        $parameters
    }

    $ui = @{}
    $script:WdlGuiBusy = $false
    $script:WdlGuiLocalAssociation = $null
    $script:WdlGuiCloudStatus = $null
    $script:WdlGuiSupport = $null

    $deviceCard = New-Card -Title 'Device' -X 14 -Y 12 -Width 508 -Height 126
    $associationCard = New-Card -Title 'Association' -X 536 -Y 12 -Width 508 -Height 126

    $ui.DeviceName = New-ValuePair -Parent $deviceCard -Caption 'Device' -Y 34
    $ui.Serial     = New-ValuePair -Parent $deviceCard -Caption 'Serial number' -Y 56
    $ui.Environment = New-ValuePair -Parent $deviceCard -Caption 'Environment' -Y 78
    $ui.Auth       = New-ValuePair -Parent $deviceCard -Caption 'Authentication' -Y 100

    $ui.Firmware   = New-ValuePair -Parent $associationCard -Caption 'Firmware' -Y 34
    $ui.LocalState = New-ValuePair -Parent $associationCard -Caption 'Local state' -Y 56
    $ui.TenantId   = New-ValuePair -Parent $associationCard -Caption 'Tenant ID' -Y 78
    $ui.Source     = New-ValuePair -Parent $associationCard -Caption 'Source' -Y 100

    $cloudCard = New-Card -Title 'Cloud association' -X 14 -Y 150 -Width 1030 -Height 84
    $ui.CloudState  = New-ValuePair -Parent $cloudCard -Caption 'State' -Y 34 -CaptionWidth 80
    $ui.CloudState.Text = 'Not checked'
    $ui.CloudTenant = New-ValuePair -Parent $cloudCard -Caption 'Tenant ID' -Y 56 -CaptionWidth 80
    $ui.CloudTenant.Text = 'Unavailable'

    $cloudIdCaption = New-Object System.Windows.Forms.Label
    $cloudIdCaption.Text = 'Association ID'
    $cloudIdCaption.Font = New-Object System.Drawing.Font('Segoe UI',8.5)
    $cloudIdCaption.ForeColor = [System.Drawing.Color]::FromArgb(102,102,102)
    $cloudIdCaption.Location = [System.Drawing.Point]::new(500,34)
    $cloudIdCaption.Size = [System.Drawing.Size]::new(96,20)
    $cloudCard.Controls.Add($cloudIdCaption)

    $ui.CloudId = New-Object System.Windows.Forms.Label
    $ui.CloudId.Text = 'Unavailable'
    $ui.CloudId.Font = New-Object System.Drawing.Font('Segoe UI',8.3,[System.Drawing.FontStyle]::Bold)
    $ui.CloudId.Location = [System.Drawing.Point]::new(600,33)
    $ui.CloudId.Size = [System.Drawing.Size]::new(380,22)
    $ui.CloudId.AutoEllipsis = $true
    $cloudCard.Controls.Add($ui.CloudId)

    $tenantCaption = New-Object System.Windows.Forms.Label
    $tenantCaption.Text = 'Tenant'
    $tenantCaption.Font = New-Object System.Drawing.Font('Segoe UI',8.5)
    $tenantCaption.ForeColor = [System.Drawing.Color]::FromArgb(102,102,102)
    $tenantCaption.Location = [System.Drawing.Point]::new(500,56)
    $tenantCaption.Size = [System.Drawing.Size]::new(96,18)
    $cloudCard.Controls.Add($tenantCaption)

    $tenantSelector = New-Object System.Windows.Forms.ComboBox
    $tenantSelector.DropDownStyle = [System.Windows.Forms.ComboBoxStyle]::DropDownList
    $tenantSelector.Font = New-Object System.Drawing.Font('Segoe UI',8.5)
    $tenantSelector.Location = [System.Drawing.Point]::new(600,53)
    $tenantSelector.Size = [System.Drawing.Size]::new(380,24)
    foreach ($choice in $tenantChoices) {
        [void]$tenantSelector.Items.Add($choice)
    }
    $tenantSelector.SelectedIndex = 0
    $cloudCard.Controls.Add($tenantSelector)

    $actionsTitle = New-Object System.Windows.Forms.Label
    $actionsTitle.Text = 'Actions'
    $actionsTitle.Font = New-Object System.Drawing.Font('Segoe UI',11.5,[System.Drawing.FontStyle]::Bold)
    $actionsTitle.Location = [System.Drawing.Point]::new(16,246)
    $actionsTitle.AutoSize = $true
    $content.Controls.Add($actionsTitle)

    $actionsPanel = New-Card -Title '' -X 14 -Y 272 -Width 1030 -Height 232
    $rowRefresh = New-ActionRow -Parent $actionsPanel -Title 'Refresh' -Description 'Refresh local DeviceLink and firmware information.' -Y 0 -Buttons @('Refresh')
    $rowOnline  = New-ActionRow -Parent $actionsPanel -Title 'Check online' -Description 'Query the tenant-side Device Association using the selected tenant context.' -Y 46 -Buttons @('Check online')
    $rowExport  = New-ActionRow -Parent $actionsPanel -Title 'Export DeviceLink CSV' -Description 'Export the Microsoft-generated .devicelink.csv.' -Y 92 -Buttons @('Export CSV')
    $rowOnboard = New-ActionRow -Parent $actionsPanel -Title 'Onboarding' -Description 'Create only the pre-association, or perform the full association flow.' -Y 138 -Buttons @('Pre-associate','Full associate')
    $rowOffboard = New-ActionRow -Parent $actionsPanel -Title 'Offboarding' -Description 'Remove cloud state, local state, or both.' -Y 184 -Buttons @('Cloud','Local','Full')

    $offboardSeparator = @(
        $rowOffboard.Panel.Controls |
            Where-Object { $_ -is [System.Windows.Forms.Panel] -and $_.Height -eq 1 }
    ) | Select-Object -First 1
    if ($offboardSeparator) {
        $rowOffboard.Panel.Controls.Remove($offboardSeparator)
        $offboardSeparator.Dispose()
    }

    function Get-ActionButtonByText {
        param(
            [Parameter(Mandatory)][System.Windows.Forms.Control]$Row,
            [Parameter(Mandatory)][string]$Text
        )

        $button = @(
            $Row.Controls |
                Where-Object { $_ -is [System.Windows.Forms.Button] -and $_.Text -eq $Text }
        ) | Select-Object -First 1

        if (-not $button) {
            throw "GUI action button '$Text' could not be resolved."
        }

        $button
    }

    $btnRefresh = Get-ActionButtonByText -Row $rowRefresh.Panel -Text 'Refresh'
    $btnOnline = Get-ActionButtonByText -Row $rowOnline.Panel -Text 'Check online'
    $btnExport = Get-ActionButtonByText -Row $rowExport.Panel -Text 'Export CSV'
    $btnPreassociate = Get-ActionButtonByText -Row $rowOnboard.Panel -Text 'Pre-associate'
    $btnFullAssociate = Get-ActionButtonByText -Row $rowOnboard.Panel -Text 'Full associate'
    $btnCloudOffboard = Get-ActionButtonByText -Row $rowOffboard.Panel -Text 'Cloud'
    $btnLocalOffboard = Get-ActionButtonByText -Row $rowOffboard.Panel -Text 'Local'
    $btnFullOffboard = Get-ActionButtonByText -Row $rowOffboard.Panel -Text 'Full'

    $allActionButtons = @(
        $btnRefresh,
        $btnOnline,
        $btnExport,
        $btnPreassociate,
        $btnFullAssociate,
        $btnCloudOffboard,
        $btnLocalOffboard,
        $btnFullOffboard
    )

    $activityTitle = New-Object System.Windows.Forms.Label
    $activityTitle.Text = 'Activity'
    $activityTitle.Font = New-Object System.Drawing.Font('Segoe UI',11,[System.Drawing.FontStyle]::Bold)
    $activityTitle.Location = [System.Drawing.Point]::new(16,518)
    $activityTitle.AutoSize = $true
    $content.Controls.Add($activityTitle)

    $btnClearActivity = New-Object System.Windows.Forms.Button
    $btnClearActivity.Text = 'Clear'
    $btnClearActivity.Font = New-Object System.Drawing.Font('Segoe UI',8.3)
    $btnClearActivity.Size = [System.Drawing.Size]::new(64,24)
    $btnClearActivity.FlatStyle = [System.Windows.Forms.FlatStyle]::System
    $content.Controls.Add($btnClearActivity)

    $activityCard = New-Card -Title '' -X 14 -Y 544 -Width 1030 -Height 118

    $consoleBox = New-Object System.Windows.Forms.TextBox
    $consoleBox.Location = [System.Drawing.Point]::new(12,10)
    $consoleBox.Size = [System.Drawing.Size]::new(1006,96)
    $consoleBox.Multiline = $true
    $consoleBox.ReadOnly = $true
    $consoleBox.ScrollBars = [System.Windows.Forms.ScrollBars]::Vertical
    $consoleBox.WordWrap = $false
    $consoleBox.Font = New-Object System.Drawing.Font('Consolas',8)
    $consoleBox.BackColor = [System.Drawing.Color]::FromArgb(250,250,250)
    $consoleBox.BorderStyle = [System.Windows.Forms.BorderStyle]::None
    $activityCard.Controls.Add($consoleBox)

    $statusStrip = New-Object System.Windows.Forms.StatusStrip
    $statusStrip.Dock = [System.Windows.Forms.DockStyle]::Bottom

    $statusLabel = New-Object System.Windows.Forms.ToolStripStatusLabel
    $statusLabel.Text = 'Ready'
    $statusLabel.Spring = $true
    $statusLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft

    $statusProgress = New-Object System.Windows.Forms.ToolStripProgressBar
    $statusProgress.Style = [System.Windows.Forms.ProgressBarStyle]::Marquee
    $statusProgress.MarqueeAnimationSpeed = 30
    $statusProgress.Size = [System.Drawing.Size]::new(140,16)
    $statusProgress.Visible = $false

    [void]$statusStrip.Items.Add($statusLabel)
    [void]$statusStrip.Items.Add($statusProgress)
    $form.Controls.Add($statusStrip)

    function Set-GuiStatus {
        param([string]$Text)
        $statusLabel.Text = $Text
        [System.Windows.Forms.Application]::DoEvents()
    }

    function Write-GuiConsole {
        param(
            [AllowNull()][AllowEmptyString()][string]$Message,
            [switch]$Command,
            [switch]$ErrorMessage
        )

        if ([string]::IsNullOrWhiteSpace($Message)) { return }

        $timestamp = (Get-Date).ToString('HH:mm:ss')
        $prefix = if ($Command) { '>' } elseif ($ErrorMessage) { '!' } else { '-' }
        $line = "[$timestamp] $prefix $Message"

        $consoleBox.AppendText($line + [Environment]::NewLine)
        $consoleBox.SelectionStart = $consoleBox.TextLength
        $consoleBox.ScrollToCaret()
        [System.Windows.Forms.Application]::DoEvents()
        Write-Host $line
    }

    function Write-GuiObject {
        param($InputObject)

        if ($null -eq $InputObject) { return }

        foreach ($item in @($InputObject)) {
            if ($null -eq $item) { continue }

            $properties = @($item.PSObject.Properties)
            if ($properties.Count -eq 0) {
                $text = [string]$item
                if (-not [string]::IsNullOrWhiteSpace($text)) {
                    Write-GuiConsole -Message $text
                }
                continue
            }

            $rows = New-Object System.Collections.Generic.List[object]
            $compactHiddenProperties = @(
                'RegistrationResult',
                'BeforeStatus',
                'AfterStatus',
                'FullAssociationDetails'
            )

            foreach ($property in $properties) {
                if ([string]$property.Name -in $compactHiddenProperties) { continue }

                $value = $property.Value
                if ($null -eq $value) { continue }

                if ($value -is [string] -and [string]::IsNullOrWhiteSpace($value)) { continue }

                if ($value -is [datetime]) {
                    $displayValue = $value.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
                }
                elseif ($value -is [datetimeoffset]) {
                    $displayValue = $value.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
                }
                elseif ($value -is [System.Collections.IEnumerable] -and -not ($value -is [string])) {
                    $items = @($value)
                    if ($items.Count -eq 0) { continue }
                    $displayValue = ($items | ForEach-Object { [string]$_ }) -join ', '
                }
                else {
                    $displayValue = [string]$value
                }

                if ([string]::IsNullOrWhiteSpace($displayValue)) { continue }

                $rows.Add([pscustomobject]@{
                    Name = [string]$property.Name
                    Value = $displayValue
                })
            }

            if ($rows.Count -eq 0) { continue }

            $nameWidth = [Math]::Min(
                34,
                [Math]::Max(
                    8,
                    (@($rows | ForEach-Object { $_.Name.Length }) | Measure-Object -Maximum).Maximum
                )
            )

            foreach ($row in $rows) {
                $name = $row.Name
                if ($name.Length -gt $nameWidth) {
                    $name = $name.Substring(0,$nameWidth)
                }
                Write-GuiConsole -Message (("{0,-$nameWidth} : {1}" -f $name,$row.Value))
            }
        }
    }

    function Invoke-GuiInformationCommand {
        param(
            [Parameter(Mandatory)]
            [scriptblock]$ScriptBlock
        )

        $resultObjects = New-Object System.Collections.Generic.List[object]

        & $ScriptBlock 6>&1 | ForEach-Object {
            if ($_ -is [System.Management.Automation.InformationRecord]) {
                $message = [string]$_.MessageData
                if (-not [string]::IsNullOrWhiteSpace($message)) {
                    Write-GuiConsole -Message $message
                }
            }
            else {
                $resultObjects.Add($_)
            }

            [System.Windows.Forms.Application]::DoEvents()
        }

        return @($resultObjects.ToArray())
    }

    function Show-GuiError {
        param([string]$Message)

        Write-GuiConsole -Message $Message -ErrorMessage
        [void][System.Windows.Forms.MessageBox]::Show(
            $form,$Message,'WindowsDeviceLink',
            [System.Windows.Forms.MessageBoxButtons]::OK,
            [System.Windows.Forms.MessageBoxIcon]::Error
        )
    }

    function Confirm-GuiAction {
        param(
            [string]$Title,
            [string]$Message
        )

        $result = [System.Windows.Forms.MessageBox]::Show(
            $form,
            $Message,
            $Title,
            [System.Windows.Forms.MessageBoxButtons]::YesNo,
            [System.Windows.Forms.MessageBoxIcon]::Warning
        )

        return ($result -eq [System.Windows.Forms.DialogResult]::Yes)
    }

    function Set-GuiCapabilities {
        $support = $script:WdlGuiSupport
        $local = $script:WdlGuiLocalAssociation
        $cloud = $script:WdlGuiCloudStatus

        $runtimeReady = $support -and [bool]$support.Supported
        $canFullAssociation = $runtimeReady -and [string]$support.Environment -ne 'WindowsPE'

        $cloudState = if ($cloud) { ([string]$cloud.AssociationState).Trim().ToLowerInvariant() } else { '' }
        $cloudKnownAbsent = $cloud -and (
            $cloud.AssociationPresent -eq $false -or
            $cloudState -eq 'notassociated'
        )
        $cloudAlreadyPresent = $cloudState -in @('preassociated','associated')
        $localFullyAssociated = $local -and [string]$local.FirmwareState -eq '4/4'
        $alreadyFullyAssociated = $localFullyAssociated -and $cloudState -eq 'associated'

        $btnRefresh.Enabled = $true
        $btnOnline.Enabled = $runtimeReady
        $btnExport.Enabled = $runtimeReady
        $btnPreassociate.Enabled = $runtimeReady -and -not $cloudAlreadyPresent
        $btnFullAssociate.Enabled = $canFullAssociation -and -not $alreadyFullyAssociated
        $btnCloudOffboard.Enabled = -not $cloudKnownAbsent
        $btnLocalOffboard.Enabled = $true
        $btnFullOffboard.Enabled = $runtimeReady

        $toolTip.SetToolTip($btnPreassociate, 'Create the tenant-side Device Association pre-association.')
        $toolTip.SetToolTip($btnCloudOffboard, 'Remove only the tenant-side Device Association record.')
        $toolTip.SetToolTip($btnFullAssociate, 'Ensure pre-association exists and perform full Device Association on this device.')

        if ($cloudAlreadyPresent) {
            $toolTip.SetToolTip($btnPreassociate, 'The cloud association already exists; pre-association is not required.')
        }

        if ($alreadyFullyAssociated) {
            $toolTip.SetToolTip($btnFullAssociate, 'The device is already fully associated.')
        }

        if ($cloudKnownAbsent) {
            $toolTip.SetToolTip($btnCloudOffboard, 'Cloud state is already known to be Not associated; there is nothing to remove.')
        }

        if ($support -and [string]$support.Environment -eq 'WindowsPE') {
            $toolTip.SetToolTip(
                $btnFullAssociate,
                'Full association is not currently supported in Windows PE. Pre-associate the device and let Windows complete Device Association during OOBE.'
            )
            $toolTip.SetToolTip(
                $btnOnline,
                "Windows PE online operations use authentication method '$Method'. Interactive browser authentication is not supported in WinPE."
            )
        }

        if (-not $runtimeReady -and $support) {
            $runtimeReason = if ([string]::IsNullOrWhiteSpace([string]$support.Reason)) {
                'The local DeviceLink runtime is unavailable.'
            }
            else {
                [string]$support.Reason
            }

            foreach ($button in @($btnOnline,$btnExport,$btnPreassociate,$btnFullOffboard)) {
                $toolTip.SetToolTip($button,$runtimeReason)
            }
        }
    }

    function Set-GuiBusy {
        param(
            [Parameter(Mandatory)][bool]$Busy,
            [string]$StatusText
        )

        $script:WdlGuiBusy = $Busy

        if ($Busy) {
            foreach ($button in $allActionButtons) {
                $button.Enabled = $false
            }
            $actionsPanel.Enabled = $false
            $tenantSelector.Enabled = $false
        }
        else {
            $actionsPanel.Enabled = $true
            $tenantSelector.Enabled = $true
            Set-GuiCapabilities
        }

        $btnClearActivity.Enabled = -not $Busy
        $statusProgress.Visible = $Busy
        $form.UseWaitCursor = $Busy

        if (-not [string]::IsNullOrWhiteSpace($StatusText)) {
            Set-GuiStatus $StatusText
        }

        foreach ($button in $allActionButtons) {
            $button.Invalidate()
            $button.Update()
        }

        $actionsPanel.Invalidate($true)
        $actionsPanel.Update()
        $tenantSelector.Invalidate()
        $tenantSelector.Update()
        [System.Windows.Forms.Application]::DoEvents()
    }

    function Refresh-LocalView {
        Set-GuiStatus 'Refreshing local state...'
        Write-GuiConsole -Message 'Refresh local state' -Command

        $bios = Get-CimInstance -ClassName Win32_BIOS -ErrorAction Stop
        $cs = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Stop
        $os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction SilentlyContinue
        $runtimeParameters = Get-GuiRuntimeParameters
        $support = Test-WindowsDeviceLinkSupport @runtimeParameters
        $local = Get-WindowsDeviceLinkLocalAssociation

        $script:WdlGuiSupport = $support
        $script:WdlGuiLocalAssociation = $local

        $ui.DeviceName.Text = "$($cs.Manufacturer) $($cs.Model)".Trim()
        $ui.Serial.Text = [string]$bios.SerialNumber

        if ($support.Environment -eq 'WindowsPE') {
            $environmentText = 'Windows PE'
        }
        elseif ($os -and [string]$os.Caption -match 'Windows 11') {
            $environmentText = 'Windows 11'
        }
        elseif ($os -and -not [string]::IsNullOrWhiteSpace([string]$os.Caption)) {
            $environmentText = ([string]$os.Caption).Replace('Microsoft ','')
        }
        else {
            $environmentText = [string]$support.Environment
        }

        $ui.Environment.Text = $environmentText

        $environmentToolTip = if ($support.Supported) {
            @(
                $environmentText
                "Activation: $($support.ActivationMode)"
                "Runtime: $($support.DllVersion)"
            ) -join [Environment]::NewLine
        }
        else {
            @(
                $environmentText
                "Runtime unavailable: $($support.Reason)"
            ) -join [Environment]::NewLine
        }
        $toolTip.SetToolTip($ui.Environment,$environmentToolTip)

        if (-not $support.Supported) {
            Write-GuiConsole -Message "DeviceLink runtime unavailable: $($support.Reason)"
        }

        $selectedTenant = Get-SelectedTenantId
        $ui.Auth.Text = if ($selectedTenant) { "$Method | $selectedTenant" } else { $Method }

        $ui.Firmware.Text = [string]$local.FirmwareState

        $friendlyLocalState = switch ([string]$local.LocalAssociationState) {
            'CompleteAssociationFirmwareState' { 'Full association' }
            'BaseIdentity' { 'Base identity' }
            'NoFirmwareState' { 'No firmware state' }
            'IncompleteFirmwareState' { 'Incomplete firmware state' }
            default { [string]$local.LocalAssociationState }
        }
        $ui.LocalState.Text = $friendlyLocalState

        $ui.TenantId.Text = if ($local.TenantId) { [string]$local.TenantId } else { 'Unavailable' }

        $technicalSource = if ([string]$local.TrustLevel -eq 'Unavailable' -and [string]$local.Source -eq 'Unavailable') {
            'Unavailable'
        }
        elseif ([string]::IsNullOrWhiteSpace([string]$local.TrustLevel)) {
            [string]$local.Source
        }
        elseif ([string]::IsNullOrWhiteSpace([string]$local.Source)) {
            [string]$local.TrustLevel
        }
        else {
            "$($local.TrustLevel) | $($local.Source)"
        }

        $friendlySource = switch ([string]$local.TrustLevel) {
            'CorrelatedLocalSources' { 'Registry + JWT' }
            'LocalRegistryHint' { 'Registry' }
            'StructurallyObservedJwtClaim' { 'JWT' }
            'Conflict' { 'Conflict' }
            'Unavailable' { 'Unavailable' }
            default { $technicalSource }
        }

        $ui.Source.Text = $friendlySource

        $toolTip.SetToolTip($ui.TenantId, [string]$ui.TenantId.Text)
        $toolTip.SetToolTip($ui.Source, $technicalSource)

        Set-GuiCapabilities
        Set-GuiStatus "Local state refreshed | $($local.FirmwareState)"
        Write-GuiConsole -Message "Local state: $($local.FirmwareState), tenant source: $($local.Source)"
    }

    function Get-GuiCloudStateText {
        param([AllowNull()][object]$State)

        switch (([string]$State).Trim().ToLowerInvariant()) {
            'associated'    { return 'Associated' }
            'preassociated' { return 'Pre-associated' }
            'notassociated' { return 'Not associated' }
            default {
                if ([string]::IsNullOrWhiteSpace([string]$State)) {
                    return 'Not checked'
                }
                return [string]$State
            }
        }
    }

    function Refresh-CloudView {
        param(
            [switch]$WriteCommand
        )

        $parameters = Get-GuiAuthParameters
        $runtimeParameters = Get-GuiRuntimeParameters
        foreach ($key in $runtimeParameters.Keys) {
            $parameters[$key] = $runtimeParameters[$key]
        }
        $parameters.Online = $true

        if ($WriteCommand) {
            Write-GuiConsole -Message "Get-WindowsDeviceLinkStatus -Online -Method $Method" -Command
        }

        $cloudResults = @(Invoke-GuiInformationCommand -ScriptBlock {
            Get-WindowsDeviceLinkStatus @parameters
        })
        $cloud = $cloudResults | Select-Object -Last 1
        $script:WdlGuiCloudStatus = $cloud

        $ui.CloudState.Text = Get-GuiCloudStateText -State $cloud.AssociationState
        $ui.CloudTenant.Text = if ($cloud.TenantId) { [string]$cloud.TenantId } else { 'Unavailable' }
        $ui.CloudId.Text = if ($cloud.AssociationId) { [string]$cloud.AssociationId } else { 'Unavailable' }

        $toolTip.SetToolTip($ui.CloudTenant, [string]$ui.CloudTenant.Text)
        $toolTip.SetToolTip($ui.CloudId, [string]$ui.CloudId.Text)

        return $cloud
    }

    function Invoke-GuiRefresh {
        if ($script:WdlGuiBusy) { return }

        Set-GuiBusy -Busy $true -StatusText 'Refreshing local state...'
        try { Refresh-LocalView }
        catch { Show-GuiError $_.Exception.Message }
        finally { Set-GuiBusy -Busy $false }
    }

    function Invoke-GuiOnline {
        if ($script:WdlGuiBusy) { return }

        Set-GuiBusy -Busy $true -StatusText 'Checking tenant-side Device Association...'
        try {
            $cloud = Refresh-CloudView -WriteCommand
            Write-GuiObject $cloud

            Set-GuiStatus 'Cloud lookup completed'
        }
        catch {
            Set-GuiStatus 'Cloud lookup failed'
            Show-GuiError $_.Exception.Message
        }
        finally { Set-GuiBusy -Busy $false }
    }

    function Select-GuiExportDirectory {
        if (-not $isWinPE) {
            $dialog = New-Object System.Windows.Forms.FolderBrowserDialog
            $dialog.Description = 'Choose a folder for the DeviceLink CSV'
            try {
                if ($dialog.ShowDialog($form) -ne [System.Windows.Forms.DialogResult]::OK) {
                    return $null
                }
                return [string]$dialog.SelectedPath
            }
            finally {
                $dialog.Dispose()
            }
        }

        $defaultPath = $null
        if ($outerBoundParameters.ContainsKey('WindowsManagementServicePath')) {
            try {
                $defaultPath = Split-Path -Parent $WindowsManagementServicePath
            }
            catch {}
        }
        if ([string]::IsNullOrWhiteSpace($defaultPath)) {
            try { $defaultPath = (Get-Location).Path } catch {}
        }
        if ([string]::IsNullOrWhiteSpace($defaultPath)) {
            $defaultPath = 'X:\'
        }

        $pathForm = New-Object System.Windows.Forms.Form
        $pathForm.Text = 'Export DeviceLink CSV'
        $pathForm.StartPosition = 'CenterParent'
        $pathForm.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedDialog
        $pathForm.MinimizeBox = $false
        $pathForm.MaximizeBox = $false
        $pathForm.ShowInTaskbar = $false
        $pathForm.Size = [System.Drawing.Size]::new(520,155)

        $pathLabel = New-Object System.Windows.Forms.Label
        $pathLabel.Text = 'Output folder'
        $pathLabel.Font = New-Object System.Drawing.Font('Segoe UI',9)
        $pathLabel.Location = [System.Drawing.Point]::new(14,14)
        $pathLabel.AutoSize = $true
        $pathForm.Controls.Add($pathLabel)

        $pathBox = New-Object System.Windows.Forms.TextBox
        $pathBox.Text = $defaultPath
        $pathBox.Font = New-Object System.Drawing.Font('Consolas',9)
        $pathBox.Location = [System.Drawing.Point]::new(16,40)
        $pathBox.Size = [System.Drawing.Size]::new(472,24)
        $pathForm.Controls.Add($pathBox)

        $okButton = New-Object System.Windows.Forms.Button
        $okButton.Text = 'Export'
        $okButton.DialogResult = [System.Windows.Forms.DialogResult]::OK
        $okButton.Location = [System.Drawing.Point]::new(308,78)
        $okButton.Size = [System.Drawing.Size]::new(86,30)
        $pathForm.Controls.Add($okButton)

        $cancelButton = New-Object System.Windows.Forms.Button
        $cancelButton.Text = 'Cancel'
        $cancelButton.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
        $cancelButton.Location = [System.Drawing.Point]::new(402,78)
        $cancelButton.Size = [System.Drawing.Size]::new(86,30)
        $pathForm.Controls.Add($cancelButton)

        $pathForm.AcceptButton = $okButton
        $pathForm.CancelButton = $cancelButton

        try {
            while ($true) {
                if ($pathForm.ShowDialog($form) -ne [System.Windows.Forms.DialogResult]::OK) {
                    return $null
                }

                $selectedPath = [string]$pathBox.Text
                if (-not [string]::IsNullOrWhiteSpace($selectedPath)) {
                    $selectedPath = $selectedPath.Trim()
                }

                if (-not [string]::IsNullOrWhiteSpace($selectedPath) -and [System.IO.Directory]::Exists($selectedPath)) {
                    return $selectedPath
                }

                [void][System.Windows.Forms.MessageBox]::Show(
                    $pathForm,
                    'The specified output folder does not exist. Enter an existing folder path.',
                    'WindowsDeviceLink',
                    [System.Windows.Forms.MessageBoxButtons]::OK,
                    [System.Windows.Forms.MessageBoxIcon]::Warning
                )
            }
        }
        finally {
            $pathForm.Dispose()
        }
    }

    function Invoke-GuiExport {
        if ($script:WdlGuiBusy) { return }

        Set-GuiBusy -Busy $true -StatusText 'Choose export folder...'

        try {
            $selectedPath = Select-GuiExportDirectory
            if ([string]::IsNullOrWhiteSpace($selectedPath)) {
                Set-GuiStatus 'Export cancelled'
                return
            }

            Set-GuiStatus 'Exporting DeviceLink CSV...'
            Write-GuiConsole -Message "Get-WindowsDeviceLink -OutputDirectory '$selectedPath'" -Command

            $deviceLinkParameters = Get-GuiRuntimeParameters
            $deviceLinkParameters.OutputDirectory = $selectedPath
            $file = Get-WindowsDeviceLink @deviceLinkParameters
            Write-GuiObject $file

            [void][System.Windows.Forms.MessageBox]::Show(
                $form,
                ('Exported to:' + [Environment]::NewLine + $file.FullName),
                'WindowsDeviceLink',
                [System.Windows.Forms.MessageBoxButtons]::OK,
                [System.Windows.Forms.MessageBoxIcon]::Information
            )

            Refresh-LocalView
        }
        catch { Show-GuiError $_.Exception.Message }
        finally {
            Set-GuiBusy -Busy $false
        }
    }

    function Invoke-GuiPreassociate {
        if ($script:WdlGuiBusy) { return }

        if (-not (Confirm-GuiAction -Title 'Create pre-association' -Message 'Ensure the tenant-side Device Association pre-association exists for this device?')) {
            return
        }

        Set-GuiBusy -Busy $true -StatusText 'Creating pre-association...'
        try {
            $parameters = Get-GuiAuthParameters
            $runtimeParameters = Get-GuiRuntimeParameters
            foreach ($key in $runtimeParameters.Keys) {
                $parameters[$key] = $runtimeParameters[$key]
            }
            $parameters.Confirm = $false

            Write-GuiConsole -Message "Initialize-WindowsDeviceLink -Method $Method" -Command

            $resultObjects = @(Invoke-GuiInformationCommand -ScriptBlock {
                Initialize-WindowsDeviceLink @parameters
            })
            $result = $resultObjects | Select-Object -Last 1
            Write-GuiObject $result

            $script:WdlGuiCloudStatus = $null
            Refresh-LocalView

            if ($result -and $result.PSObject.Properties.Name -contains 'AfterStatus' -and $result.AfterStatus) {
                $cloud = $result.AfterStatus
                $script:WdlGuiCloudStatus = $cloud
                $ui.CloudState.Text = [string]$cloud.AssociationState
                $ui.CloudTenant.Text = if ($cloud.TenantId) { [string]$cloud.TenantId } else { 'Unavailable' }
                $ui.CloudId.Text = if ($cloud.AssociationId) { [string]$cloud.AssociationId } else { 'Unavailable' }
            }

            $statusText = 'Pre-association completed'
            if ($result) {
                if ($result.PSObject.Properties.Name -contains 'Changed' -and -not [bool]$result.Changed) {
                    if ($result.PSObject.Properties.Name -contains 'AfterState' -and [string]$result.AfterState -eq 'Preassociated') {
                        $statusText = 'Already pre-associated - no change made'
                    }
                    elseif ($result.PSObject.Properties.Name -contains 'AfterState' -and [string]$result.AfterState -eq 'Associated') {
                        $statusText = 'Already associated - no change made'
                    }
                    elseif ($result.PSObject.Properties.Name -contains 'Message' -and -not [string]::IsNullOrWhiteSpace([string]$result.Message)) {
                        $statusText = [string]$result.Message
                    }
                }
                elseif ($result.PSObject.Properties.Name -contains 'Changed' -and [bool]$result.Changed) {
                    $statusText = 'Pre-association created successfully'
                }
                elseif ($result.PSObject.Properties.Name -contains 'Message' -and -not [string]::IsNullOrWhiteSpace([string]$result.Message)) {
                    $statusText = [string]$result.Message
                }
            }

            Set-GuiStatus $statusText
            Write-GuiConsole -Message $statusText
        }
        catch {
            Set-GuiStatus 'Pre-association failed'
            Show-GuiError $_.Exception.Message
        }
        finally {
            Set-GuiBusy -Busy $false
        }
    }

    function Invoke-GuiFullAssociate {
        if ($script:WdlGuiBusy) { return }

        if ($isWinPE) {
            Show-GuiError 'Full DeviceLink association is not supported in Windows PE. Use Pre-associate in WinPE and let full Windows/OOBE complete Device Association.'
            return
        }

        if (-not (Confirm-GuiAction -Title 'Full association' -Message 'Ensure pre-association exists and complete Device Association on this device?')) {
            return
        }

        Set-GuiBusy -Busy $true -StatusText 'Completing onboarding...'
        try {
            $parameters = Get-GuiAuthParameters
            $runtimeParameters = Get-GuiRuntimeParameters
            foreach ($key in $runtimeParameters.Keys) {
                $parameters[$key] = $runtimeParameters[$key]
            }
            $parameters.FullAssociation = $true
            $parameters.Confirm = $false

            Write-GuiConsole -Message "Initialize-WindowsDeviceLink -Method $Method -FullAssociation" -Command

            $resultObjects = New-Object System.Collections.Generic.List[object]
            & {
                Initialize-WindowsDeviceLink @parameters
            } 6>&1 | ForEach-Object {
                if ($_ -is [System.Management.Automation.InformationRecord]) {
                    $message = [string]$_.MessageData
                    if ($message) { Write-GuiConsole -Message $message }
                }
                else {
                    $resultObjects.Add($_)
                }
                [System.Windows.Forms.Application]::DoEvents()
            }

            $result = @($resultObjects.ToArray()) | Select-Object -Last 1
            Write-GuiObject $result

            $script:WdlGuiCloudStatus = $null
            Refresh-LocalView
            $verifiedCloud = Refresh-CloudView -WriteCommand
            Write-GuiObject $verifiedCloud
            Set-GuiStatus 'Full association completed'
        }
        catch {
            Set-GuiStatus 'Full association failed'
            Show-GuiError $_.Exception.Message
        }
        finally { Set-GuiBusy -Busy $false }
    }

    function Invoke-GuiCloudOffboard {
        if ($script:WdlGuiBusy) { return }

        if (-not (Confirm-GuiAction -Title 'Cloud offboarding' -Message 'Remove only the tenant-side Device Association record? Local DeviceLink firmware will remain unchanged.')) {
            return
        }

        Set-GuiBusy -Busy $true -StatusText 'Removing cloud association...'
        try {
            $parameters = Get-GuiAuthParameters
            $parameters.Confirm = $false

            Write-GuiConsole -Message "Remove-WindowsDeviceLinkAssociation -Method $Method" -Command
            $removalResults = @(Invoke-GuiInformationCommand -ScriptBlock {
                Remove-WindowsDeviceLinkAssociation @parameters
            })
            $result = $removalResults | Select-Object -Last 1
            Write-GuiObject $result

            $script:WdlGuiCloudStatus = $null
            $ui.CloudState.Text = 'Not checked'
            $ui.CloudTenant.Text = 'Unavailable'
            $ui.CloudId.Text = 'Unavailable'

            Refresh-LocalView
            Set-GuiStatus 'Cloud offboarding completed'
        }
        catch {
            $message = [string]$_.Exception.Message
            if ($message -like "No Device Association record was found for serial number *") {
                $noChangeMessage = 'No cloud association was found. Nothing was removed.'
                Write-GuiConsole -Message $noChangeMessage
                $script:WdlGuiCloudStatus = $null
                $ui.CloudState.Text = 'Not associated'
                $ui.CloudTenant.Text = 'Unavailable'
                $ui.CloudId.Text = 'Unavailable'
                Refresh-LocalView
                Set-GuiStatus $noChangeMessage
                [void][System.Windows.Forms.MessageBox]::Show(
                    $form,
                    $noChangeMessage,
                    'WindowsDeviceLink',
                    [System.Windows.Forms.MessageBoxButtons]::OK,
                    [System.Windows.Forms.MessageBoxIcon]::Information
                )
            }
            else {
                Set-GuiStatus 'Cloud offboarding failed'
                Show-GuiError $message
            }
        }
        finally { Set-GuiBusy -Busy $false }
    }

    function Invoke-GuiLocalOffboard {
        if ($script:WdlGuiBusy) { return }

        if (-not (Confirm-GuiAction -Title 'Local offboarding' -Message 'Reset all known local DeviceLink UEFI variables? The tenant-side Device Association record will remain unchanged.')) {
            return
        }

        Set-GuiBusy -Busy $true -StatusText 'Resetting local DeviceLink state...'
        try {
            Write-GuiConsole -Message 'Reset-WindowsDeviceLinkFirmwareState' -Command
            $result = Reset-WindowsDeviceLinkFirmwareState -Confirm:$false
            Write-GuiObject $result

            $script:WdlGuiCloudStatus = $null
            Refresh-LocalView
            Set-GuiStatus 'Local offboarding completed'
        }
        catch {
            Set-GuiStatus 'Local offboarding failed'
            Show-GuiError $_.Exception.Message
        }
        finally { Set-GuiBusy -Busy $false }
    }

    function Invoke-GuiFullOffboard {
        if ($script:WdlGuiBusy) { return }

        $message = @(
            'This performs full DeviceLink offboarding:'
            ''
            '1. Verify tenant-side Device Association state'
            '2. Remove the cloud Device Association if present'
            '3. Reset all known local DeviceLink UEFI variables'
            ''
            'This does not remove the Entra device or MDM enrollment record.'
            ''
            'Continue?'
        ) -join [Environment]::NewLine

        if (-not (Confirm-GuiAction -Title 'Full DeviceLink offboarding' -Message $message)) {
            return
        }

        Set-GuiBusy -Busy $true -StatusText 'Performing full offboarding...'
        $effectiveAccessToken = $null
        try {
            $auth = Get-GuiAuthParameters
            $effectiveAuth = @{}
            foreach ($key in $auth.Keys) { $effectiveAuth[$key] = $auth[$key] }

            if ($Method -eq 'DeviceCode') {
                Write-GuiConsole -Message 'Acquiring one DeviceCode token for the complete offboarding flow.' -Command

                $tokenParameters = @{}
                if ($auth.ContainsKey('TenantId')) { $tokenParameters.TenantId = $auth.TenantId }
                if ($auth.ContainsKey('ClientId')) { $tokenParameters.ClientId = $auth.ClientId }

                $tokenResults = @(Invoke-GuiInformationCommand -ScriptBlock {
                    Get-WindowsDeviceLinkDeviceCodeToken @tokenParameters
                })
                $token = $tokenResults | Select-Object -Last 1

                if (-not $token -or [string]::IsNullOrWhiteSpace([string]$token.AccessToken)) {
                    throw 'Device code authentication did not return an access token.'
                }

                $effectiveAccessToken = ConvertTo-SecureString ([string]$token.AccessToken) -AsPlainText -Force
                $effectiveAuth = @{
                    Method = 'AccessToken'
                    AccessToken = $effectiveAccessToken
                    Environment = $Environment
                    ClientTimeout = $ClientTimeout
                }
                if ($token.TenantId) {
                    $effectiveAuth.TenantId = [string]$token.TenantId
                }

                $token = $null
                Write-GuiConsole -Message 'DeviceCode token acquired once; reusing it for cloud verification and removal.'
            }

            $statusParameters = @{}
            foreach ($key in $effectiveAuth.Keys) { $statusParameters[$key] = $effectiveAuth[$key] }
            $runtimeParameters = Get-GuiRuntimeParameters
            foreach ($key in $runtimeParameters.Keys) {
                $statusParameters[$key] = $runtimeParameters[$key]
            }
            $statusParameters.Online = $true

            $displayMethod = [string]$effectiveAuth.Method
            Write-GuiConsole -Message "Get-WindowsDeviceLinkStatus -Online -Method $displayMethod" -Command
            $cloudResults = @(Invoke-GuiInformationCommand -ScriptBlock {
                Get-WindowsDeviceLinkStatus @statusParameters
            })
            $cloud = $cloudResults | Select-Object -Last 1
            Write-GuiObject $cloud

            if ($null -eq $cloud.AssociationPresent -or [string]$cloud.AssociationState -eq 'Unknown') {
                throw 'Cloud Device Association state is indeterminate. Local firmware was not reset.'
            }

            if ($cloud.AssociationPresent) {
                $removeParameters = @{}
                foreach ($key in $effectiveAuth.Keys) { $removeParameters[$key] = $effectiveAuth[$key] }
                $removeParameters.Confirm = $false

                Write-GuiConsole -Message "Remove-WindowsDeviceLinkAssociation -Method $displayMethod" -Command
                $removeResults = @(Invoke-GuiInformationCommand -ScriptBlock {
                    Remove-WindowsDeviceLinkAssociation @removeParameters
                })
                $removed = $removeResults | Select-Object -Last 1
                Write-GuiObject $removed
            }
            else {
                Write-GuiConsole -Message 'No tenant-side Device Association exists. Cloud removal skipped.'
            }

            Write-GuiConsole -Message 'Reset-WindowsDeviceLinkFirmwareState' -Command
            $reset = Reset-WindowsDeviceLinkFirmwareState -Confirm:$false
            Write-GuiObject $reset

            $script:WdlGuiCloudStatus = $null
            $ui.CloudState.Text = 'Not checked'
            $ui.CloudTenant.Text = 'Unavailable'
            $ui.CloudId.Text = 'Unavailable'

            Refresh-LocalView
            Set-GuiStatus 'Full offboarding completed'
        }
        catch {
            Set-GuiStatus 'Full offboarding failed'
            Show-GuiError $_.Exception.Message
        }
        finally {
            $effectiveAccessToken = $null
            Set-GuiBusy -Busy $false
        }
    }

    $btnRefresh.Add_Click({ Invoke-GuiRefresh })
    $btnOnline.Add_Click({ Invoke-GuiOnline })
    $btnExport.Add_Click({ Invoke-GuiExport })
    $btnPreassociate.Add_Click({ Invoke-GuiPreassociate })
    $btnFullAssociate.Add_Click({ Invoke-GuiFullAssociate })
    $btnCloudOffboard.Add_Click({ Invoke-GuiCloudOffboard })
    $btnLocalOffboard.Add_Click({ Invoke-GuiLocalOffboard })
    $btnFullOffboard.Add_Click({ Invoke-GuiFullOffboard })

    $tenantSelector.Add_SelectedIndexChanged({
        if (-not $script:WdlGuiBusy) {
            $selectedTenant = Get-SelectedTenantId
            $ui.Auth.Text = if ($selectedTenant) { "$Method | $selectedTenant" } else { $Method }
        }
    })

    $btnClearActivity.Add_Click({
        $consoleBox.Clear()
        Write-GuiConsole -Message 'Activity log cleared.'
    })

    $form.Add_FormClosing({
        param($sender,$eventArgs)

        if ($script:WdlGuiBusy) {
            $eventArgs.Cancel = $true
            Write-GuiConsole -Message 'Close request ignored because an operation is still running.'
        }
    })

    $form.Add_Shown({
        $environmentName = if ($isWinPE) { 'Windows PE' } else { 'Windows' }
        Write-GuiConsole -Message "WindowsDeviceLink dashboard opened in $environmentName. Authentication method: $Method."
        Set-GuiBusy -Busy $true -StatusText 'Loading local state...'
        try { Refresh-LocalView }
        catch { Show-GuiError $_.Exception.Message }
        finally { Set-GuiBusy -Busy $false }
    })

    function Resize-GuiLayout {
        $fullWidth = [Math]::Max(760,$content.ClientSize.Width - 32)
        $gap = 14
        $halfWidth = [Math]::Floor(($fullWidth - $gap) / 2)

        if ($fullWidth -ge 900) {
            $deviceCard.Location = [System.Drawing.Point]::new(14,12)
            $deviceCard.Size = [System.Drawing.Size]::new([int]$halfWidth,126)
            $associationCard.Location = [System.Drawing.Point]::new((14 + $halfWidth + $gap),12)
            $associationCard.Size = [System.Drawing.Size]::new([int]$halfWidth,126)
            $cloudY = 150
        }
        else {
            $deviceCard.Location = [System.Drawing.Point]::new(14,12)
            $deviceCard.Size = [System.Drawing.Size]::new([int]$fullWidth,126)
            $associationCard.Location = [System.Drawing.Point]::new(14,150)
            $associationCard.Size = [System.Drawing.Size]::new([int]$fullWidth,126)
            $cloudY = 288
        }

        $cloudCard.Location = [System.Drawing.Point]::new(14,$cloudY)
        $cloudCard.Width = $fullWidth

        $actionsY = $cloudY + 96
        $actionsTitle.Location = [System.Drawing.Point]::new(16,$actionsY)
        $actionsPanel.Location = [System.Drawing.Point]::new(14,($actionsY + 26))
        $actionsPanel.Width = $fullWidth

        $activityY = $actionsY + 270
        $activityTitle.Location = [System.Drawing.Point]::new(16,$activityY)

        # Align Clear to the same right edge used by the action buttons.
        # Action buttons sit 16 px inside the right edge of the Actions panel.
        $clearX = $actionsPanel.Right - 16 - $btnClearActivity.Width
        $clearY = $activityY - 5
        $btnClearActivity.Location = [System.Drawing.Point]::new($clearX,$clearY)

        $activityCard.Location = [System.Drawing.Point]::new(14,($activityY + 26))
        $activityCard.Width = $fullWidth
        $consoleBox.Width = $fullWidth - 24


        foreach ($row in @($rowRefresh,$rowOnline,$rowExport,$rowOnboard,$rowOffboard)) {
            $row.Panel.Width = $fullWidth

            $buttons = @($row.Buttons)
            $right = $fullWidth - 16
            for ($i = $buttons.Count - 1; $i -ge 0; $i--) {
                $right -= $buttons[$i].Width
                $buttons[$i].Left = $right
                $right -= 7
            }

            $row.Panel.Controls |
                Where-Object { $_ -is [System.Windows.Forms.Panel] -and $_.Height -eq 1 } |
                ForEach-Object { $_.Width = [Math]::Max(480,$fullWidth - 28) }
        }

        $requiredHeight = $activityCard.Bottom + 2
        $availableHeight = [Math]::Max(0,$content.ClientSize.Height - 2)
        $scrollTolerance = 10

        if ($requiredHeight -gt ($availableHeight + $scrollTolerance)) {
            $content.AutoScroll = $true
            $content.HorizontalScroll.Enabled = $false
            $content.HorizontalScroll.Visible = $false
            $content.AutoScrollMinSize = [System.Drawing.Size]::new(1,$requiredHeight)
        }
        else {
            $content.AutoScrollMinSize = [System.Drawing.Size]::Empty
            $content.AutoScroll = $false
            $content.VerticalScroll.Value = 0
        }
    }

    $form.Add_Resize({ Resize-GuiLayout })

    try {
        Resize-GuiLayout
        [void]$form.ShowDialog()
    }
    finally {
        $toolTip.Dispose()
        $form.Dispose()
        Remove-Variable WdlGuiLocalAssociation -Scope Script -ErrorAction SilentlyContinue
        Remove-Variable WdlGuiCloudStatus -Scope Script -ErrorAction SilentlyContinue
        Remove-Variable WdlGuiBusy -Scope Script -ErrorAction SilentlyContinue
        Remove-Variable WdlGuiSupport -Scope Script -ErrorAction SilentlyContinue
    }
}