#requires -Version 5

[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)]
    [string[]]$Capability,

    [switch]$SkipRefresh,

    [switch]$StartServices,

    [ValidateSet('Claude', 'Codex', 'Both')]
    [string]$McpHostTarget = 'Both'
)

# 临时目录统一入口（$env:TEMP 在 Linux/macOS 上可能未设置）
$tmpBase = if ($env:TEMP) { $env:TEMP } else { [System.IO.Path]::GetTempPath() }

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false)
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
$OutputEncoding = [System.Text.UTF8Encoding]::new($false)

. (Join-Path $PSScriptRoot 'lib\ToolDiscovery.ps1')

$Capability = @(
    foreach ($item in @($Capability)) {
        if ([string]::IsNullOrWhiteSpace($item)) {
            continue
        }
        foreach ($name in ($item -split ',')) {
            $trimmed = $name.Trim()
            if (-not [string]::IsNullOrWhiteSpace($trimmed)) {
                $trimmed
            }
        }
    }
) | Select-Object -Unique

function Get-FirstCommandPath {
    param(
        [string[]]$Names,
        [switch]$PreferApplication
    )

    foreach ($name in $Names) {
        $commands = @(Get-Command $name -All -ErrorAction SilentlyContinue)
        if ($PreferApplication) {
            $preferred = $commands | Where-Object { $_.CommandType -eq 'Application' } | Select-Object -First 1
            if ($preferred) {
                return $preferred.Source
            }
        }
        $cmd = $commands | Select-Object -First 1
        if ($cmd) {
            return $cmd.Source
        }
    }
    return ''
}

function Get-PreferredPowerShellPath {
    $pwsh = Get-FirstCommandPath -Names @('pwsh.exe', 'pwsh') -PreferApplication
    if (-not [string]::IsNullOrWhiteSpace($pwsh)) {
        return $pwsh
    }
    return Get-FirstCommandPath -Names @('powershell.exe', 'powershell') -PreferApplication
}

function Get-NodeCommandPath {
    param([Parameter(Mandatory = $true)][string]$Name)

    return Get-FirstCommandPath -Names @("$Name.cmd", "$Name.exe", $Name) -PreferApplication
}

function Test-SuccessExitCode {
    param([int]$ExitCode)

    return $ExitCode -in @(0, 3010, 1641)
}

function Get-McpHostTargets {
    switch ($McpHostTarget) {
        'Claude' { return @('Claude') }
        'Codex' { return @('Codex') }
        default { return @('Claude', 'Codex') }
    }
}

function Test-ReverseIsWindows {
    return $env:OS -eq 'Windows_NT'
}

function Test-ReverseIsElevated {
    if (-not (Test-ReverseIsWindows)) {
        return $true
    }

    $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
    $principal = New-Object Security.Principal.WindowsPrincipal($identity)
    return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}

function Ensure-WingetPackage {
    param(
        [Parameter(Mandatory = $true)][string]$Id,
        [Parameter(Mandatory = $true)][string]$Label,
        [string]$Override
    )

    $winget = Get-FirstCommandPath -Names @('winget')
    if ([string]::IsNullOrWhiteSpace($winget)) {
        throw "Cannot auto-install $Label because winget is not available."
    }

    $arguments = @(
        'install',
        '--id', $Id,
        '--source', 'winget',
        '--accept-package-agreements',
        '--accept-source-agreements',
        '--disable-interactivity'
    )
    if (-not [string]::IsNullOrWhiteSpace($Override)) {
        $arguments += @('--override', $Override)
    }

    & $winget @arguments
    if (-not (Test-SuccessExitCode -ExitCode $LASTEXITCODE)) {
        throw "winget install failed for $Label ($Id)."
    }
}

function Ensure-NodeRuntime {
    if (-not (Get-FirstCommandPath -Names @('node'))) {
        Ensure-WingetPackage -Id 'OpenJS.NodeJS.22' -Label 'Node.js 22'
    }
    if (-not (Get-NodeCommandPath -Name 'npx')) {
        Ensure-WingetPackage -Id 'OpenJS.NodeJS.22' -Label 'Node.js 22'
    }
}

function Ensure-PythonRuntime {
    if (-not (Get-FirstCommandPath -Names @('python', 'python3'))) {
        Ensure-WingetPackage -Id 'Python.Python.3.13' -Label 'Python 3.13'
    }
}

function Ensure-JavaRuntime {
    if (-not (Get-FirstCommandPath -Names @('java'))) {
        Ensure-WingetPackage -Id 'Microsoft.OpenJDK.21' -Label 'OpenJDK 21'
    }
}

function Ensure-Pnpm {
    Ensure-NodeRuntime
    if (-not (Get-NodeCommandPath -Name 'pnpm')) {
        $npm = Get-NodeCommandPath -Name 'npm'
        if ([string]::IsNullOrWhiteSpace($npm)) {
            throw 'npm is not available after Node.js installation.'
        }
        & $npm install -g pnpm
        if ($LASTEXITCODE -ne 0) {
            throw 'Failed to install pnpm globally.'
        }
    }
}

function Get-AnythingAnalyzerUserDataPaths {
    $candidates = @(
        (Join-Path $env:APPDATA 'anything-analyzer'),
        (Join-Path $env:APPDATA 'Anything Analyzer')
    )

    $existing = @($candidates | Where-Object { Test-Path -LiteralPath $_ -PathType Container })
    if ($existing.Count -gt 0) {
        return @($existing | Sort-Object -Unique)
    }

    return @($candidates | Sort-Object -Unique)
}

