<#
.SYNOPSIS
  Back up ClaudeTools secrets + identity (and optionally large client data) to a
  recovery bundle on a removable drive. The inverse of restore-secrets.ps1.

.DESCRIPTION
  Captures everything that will NOT come back from a `git clone`:
    - out-of-repo secrets under the user profile (age key, ssh, tool auth, git, PS profile)
    - repo-local gitignored identity files
    - environment manifests (installed tools, ollama models, scheduled-task XML, vscode ext)
    - (optional) large gitignored client/project data clusters

  Safe to re-run; it refreshes the bundle in place.

.PARAMETER Drives        Target drive roots. Default 'E:','F:' (writes the small bundle to both).
.PARAMETER IncludeData   Also copy the large client-data clusters (only to the FIRST drive with room; exFAT recommended).
.PARAMETER ClaudeToolsRoot  Default D:\claudetools.

.EXAMPLE
  .\backup-to-bundle.ps1                 # secrets+identity+manifests to E: and F:
  .\backup-to-bundle.ps1 -IncludeData    # also large data (to F:)
#>
[CmdletBinding()]
param(
  [string[]]$Drives = @('E:','F:'),
  [switch]$IncludeData,
  [string]$ClaudeToolsRoot = 'D:\claudetools',
  [string]$DataDrive = 'F:'
)
$ErrorActionPreference = 'Stop'
$u = $env:USERPROFILE

# Decode native (git) stdout as UTF-8 so captured patch text is not mangled, and give
# us a UTF-8 (no BOM) encoding for writing patches `git apply` can actually parse.
try { [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false) } catch {}
$Utf8NoBom = New-Object System.Text.UTF8Encoding($false)

function Save($src,$dst){
  if (Test-Path -LiteralPath $src) {
    $p = Split-Path $dst -Parent; if (-not (Test-Path $p)) { New-Item -ItemType Directory -Force -Path $p | Out-Null }
    Copy-Item -LiteralPath $src -Destination $dst -Force; Write-Host "[OK]   $src"
  } else { Write-Host "[MISS] $src" }
}

