@echo off
rem ClaudeCodes WorkBuddy Windows 模型一键配置工具。
rem 安装器版本：v1.0.2
setlocal EnableExtensions
chcp 65001 >nul
title ClaudeCodes WorkBuddy 模型一键配置

set "CLAUDECODES_PS=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe"
if not exist "%CLAUDECODES_PS%" (
  where powershell.exe >nul 2>&1
  if errorlevel 1 (
    echo.
    echo 配置失败：未找到 Windows PowerShell。
    goto :failed
  )
  set "CLAUDECODES_PS=powershell.exe"
)

set "CLAUDECODES_SOURCE=%~f0"
set "CLAUDECODES_PS1=%TEMP%\claudecodes-workbuddy-%RANDOM%-%RANDOM%.ps1"
"%CLAUDECODES_PS%" -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "$lines=[IO.File]::ReadAllLines($env:CLAUDECODES_SOURCE,[Text.Encoding]::UTF8); $marker=[Array]::IndexOf($lines,'# CLAUDECODES_POWERSHELL_START'); if($marker -lt 0){exit 2}; [IO.File]::WriteAllLines($env:CLAUDECODES_PS1,$lines[($marker+1)..($lines.Length-1)],(New-Object Text.UTF8Encoding($true)))"
if errorlevel 1 (
  del /f /q "%CLAUDECODES_PS1%" >nul 2>&1
  echo.
  echo 配置失败：无法提取内嵌配置脚本，请重新下载安装器。
  goto :failed
)

"%CLAUDECODES_PS%" -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%CLAUDECODES_PS1%"
set "CLAUDECODES_EXIT=%ERRORLEVEL%"
del /f /q "%CLAUDECODES_PS1%" >nul 2>&1
echo.
echo 请按任意键退出……
pause >nul
exit /b %CLAUDECODES_EXIT%

:failed
echo.
echo 请按任意键退出……
pause >nul
exit /b 1

# CLAUDECODES_POWERSHELL_START
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

$productName = 'WorkBuddy'
$edition = '默认版'
$installerVersion = '1.0.2'
$configDirectoryName = '.workbuddy'
$rootMode = 'flexible'
$urlMode = 'base'
$apiBaseUrl = 'https://api.claudecodes.org/v1'
$modelsUrl = $apiBaseUrl.TrimEnd('/') + '/models'
$capabilitiesUrl = 'https://docs.claudecodes.org/downloads/claudecodes-chat-model-capabilities.txt'
$backupFile = $null
$temporaryFile = $null
$apiKey = $null

function Enable-Tls12 {
    try {
        $current = [int][Net.ServicePointManager]::SecurityProtocol
        [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]($current -bor 3072)
    } catch {
        throw '此 Windows 系统无法启用 TLS 1.2。'
    }
}

function Assert-NotReparsePoint {
    param([Parameter(Mandatory = $true)][string]$Path)
    if (Test-Path -LiteralPath $Path) {
        $item = Get-Item -LiteralPath $Path -Force
        if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
            throw ('不允许使用符号链接或重解析点：{0}' -f $Path)
        }
    }
}

function Get-PropertyValue {
    param($Object, [string]$Name)
    if ($null -eq $Object) { return $null }
    $property = $Object.PSObject.Properties[$Name]
    if ($null -eq $property) { return $null }
    return $property.Value
}

function Install-ConfigFile {
    param(
        [Parameter(Mandatory = $true)][string]$Source,
        [Parameter(Mandatory = $true)][string]$Destination
    )
    if (-not (Test-Path -LiteralPath $Source -PathType Leaf)) {
        throw ('临时配置文件不存在：{0}' -f $Source)
    }
    if (-not (Test-Path -LiteralPath $Destination)) {
        [IO.File]::Move($Source, $Destination)
        return $null
    }

    $timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
    $backup = '{0}.backup-{1}-{2}' -f $Destination, $timestamp, $PID
    try {
        [IO.File]::Replace($Source, $Destination, $backup, $true)
        return $backup
    } catch {
        $replaceError = $_.Exception.Message
    }

    try {
        [IO.File]::Copy($Destination, $backup, $false)
        [IO.File]::Copy($Source, $Destination, $true)
        [IO.File]::Delete($Source)
        return $backup
    } catch {
        $fallbackError = $_.Exception.Message
        if (Test-Path -LiteralPath $backup -PathType Leaf) {
            try { [IO.File]::Copy($backup, $Destination, $true) } catch { }
        }
        throw ('无法安全替换配置文件。原子替换错误：{0}；兼容替换错误：{1}' -f $replaceError, $fallbackError)
    }
}