function Ensure-AnythingAnalyzerMcpConfig {
    param([int]$Port = 23816)

    $token = ''
    foreach ($userDataPath in Get-AnythingAnalyzerUserDataPaths) {
        $configPath = Join-Path $userDataPath 'mcp-server-config.json'
        if (-not (Test-Path -LiteralPath $configPath)) {
            continue
        }
        try {
            $existing = Get-Content -LiteralPath $configPath -Raw -Encoding utf8 | ConvertFrom-Json
            if (-not [string]::IsNullOrWhiteSpace([string]$existing.authToken)) {
                $token = [string]$existing.authToken
                break
            }
        }
        catch {
            $token = ''
        }
    }
    if ([string]::IsNullOrWhiteSpace($token)) {
        $tokenBytes = New-Object byte[] 32
        $rng = [Security.Cryptography.RandomNumberGenerator]::Create()
        try {
            $rng.GetBytes($tokenBytes)
        }
        finally {
            $rng.Dispose()
        }
        $token = [Convert]::ToBase64String($tokenBytes)
    }

    $payload = [ordered]@{
        enabled     = $true
        port        = $Port
        authEnabled = $true
        authToken   = $token
    }

    foreach ($userDataPath in Get-AnythingAnalyzerUserDataPaths) {
        if (-not (Test-Path -LiteralPath $userDataPath)) {
            New-Item -ItemType Directory -Path $userDataPath -Force | Out-Null
        }
        $configPath = Join-Path $userDataPath 'mcp-server-config.json'
        $payload | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $configPath -Encoding utf8
    }

    return $token
}

function Test-VsBuildToolsInstalled {
    $roots = @(
        (Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\2022\BuildTools'),
        (Join-Path $env:ProgramFiles 'Microsoft Visual Studio\2022\BuildTools')
    ) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }

    foreach ($root in $roots) {
        $vcToolsPath = Join-Path $root 'VC\Tools\MSVC'
        if (Test-Path -LiteralPath $vcToolsPath -PathType Container) {
            return $true
        }
    }

    return $false
}

function Ensure-VsBuildTools {
    if (Test-VsBuildToolsInstalled) {
        return
    }

    $override = '--wait --quiet --norestart --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended'
    try {
        Ensure-WingetPackage -Id 'Microsoft.VisualStudio.2022.BuildTools' -Label 'Visual Studio Build Tools 2022' -Override $override
    }
    catch {
        if (-not (Test-ReverseIsElevated)) {
            throw "$($_.Exception.Message) Current PowerShell process is not elevated; Visual Studio Build Tools may require an administrator/UAC install."
        }
        throw
    }

    if (-not (Test-VsBuildToolsInstalled)) {
        throw 'Visual Studio Build Tools installed command completed, but the C++ workload was not detected.'
    }
}

function Test-AnythingAnalyzerElectronHealthy {
    param(
        [Parameter(Mandatory = $true)][string]$RepoDir,
        [Parameter(Mandatory = $true)][string]$PnpmPath
    )

    Push-Location $RepoDir
    try {
        & $PnpmPath exec electron --version *> $null
        return $LASTEXITCODE -eq 0
    }
    catch {
        return $false
    }
    finally {
        Pop-Location
    }
}

