<#
.SYNOPSIS
    AI 遠程維修工具 — 完整移除 / 收尾
.DESCRIPTION
    反向移除 install.ps1 所做的一切,且具冪等性(可重複執行)。
    只移除本工具新增的項目;若客戶原本就有 OpenSSH,不會移除。
      1. Tailscale 登出(讓 ephemeral 節點下線)並靜默移除
      2. 停用並刪除維修帳號 + 其使用者設定檔
      3. 移除本工具新增的防火牆規則
      4. 清除 administrators_authorized_keys 中的技師公鑰
      5. 移除技師連線後安裝的 Claude Code(若有)
      6. 僅在「本工具安裝」OpenSSH 時才移除 OpenSSH
      7. 移除自毀排程

    ⚠️ 收尾後請記得到 Tailscale 管理主控台刪除該裝置(tagged 裝置不會自動過期)。
.NOTES
    需以系統管理員身分執行。可用 run-cleanup.cmd 一鍵提權。
#>
[CmdletBinding()]
param(
    [switch]$Silent,
    # 即使不是本工具安裝的,也強制移除 OpenSSH
    [switch]$RemoveOpenSSH,
    # 同時清掉本工具的紀錄檔/狀態檔
    [switch]$PurgeLogs
)

$ErrorActionPreference = 'Continue'

$LogDir       = Join-Path $env:ProgramData 'AIRemoteRepair'
$LogFile      = Join-Path $LogDir 'cleanup.log'
$StateFile    = Join-Path $LogDir 'state.json'
# Mirror the log into the script folder for easy access during testing; best-effort.
$FolderLogFile = if ($PSScriptRoot) { Join-Path $PSScriptRoot 'cleanup.log' } else { $null }
$TailscaleExe = Join-Path $env:ProgramFiles 'Tailscale\tailscale.exe'
$SshDir       = Join-Path $env:ProgramData 'ssh'
$AdminKeysFile = Join-Path $SshDir 'administrators_authorized_keys'

function Write-Log {
    param([string]$Message, [string]$Level = 'INFO')
    $ts = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
    $line = "[$ts] [$Level] $Message"
    if (-not (Test-Path $LogDir)) { New-Item -ItemType Directory -Path $LogDir -Force | Out-Null }
    Add-Content -Path $LogFile -Value $line -Encoding utf8
    if ($FolderLogFile) { try { Add-Content -Path $FolderLogFile -Value $line -Encoding utf8 -ErrorAction Stop } catch {} }
    $color = switch ($Level) { 'ERROR' { 'Red' } 'WARN' { 'Yellow' } 'OK' { 'Green' } default { 'Gray' } }
    Write-Host $line -ForegroundColor $color
}

function Assert-Admin {
    $id = [Security.Principal.WindowsIdentity]::GetCurrent()
    $p = New-Object Security.Principal.WindowsPrincipal($id)
    if (-not $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
        throw '必須以系統管理員身分執行。'
    }
}

function Get-State {
    if (Test-Path $StateFile) {
        try { return Get-Content $StateFile -Raw | ConvertFrom-Json } catch {}
    }
    return $null
}

Assert-Admin
Write-Log '═══════════════ AI 遠程維修工具 收尾開始 ═══════════════'

if (-not $Silent) {
    $ans = Read-Host '這會移除維修帳號、Tailscale、相關設定。確定收尾?輸入 Y 繼續'
    if ($ans -notmatch '^[Yy]') { Write-Log '已取消。' 'WARN'; exit 2 }
}

$state = Get-State
$acct = if ($state -and $state.PSObject.Properties.Name -contains 'RepairAccountName' -and $state.RepairAccountName) { $state.RepairAccountName } else { 'svc-vendor-repair' }