try {
    if ($PSVersionTable.PSVersion.Major -lt 5) {
        throw '需要 Windows PowerShell 5.1 或更高版本。'
    }

    $userProfile = [Environment]::GetFolderPath([Environment+SpecialFolder]::UserProfile)
    if ([string]::IsNullOrWhiteSpace($userProfile)) { $userProfile = $env:USERPROFILE }
    if ([string]::IsNullOrWhiteSpace($userProfile)) { throw '无法确定当前用户目录。' }

    $configDirectory = Join-Path $userProfile $configDirectoryName
    $configFile = Join-Path $configDirectory 'models.json'
    $temporaryFile = Join-Path $configDirectory ('.models.json.tmp.' + [Guid]::NewGuid().ToString('N'))

    Write-Host ''
    Write-Host '========================================' -ForegroundColor Cyan
    Write-Host (' ClaudeCodes {0} 模型一键配置' -f $productName) -ForegroundColor Cyan
    Write-Host '========================================' -ForegroundColor Cyan
    Write-Host ('安装器版本：v{0}' -f $installerVersion)
    Write-Host ('客户端版本：{0}' -f $edition)
    Write-Host ('API 地址：  {0}' -f $apiBaseUrl)
    Write-Host ('配置文件：  {0}' -f $configFile)
    Write-Host ''

    if (-not [string]::IsNullOrWhiteSpace($env:CLAUDECODES_API_KEY)) {
        $apiKey = $env:CLAUDECODES_API_KEY.Trim()
    } else {
        $apiKey = (Read-Host '请输入 ClaudeCodes API Key（输入内容会明文显示）').Trim()
    }
    if ([string]::IsNullOrWhiteSpace($apiKey)) { throw 'API Key 不能为空。' }

    Enable-Tls12
    Write-Host '正在读取此 API Key 可用的模型列表……'
    try {
        $modelResponse = Invoke-RestMethod -UseBasicParsing -Method Get -Uri $modelsUrl -Headers @{ Authorization = ('Bearer ' + $apiKey) } -TimeoutSec 60
    } catch {
        throw ('无法读取 {0}，未修改任何文件。详细信息：{1}' -f $modelsUrl, $_.Exception.Message)
    }
    if ($null -eq $modelResponse -or $null -eq $modelResponse.PSObject.Properties['data']) {
        throw '/v1/models 返回内容缺少 data 数组，未修改任何文件。'
    }
    if (-not ($modelResponse.data -is [Array])) {
        throw '/v1/models 的 data 字段不是数组，未修改任何文件。'
    }
    $onlineModels = @($modelResponse.data)
    if ($onlineModels.Count -eq 0) { throw '/v1/models 的 data 数组为空，未修改任何文件。' }

    Write-Host '正在读取已验证的 Chat Completions 与图片输入能力表……'
    try {
        $capabilitiesResponse = Invoke-WebRequest -UseBasicParsing -Method Get -Uri $capabilitiesUrl -Headers @{ 'Cache-Control' = 'no-cache' } -TimeoutSec 60
        $capabilitiesText = [string]$capabilitiesResponse.Content
    } catch {
        throw ('无法读取已验证能力表，未修改任何文件。详细信息：{0}' -f $_.Exception.Message)
    }

    $capabilities = New-Object 'Collections.Generic.Dictionary[string,object]' ([StringComparer]::OrdinalIgnoreCase)
    foreach ($rawLine in ($capabilitiesText -split "`r?`n")) {
        $line = $rawLine.Trim().TrimStart([char]0xFEFF)
        if ([string]::IsNullOrWhiteSpace($line) -or $line.StartsWith('#')) { continue }
        if ($line -notmatch '^([^|]+)\|(true|false)$') {
            throw ('能力表包含格式无效的行：{0}' -f $line)
        }
        $canonicalID = $matches[1].Trim()
        if ([string]::IsNullOrWhiteSpace($canonicalID)) { throw '能力表包含空模型 ID。' }
        $capabilities[$canonicalID] = [PSCustomObject]@{
            ID = $canonicalID
            SupportsImages = ($matches[2] -eq 'true')
        }
    }
    if ($capabilities.Count -eq 0) { throw '已验证能力表为空，未修改任何文件。' }

    $selected = New-Object 'Collections.Generic.List[object]'
    $selectedIDs = New-Object 'Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase)
    foreach ($entry in $onlineModels) {
        $onlineID = [string](Get-PropertyValue $entry 'id')
        if ([string]::IsNullOrWhiteSpace($onlineID)) { continue }
        $onlineID = $onlineID.Trim()
        if ($capabilities.ContainsKey($onlineID) -and $selectedIDs.Add($onlineID)) {
            $selected.Add($capabilities[$onlineID])
        }
    }
    if ($selected.Count -eq 0) {
        throw '此 API Key 没有命中已验证的 Chat Completions 白名单模型，未修改任何文件。'
    }

    Assert-NotReparsePoint $configDirectory
    Assert-NotReparsePoint $configFile
    if (Test-Path -LiteralPath $configDirectory -PathType Leaf) {
        throw ('配置目录路径已被普通文件占用：{0}' -f $configDirectory)
    }
    if (-not (Test-Path -LiteralPath $configDirectory)) {
        [void](New-Item -ItemType Directory -Path $configDirectory)
    }
    if (Test-Path -LiteralPath $configFile -PathType Container) {
        throw ('配置文件路径已被目录占用：{0}' -f $configFile)
    }

    $root = $null
    $models = @()
    $arrayRoot = $false
    if (Test-Path -LiteralPath $configFile -PathType Leaf) {
        $source = [IO.File]::ReadAllText($configFile, [Text.Encoding]::UTF8).Trim()
        if ($source) {
            try { $parsed = ConvertFrom-Json -InputObject $source } catch { throw '现有 models.json 不是有效 JSON，原文件已保留。' }
            if ($source.TrimStart().StartsWith('[')) {
                $models = @($parsed)
                $arrayRoot = $true
            } elseif ($null -ne $parsed -and -not ($parsed -is [Array])) {
                $root = $parsed
                $modelsProperty = $parsed.PSObject.Properties['models']
                if ($null -ne $modelsProperty) {
                    if ($null -eq $modelsProperty.Value) { $models = @() }
                    elseif ($modelsProperty.Value -is [Array]) { $models = @($modelsProperty.Value) }
                    else { throw '现有 models.json 的 models 字段不是数组。' }
                }
            } else {
                throw '现有 models.json 必须包含数组或对象。'
            }
        }
    }

    if ($rootMode -eq 'flexible') {
        if ($null -eq $root -and -not $arrayRoot) { $arrayRoot = $true }
    } else {
        if ($null -eq $root) { $root = [PSCustomObject][ordered]@{} }
        $arrayRoot = $false
    }

    $managedUrl = if ($urlMode -eq 'base') { $apiBaseUrl.TrimEnd('/') } else { $apiBaseUrl.TrimEnd('/') + '/chat/completions' }
    $fixedByID = New-Object 'Collections.Generic.Dictionary[string,object]' ([StringComparer]::OrdinalIgnoreCase)
    foreach ($capability in $selected) {
        $properties = [ordered]@{
            id = $capability.ID
            name = $capability.ID
            vendor = 'Custom'
            url = $managedUrl
            apiKey = $apiKey
            supportsToolCall = $true
            supportsImages = [bool]$capability.SupportsImages
            supportsReasoning = $false
            maxInputTokens = 200000
            maxOutputTokens = 65536
        }
        if ($urlMode -eq 'base') { $properties['useCustomProtocol'] = $false }
        $fixedByID[$capability.ID] = [PSCustomObject]$properties
    }

    $updated = New-Object 'Collections.Generic.List[object]'
    $installed = New-Object 'Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase)
    foreach ($item in @($models)) {
        $itemID = [string](Get-PropertyValue $item 'id')
        if (-not [string]::IsNullOrWhiteSpace($itemID) -and $fixedByID.ContainsKey($itemID)) {
            if ($installed.Add($itemID)) { $updated.Add($fixedByID[$itemID]) }
            continue
        }
        $itemUrl = [string](Get-PropertyValue $item 'url')
        if (-not [string]::IsNullOrWhiteSpace($itemUrl) -and $itemUrl.TrimEnd('/') -eq $managedUrl.TrimEnd('/')) { continue }
        $updated.Add($item)
    }
    foreach ($capability in $selected) {
        if ($installed.Add($capability.ID)) { $updated.Add($fixedByID[$capability.ID]) }
    }

    if ($arrayRoot) {
        $outputRoot = $updated.ToArray()
    } else {
        if ($null -eq $root) { $root = [PSCustomObject][ordered]@{} }
        if ($null -eq $root.PSObject.Properties['models']) {
            Add-Member -InputObject $root -MemberType NoteProperty -Name models -Value $updated.ToArray()
        } else {
            $root.models = $updated.ToArray()
        }
        $outputRoot = $root
    }

    $json = ConvertTo-Json -InputObject $outputRoot -Depth 20
    [IO.File]::WriteAllText($temporaryFile, $json + [Environment]::NewLine, (New-Object Text.UTF8Encoding($false)))
    try { $null = ConvertFrom-Json -InputObject ([IO.File]::ReadAllText($temporaryFile, [Text.Encoding]::UTF8)) } catch { throw '生成的配置未通过 JSON 校验。' }

    $backupFile = Install-ConfigFile -Source $temporaryFile -Destination $configFile
    $temporaryFile = $null

    Write-Host ''
    Write-Host '配置成功。' -ForegroundColor Green
    Write-Host ('客户端：{0}（{1}）' -f $productName, $edition)
    Write-Host ('模型数：{0}' -f $selected.Count)
    Write-Host ('模型名：{0}' -f (($selected | ForEach-Object { $_.ID }) -join ','))
    Write-Host ('配置文件：{0}' -f $configFile)
    if ($backupFile) { Write-Host ('备份文件：{0}' -f $backupFile) }
    Write-Host ''
    Write-Host ('请完整退出并重新打开 {0}，然后选择自定义模型。' -f $productName)
    exit 0
} catch {
    Write-Host ''
    Write-Host ('配置失败：{0}' -f $_.Exception.Message) -ForegroundColor Red
    exit 1
} finally {
    if ($temporaryFile -and (Test-Path -LiteralPath $temporaryFile -PathType Leaf)) {
        Remove-Item -LiteralPath $temporaryFile -Force -ErrorAction SilentlyContinue
    }
    $apiKey = $null
}
