<#
.SYNOPSIS
  ClaudeTools Windows bootstrap - rebuild a workstation after a clean OS reset.

.DESCRIPTION
  Installs every tool ClaudeTools needs, restores secrets + identity from the
  recovery bundle, clones the repos, wires up scheduled tasks, and verifies.
  Designed to be run top-to-bottom on a fresh Windows 11 install. Idempotent:
  re-running skips anything already present.

  ORDER OF OPERATIONS (each phase depends on the previous):
    0. Preflight        - winget, execution policy, UTF-8
    1. Core tooling     - git, node, python, rust, vscode, ollama, jq, sops, age, gh, op
    2. PATH refresh     - make freshly-installed tools callable this session
    3. AI CLIs          - claude (native), gemini (npm), grok (git-bash installer)
    4. Restore secrets  - age key, ssh, tool auth, git config, PS profile  [home group]
    5. Clone repos      - claudetools + vault + submodules
    6. Restore identity - identity.json, settings.local, .mcp.json, state    [repo group]
    7. Python deps      - pip installs for MCP servers / scripts
    8. Ollama models    - pull qwen/codestral/nomic (optional, large)
    9. Scheduled tasks  - GrepAI watcher, orphan detector, smartbadge
   10. Large data       - restore client data from bundle (optional)
   11. Verify           - onboarding diagnostic

.PARAMETER BundlePath
  Recovery bundle root (folder containing 'secrets'/'identity'). Auto-detect F:\ then E:\.

.PARAMETER SkipModels      Skip the multi-GB ollama model pulls.
.PARAMETER RestoreData     Also restore the large client data from <bundle>\data.
.PARAMETER GiteaHost       Gitea base URL. Default git.azcomputerguru.com (use 172.16.3.20:3000 on-network).
.PARAMETER OnlyPhases      Comma list of phase numbers to run (e.g. "1,2,3"). Default: all.

.EXAMPLE
  # full rebuild, skip giant model downloads for now
  .\windows-bootstrap.ps1 -SkipModels

.NOTES
  Run from an elevated PowerShell for cleanest winget machine-scope installs,
  though most packages also install at user scope without admin.
#>
[CmdletBinding()]
param(
  [string]$BundlePath,
  [switch]$SkipModels,
  [switch]$RestoreData,
  [string]$GiteaHost = 'https://git.azcomputerguru.com',
  [string]$ClaudeToolsRoot = 'D:\claudetools',
  [string]$VaultRoot = 'D:\vault',
  [string]$Hostname,          # target computer name; default = identity.json .machine, else GURU-5070
  [string]$OnlyPhases
)
$ErrorActionPreference = 'Stop'
$here = Split-Path -Parent $MyInvocation.MyCommand.Path

function Phase($n,$title){ if ($OnlyPhases -and ($OnlyPhases -split ',').Trim() -notcontains "$n") { return $false }; Write-Host "`n========== PHASE $n : $title ==========" -ForegroundColor Cyan; return $true }
function Info($m){ Write-Host "[INFO] $m" }
function Ok($m){ Write-Host "[OK]   $m" -ForegroundColor Green }
function Warn($m){ Write-Host "[WARN] $m" -ForegroundColor Yellow }
function Have($cmd){ [bool](Get-Command $cmd -ErrorAction SilentlyContinue) }
function Refresh-Path { $env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [Environment]::GetEnvironmentVariable('Path','User') }

function Find-Bundle {
  if ($BundlePath -and (Test-Path "$BundlePath\secrets")) { return $BundlePath }
  foreach ($d in 'F:','E:','D:') { if (Test-Path "$d\claudetools-recovery\secrets") { return "$d\claudetools-recovery" } }
  return $null
}