# 1) Tailscale 登出 + 移除
if (Test-Path $TailscaleExe) {
    Write-Log 'Tailscale 登出...'
    & $TailscaleExe logout 2>&1 | ForEach-Object { Write-Log "  $_" }
    # 同時查 64 位元與 WOW6432Node 兩個解除安裝登錄區
    $g = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
                           'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' -ErrorAction SilentlyContinue |
          Where-Object { $_.DisplayName -eq 'Tailscale' } | Select-Object -First 1).PSChildName
    if ($g) {
        Write-Log "Tailscale 靜默移除($g)..."
        Start-Process msiexec.exe -ArgumentList '/x', $g, '/quiet', '/norestart' -Wait
        Write-Log 'Tailscale 已移除。' 'OK'
    }
    else {
        $arch = $env:PROCESSOR_ARCHITECTURE; if ($env:PROCESSOR_ARCHITEW6432) { $arch = $env:PROCESSOR_ARCHITEW6432 }
        $a = switch ($arch) { 'ARM64' { 'arm64' } 'x86' { 'x86' } default { 'amd64' } }
        $msi = Join-Path $PSScriptRoot "payload\tailscale-setup-$a.msi"
        if (Test-Path $msi) { Start-Process msiexec.exe -ArgumentList '/x', "`"$msi`"", '/quiet', '/norestart' -Wait }
        Write-Log '已嘗試移除 Tailscale(登錄區未找到 GUID,改用 payload MSI)。' 'WARN'
    }
}
else { Write-Log 'Tailscale 未安裝,略過。' }

# 2) 帳號 + 設定檔
# 不只依賴 state/後備名:state.json 遺失或損毀、且該案件用了自訂 RepairAccountName 時,單靠
# 'svc-vendor-repair' 後備會漏刪真正的維修帳號。額外依安裝標記(install 為新建帳號設定的
# Description)列舉,確保不論 state 是否可用都能完整移除本工具建立的帳號(冪等、唯讀失敗即略過)。
$marker = 'Created by AI Remote Repair installer'
$markerAccts = @(Get-LocalUser -ErrorAction SilentlyContinue |
    Where-Object { $_.Description -eq $marker } |
    Select-Object -ExpandProperty Name)
$acctsToRemove = @(@($acct) + $markerAccts | Where-Object { $_ } | Select-Object -Unique)

foreach ($name in $acctsToRemove) {
    $u = Get-LocalUser -Name $name -ErrorAction SilentlyContinue
    if ($u) {
        Disable-LocalUser -Name $name -ErrorAction SilentlyContinue
        $sid = $u.SID.Value
        Remove-LocalUser -Name $name -ErrorAction SilentlyContinue
        Write-Log "已刪除帳號:$name" 'OK'
        # 刪除使用者設定檔:用 SID 精準比對,並以「完全相等」的 <SystemDrive>\Users\<帳號> 路徑作後備
        # (用相等而非萬用字元,避免誤刪名稱前綴相同的其他帳號)。SSH 首次登入會建立此設定檔。
        $expectedProfile = Join-Path $env:SystemDrive ('Users\' + $name)
        Get-CimInstance Win32_UserProfile -ErrorAction SilentlyContinue |
            Where-Object { $_.SID -eq $sid -or $_.LocalPath -eq $expectedProfile } |
            ForEach-Object { Remove-CimInstance $_ -ErrorAction SilentlyContinue }
        if (Test-Path $expectedProfile) { Remove-Item $expectedProfile -Recurse -Force -ErrorAction SilentlyContinue }
        Write-Log '已清除使用者設定檔。' 'OK'
    }
    else { Write-Log "帳號不存在,略過:$name" }
}

# 2b) 還原登入畫面可見性:UserList 下以維修帳號命名的值只由本工具寫入,故只要存在就移除。
#     刻意「不」依賴 state.AccountHidden —— state.json 遺失或先前經 -PurgeLogs 清掉時仍能清乾淨,
#     避免 SpecialAccounts\UserList 殘留可疑登錄痕跡(違反一鍵完整移除承諾)。
$userListKey = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList'
if (Test-Path $userListKey) {
    foreach ($name in $acctsToRemove) {
        if ($null -ne (Get-ItemProperty -Path $userListKey -Name $name -ErrorAction SilentlyContinue)) {
            Remove-ItemProperty -Path $userListKey -Name $name -ErrorAction SilentlyContinue
            Write-Log "已還原登入畫面可見性(移除 UserList 隱藏項):$name" 'OK'
        }
    }
}

# 3) 防火牆規則
Get-NetFirewallRule -Name 'sshd-tailscale-only', 'sshd-tailscale-only-v6' -ErrorAction SilentlyContinue |
    ForEach-Object { Remove-NetFirewallRule -Name $_.Name; Write-Log "已移除防火牆規則:$($_.Name)" 'OK' }
# 若 OpenSSH 非本工具安裝,install 當初移除了客戶預設的「任何來源」OpenSSH 允許規則;cleanup 不自動
# 重建(避免擅自對外開放 22 埠)。若客戶原本透過非 Tailscale 連 SSH,提醒操作者視需要自行還原。
if ($state -and ($state.PSObject.Properties.Name -contains 'OpenSSHInstalledByUs') -and -not [bool]$state.OpenSSHInstalledByUs) {
    Write-Log "注意:本工具當初移除了客戶既有 OpenSSH 的預設防火牆允許規則,未自動重建。若客戶原本透過非 Tailscale 連 SSH,請手動還原(例:New-NetFirewallRule -DisplayName 'OpenSSH SSH Server (sshd)' -Direction Inbound -Action Allow -Protocol TCP -LocalPort 22)。" 'WARN'
}

# 4) 還原/移除 administrators_authorized_keys
#    用 install 持久化的 AdminKeysCreatedByUs 旗標消除歧義,並以 techpubkey.txt 的內容比對作最終把關:
#      - 有 .airr.bak → 「驗證成功後」才還原客戶既有金鑰並刪除備份;還原失敗則保留備份並直接剝除技師公鑰。
#      - 無 .bak 且旗標明確為 true(本工具建立此檔)→ 刪整檔。
#      - 其餘(state 遺失/旗標 false 但無 .bak/還原失敗)→ 依檔案內容「只剝除技師公鑰、保留客戶金鑰」;
#        僅在 state 遺失且無法驗證內容(techpubkey.txt 不存在)時,才安全優先刪整檔。
#    如此既不留後門(技師公鑰一旦存在必被移除),也不會誤刪客戶金鑰,且重複執行(含 -PurgeLogs)維持冪等。
$adminKeysBak = "$AdminKeysFile.airr.bak"
$adminKeysCreatedByUs = $true
$adminKeysFlagKnown = $false
if ($state -and ($state.PSObject.Properties.Name -contains 'AdminKeysCreatedByUs')) {
    $adminKeysCreatedByUs = [bool]$state.AdminKeysCreatedByUs
    $adminKeysFlagKnown = $true
}
# Technician public key(s) for CONTENT-BASED stripping — the authoritative no-backdoor guard when
# flags/.bak are ambiguous (install re-run with flag=false + missing .bak; state lost; failed
# restore). techpubkey.txt may be absent when cleanup runs from the self-destruct copy; the
# flag/.bak paths still cover those cases.
$techPubFile = Join-Path $PSScriptRoot 'techpubkey.txt'
$techKeys = @()
if (Test-Path $techPubFile) {
    $techKeys = @(Get-Content -Path $techPubFile -ErrorAction SilentlyContinue |
        ForEach-Object { $_.Trim() } |
        Where-Object { $_ -and -not $_.StartsWith('#') })
}
function Remove-TechKeyFromAdminKeys {
    # Strip exactly the technician key line(s), keeping any customer keys. Returns $true if it
    # removed/deleted anything. Reads $techKeys / $AdminKeysFile from script scope.
    if ($techKeys.Count -eq 0 -or -not (Test-Path $AdminKeysFile)) { return $false }
    $cur  = @(Get-Content -Path $AdminKeysFile -ErrorAction SilentlyContinue)
    $kept = @($cur | Where-Object { $techKeys -notcontains $_.Trim() })
    if ($kept.Count -ge $cur.Count) { return $false }
    if (@($kept | Where-Object { $_.Trim() }).Count -eq 0) {
        Remove-Item $AdminKeysFile -Force -ErrorAction SilentlyContinue
        Write-Log '偵測到殘留技師公鑰且無其他金鑰,已刪除 administrators_authorized_keys。' 'WARN'
    }
    else {
        Set-Content -Path $AdminKeysFile -Value ($kept -join "`n") -Encoding ascii
        Write-Log '偵測到殘留技師公鑰,已從 administrators_authorized_keys 移除(保留客戶其他金鑰)。' 'WARN'
    }
    return $true
}
if (Test-Path $adminKeysBak) {
    # Gate the .bak deletion on a VERIFIED restore: never destroy the only clean copy of the
    # customer keys if the copy fails (e.g. file transiently locked by sshd during an auth).
    try {
        Copy-Item $adminKeysBak $AdminKeysFile -Force -ErrorAction Stop
        Remove-Item $adminKeysBak -Force -ErrorAction SilentlyContinue
        Write-Log '已還原客戶既有 administrators_authorized_keys(移除技師公鑰)。' 'OK'
    }
    catch {
        Write-Log "還原 administrators_authorized_keys 失敗,保留備份 $adminKeysBak 以便重試;改為直接移除殘留技師公鑰:$($_.Exception.Message)" 'ERROR'
        Remove-TechKeyFromAdminKeys | Out-Null
    }
}
elseif ($adminKeysFlagKnown -and $adminKeysCreatedByUs -and (Test-Path $AdminKeysFile)) {
    Remove-Item $AdminKeysFile -Force -ErrorAction SilentlyContinue
    Write-Log '已移除 administrators_authorized_keys(本工具建立)。' 'OK'
}
elseif (Test-Path $AdminKeysFile) {
    # State unknown, or flag says customer-owned but no .bak. NEVER blindly delete (would lose
    # customer keys) and NEVER blindly leave (could strand the technician key = backdoor). Strip
    # only the technician key by content; fall back to safety-first removal only when contents
    # cannot be verified (techpubkey.txt absent) and the flag still defaults to "ours".
    if (Remove-TechKeyFromAdminKeys) { }
    elseif ((-not $adminKeysFlagKnown) -and $techKeys.Count -eq 0) {
        Remove-Item $AdminKeysFile -Force -ErrorAction SilentlyContinue
        Write-Log '已移除 administrators_authorized_keys(state 遺失且無法驗證內容,安全優先)。' 'WARN'
    }
    else {
        Write-Log 'administrators_authorized_keys 未發現技師公鑰(客戶既有且已還原),保留不動。'
    }
}
else {
    Write-Log 'administrators_authorized_keys 不存在,略過。'
}