# Build the bundle once under the first available target, then mirror to the rest.
$primary = $Drives | Where-Object { Test-Path "$_\" } | Select-Object -First 1
if (-not $primary) { throw "None of the target drives are accessible: $($Drives -join ', ')" }
$root = "$primary\claudetools-recovery"
Write-Host "=== building bundle at $root ===" -ForegroundColor Cyan
foreach ($d in 'secrets\sops-age','secrets\ssh','secrets\claude','secrets\grok','secrets\gemini','secrets\git','secrets\powershell','identity\state','manifests\scheduled-tasks','bootstrap') {
  New-Item -ItemType Directory -Force -Path "$root\$d" | Out-Null
}

# --- secrets ---
Save "$u\.config\sops\age\keys.txt"          "$root\secrets\sops-age\keys.txt"
if (Test-Path "$u\.ssh") { Copy-Item "$u\.ssh\*" "$root\secrets\ssh\" -Force; Write-Host "[OK]   ~/.ssh/*" }
Save "$u\.claude.json"                        "$root\secrets\claude\.claude.json"
Save "$u\.claude\.credentials.json"           "$root\secrets\claude\.credentials.json"
Save "$u\.claude\settings.json"               "$root\secrets\claude\settings.json"
Save "$u\.claude\keybindings.json"            "$root\secrets\claude\keybindings.json"
Save "$u\.claude\statusline-command.sh"       "$root\secrets\claude\statusline-command.sh"
Save "$u\.grok\auth.json"                     "$root\secrets\grok\auth.json"
Save "$u\.grok\config.toml"                   "$root\secrets\grok\config.toml"
Save "$u\.grok\agent_id"                      "$root\secrets\grok\agent_id"
Save "$u\.gemini\oauth_creds.json"            "$root\secrets\gemini\oauth_creds.json"
Save "$u\.gemini\google_accounts.json"        "$root\secrets\gemini\google_accounts.json"
Save "$u\.gemini\settings.json"               "$root\secrets\gemini\settings.json"
Save "$u\.gemini\installation_id"             "$root\secrets\gemini\installation_id"
Save "$u\.gitconfig"                          "$root\secrets\git\.gitconfig"
# user-global Claude commands + plugins (not in repo)
if (Test-Path "$u\.claude\commands") { New-Item -ItemType Directory -Force -Path "$root\secrets\claude-global\commands" | Out-Null; robocopy "$u\.claude\commands" "$root\secrets\claude-global\commands" /E /R:1 /W:1 /NFL /NDL /NJH /NJS /NP | Out-Null; Write-Host "[OK]   ~/.claude/commands" }
if (Test-Path "$u\.claude\plugins")  { New-Item -ItemType Directory -Force -Path "$root\secrets\claude-global\plugins"  | Out-Null; robocopy "$u\.claude\plugins"  "$root\secrets\claude-global\plugins"  /E /R:1 /W:1 /NFL /NDL /NJH /NJS /NP | Out-Null; Write-Host "[OK]   ~/.claude/plugins" }
Save $PROFILE                                 "$root\secrets\powershell\Microsoft.PowerShell_profile.ps1"

# --- repo-local identity ---
Save "$ClaudeToolsRoot\.claude\identity.json"            "$root\identity\identity.json"
Save "$ClaudeToolsRoot\.claude\settings.local.json"      "$root\identity\settings.local.json"
Save "$ClaudeToolsRoot\.claude\current-mode"             "$root\identity\current-mode"
Save "$ClaudeToolsRoot\.claude\coord-broadcasts-seen"    "$root\identity\coord-broadcasts-seen"
Save "$ClaudeToolsRoot\.mcp.json"                        "$root\identity\mcp.json"
Save "$ClaudeToolsRoot\mcp-servers\ticktick\.tokens.json" "$root\identity\ticktick-tokens.json"
Save "$ClaudeToolsRoot\clients\dataforth\Oauth.txt"      "$root\identity\dataforth-oauth.txt"
if (Test-Path "$ClaudeToolsRoot\.claude\state") { Copy-Item "$ClaudeToolsRoot\.claude\state\*" "$root\identity\state\" -Recurse -Force -ErrorAction SilentlyContinue }

# --- bootstrap scripts (so the drive is self-contained) ---
Copy-Item "$ClaudeToolsRoot\.claude\bootstrap\*.ps1" "$root\bootstrap\" -Force -ErrorAction SilentlyContinue
Copy-Item "$ClaudeToolsRoot\.claude\bootstrap\RESTORE.md" "$root\bootstrap\" -Force -ErrorAction SilentlyContinue

# --- at-risk local WIP: stashes + untracked diffs that are on NO remote ---
# Written as UTF-8 (no BOM, LF) so restore-at-risk-work.ps1 / `git apply` can parse them.
# (Earlier ad-hoc captures used PowerShell `>` redirection = UTF-16, which git apply
#  rejects with "No valid patches in input" - hence the explicit byte-level write here.)
$awRoot = "$root\at-risk-work"
function Save-RepoStashes($repo,$label){
  if (-not (Test-Path "$repo\.git")) { return }
  $marks = @(& git -C $repo stash list --format='%gd' 2>$null)
  if (-not $marks) { return }
  $dir = "$awRoot\$label"; New-Item -ItemType Directory -Force -Path $dir | Out-Null
  $base = (& git -C $repo rev-parse HEAD 2>$null)
  [System.IO.File]::WriteAllText("$dir\BASE-COMMIT.txt", "$base`n", $Utf8NoBom)
  for ($i=0; $i -lt $marks.Count; $i++) {
    $files = @(& git -C $repo stash show --name-only "stash@{$i}" 2>$null)
    $slug  = if ($files.Count) { ([IO.Path]::GetFileNameWithoutExtension($files[0])) -replace '[^\w\-]','_' } else { "stash$i" }
    $lines = @(& git -C $repo --no-pager stash show -p "stash@{$i}" 2>$null)
    [System.IO.File]::WriteAllText("$dir\stash$i-$slug.patch", (($lines -join "`n") + "`n"), $Utf8NoBom)
    Write-Host "[OK]   at-risk stash: $label stash@{$i} -> stash$i-$slug.patch"
  }
}
Save-RepoStashes "$ClaudeToolsRoot\projects\msp-tools\guru-rmm"     'guru-rmm'
Save-RepoStashes "$ClaudeToolsRoot\projects\msp-tools\guru-connect" 'guru-connect'
# untracked working diffs (e.g. tmp-*.diff) that aren't committed anywhere
$gcRepo = "$ClaudeToolsRoot\projects\msp-tools\guru-connect"
if (Test-Path $gcRepo) {
  Get-ChildItem $gcRepo -Filter 'tmp-*.diff' -File -ErrorAction SilentlyContinue | ForEach-Object {
    $dir = "$awRoot\guru-connect"; New-Item -ItemType Directory -Force -Path $dir | Out-Null
    Copy-Item $_.FullName "$dir\$($_.Name)" -Force; Write-Host "[OK]   at-risk untracked diff: guru-connect\$($_.Name)"
  }
}

# --- manifests ---
$m = "$root\manifests"
$tools = 'node','npm','claude','gemini','grok','ollama','py','git','gh','jq','sops','age','cargo','rustc','code','op'
($tools | ForEach-Object { $c = Get-Command $_ -ErrorAction SilentlyContinue; if ($c) { $v = try { (& $_ --version 2>$null | Select-Object -First 1) } catch {''}; "{0,-10} {1,-55} {2}" -f $_,$c.Source,$v } else { "{0,-10} NOT INSTALLED" -f $_ } }) | Out-File "$m\installed-tools.txt" -Encoding utf8
ollama list 2>$null | Out-File "$m\ollama-models.txt" -Encoding utf8
git config --global --list | Out-File "$m\git-global-config.txt" -Encoding utf8
$ext = & code --list-extensions 2>$null; if ($ext) { $ext | Out-File "$m\vscode-extensions.txt" -Encoding utf8 }
foreach ($tn in "GrepAI Watcher - claudetools","ClaudeTools - Orphaned Session Detector","ClaudeTools - KSTEEN SmartBadge Daily") {
  $safe = ($tn -replace '[^\w\-]','_')
  try { Export-ScheduledTask -TaskName $tn 2>$null | Out-File "$m\scheduled-tasks\$safe.xml" -Encoding utf8 } catch {}
}
# user environment vars (.reg restorable + readable)
reg export "HKCU\Environment" "$m\user-environment.reg" /y 2>$null | Out-Null
(Get-Item 'HKCU:\Environment' | Select-Object -ExpandProperty Property | ForEach-Object { "{0}={1}" -f $_, (Get-ItemProperty 'HKCU:\Environment' -Name $_).$_ }) | Out-File "$m\user-environment.txt" -Encoding utf8

# --- machine config (Windows Terminal, hosts, repo-local real .env files) ---
New-Item -ItemType Directory -Force -Path "$root\config" | Out-Null
$wt = "$env:LOCALAPPDATA\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json"
if (Test-Path $wt) { Save $wt "$root\config\windows-terminal-settings.json" }
Save "$env:WINDIR\System32\drivers\etc\hosts" "$root\config\hosts"
Save "$ClaudeToolsRoot\projects\msp-tools\quote-wizard\frontend\.env.production" "$root\config\quote-wizard.frontend.env.production"

# --- large data (optional) ---
if ($IncludeData) {
  $base = "$DataDrive\claudetools-recovery\data"
  $xd = @('node_modules','.venv','venv','__pycache__','target','.grepai','.pytest_cache','dist','build')
  $xf = @('Thumbs.db','desktop.ini','*.pyc','*.mp3')  # radio-show MP3s live on IX Web Hosting - not backed up here
  $clusters = @(
    'clients\valleywide\app-modernization\source-analysis',
    'clients\grabb-durando\ai-demand-review',
    'projects\dataforth-dos\datasheet-pipeline',
    'projects\dataforth-dos\dfwds-research',
    'projects\radio-show\audio-processor'
  )
  Write-Host "=== copying large data to $base ===" -ForegroundColor Cyan
  foreach ($c in $clusters) {
    if (Test-Path "$ClaudeToolsRoot\$c") { robocopy "$ClaudeToolsRoot\$c" "$base\$c" /E /R:1 /W:1 /XD $xd /XF $xf /NFL /NDL /NP | Out-Null; Write-Host "[OK]   $c" }
  }
}

# --- mirror small bundle to the other drives ---
foreach ($d in $Drives) {
  if ($d -eq $primary) { continue }
  if (Test-Path "$d\") {
    Write-Host "=== mirroring bundle -> $d\claudetools-recovery ===" -ForegroundColor Cyan
    robocopy $root "$d\claudetools-recovery" /E /R:1 /W:1 /XD data /NFL /NDL /NP | Out-Null
    Write-Host "[OK]   mirrored to $d"
  }
}
Write-Host "`n[DONE] backup-to-bundle.ps1" -ForegroundColor Green