# ============================================================ PHASE 0
if (Phase 0 'Preflight') {
  [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
  try { Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force } catch {}
  if (-not (Have winget)) { throw "winget not found. Install 'App Installer' from the Microsoft Store, then re-run." }
  Ok "winget present: $((Get-Command winget).Source)"
  $script:Bundle = Find-Bundle
  if ($script:Bundle) { Ok "recovery bundle: $script:Bundle" } else { Warn "no recovery bundle found - secret/identity restore phases will be skipped" }

  # Hostname - a fresh Windows install is DESKTOP-xxxxx; identity.json + scheduled tasks
  # + coord session IDs all expect the real name. Rename needs admin and a reboot to apply.
  $target = $Hostname
  if (-not $target -and $script:Bundle -and (Test-Path "$script:Bundle\identity\identity.json")) {
    try { $target = (Get-Content "$script:Bundle\identity\identity.json" -Raw | ConvertFrom-Json).machine } catch {}
  }
  if (-not $target) { $target = 'GURU-5070' }
  if ($env:COMPUTERNAME -ne $target) {
    $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
    if ($isAdmin) {
      try { Rename-Computer -NewName $target -Force -ErrorAction Stop; $script:RebootNeeded = $true; Ok "hostname: $env:COMPUTERNAME -> $target (takes effect after reboot)" }
      catch { Warn "rename to '$target' failed: $($_.Exception.Message)" }
    } else { Warn "hostname is '$env:COMPUTERNAME', target '$target' - run this script as Administrator to rename (or manually: Rename-Computer -NewName $target -Restart)" }
  } else { Ok "hostname already '$target'" }
}

# ============================================================ PHASE 1
if (Phase 1 'Core tooling (winget)') {
  $pkgs = @(
    @{id='Git.Git';                     cmd='git'},
    @{id='OpenJS.NodeJS.LTS';           cmd='node'},
    @{id='Python.Python.3.14';          cmd='py'},
    @{id='Rustlang.Rustup';             cmd='cargo'},
    @{id='Microsoft.VisualStudioCode';  cmd='code'},
    @{id='Ollama.Ollama';               cmd='ollama'},
    @{id='jqlang.jq';                   cmd='jq'},
    @{id='SecretsOPerationS.SOPS';      cmd='sops'},
    @{id='FiloSottile.age';             cmd='age'},
    @{id='GitHub.cli';                  cmd='gh'},
    @{id='AgileBits.1Password.CLI';     cmd='op'},
    @{id='Microsoft.DotNet.SDK.8';      cmd='dotnet'},   # MSI builds / wix
    @{id='Google.Protobuf';             cmd='protoc'},   # gururmm prost builds (PROTOC env)
    @{id='oschwartz10612.Poppler';      cmd='pdftoppm'}, # dataforth datasheet PDF pipeline
    @{id='Tailscale.Tailscale';         cmd='tailscale'} # fleet connectivity (100.x mesh)
  )
  foreach ($p in $pkgs) {
    if (Have $p.cmd) { Ok "$($p.cmd) already installed"; continue }
    Info "installing $($p.id) ..."
    winget install --id $p.id --exact --silent --accept-package-agreements --accept-source-agreements --disable-interactivity
    if ($LASTEXITCODE -ne 0) { Warn "winget returned $LASTEXITCODE for $($p.id) (may already be installed or need elevation)" }
  }
  Refresh-Path
}

# ============================================================ PHASE 2
if (Phase 2 'PATH refresh') {
  Refresh-Path
  foreach ($c in 'git','node','npm','py','cargo','jq','sops','age','gh','op','ollama','code','dotnet','protoc','tailscale') {
    if (Have $c) { Ok "$c -> $((Get-Command $c).Source)" } else { Warn "$c still not on PATH (open a new shell after install)" }
  }
  # PROTOC env var for Rust prost builds (path is version-specific, so resolve it live)
  $protoc = (Get-Command protoc -ErrorAction SilentlyContinue).Source
  if ($protoc) { [Environment]::SetEnvironmentVariable('PROTOC',$protoc,'User'); $env:PROTOC=$protoc; Ok "PROTOC=$protoc" }
}

# ============================================================ PHASE 3
if (Phase 3 'AI CLIs') {
  # Claude Code - official native installer -> %USERPROFILE%\.local\bin\claude.exe
  if (Have claude) { Ok "claude already installed" } else {
    Info "installing Claude Code (native installer)"
    try { irm https://claude.ai/install.ps1 | iex } catch { Warn "claude install failed: $_  (manual: irm https://claude.ai/install.ps1 | iex)" }
  }
  # Gemini CLI - npm global
  if (Have gemini) { Ok "gemini already installed" } else {
    Info "installing @google/gemini-cli"
    npm install -g @google/gemini-cli
  }
  # Grok CLI - xAI installer (bash; needs Git Bash from Phase 1)
  if (Have grok) { Ok "grok already installed" } else {
    $bash = 'C:\Program Files\Git\bin\bash.exe'
    if (Test-Path $bash) { Info "installing grok via $bash"; & $bash -lc "curl -fsSL https://x.ai/cli/install.sh | bash" }
    else { Warn "Git Bash not found; install Git first, then: bash -c 'curl -fsSL https://x.ai/cli/install.sh | bash'" }
  }
  Refresh-Path
  $env:Path += ";$env:USERPROFILE\.local\bin;$env:USERPROFILE\.grok\bin;$env:APPDATA\npm"
  # Persist the AI-CLI dirs to the User PATH so claude/grok/gemini stay callable in
  # every new shell (their installers don't always add these; grok especially is a
  # bare ~\.grok\bin drop that was session-only after the 2026-06-06 rebuild).
  $userPath = [Environment]::GetEnvironmentVariable('Path','User')
  foreach ($d in "$env:USERPROFILE\.local\bin", "$env:USERPROFILE\.grok\bin", "$env:APPDATA\npm") {
    if ((Test-Path $d) -and ($userPath -notmatch [regex]::Escape($d))) { $userPath = $userPath.TrimEnd(';') + ";$d" }
  }
  [Environment]::SetEnvironmentVariable('Path', $userPath, 'User')
  Ok "AI-CLI dirs persisted to User PATH"
}

# ============================================================ PHASE 4
if (Phase 4 'Restore home secrets + machine config') {
  if ($script:Bundle) {
    & "$here\restore-secrets.ps1" -BundlePath $script:Bundle -Group home

    # Stable machine env vars (NOT a blanket reg import - the saved PATH has stale
    # version-pinned winget paths. user-environment.reg is kept as reference only.)
    [Environment]::SetEnvironmentVariable('OLLAMA_MODELS','D:\OllamaModels','User'); $env:OLLAMA_MODELS='D:\OllamaModels'
    [Environment]::SetEnvironmentVariable('OLLAMA_HOST','0.0.0.0:11434','User');     $env:OLLAMA_HOST='0.0.0.0:11434'
    Ok "set OLLAMA_MODELS=D:\OllamaModels, OLLAMA_HOST=0.0.0.0:11434"

    # Windows Terminal settings
    $wtDst = "$env:LOCALAPPDATA\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json"
    if (Test-Path "$script:Bundle\config\windows-terminal-settings.json") {
      $p = Split-Path $wtDst -Parent
      if (Test-Path $p) { Copy-Item "$script:Bundle\config\windows-terminal-settings.json" $wtDst -Force; Ok "Windows Terminal settings restored" }
      else { Warn "Windows Terminal not installed yet - restore its settings.json later from config\" }
    }

    # hosts file (fleet Tailscale MagicDNS entries) - needs admin; merge note only
    if (Test-Path "$script:Bundle\config\hosts") {
      Warn "fleet hosts entries are in config\hosts - merge into $env:WINDIR\System32\drivers\etc\hosts as admin if Tailscale MagicDNS isn't resolving"
    }
  }
  else { Warn "no bundle - skipping. Restore the SOPS age key + SSH keys manually or the vault will not decrypt." }
}

# ============================================================ PHASE 5
if (Phase 5 'Clone repos') {
  if (-not (Test-Path "$ClaudeToolsRoot\.git")) {
    Info "cloning claudetools -> $ClaudeToolsRoot"
    git clone "$GiteaHost/azcomputerguru/claudetools.git" $ClaudeToolsRoot
    Push-Location $ClaudeToolsRoot
    Info "initializing submodules (gururmm / guruconnect)"
    git submodule update --init --recursive
    Pop-Location
  } else { Ok "claudetools repo already present" }

  if (-not (Test-Path "$VaultRoot\.git")) {
    Info "cloning vault -> $VaultRoot"
    git clone "$GiteaHost/azcomputerguru/vault.git" $VaultRoot
  } else { Ok "vault repo already present" }

  # safe.directory entries (mirror the prior machine)
  foreach ($d in $ClaudeToolsRoot,$VaultRoot,"$ClaudeToolsRoot/projects/msp-tools/guru-rmm") {
    git config --global --add safe.directory ($d -replace '\\','/') 2>$null
  }
}

# ============================================================ PHASE 6
if (Phase 6 'Restore repo-local identity + at-risk WIP') {
  if ($script:Bundle) {
    & "$here\restore-secrets.ps1" -BundlePath $script:Bundle -Group repo -ClaudeToolsRoot $ClaudeToolsRoot
    # Recreate local-only WIP (guru-rmm stashes, guru-connect untracked diff) that
    # would otherwise have been lost - faithfully puts the stashes back as stashes.
    & "$here\restore-at-risk-work.ps1" -BundlePath $script:Bundle -ClaudeToolsRoot $ClaudeToolsRoot
  }
  else { Warn "no bundle - you must hand-create .claude/identity.json (see CLAUDE.md multi-user section)" }

  # Non-interactive git auth (Mike's hard requirement: git must NEVER hang on a
  # Git Credential Manager password prompt). setup-git-auth.sh primes the `store`
  # credential helper from the vault Gitea token, scoped to each repo's actual remote
  # host. Needs the age key (Phase 4) + identity.json (above) + vault repo (Phase 5).
  # Idempotent + fail-silent; also runs from the SessionStart hook in settings.json.
  $ghauth = "$ClaudeToolsRoot\.claude\scripts\setup-git-auth.sh"
  $gbash  = 'C:\Program Files\Git\bin\bash.exe'
  if ((Test-Path $ghauth) -and (Test-Path $gbash)) {
    Info "priming non-interactive git auth (vault token -> credential store)"
    & $gbash "$ghauth"
    Ok "git credential store primed; GIT_TERMINAL_PROMPT=0 enforced via .claude/settings.json env"
  } else { Warn "setup-git-auth.sh or Git Bash missing - prime git creds manually so pushes don't prompt" }
}

# ============================================================ PHASE 7
if (Phase 7 'Python deps + .NET tools') {
  # WiX toolset (MSI builds, e.g. gururmm agent) - dotnet global tool
  if (Have dotnet) {
    if (dotnet tool list --global 2>$null | Select-String '\bwix\b') { Ok "wix tool already installed" }
    else { Info "installing wix dotnet tool"; dotnet tool install --global wix 2>$null }
  }
  # IMPORTANT: ClaudeTools uses TWO python interpreters on Windows and they must
  # BOTH have the deps, or pieces silently break:
  #   - `py`     -> Python 3.14 : vault yaml-query.py (get-field), helper/skill
  #                               scripts, scheduled tasks (detect_orphaned_sessions)
  #   - `python` -> Python 3.12 : the interpreter `.mcp.json` launches the MCP
  #                               servers with (ticktick needs httpx + mcp)
  # Installing into only one leaves the other broken (the 2026-06-06 rebuild shipped
  # with ticktick MCP dead = no httpx/mcp in 3.12, and vault get-field dead = no
  # PyYAML in 3.14). De-dupe by real sys.executable so a single install isn't run twice.
  $interps = @(); $seen = @{}
  foreach ($cand in 'py','python','python3') {
    if (Have $cand) {
      $real = (& $cand -c "import sys;print(sys.executable)" 2>$null)
      if ($real -and -not $seen[$real]) { $seen[$real] = $true; $interps += $cand }
    }
  }
  if (-not $interps) { Warn "no python interpreter found - skip python deps" }
  else {
    $reqs = Get-ChildItem $ClaudeToolsRoot -Recurse -Filter 'requirements*.txt' -ErrorAction SilentlyContinue |
            Where-Object { $_.FullName -notmatch '\\(node_modules|\.venv|venv|target)\\' }
    # baseline libs used by helper scripts / MCP / vault across the harness
    $baseline = @('requests','paramiko','mcp','httpx','pyyaml','websocket-client')
    foreach ($ic in $interps) {
      Info "[$ic] upgrading pip"; & $ic -m pip install --upgrade pip 2>$null
      foreach ($r in $reqs) { Info "[$ic] pip install -r $($r.Name)"; & $ic -m pip install -r $r.FullName 2>$null }
      Info "[$ic] baseline libs"; & $ic -m pip install @baseline 2>$null
    }
    Ok "python deps installed into: $($interps -join ', ') (best-effort)"
  }
}

# ============================================================ PHASE 8
if (Phase 8 'Ollama models') {
  # Expected model set for THIS machine (identity.json prose_model + OLLAMA.md routing):
  #   nomic-embed-text - REQUIRED for GrepAI semantic search (embeddings)
  #   qwen3:8b         - prose_model           qwen3:14b      - heavier prose
  #   codestral:22b    - code suggestions      qwen3.6:latest - structured/JSON + classify
  # All five live on D:\OllamaModels (~48 GB) and SURVIVE an OS reset when D: is intact,
  # so a normal rebuild pulls NOTHING. Only a wiped D: triggers the full re-download.
  $models = @('nomic-embed-text:latest','qwen3:8b','qwen3:14b','codestral:22b','qwen3.6:latest')
  if ($SkipModels) { Warn "-SkipModels set, skipping model pulls" }
  elseif (Have ollama) {
    if (-not $env:OLLAMA_MODELS) { [Environment]::SetEnvironmentVariable('OLLAMA_MODELS','D:\OllamaModels','User'); $env:OLLAMA_MODELS='D:\OllamaModels' }
    # GOTCHA (2026-06-06): right after login `ollama list` can return EMPTY even though
    # D:\OllamaModels is fully populated - the tray app's server needs a few seconds to
    # hydrate its model-list cache. Do NOT treat an empty list as "models gone" or you
    # re-download 48 GB for nothing. If manifests are on disk, restart + wait first.
    $listed = (ollama list 2>$null | Out-String).Trim() -split "`n" | Select-Object -Skip 1
    if ((Test-Path 'D:\OllamaModels\manifests') -and -not $listed) {
      Warn "ollama list empty but D:\OllamaModels populated - restarting ollama, waiting for hydration"
      Get-Process 'ollama','ollama app' -ErrorAction SilentlyContinue | Stop-Process -Force; Start-Sleep 2
      $oapp = "$env:LOCALAPPDATA\Programs\Ollama\ollama app.exe"
      if (Test-Path $oapp) { Start-Process $oapp } else { Start-Process ollama -ArgumentList 'serve' -WindowStyle Hidden }
      Start-Sleep 10
    }
    $have = (ollama list 2>$null | Out-String)
    foreach ($m in $models) {
      $short = $m -replace ':latest$',''
      if ($have -match [regex]::Escape($short)) { Ok "$m already present on D:\OllamaModels (no download)" }
      else { Info "ollama pull $m"; ollama pull $m }
    }
  } else { Warn "ollama missing - skip" }
}

# ============================================================ PHASE 9
if (Phase 9 'Scheduled tasks') {
  $tdir = "$script:Bundle\manifests\scheduled-tasks"
  if ($script:Bundle -and (Test-Path $tdir)) {
    Get-ChildItem $tdir -Filter *.xml | ForEach-Object {
      $name = ($_.BaseName -replace '_',' ')
      try {
        $xml = Get-Content $_.FullName -Raw
        Register-ScheduledTask -TaskName $name -Xml $xml -Force -ErrorAction Stop | Out-Null
        Ok "registered task: $name"
      } catch { Warn "task '$name' import failed: $($_.Exception.Message) (paths/user may differ - re-create manually)" }
    }
  } else { Warn "no exported tasks in bundle - skip (see manifests\scheduled-tasks)" }
}

# ============================================================ PHASE 10
if (Phase 10 'Large client data (optional)') {
  if ($RestoreData -and $script:Bundle -and (Test-Path "$script:Bundle\data")) {
    Info "restoring large data $script:Bundle\data -> $ClaudeToolsRoot"
    robocopy "$script:Bundle\data" $ClaudeToolsRoot /E /R:1 /W:1 /NFL /NDL /NP | Out-Null
    Ok "large data restored"
  } else { Warn "skipped (pass -RestoreData to restore client data clusters)" }
}

# ============================================================ PHASE 11
if (Phase 11 'Verify') {
  $diag = "$ClaudeToolsRoot\.claude\scripts\onboarding-diagnostic.ps1"
  if (Test-Path $diag) { Info "running onboarding diagnostic"; & $diag }
  else { Warn "diagnostic not found - run '/self-check' inside Claude Code to verify wiring" }
  Write-Host "`n[NEXT] Interactive logins that may need a refresh (tokens expire):" -ForegroundColor Cyan
  Write-Host "   claude  (if .credentials.json expired: run 'claude' and /login)"
  Write-Host "   gh auth login        op signin        gemini (browser)        grok login"
  Write-Host "   Verify vault: bash $ClaudeToolsRoot/.claude/scripts/vault.sh list"
}

if ($script:RebootNeeded) {
  Write-Host "`n[REBOOT] Hostname was changed to '$target' - REBOOT for it to take effect." -ForegroundColor Yellow
  Write-Host "         (scheduled tasks + coord session IDs read the hostname, so reboot before relying on them)"
}
Write-Host "`n[DONE] windows-bootstrap.ps1 complete." -ForegroundColor Green