# 4b) 還原 sshd_config、移除 DefaultShell、清掉殘留 authkey.txt
$sshdConfig = Join-Path $SshDir 'sshd_config'
$bak = "$sshdConfig.airr.bak"
if (Test-Path $bak) {
    Copy-Item $bak $sshdConfig -Force -ErrorAction SilentlyContinue
    Remove-Item $bak -Force -ErrorAction SilentlyContinue
    Restart-Service sshd -ErrorAction SilentlyContinue
    Write-Log '已還原原始 sshd_config。' 'OK'
}
try {
    # install 覆寫 DefaultShell 前已把原值備份到 state.PrevDefaultShell;有備份則還原客戶原值,否則移除。
    $prevShell = $null
    if ($state -and ($state.PSObject.Properties.Name -contains 'PrevDefaultShell')) { $prevShell = $state.PrevDefaultShell }
    if ($prevShell) {
        if (-not (Test-Path 'HKLM:\SOFTWARE\OpenSSH')) { New-Item -Path 'HKLM:\SOFTWARE' -Name 'OpenSSH' -Force | Out-Null }
        New-ItemProperty -Path 'HKLM:\SOFTWARE\OpenSSH' -Name 'DefaultShell' -Value $prevShell -PropertyType String -Force -ErrorAction SilentlyContinue | Out-Null
        Write-Log '已還原客戶原本的 DefaultShell 登錄值。' 'OK'
    }
    elseif (Get-ItemProperty -Path 'HKLM:\SOFTWARE\OpenSSH' -Name 'DefaultShell' -ErrorAction SilentlyContinue) {
        Remove-ItemProperty -Path 'HKLM:\SOFTWARE\OpenSSH' -Name 'DefaultShell' -ErrorAction SilentlyContinue
        Write-Log '已移除 DefaultShell 登錄值。' 'OK'
    }
} catch {}
$strayKey = Join-Path $PSScriptRoot 'authkey.txt'
if (Test-Path $strayKey) { Remove-Item $strayKey -Force -ErrorAction SilentlyContinue; Write-Log '已清除殘留 authkey.txt。' 'OK' }