function Set-AnythingAnalyzerPnpmBuildApprovals {
    param(
        [Parameter(Mandatory = $true)][string]$RepoDir,
        [Parameter(Mandatory = $true)][string[]]$Packages
    )

    $workspacePath = Join-Path $RepoDir 'pnpm-workspace.yaml'
    $lines = @()
    if (Test-Path -LiteralPath $workspacePath) {
        $lines = @(Get-Content -LiteralPath $workspacePath)
    }

    $existingPackages = New-Object System.Collections.Generic.List[string]
    $preserved = New-Object System.Collections.Generic.List[string]
    $skipOnlyBuilt = $false

    foreach ($line in $lines) {
        if ($line -match '^\s*onlyBuiltDependencies\s*:') {
            $skipOnlyBuilt = $true
            continue
        }

        if ($skipOnlyBuilt) {
            if ($line -match '^\s*-\s*(.+?)\s*$') {
                $existingPackages.Add($matches[1].Trim("'`""))
                continue
            }
            if ($line -match '^\s+' -or [string]::IsNullOrWhiteSpace($line)) {
                continue
            }
            $skipOnlyBuilt = $false
        }

        $preserved.Add($line)
    }

    while ($preserved.Count -gt 0 -and [string]::IsNullOrWhiteSpace($preserved[$preserved.Count - 1])) {
        $preserved.RemoveAt($preserved.Count - 1)
    }

    $approvedPackages = @($existingPackages + @($Packages)) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Sort-Object -Unique
    if ($preserved.Count -gt 0) {
        $preserved.Add('')
    }
    $preserved.Add('onlyBuiltDependencies:')
    foreach ($package in $approvedPackages) {
        $preserved.Add("  - $package")
    }

    Set-Content -LiteralPath $workspacePath -Value @($preserved) -Encoding utf8
}

function Approve-AnythingAnalyzerBuildScripts {
    param(
        [Parameter(Mandatory = $true)][string]$RepoDir,
        [Parameter(Mandatory = $true)][string]$PnpmPath
    )

    $buildPackages = @('electron', 'esbuild', 'better-sqlite3')

    Push-Location $RepoDir
    try {
        $approveExitCode = 1
        try {
            $approveOutput = & $PnpmPath approve-builds --all 2>&1
            $approveExitCode = $LASTEXITCODE
        }
        catch {
            $approveOutput = $_.Exception.Message
            $approveExitCode = 1
        }

        if ($approveExitCode -eq 0) {
            return
        }

        Write-Warning 'pnpm approve-builds --all is unavailable or failed; writing pnpm-workspace.yaml build approvals directly.'
    }
    finally {
        Pop-Location
    }

    Set-AnythingAnalyzerPnpmBuildApprovals -RepoDir $RepoDir -Packages $buildPackages
}

function Get-GitHubLatestReleaseAsset {
    param(
        [Parameter(Mandatory = $true)][string]$Repo,
        [Parameter(Mandatory = $true)][string]$AssetRegex,
        [string]$ReleaseTag = ''
    )

    if (-not [string]::IsNullOrWhiteSpace($ReleaseTag)) {
        $uri = "https://api.github.com/repos/$Repo/releases/tags/$ReleaseTag"
    }
    else {
        $uri = "https://api.github.com/repos/$Repo/releases/latest"
    }
    $release = Invoke-RestMethod -Uri $uri -Headers @{ 'User-Agent' = 'reverse-skill-bootstrap' }
    $asset = @($release.assets) | Where-Object { $_.name -match $AssetRegex } | Select-Object -First 1
    if ($null -eq $asset) {
        throw "No release asset matched $AssetRegex for $Repo (tag=$ReleaseTag)"
    }
    return $asset
}

function Get-FileSha256Hex {
    param([Parameter(Mandatory = $true)][string]$Path)
    return (Get-FileHash -Algorithm SHA256 -LiteralPath $Path).Hash.ToLowerInvariant()
}

function Assert-DownloadedFileIntegrity {
    param(
        [Parameter(Mandatory = $true)][string]$Path,
        $Definition = $null,
        $Asset = $null
    )

    if (-not (Test-Path -LiteralPath $Path)) {
        throw "Integrity check failed: file missing $Path"
    }

    $actual = Get-FileSha256Hex -Path $Path
    $expected = $null
    $source = $null

    if ($null -ne $Definition -and $Definition.PSObject.Properties['assetSha256'] -and -not [string]::IsNullOrWhiteSpace([string]$Definition.assetSha256)) {
        $expected = ([string]$Definition.assetSha256 -replace '^(?i)sha256:', '').Trim().ToLowerInvariant()
        $source = 'manifest.assetSha256'
    }
    elseif ($null -ne $Asset -and $Asset.PSObject.Properties['digest'] -and -not [string]::IsNullOrWhiteSpace([string]$Asset.digest)) {
        $expected = ([string]$Asset.digest -replace '^(?i)sha256:', '').Trim().ToLowerInvariant()
        $source = 'github.api.digest'
    }

    if (-not [string]::IsNullOrWhiteSpace($expected)) {
        if ($actual -ne $expected) {
            Remove-Item -LiteralPath $Path -Force -ErrorAction SilentlyContinue
            throw "SHA256 mismatch for $(Split-Path -Leaf $Path) (via $source): expected $expected got $actual — file deleted"
        }
        Write-Host ("[integrity] SHA256 OK ({0}): {1}" -f $source, $actual) -ForegroundColor Green
        return $actual
    }

    Write-Warning ("[integrity] No pinned digest for {0}; recorded sha256={1} (supply-chain residual — prefer assetSha256 in manifest)" -f (Split-Path -Leaf $Path), $actual)
    return $actual
}

function Expand-ArchiveIntoDirectory {
    param(
        [Parameter(Mandatory = $true)][string]$ZipPath,
        [Parameter(Mandatory = $true)][string]$Destination
    )

    $tempExtract = Join-Path $tmpBase ("reverse-bootstrap-" + [System.Guid]::NewGuid().ToString('N'))
    New-Item -ItemType Directory -Path $tempExtract -Force | Out-Null
    Expand-Archive -LiteralPath $ZipPath -DestinationPath $tempExtract -Force

    if (Test-Path -LiteralPath $Destination) {
        Remove-Item -LiteralPath $Destination -Recurse -Force
    }
    New-Item -ItemType Directory -Path $Destination -Force | Out-Null

    $children = Get-ChildItem -LiteralPath $tempExtract
    if ($children.Count -eq 1 -and $children[0].PSIsContainer) {
        $sourceDir = $children[0].FullName
    }
    else {
        $sourceDir = $tempExtract
    }

    Get-ChildItem -LiteralPath $sourceDir -Force | ForEach-Object {
        Move-Item -LiteralPath $_.FullName -Destination $Destination -Force
    }

    Remove-Item -LiteralPath $tempExtract -Recurse -Force
}

function Ensure-DownloadDirectory {
    param([Parameter(Mandatory = $true)][string]$Path)

    if (-not (Test-Path -LiteralPath $Path)) {
        New-Item -ItemType Directory -Path $Path -Force | Out-Null
    }
}

function Ensure-GitHubZipInstall {
    param(
        [Parameter(Mandatory = $true)]$Definition,
        [Parameter(Mandatory = $true)][string]$TargetPath,
        [Parameter(Mandatory = $true)][string]$VerifyName
    )

    $existing = Resolve-ReverseToolSpec -Name $VerifyName
    if ($existing.Available) {
        return $existing
    }

    $releaseTag = if ($Definition.PSObject.Properties['releaseTag']) { [string]$Definition.releaseTag } else { '' }
    $asset = Get-GitHubLatestReleaseAsset -Repo $Definition.repo -AssetRegex $Definition.assetRegex -ReleaseTag $releaseTag
    $downloadUrl = if ($asset.PSObject.Properties['browser_download_url']) { $asset.browser_download_url } else { $asset.url }
    $downloadPath = Join-Path $tmpBase $asset.name
    Invoke-WebRequest -Uri $downloadUrl -OutFile $downloadPath -Headers @{ 'Accept' = 'application/octet-stream' }
    Assert-DownloadedFileIntegrity -Path $downloadPath -Definition $Definition -Asset $asset | Out-Null
    Ensure-DownloadDirectory -Path (Split-Path -Path $TargetPath -Parent)
    Expand-ArchiveIntoDirectory -ZipPath $downloadPath -Destination $TargetPath
    Remove-Item -LiteralPath $downloadPath -Force

    # Refresh PATH so newly installed tools are discoverable
    $binCandidates = @(
        (Join-Path $TargetPath 'bin'),
        $TargetPath
    )
    foreach ($binDir in $binCandidates) {
        if ((Test-Path -LiteralPath $binDir) -and ($env:PATH -notlike "*$binDir*")) {
            $env:PATH = "$binDir;$env:PATH"
        }
    }

    return (Resolve-ReverseToolSpec -Name $VerifyName)
}

function Ensure-ApktoolInstall {
    param([Parameter(Mandatory = $true)]$Definition)

    $existing = Resolve-ReverseToolSpec -Name 'apktool'
    if ($existing.Available) {
        return $existing
    }

    Ensure-JavaRuntime
    $releaseTag = if ($Definition.PSObject.Properties['releaseTag']) { [string]$Definition.releaseTag } else { '' }
    $asset = Get-GitHubLatestReleaseAsset -Repo $Definition.repo -AssetRegex $Definition.assetRegex -ReleaseTag $releaseTag
    $installDir = $Definition.installDir
    Ensure-DownloadDirectory -Path $installDir

    $jarName = [System.IO.Path]::GetFileName($asset.name)
    $jarPath = Join-Path $installDir 'apktool.jar'
    $downloadUrl = if ($asset.PSObject.Properties['browser_download_url']) { $asset.browser_download_url } else { $asset.url }
    Invoke-WebRequest -Uri $downloadUrl -OutFile $jarPath -Headers @{ 'Accept' = 'application/octet-stream' }
    Assert-DownloadedFileIntegrity -Path $jarPath -Definition $Definition -Asset $asset | Out-Null

    $wrapperPath = Join-Path $installDir $Definition.wrapperName
    @(
        '@echo off',
        'setlocal',
        'java -jar "%~dp0apktool.jar" %*'
    ) | Set-Content -LiteralPath $wrapperPath -Encoding ascii

    # Add to PATH so discovery can find it immediately
    if ($env:PATH -notlike "*$installDir*") {
        $env:PATH = "$installDir;$env:PATH"
    }

    return (Resolve-ReverseToolSpec -Name 'apktool')
}

function Ensure-PipPackageInstall {
    param([Parameter(Mandatory = $true)]$Definition)

    Ensure-PythonRuntime
    $python = Get-FirstCommandPath -Names @('python', 'python3')
    # Use pipSource (git URL) if available, otherwise use pipPackage name
    $installTarget = if ($Definition.PSObject.Properties['pipSource'] -and -not [string]::IsNullOrWhiteSpace($Definition.pipSource)) {
        $Definition.pipSource
    } else {
        $Definition.pipPackage
    }
    & $python -m pip install --upgrade $installTarget
    if ($LASTEXITCODE -ne 0) {
        throw "pip install failed for $installTarget"
    }
}

function Get-ClaudeMcpConfig {
    $path = Get-ClaudeMcpConfigPath
    if (-not (Test-Path -LiteralPath $path)) {
        return @{ path = $path; json = @{ mcpServers = @{} } }
    }

    $json = Read-ReverseJsonAsHashtable -Path $path
    if ($null -eq $json) {
        $json = @{ mcpServers = @{} }
    }
    if (-not $json.Contains('mcpServers')) {
        $json['mcpServers'] = @{}
    }
    return @{ path = $path; json = $json }
}

function Save-ClaudeMcpConfig {
    param([Parameter(Mandatory = $true)]$Config)

    $parent = Split-Path -Path $Config.path -Parent
    if (-not (Test-Path -LiteralPath $parent)) {
        New-Item -ItemType Directory -Path $parent -Force | Out-Null
    }

    $Config.json | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $Config.path -Encoding utf8
}

function ConvertTo-TomlLiteral {
    param([Parameter(Mandatory = $true)]$Value)

    if ($Value -is [bool]) {
        return $Value.ToString().ToLowerInvariant()
    }
    if ($Value -is [int] -or $Value -is [long] -or $Value -is [double] -or $Value -is [decimal]) {
        return [string]$Value
    }
    if ($Value -is [System.Collections.IEnumerable] -and -not ($Value -is [string])) {
        $items = foreach ($item in $Value) {
            ConvertTo-TomlLiteral -Value $item
        }
        return "[{0}]" -f ($items -join ', ')
    }

    $escaped = ([string]$Value).Replace('\', '\\').Replace('"', '\"')
    return '"' + $escaped + '"'
}

function Remove-CodexMcpServerBlocks {
    param(
        [AllowEmptyCollection()]
        [string[]]$Lines,
        [Parameter(Mandatory = $true)][string]$ServerName
    )

    $targetPattern = '^\[mcp_servers\.{0}(?:\.env)?\]\s*$' -f [regex]::Escape($ServerName)
    $result = New-Object System.Collections.Generic.List[string]
    $skipBlock = $false

    if ($null -eq $Lines) {
        return @()
    }

    foreach ($line in $Lines) {
        if ($line -match '^\[') {
            if ($line -match $targetPattern) {
                $skipBlock = $true
                continue
            }

            if ($skipBlock) {
                $skipBlock = $false
            }
        }

        if (-not $skipBlock) {
            $result.Add($line)
        }
    }

    return @($result)
}

function Set-CodexMcpServer {
    param(
        [Parameter(Mandatory = $true)][string]$ServerName,
        [Parameter(Mandatory = $true)][hashtable]$ServerDefinition
    )

    $path = Get-CodexConfigPath
    $parent = Split-Path -Path $path -Parent
    if (-not (Test-Path -LiteralPath $parent)) {
        New-Item -ItemType Directory -Path $parent -Force | Out-Null
    }

    $lines = @()
    if (Test-Path -LiteralPath $path) {
        $rawLines = @(Get-Content -LiteralPath $path)
        if ($rawLines.Count -eq 1 -and [string]::IsNullOrEmpty($rawLines[0])) {
            $lines = @()
        }
        else {
            $lines = $rawLines
        }
    }
    $lines = @(Remove-CodexMcpServerBlocks -Lines $lines -ServerName $ServerName)

    while ($lines.Count -gt 0 -and [string]::IsNullOrWhiteSpace($lines[-1])) {
        $lines = if ($lines.Count -gt 1) { $lines[0..($lines.Count - 2)] } else { @() }
    }
    if ($lines.Count -gt 0) {
        $lines += ''
    }

    $lines += "[mcp_servers.$ServerName]"
    foreach ($key in @('type', 'url', 'command', 'args', 'bearer_token_env_var')) {
        if ($ServerDefinition.Contains($key)) {
            $lines += "$key = $(ConvertTo-TomlLiteral -Value $ServerDefinition[$key])"
        }
    }
    foreach ($key in ($ServerDefinition.Keys | Where-Object { $_ -notin @('type', 'url', 'command', 'args', 'bearer_token_env_var', 'env') } | Sort-Object)) {
        $lines += "$key = $(ConvertTo-TomlLiteral -Value $ServerDefinition[$key])"
    }

    if ($ServerDefinition.Contains('env') -and $ServerDefinition['env'] -is [System.Collections.IDictionary] -and $ServerDefinition['env'].Count -gt 0) {
        $lines += ''
        $lines += "[mcp_servers.$ServerName.env]"
        foreach ($envKey in ($ServerDefinition['env'].Keys | Sort-Object)) {
            $lines += "$envKey = $(ConvertTo-TomlLiteral -Value $ServerDefinition['env'][$envKey])"
        }
    }

    Set-Content -LiteralPath $path -Value $lines -Encoding utf8
}

function Ensure-McpServer {
    param(
        [Parameter(Mandatory = $true)][string]$ServerName,
        [Parameter(Mandatory = $true)][hashtable]$ServerDefinition
    )

    foreach ($target in Get-McpHostTargets) {
        switch ($target) {
            'Claude' {
                $config = Get-ClaudeMcpConfig
                $claudeDefinition = @{}
                foreach ($key in $ServerDefinition.Keys) {
                    if ($key -ne 'bearer_token_env_var') {
                        $claudeDefinition[$key] = $ServerDefinition[$key]
                    }
                }
                $config.json.mcpServers[$ServerName] = $claudeDefinition
                Save-ClaudeMcpConfig -Config $config
            }
            'Codex' {
                $codexDefinition = @{}
                foreach ($key in $ServerDefinition.Keys) {
                    if ($key -ne 'headers') {
                        $codexDefinition[$key] = $ServerDefinition[$key]
                    }
                }
                Set-CodexMcpServer -ServerName $ServerName -ServerDefinition $codexDefinition
            }
        }
    }
}

function Get-McpCommandServerDefinition {
    param(
        [Parameter(Mandatory = $true)][string]$Command,
        [Parameter(Mandatory = $true)][string[]]$Arguments,
        [hashtable]$Env = @{}
    )

    if ((Test-ReverseIsWindows) -and $Command -in @('npx', 'pnpm', 'npm')) {
        return @{
            type = 'stdio'
            command = 'cmd'
            args = @('/c', $Command) + @($Arguments)
            env = $Env
        }
    }

    return @{
        type = 'stdio'
        command = $Command
        args = @($Arguments)
        env = $Env
    }
}

function Wait-ForPort {
    param(
        [Parameter(Mandatory = $true)][int]$Port,
        [int]$TimeoutSeconds = 90
    )

    $deadline = (Get-Date).AddSeconds($TimeoutSeconds)
    while ((Get-Date) -lt $deadline) {
        if (Test-ReverseTcpPort -Port $Port) {
            return $true
        }
        Start-Sleep -Seconds 2
    }
    return $false
}

function Start-AnythingAnalyzerService {
    param(
        [Parameter(Mandatory = $true)]$Definition,
        [string]$AuthToken = ''
    )

    if ([string]::IsNullOrWhiteSpace($AuthToken)) {
        $AuthToken = Ensure-AnythingAnalyzerMcpConfig -Port ([int]$Definition.servicePort)
    }

    if (Test-ReverseTcpPort -Port ([int]$Definition.servicePort)) {
        return
    }

Ensure-Pnpm
$vsBuildToolsError = ''
if (Test-ReverseIsWindows) {
    try {
        Ensure-VsBuildTools
    }
    catch {
        $vsBuildToolsError = $_.Exception.Message
        Write-Warning "Visual Studio Build Tools auto-install failed; continuing with pnpm prebuilt/native rebuild path. $vsBuildToolsError"
    }
}

    $repoDir = @($Definition.startupDirCandidates) | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
    if ([string]::IsNullOrWhiteSpace($repoDir)) {
        $installDir = $Definition.installDir
        $gh = Get-FirstCommandPath -Names @('gh')
        $git = Get-FirstCommandPath -Names @('git')
        if ($gh) {
            & $gh repo clone 'Mouseww/anything-analyzer' $installDir
        }
        elseif ($git) {
            & $git clone $Definition.repoUrl $installDir
        }
        else {
            throw 'Cannot clone anything-analyzer because neither gh nor git is available.'
        }
        if ($LASTEXITCODE -ne 0) {
            throw 'Failed to clone anything-analyzer.'
        }
        $repoDir = $installDir
    }

    $pnpm = Get-NodeCommandPath -Name 'pnpm'
    if ([string]::IsNullOrWhiteSpace($pnpm)) {
        throw 'pnpm is not available after installation.'
    }

    Push-Location $repoDir
    try {
        Approve-AnythingAnalyzerBuildScripts -RepoDir $repoDir -PnpmPath $pnpm

        if (-not (Test-AnythingAnalyzerElectronHealthy -RepoDir $repoDir -PnpmPath $pnpm)) {
            $nodeModules = Join-Path $repoDir 'node_modules'
            if (Test-Path -LiteralPath $nodeModules) {
                Remove-Item -LiteralPath $nodeModules -Recurse -Force
            }
        }

        & $pnpm install
        if ($LASTEXITCODE -ne 0) {
            if (-not [string]::IsNullOrWhiteSpace($vsBuildToolsError)) {
                throw "pnpm install failed for anything-analyzer. Visual Studio Build Tools auto-install also failed earlier: $vsBuildToolsError"
            }
            throw 'pnpm install failed for anything-analyzer.'
        }

        & $pnpm rebuild electron esbuild better-sqlite3
        if ($LASTEXITCODE -ne 0) {
            if (-not [string]::IsNullOrWhiteSpace($vsBuildToolsError)) {
                throw "pnpm rebuild failed for anything-analyzer. Visual Studio Build Tools auto-install also failed earlier: $vsBuildToolsError"
            }
            throw 'pnpm rebuild failed for anything-analyzer.'
        }

        if (-not (Test-AnythingAnalyzerElectronHealthy -RepoDir $repoDir -PnpmPath $pnpm)) {
            throw 'Electron is still not healthy after reinstall/rebuild.'
        }
    }
    finally {
        Pop-Location
    }

    $stdoutLog = Join-Path $repoDir 'anything-analyzer-dev.log'
    $stderrLog = Join-Path $repoDir 'anything-analyzer-dev.err.log'
    Remove-Item -LiteralPath $stdoutLog, $stderrLog -Force -ErrorAction SilentlyContinue

    Start-Process -FilePath $pnpm -ArgumentList @('dev') -WorkingDirectory $repoDir -WindowStyle Hidden -RedirectStandardOutput $stdoutLog -RedirectStandardError $stderrLog | Out-Null
    if (-not (Wait-ForPort -Port ([int]$Definition.servicePort) -TimeoutSeconds 120)) {
        throw "anything-analyzer did not open port 23816 in time. Logs: $stdoutLog ; $stderrLog"
    }
}

function Start-IdaProService {
    param([Parameter(Mandatory = $true)]$Definition)

    if (Test-ReverseTcpPort -Port ([int]$Definition.servicePort)) {
        return
    }

    $startScript = $Definition.startScript
    $shell = Get-PreferredPowerShellPath
    & $shell -NoProfile -ExecutionPolicy Bypass -File $startScript
    if ($LASTEXITCODE -ne 0 -and -not (Test-ReverseTcpPort -Port ([int]$Definition.servicePort))) {
        throw 'Failed to start idapro service.'
    }

    if (-not (Wait-ForPort -Port ([int]$Definition.servicePort) -TimeoutSeconds 45)) {
        throw 'idapro service did not open port 13337 in time.'
    }
}

function Ensure-AndroidPlatformTools {
    $adb = Resolve-ReverseToolSpec -Name 'adb'
    if ($adb.Available) {
        return $adb
    }

    Ensure-WingetPackage -Id 'Google.PlatformTools' -Label 'Android SDK Platform-Tools'
    return (Resolve-ReverseToolSpec -Name 'adb')
}

function Ensure-GitCloneInstall {
    param(
        [Parameter(Mandatory = $true)]$Definition,
        [Parameter(Mandatory = $true)][string]$TargetPath
    )

    $pinnedCommit = if ($Definition.PSObject.Properties['pinnedCommit']) { [string]$Definition.pinnedCommit } else { '' }
    $git = Get-FirstCommandPath -Names @('git')
    if ([string]::IsNullOrWhiteSpace($git)) {
        throw "Cannot clone $($Definition.repo) because git is not available."
    }

    if ((Test-Path -LiteralPath $TargetPath -PathType Container) -and (Test-Path -LiteralPath (Join-Path $TargetPath '.git'))) {
        if (-not [string]::IsNullOrWhiteSpace($pinnedCommit)) {
            $currentCommit = (& $git -C $TargetPath rev-parse HEAD).Trim()
            if ($LASTEXITCODE -ne 0 -or $currentCommit -ne $pinnedCommit) {
                throw "Existing checkout is not at pinned commit $pinnedCommit. Move it aside explicitly, then retry: $TargetPath"
            }
        }
        return $true
    }

    if (Test-Path -LiteralPath $TargetPath) {
        $backupPath = "$TargetPath.bak-$([DateTime]::UtcNow.ToString('yyyyMMddHHmmss'))"
        Move-Item -LiteralPath $TargetPath -Destination $backupPath -Force
    }

    Ensure-DownloadDirectory -Path (Split-Path -Path $TargetPath -Parent)

    if ([string]::IsNullOrWhiteSpace($pinnedCommit)) {
        & $git clone --depth 1 $Definition.repo $TargetPath
        if ($LASTEXITCODE -ne 0) {
            throw "git clone failed for $($Definition.repo)"
        }
    }
    else {
        & $git init --quiet $TargetPath
        & $git -C $TargetPath remote add origin $Definition.repo
        & $git -C $TargetPath fetch --depth 1 origin $pinnedCommit
        & $git -C $TargetPath checkout --quiet --detach FETCH_HEAD
        $resolvedCommit = (& $git -C $TargetPath rev-parse HEAD).Trim()
        if ($LASTEXITCODE -ne 0 -or $resolvedCommit -ne $pinnedCommit) {
            throw "Pinned checkout verification failed for $($Definition.repo): expected $pinnedCommit, got $resolvedCommit"
        }
    }

    return $true
}

function Ensure-Capability {
    param([Parameter(Mandatory = $true)][string]$Name)

    $definition = Get-ReverseBootstrapDefinition -Name $Name
    if ($null -eq $definition) {
        throw "No bootstrap definition for capability: $Name"
    }

    # If capability is marked as not auto-installable, output guidance and skip
    if ($definition.PSObject.Properties['canAutoInstall'] -and $definition.canAutoInstall -eq $false) {
        $hint = if ($definition.PSObject.Properties['manualInstallHint']) { $definition.manualInstallHint } else { "Please install $Name manually. Docs: $($definition.docsUrl)" }
        Write-Warning "MANUAL_INSTALL_REQUIRED: $Name — $hint"
        # Still try to register MCP URL if applicable
        if ($definition.PSObject.Properties['mcpNames'] -and $definition.PSObject.Properties['mcpUrl']) {
            Ensure-McpServer -ServerName $definition.mcpNames[0] -ServerDefinition @{ url = $definition.mcpUrl }
        }
        return $false
    }

    $existingState = Get-ReverseCapabilityState -Name $Name
    if ($existingState -and -not $definition.PSObject.Properties['mcpNames']) {
        $toolSpec = $null
        try {
            $toolSpec = Resolve-ReverseToolSpec -Name $Name
        }
        catch {
            $toolSpec = $null
        }

        if ($toolSpec -and $toolSpec.Available) {
            return $toolSpec
        }
    }

    switch ($definition.bootstrapKind) {
        'github-release-zip' {
            # Generic handler for all github-release-zip capabilities
            $verifyName = if ($definition.PSObject.Properties['verifyCommand'] -and -not [string]::IsNullOrWhiteSpace($definition.verifyCommand)) {
                $definition.verifyCommand
            } else { $Name }
            return Ensure-GitHubZipInstall -Definition $definition -TargetPath $definition.installDir -VerifyName $verifyName
        }
        'git-clone' {
            return Ensure-GitCloneInstall -Definition $definition -TargetPath $definition.installDir
        }
        'github-release-jar-wrapper' {
            return Ensure-ApktoolInstall -Definition $definition
        }
        'pip-package' {
            Ensure-PipPackageInstall -Definition $definition
            return $true
        }
        'winget-package' {
            $wingetId = $definition.wingetId
            Ensure-WingetPackage -Id $wingetId -Label $Name
            $toolSpec = Resolve-ReverseToolSpec -Name $Name
            if (-not $toolSpec.Available) {
                throw "$Name was installed, but bootstrap could not resolve the executable in this process."
            }
            return $toolSpec
        }
        'npm-mcp' {
            Ensure-NodeRuntime
            $envMap = @{}
            if ($definition.PSObject.Properties['mcpEnv'] -and $null -ne $definition.mcpEnv) {
                foreach ($property in $definition.mcpEnv.PSObject.Properties) {
                    $envMap[$property.Name] = $property.Value
                }
            }
            $serverDefinition = Get-McpCommandServerDefinition -Command $definition.mcpCommand -Arguments @($definition.mcpArgs) -Env $envMap
            Ensure-McpServer -ServerName $definition.mcpNames[0] -ServerDefinition $serverDefinition
            return $true
        }
        'npm-global' {
            Ensure-NodeRuntime
            $npm = Get-NodeCommandPath -Name 'npm'
            if ([string]::IsNullOrWhiteSpace($npm)) {
                throw 'npm is not available after Node.js installation.'
            }
            & $npm install -g $definition.npmPackage
            if ($LASTEXITCODE -ne 0) {
                throw "npm install -g $($definition.npmPackage) failed."
            }
            # Run post-install command if specified (e.g. playwright install)
            if ($definition.PSObject.Properties['postInstall'] -and -not [string]::IsNullOrWhiteSpace($definition.postInstall)) {
                $postParts = $definition.postInstall -split ' ', 2
                $postCmd = Get-FirstCommandPath -Names @($postParts[0])
                if (-not [string]::IsNullOrWhiteSpace($postCmd)) {
                    if ($postParts.Count -gt 1) {
                        & $postCmd $postParts[1].Split(' ')
                    }
                    else {
                        & $postCmd
                    }
                }
                else {
                    # Try via npx
                    $npx = Get-NodeCommandPath -Name 'npx'
                    if ($npx) {
                        & $npx $definition.postInstall.Split(' ')
                    }
                }
            }
            # Run setup script if specified
            if ($definition.PSObject.Properties['setupScript'] -and -not [string]::IsNullOrWhiteSpace($definition.setupScript)) {
                $setupPath = $definition.setupScript
                if (Test-Path -LiteralPath $setupPath) {
                    $shell = Get-PreferredPowerShellPath
                    & $shell -NoProfile -ExecutionPolicy Bypass -File $setupPath -SkipBrowserInstall
                }
            }
            return $true
        }
        'local-http-mcp' {
            if ($Name -eq 'anything-analyzer') {
                $authToken = Ensure-AnythingAnalyzerMcpConfig -Port ([int]$definition.servicePort)
                $env:ANYTHING_ANALYZER_MCP_TOKEN = $authToken
                try {
                    [Environment]::SetEnvironmentVariable('ANYTHING_ANALYZER_MCP_TOKEN', $authToken, 'User')
                }
                catch {
                    Write-Warning "Could not persist ANYTHING_ANALYZER_MCP_TOKEN for future MCP clients: $($_.Exception.Message)"
                }
                $serverDefinition = @{
                    url                  = $definition.mcpUrl
                    headers              = @{ Authorization = "Bearer $authToken" }
                    bearer_token_env_var = 'ANYTHING_ANALYZER_MCP_TOKEN'
                }
                Ensure-McpServer -ServerName 'anything-analyzer' -ServerDefinition $serverDefinition
                if ($StartServices) {
                    Start-AnythingAnalyzerService -Definition $definition -AuthToken $authToken
                }
                return $true
            }
            if ($Name -eq 'idapro') {
                Ensure-Capability -Name 'idalib-mcp'
                Ensure-McpServer -ServerName 'idapro' -ServerDefinition @{ url = $definition.mcpUrl }
                if ($StartServices) {
                    Start-IdaProService -Definition $definition
                }
                return $true
            }
        }
        'go-install' {
            if (-not $definition.PSObject.Properties['goPackage'] -or [string]::IsNullOrWhiteSpace($definition.goPackage)) {
                throw "go-install capability $Name is missing goPackage in bootstrap-manifest.json."
            }
            $goCmd = Get-FirstCommandPath -Names @('go')
            if ([string]::IsNullOrWhiteSpace($goCmd)) {
                try {
                    Ensure-WingetPackage -Id 'GoLang.Go' -Label 'Go'
                }
                catch {
                    Write-Warning "Go runtime unavailable: $($_.Exception.Message)"
                }
                $goCmd = Get-FirstCommandPath -Names @('go')
                if ([string]::IsNullOrWhiteSpace($goCmd)) {
                    $goCandidates = @(
                        (Join-Path $env:LOCALAPPDATA 'Programs\Go\bin\go.exe'),
                        'C:\Program Files\Go\bin\go.exe',
                        (Join-Path $env:USERPROFILE 'go\bin\go.exe')
                    ) | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
                    if ($goCandidates) { $goCmd = $goCandidates }
                }
            }
            if (-not [string]::IsNullOrWhiteSpace($goCmd)) {
                & $goCmd install $definition.goPackage
                if ($LASTEXITCODE -eq 0) {
                    if ($definition.PSObject.Properties['mcpNames'] -and -not [string]::IsNullOrWhiteSpace($definition.mcpCommand)) {
                        $serverDefinition = Get-McpCommandServerDefinition -Command $definition.mcpCommand -Arguments @($definition.mcpArgs)
                        Ensure-McpServer -ServerName $definition.mcpNames[0] -ServerDefinition $serverDefinition
                    }
                    return $true
                }
                Write-Warning "go install failed for $Name (exit code $LASTEXITCODE)."
            }
            # Docker fallback (parity with bootstrap-reverse.sh ensure_pentestswarm)
            if ($definition.PSObject.Properties['fallbackKind'] -and $definition.fallbackKind -eq 'docker-image' -and $definition.PSObject.Properties['dockerImage']) {
                $docker = Get-FirstCommandPath -Names @('docker')
                if (-not [string]::IsNullOrWhiteSpace($docker)) {
                    $serverDefinition = @{
                        type = 'stdio'
                        command = 'docker'
                        args = @('run', '--rm', '-i', $definition.dockerImage) + @($definition.mcpArgs)
                    }
                    Ensure-McpServer -ServerName $definition.mcpNames[0] -ServerDefinition $serverDefinition
                    Write-Warning "$Name Go install failed; registered Docker fallback $($definition.dockerImage)"
                    return $true
                }
            }
            throw "Capability $Name requires Go 1.24+ or Docker to install. Docs: $($definition.docsUrl)"
        }
        default {
            throw "Unsupported bootstrap kind: $($definition.bootstrapKind)"
        }
    }

    throw "Capability bootstrap fell through without action: $Name"
}

function Expand-CapabilityDependencies {
    param([Parameter(Mandatory = $true)][string[]]$Names)

    $ordered = New-Object System.Collections.Generic.List[string]
    $seen = @{}

    function Add-Capability {
        param([string]$CapabilityName)

        if ($seen.ContainsKey($CapabilityName)) {
            return
        }
        $seen[$CapabilityName] = $true

        $definition = Get-ReverseBootstrapDefinition -Name $CapabilityName
        if ($null -ne $definition -and $definition.PSObject.Properties['dependsOn']) {
            foreach ($dependency in @($definition.dependsOn)) {
                Add-Capability -CapabilityName $dependency
            }
        }

        $ordered.Add($CapabilityName)
    }

    foreach ($name in $Names) {
        Add-Capability -CapabilityName $name
    }

    return $ordered
}

$expandedCapabilities = Expand-CapabilityDependencies -Names $Capability
$results = @()

foreach ($name in $expandedCapabilities) {
    $definition = Get-ReverseBootstrapDefinition -Name $name
    if ($null -eq $definition) {
        $results += [pscustomobject]@{ name = $name; status = 'missing-definition' }
        continue
    }

    try {
        $manualRequired = $false
        switch ($name) {
            'adb' {
                Ensure-AndroidPlatformTools | Out-Null
            }
            default {
                $ensureResult = Ensure-Capability -Name $name
                if ($ensureResult -eq $false) {
                    $def = Get-ReverseBootstrapDefinition -Name $name
                    $hint = if ($def -and $def.PSObject.Properties['manualInstallHint']) { $def.manualInstallHint } else { "Install manually. Docs: $($def.docsUrl)" }
                    $results += [pscustomobject]@{
                        name = $name
                        status = 'manual-required'
                        hint = $hint
                        docs_url = [string]$def.docsUrl
                    }
                    $manualRequired = $true
                }
            }
        }

        if (-not $manualRequired) {
            $state = Get-ReverseCapabilityState -Name $name
            $status = if ($state -and -not $state.Ready) { 'configured-not-ready' } else { 'ready' }
            $results += [pscustomobject]@{
                name = $name
                status = $status
                ready = if ($state) { $state.Ready } else { $null }
                registered = if ($state) { $state.Registered } else { $null }
                service_online = if ($state) { $state.ServiceOnline } else { $null }
            }
        }
    }
    catch {
        $results += [pscustomobject]@{
            name = $name
            status = 'failed'
            error = $_.Exception.Message
        }
    }
}

if (-not $SkipRefresh) {
    $shell = Get-PreferredPowerShellPath
    & $shell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot 'refresh-tool-index.ps1') | Out-Null
}

$results | ConvertTo-Json -Depth 5