# 5) Claude Code:僅在「本工具安裝」時移除,且只刪我們安裝的二進位/共享檔,範圍限於安裝當時的
#    使用者 profile(install 記錄於 state.ClaudeInstallProfile)。刻意「不」刪 .claude /
#    .claude.json(使用者設定/憑證/歷史),也「不」掃整個 C:\Users —— 否則會抹掉客戶既有的
#    Claude 設定與其他帳號的資料(這在 InstallClaudeCode=false 的預設情境下尤其嚴重)。
$claudeByUs = $false
if ($state -and ($state.PSObject.Properties.Name -contains 'ClaudeInstalledByUs')) { $claudeByUs = [bool]$state.ClaudeInstalledByUs }
if ($claudeByUs) {
    $claudeProfile = $null
    if ($state -and ($state.PSObject.Properties.Name -contains 'ClaudeInstallProfile') -and $state.ClaudeInstallProfile) { $claudeProfile = "$($state.ClaudeInstallProfile)" }
    if ($claudeProfile -and (Test-Path $claudeProfile)) {
        $removedClaude = $false
        foreach ($p in @(
                (Join-Path $claudeProfile '.local\bin\claude.exe'),
                (Join-Path $claudeProfile '.local\share\claude'))) {
            if (Test-Path $p) { Remove-Item $p -Recurse -Force -ErrorAction SilentlyContinue; $removedClaude = $true }
        }
        if ($removedClaude) { Write-Log '已移除本工具安裝的 Claude Code 二進位。' 'OK' }
        else { Write-Log 'Claude Code 二進位不存在,略過。' }
    }
    else { Write-Log 'state 標記本工具曾安裝 Claude,但找不到安裝 profile,略過 Claude 清理。' 'WARN' }
}
else { Write-Log 'Claude Code 非本工具安裝(或無紀錄),保留不動。' }

# 6) OpenSSH:僅在本工具安裝時移除
$installedByUs = $false
if ($state -and $state.PSObject.Properties.Name -contains 'OpenSSHInstalledByUs') { $installedByUs = [bool]$state.OpenSSHInstalledByUs }
if ($installedByUs -or $RemoveOpenSSH) {
    Write-Log '移除 OpenSSH Server(本工具所安裝)...'
    Stop-Service sshd -ErrorAction SilentlyContinue
    Stop-Service ssh-agent -ErrorAction SilentlyContinue

    # (a) 內建功能版
    try {
        $cap = Get-WindowsCapability -Online -Name 'OpenSSH.Server~~~~0.0.1.0' -ErrorAction SilentlyContinue
        if ($cap -and $cap.State -eq 'Installed') {
            Remove-WindowsCapability -Online -Name 'OpenSSH.Server~~~~0.0.1.0' | Out-Null
            Write-Log '已移除 OpenSSH 內建功能。' 'OK'
        }
    } catch { Write-Log "移除 OpenSSH 內建功能失敗:$($_.Exception.Message)" 'WARN' }

    # (b) MSI 版(Win32-OpenSSH):從 64 位元與 WOW6432Node 解除安裝登錄找 OpenSSH 產品並 msiexec /x
    try {
        $prod = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
                                  'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' -ErrorAction SilentlyContinue |
                Where-Object { $_.DisplayName -match 'OpenSSH' }
        foreach ($p in $prod) {
            if ($p.PSChildName -match '^\{[0-9A-Fa-f\-]+\}$') {
                Start-Process msiexec.exe -ArgumentList '/x', $p.PSChildName, '/quiet', '/norestart' -Wait
                Write-Log "已移除 OpenSSH MSI:$($p.DisplayName)" 'OK'
            }
        }
    } catch { Write-Log "移除 OpenSSH MSI 失敗:$($_.Exception.Message)" 'WARN' }

    Write-Log 'OpenSSH 處理完成。' 'OK'
}
else { Write-Log 'OpenSSH 非本工具安裝(或無紀錄),保留不動。' }

# 7) 自毀排程
Unregister-ScheduledTask -TaskName 'AIRemoteRepair-SelfDestruct' -Confirm:$false -ErrorAction SilentlyContinue
Write-Log '已移除自毀排程(若存在)。'

Write-Host ''
Write-Host '════════════════════════════════════════════════════════════════' -ForegroundColor Green
Write-Host '  收尾完成。' -ForegroundColor Green
Write-Host '  ⚠️ 別忘了到 Tailscale 管理主控台刪除這台裝置(tagged 裝置不會自動過期)。' -ForegroundColor Yellow
Write-Host '════════════════════════════════════════════════════════════════' -ForegroundColor Green

if ($PurgeLogs) {
    # -PurgeLogs 承諾清掉本工具的紀錄/狀態。必須在最後一步、寫完結尾行後再清,並涵蓋
    # install.log / cleanup.log / 自毀排程複製到 ProgramData 的 cleanup.ps1 / 連線資訊 /
    # 封裝資料夾鏡像紀錄,否則違反「一鍵完整移除」。注意:清除後「不可」再呼叫 Write-Log,
    # 否則 Write-Log 會重建 LogDir 與 cleanup.log。
    Write-Log '清除本工具紀錄/狀態...'
    Write-Log '═══════════════ 收尾結束 ═══════════════'
    Remove-Item $StateFile -Force -ErrorAction SilentlyContinue
    Remove-Item (Join-Path $LogDir 'connection-info.json') -Force -ErrorAction SilentlyContinue
    Remove-Item (Join-Path $LogDir 'cleanup.ps1') -Force -ErrorAction SilentlyContinue
    Remove-Item (Join-Path $LogDir 'install.log') -Force -ErrorAction SilentlyContinue
    Remove-Item $LogFile -Force -ErrorAction SilentlyContinue
    if ($FolderLogFile) {
        Remove-Item $FolderLogFile -Force -ErrorAction SilentlyContinue
        Remove-Item (Join-Path $PSScriptRoot 'install.log') -Force -ErrorAction SilentlyContinue
    }
    # 若資料夾已空,連同移除(此後不可再 Write-Log)。
    if ((Test-Path $LogDir) -and -not (Get-ChildItem -Force $LogDir -ErrorAction SilentlyContinue)) {
        Remove-Item $LogDir -Force -Recurse -ErrorAction SilentlyContinue
    }
}
else {
    Write-Log '═══════════════ 收尾結束 ═══════════════'
}
exit 0
